initial commit-moved from vulkan_guide

This commit is contained in:
2025-10-10 22:53:54 +09:00
commit 8853429937
2484 changed files with 973414 additions and 0 deletions

73
docs/Compute.md Normal file
View File

@@ -0,0 +1,73 @@
## Compute System: Pipelines, Instances, and Dispatch
Standalone compute subsystem with a small, explicit API. Used by passes (e.g., Background) and tools. It lives under `src/compute` and is surfaced via `EngineContext::compute` and convenience wrappers on `PipelineManager`.
### Concepts
- Pipelines: Named compute pipelines created from a SPIRV module and a simple descriptor layout spec.
- Instances: Persistently bound descriptor sets keyed by instance name; useful for effects that rebind images/buffers across frames without recreating pipelines.
- Dispatch: Issue work with group counts, optional push constants, and adhoc memory barriers.
### Key Types
- `ComputePipelineCreateInfo` — shader path, descriptor types, push constant size/stages, optional specialization (src/compute/vk_compute.h).
- `ComputeDispatchInfo``groupCount{X,Y,Z}`, `bindings`, `pushConstants`, and `*_barriers` arrays for additional sync.
- `ComputeBinding` — helpers for `uniformBuffer`, `storageBuffer`, `sampledImage`, `storeImage`.
### API Surface
- Register/Destroy
- `bool ComputeManager::registerPipeline(name, ComputePipelineCreateInfo)`
- `void ComputeManager::unregisterPipeline(name)`
- Query: `bool ComputeManager::hasPipeline(name)`
- Dispatch
- `void ComputeManager::dispatch(cmd, name, ComputeDispatchInfo)`
- `void ComputeManager::dispatchImmediate(name, ComputeDispatchInfo)` — records on a transient command buffer and submits.
- Helpers: `createDispatch2D(w,h[,lsX,lsY])`, `createDispatch3D(w,h,d[,lsX,lsY,lsZ])`.
- Instances
- `bool ComputeManager::createInstance(instanceName, pipelineName)` / `destroyInstance(instanceName)`
- `setInstanceStorageImage`, `setInstanceSampledImage`, `setInstanceBuffer`
- `AllocatedImage createAndBindStorageImage(...)`, `AllocatedBuffer createAndBindStorageBuffer(...)`
- `void dispatchInstance(cmd, instanceName, info)`
### Quick Start — OneShot Dispatch
```c++
ComputePipelineCreateInfo ci{};
ci.shaderPath = context->getAssets()->shaderPath("blur.comp.spv");
ci.descriptorTypes = { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE };
ci.pushConstantSize = sizeof(ComputePushConstants);
context->compute->registerPipeline("blur", ci);
ComputeDispatchInfo di = ComputeManager::createDispatch2D(draw.w, draw.h);
di.bindings.push_back(ComputeBinding::storeImage(0, outImageView));
di.bindings.push_back(ComputeBinding::sampledImage(1, inImageView, context->getSamplers()->defaultLinear()));
ComputePushConstants pc{}; /* fill */
di.pushConstants = &pc; di.pushConstantSize = sizeof(pc);
context->compute->dispatch(cmd, "blur", di);
```
### Quick Start — Persistent Instance
```c++
context->compute->createInstance("background.sky", "sky");
context->compute->setInstanceStorageImage("background.sky", 0, ctx->getSwapchain()->drawImage().imageView);
ComputeDispatchInfo di = ComputeManager::createDispatch2D(ctx->getDrawExtent().width,
ctx->getDrawExtent().height);
di.pushConstants = &effect.data; di.pushConstantSize = sizeof(ComputePushConstants);
context->compute->dispatchInstance(cmd, "background.sky", di);
```
### Integration With Render Graph
- Compute passes declare `write(image, RGImageUsage::ComputeWrite)` in their build callback; the graph inserts layout transitions to `GENERAL` and required barriers.
- Background pass example: `src/render/vk_renderpass_background.cpp`.
### Sync Notes
- ComputeManager inserts minimal barriers needed for common cases; prefer using the Render Graph for crosspass synchronization.
- For advanced cases, add `imageBarriers`/`bufferBarriers` to `ComputeDispatchInfo`.

77
docs/Descriptors.md Normal file
View File

@@ -0,0 +1,77 @@
## Descriptors: Builders, Allocators, and Layouts
Utilities to define descriptor layouts, write descriptor sets, and efficiently allocate them per-frame or globally.
### Overview
- Layouts: `DescriptorLayoutBuilder` assembles `VkDescriptorSetLayout` with staged bindings.
- Writing: `DescriptorWriter` collects buffer/image writes and updates a set in one call.
- Pools: `DescriptorAllocatorGrowable` manages a growable pool-of-pools for resilient allocations; `FrameResources` keeps one per overlapping frame.
- Common layouts: `DescriptorManager` pre-creates reusable layouts such as `gpuSceneDataLayout()` and `singleImageLayout()`.
### Quick Start — Transient Per-Frame Set
```c++
// 1) Create/update a small uniform buffer for the frame
AllocatedBuffer ubuf = context->getResources()->create_buffer(
sizeof(GPUSceneData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
context->currentFrame->_deletionQueue.push_function([=, &ctx=*context]{ ctx.getResources()->destroy_buffer(ubuf); });
VmaAllocationInfo ai{}; vmaGetAllocationInfo(context->getDevice()->allocator(), ubuf.allocation, &ai);
*static_cast<GPUSceneData*>(ai.pMappedData) = context->getSceneData();
// 2) Allocate a set from the frame allocator using a common layout
VkDescriptorSet set = context->currentFrame->_frameDescriptors.allocate(
context->getDevice()->device(), context->getDescriptorLayouts()->gpuSceneDataLayout());
// 3) Write the buffer binding
DescriptorWriter writer;
writer.write_buffer(0, ubuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(context->getDevice()->device(), set);
```
### Defining a Custom Layout
```c++
DescriptorLayoutBuilder lb;
lb.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
lb.add_binding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
VkDescriptorSetLayout myLayout = lb.build(device, VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT);
// ... remember to vkDestroyDescriptorSetLayout(device, myLayout, nullptr) in cleanup
```
### Global Growable Allocator
For long-lived sets (e.g., materials, compute instances), use `EngineContext::descriptors` which wraps a growable allocator shared across modules.
```c++
VkDescriptorSet persistent = context->getDescriptors()->allocate(device, myLayout);
DescriptorWriter writer;
writer.write_image(0, albedoView, sampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(device, persistent);
// Freeing: call `context->getDescriptors()->destroy_pools(device)` at engine shutdown; sets die with their pools
```
### Compute Integration
`ComputeManager` uses an internal `DescriptorAllocatorGrowable` and offers higher-level bindings:
- Build pipeline: specify `descriptorTypes` and `pushConstantSize` in `ComputePipelineCreateInfo`.
- Ad-hoc dispatch: fill `ComputeDispatchInfo.bindings` with `ComputeBinding::{uniformBuffer, storageBuffer, sampledImage, storeImage}`.
- Persistent instances: create via `createInstance()` and set bindings with `setInstance*()`; descriptor sets are auto-updated and reused across dispatches.
See `PipelineManager.md` for a full compute quick start using the unified API.
### API Summary
- `DescriptorLayoutBuilder`: `add_binding(binding, type)`, `build(device, stages[, pNext, flags])`, `clear()`.
- `DescriptorWriter`: `write_buffer(binding, buffer, size, offset, type)`, `write_image(binding, view, sampler, layout, type)`, `update_set(device, set)`, `clear()`.
- `DescriptorAllocatorGrowable`: `init(device, initialSets, ratios)`, `allocate(device, layout[, pNext])`, `clear_pools(device)`, `destroy_pools(device)`.
- `DescriptorManager`: `gpuSceneDataLayout()`, `singleImageLayout()`.
### Best Practices
- Use per-frame allocator (`currentFrame->_frameDescriptors`) for transient sets to avoid lifetime pitfalls.
- Keep `DescriptorLayoutBuilder` small and local; free custom layouts in your pass/module `cleanup()`.
- Tune pool ratios to match workload; see how frames are initialized in `VulkanEngine::init_frame_resources()`.
- For persistent compute resources, prefer `ComputeManager` instances over manual descriptor lifecycle.

90
docs/EngineContext.md Normal file
View File

@@ -0,0 +1,90 @@
## Engine Context: Access to Managers + PerFrame State
Central DI-style handle that modules use to access device/managers, per-frame state, and convenience data without depending directly on `VulkanEngine`.
### Overview
- Ownership: Holds shared owners for `DeviceManager`, `ResourceManager`, and a growable `DescriptorAllocatorGrowable` used across modules.
- Global managers: Non-owning pointers to `SwapchainManager`, `DescriptorManager` (prebuilt layouts), `SamplerManager`, and `SceneManager`.
- Per-frame state: `currentFrame` (command buffer, per-frame descriptor pool, deletion queue), `stats`, and `drawExtent`.
- Subsystems: `compute` (`ComputeManager`) and `pipelines` (`PipelineManager`) exposed for unified graphics/compute API.
- Window + content: `window` (SDL handle) and convenience meshes (`cubeMesh`, `sphereMesh`).
Context is wired in `VulkanEngine::init()` and refreshed each frame before passes execute.
### Render Graph Note
- Builtin passes no longer call `vkCmdBeginRendering` or perform image layout transitions directly.
- Use your pass `register_graph(graph, ...)` to declare attachments and resource accesses; the Render Graph inserts barriers and begins/ends dynamic rendering.
- See `docs/RenderGraph.md` for the builder API and scheduling.
### Quick Start — In a Render Pass (essentials)
```c++
void MyPass::init(EngineContext* context) {
_context = context;
// Use common descriptor layouts provided by DescriptorManager
VkDescriptorSetLayout sceneLayout = _context->getDescriptorLayouts()->gpuSceneDataLayout();
// Build a pipeline via PipelineManager (re-fetch on draw for hot reload)
GraphicsPipelineCreateInfo info{};
info.vertexShaderPath = "../shaders/fullscreen.vert.spv";
info.fragmentShaderPath = "../shaders/my_pass.frag.spv";
info.setLayouts = { sceneLayout };
info.configure = [this](PipelineBuilder& b){
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.disable_depthtest();
b.set_color_attachment_format(_context->getSwapchain()->drawImage().imageFormat);
};
_context->pipelines->createGraphicsPipeline("my_pass", info);
}
void MyPass::execute(VkCommandBuffer cmd) {
// Fetch latest pipeline in case of hot reload
VkPipeline p{}; VkPipelineLayout l{};
_context->pipelines->getGraphics("my_pass", p, l);
// Per-frame uniform buffer via currentFrame allocator
AllocatedBuffer ubuf = _context->getResources()->create_buffer(
sizeof(GPUSceneData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
_context->currentFrame->_deletionQueue.push_function([=, this]{ _context->getResources()->destroy_buffer(ubuf); });
VmaAllocationInfo ai{}; vmaGetAllocationInfo(_context->getDevice()->allocator(), ubuf.allocation, &ai);
*static_cast<GPUSceneData*>(ai.pMappedData) = _context->getSceneData();
VkDescriptorSet set = _context->currentFrame->_frameDescriptors.allocate(
_context->getDevice()->device(), _context->getDescriptorLayouts()->gpuSceneDataLayout());
DescriptorWriter writer;
writer.write_buffer(0, ubuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(_context->getDevice()->device(), set);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, l, 0, 1, &set, 0, nullptr);
// Viewport/scissor from context draw extent
VkViewport vp{0,0,(float)_context->getDrawExtent().width,(float)_context->getDrawExtent().height,0.f,1.f};
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D sc{{0,0},{_context->getDrawExtent().width,_context->getDrawExtent().height}};
vkCmdSetScissor(cmd, 0, 1, &sc);
vkCmdDraw(cmd, 3, 1, 0, 0);
}
```
### Life Cycle
- Init: `VulkanEngine` constructs managers, initializes `_context`, then assigns `pipelines`, `compute`, `scene`, etc.
- Per-frame: Engine sets `currentFrame` and `drawExtent`, optionally triggers `PipelineManager::hotReloadChanged()`.
- Cleanup: Managers own their resources; modules should free layouts/sets they create and push per-frame deletions to `currentFrame->_deletionQueue`.
### Best Practices
- Prefer `EngineContext` accessors (`getDevice()`, `getResources()`, `getSwapchain()`) for clarity and testability.
- Re-fetch pipeline/layout by key every frame if using hot reload.
- Use `currentFrame->_frameDescriptors` for transient sets; use `context->descriptors` for longer-lived sets.
- Push resource cleanup to the frame or pass deletion queues to match lifetime with usage.

141
docs/PipelineManager.md Normal file
View File

@@ -0,0 +1,141 @@
## Pipeline Manager: Graphics + Compute
Centralizes pipeline creation and access with a clean, uniform API. Avoids duplication, enables hot reload (graphics), and makes passes/materials simpler.
### Overview
- Graphics pipelines: Owned by `PipelineManager` with per-name registry, hot-reloaded when shaders change.
- Compute pipelines: Created through `PipelineManager` but executed by `ComputeManager` under the hood.
- Access from anywhere via `EngineContext` (`context->pipelines`).
### Quick Start — Graphics
```c++
// In pass/material init
GraphicsPipelineCreateInfo info{};
info.vertexShaderPath = "../shaders/mesh.vert.spv";
info.fragmentShaderPath = "../shaders/mesh.frag.spv";
info.setLayouts = { context->getDescriptorLayouts()->gpuSceneDataLayout(), materialLayout };
info.pushConstants = { VkPushConstantRange{ VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants) } };
info.configure = [context](PipelineBuilder& b){
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.disable_blending();
b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);
b.set_color_attachment_format(context->getSwapchain()->drawImage().imageFormat);
b.set_depth_format(context->getSwapchain()->depthImage().imageFormat);
};
context->pipelines->createGraphicsPipeline("mesh.opaque", info);
// Fetch for binding
MaterialPipeline mp{};
context->pipelines->getMaterialPipeline("mesh.opaque", mp);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, mp.pipeline);
// ... bind sets using mp.layout
```
Notes:
- Graphics hot-reload runs each frame. If you cache pipeline handles, re-fetch with `getGraphics()` before use to pick up changes.
### Quick Start — Compute
Define and create a compute pipeline through the same manager:
```c++
ComputePipelineCreateInfo c{};
c.shaderPath = "../shaders/blur.comp.spv";
c.descriptorTypes = {
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, // out image
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER // in image
};
c.pushConstantSize = sizeof(MyBlurPC);
context->pipelines->createComputePipeline("blur", c);
```
Dispatch it when needed:
```c++
ComputeDispatchInfo di = ComputeManager::createDispatch2D(width, height, 16, 16);
di.bindings.push_back(ComputeBinding::storeImage(0, outView));
di.bindings.push_back(ComputeBinding::sampledImage(1, inView, context->getSamplers()->defaultLinear()));
di.pushConstants = &pc; // of type MyBlurPC
di.pushConstantSize = sizeof(MyBlurPC);
// Insert barriers as needed (optional)
// di.imageBarriers.push_back(...);
context->pipelines->dispatchCompute(cmd, "blur", di);
```
Tips:
- Use `dispatchComputeImmediate("name", di)` for one-off operations via an internal immediate command buffer.
- For complex synchronization, populate `memoryBarriers`, `bufferBarriers`, and `imageBarriers` in `ComputeDispatchInfo`.
- Compute pipelines are not hot-reloaded yet. If needed, re-create via `createComputePipeline(...)` and re-dispatch.
### When to Create vs. Use
- Create pipelines in pass/material `init()` and keep only the string keys around if you rely on hot reload.
- Re-fetch handles right before binding each frame to pick up changes:
```c++
VkPipeline p; VkPipelineLayout l;
if (context->pipelines->getGraphics("mesh.opaque", p, l)) { /* bind & draw */ }
```
### API Summary
- Graphics
- `createGraphicsPipeline(name, GraphicsPipelineCreateInfo)`
- `getGraphics(name, VkPipeline&, VkPipelineLayout&)`
- `getMaterialPipeline(name, MaterialPipeline&)`
- Hot reload: `hotReloadChanged()` is called by the engine each frame.
- Compute
- `createComputePipeline(name, ComputePipelineCreateInfo)`
- `destroyComputePipeline(name)` / `hasComputePipeline(name)`
- `dispatchCompute(cmd, name, ComputeDispatchInfo)`
- `dispatchComputeImmediate(name, ComputeDispatchInfo)`
### Persistent Compute Resources (Instances)
For long-lived compute workloads, create a compute instance that owns its descriptor set and (optionally) its resources.
```c++
// 1) Ensure the pipeline exists
ComputePipelineCreateInfo c{}; c.shaderPath = "../shaders/work.comp.spv"; c.descriptorTypes = { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE };
context->pipelines->createComputePipeline("work", c);
// 2) Create an instance bound to the pipeline
// You can go either via ComputeManager or PipelineManager
context->pipelines->createComputeInstance("work.main", "work");
// 3) Allocate persistent resources and bind to instance
auto img = context->pipelines->createAndBindComputeStorageImage("work.main", 0,
VkExtent3D{width, height, 1}, VK_FORMAT_R8G8B8A8_UNORM);
// 4) Optionally add more bindings (buffers, sampled images, etc.)
auto buf = context->pipelines->createAndBindComputeStorageBuffer("work.main", 1, size);
// or reference external resources
context->pipelines->setComputeInstanceStorageImage("work.main", 2, someView);
// 5) Update and dispatch repeatedly (bindings persist)
ComputeDispatchInfo di = ComputeManager::createDispatch2D(width, height);
di.pushConstants = &myPC; di.pushConstantSize = sizeof(myPC);
context->pipelines->dispatchComputeInstance(cmd, "work.main", di);
// 6) Destroy when no longer needed
context->pipelines->destroyComputeInstance("work.main");
```
Notes:
- Instances keep their descriptor set and binding specification; you can modify bindings via `setInstance*` and call `dispatchInstance()` without respecifying them each frame.
- Owned images/buffers created via `createAndBind*` are automatically destroyed when the instance is destroyed or on engine cleanup.
- Descriptor sets are allocated from a growable pool and are freed when the compute manager is cleaned up.
### Best Practices
- Keep descriptor set layouts owned by the module that defines resource interfaces (e.g., material or pass). Pipelines/layouts created by the manager are managed by the manager.
- Prefer pipeline keys over cached handles to benefit from hot reload.
- Encapsulate fixed-function state in `GraphicsPipelineCreateInfo::configure` lambdas to keep pass code tidy.

127
docs/RenderGraph.md Normal file
View File

@@ -0,0 +1,127 @@
## Render Graph: PerFrame Scheduling, Barriers, and Dynamic Rendering
Lightweight render graph that builds a perframe DAG from pass declarations, computes the necessary resource barriers/layout transitions, and records passes with dynamic rendering when attachments are declared.
### Why
- Centralize synchronization and image layout transitions across passes.
- Make passes declarative: author declares reads/writes; the graph inserts barriers and begins/ends rendering.
- Keep existing pass classes (`IRenderPass`) while migrating execution to the graph.
### HighLevel Flow
- Engine creates the graph each frame and imports swapchain/GBuffer images: `src/core/vk_engine.cpp:303`.
- Each pass registers its work by calling `register_graph(graph, ...)` and declaring resources via a builder.
- The graph appends a present chain (copy HDR `drawImage` → swapchain, then transition to `PRESENT`), optionally inserting ImGui before present.
- `compile()` topologically sorts passes by data dependencies (read/write) and computes perpass barriers.
- `execute(cmd)` emits barriers, begins dynamic rendering if attachments were declared, calls the pass record lambda, and ends rendering.
### Core API
- `RenderGraph::add_pass(name, RGPassType type, BuildCallback build, RecordCallback record)`
- Declare image/buffer accesses and attachments inside `build` using `RGPassBuilder`.
- Do your actual rendering/copies in `record` using resolved Vulkan objects from `RGPassResources`.
- See: `src/render/rg_graph.h:36`, `src/render/rg_graph.cpp:51`.
- `RenderGraph::compile()` → builds ordering and perpass `Vk*MemoryBarrier2` lists. See `src/render/rg_graph.cpp:83`.
- `RenderGraph::execute(cmd)` → emits barriers and dynamic rendering begin/end. See `src/render/rg_graph.cpp:592`.
- Import helpers for engine images: `import_draw_image()`, `import_depth_image()`, `import_gbuffer_*()`, `import_swapchain_image(index)`. See `src/render/rg_graph.cpp:740`.
- Present chain: `add_present_chain(draw, swapchain, appendExtra)` inserts Copy→Present passes and lets you inject extra passes (e.g., ImGui) in between. See `src/render/rg_graph.cpp:705`.
### Declaring a Pass
Use `register_graph(...)` on your pass to declare resources and record work. The graph handles transitions and dynamic rendering.
```c++
void MyPass::register_graph(RenderGraph* graph,
RGImageHandle draw,
RGImageHandle depth) {
graph->add_pass(
"MyPass",
RGPassType::Graphics,
// Build: declare resources + attachments
[draw, depth](RGPassBuilder& b, EngineContext*) {
b.read(draw, RGImageUsage::SampledFragment); // example read
b.write_color(draw); // render target
b.write_depth(depth, /*clear*/ false); // depth test
},
// Record: issue Vulkan commands (no begin/end rendering needed)
[this, draw](VkCommandBuffer cmd, const RGPassResources& res, EngineContext* ctx) {
VkPipeline p{}; VkPipelineLayout l{};
ctx->pipelines->getGraphics("my_pass", p, l); // hotreload safe
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
VkViewport vp{0,0,(float)ctx->getDrawExtent().width,(float)ctx->getDrawExtent().height,0,1};
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D sc{{0,0}, ctx->getDrawExtent()};
vkCmdSetScissor(cmd, 0, 1, &sc);
vkCmdDraw(cmd, 3, 1, 0, 0);
}
);
}
```
### Builder Reference (`RGPassBuilder`)
- Images
- `read(RGImageHandle, RGImageUsage)` → sample/read usage for this pass.
- `write(RGImageHandle, RGImageUsage)` → write usage (compute/storage/transfer).
- `write_color(RGImageHandle, bool clearOnLoad=false, VkClearValue clear={})` → declares a color attachment.
- `write_depth(RGImageHandle, bool clearOnLoad=false, VkClearValue clear={})` → declares a depth attachment.
- Buffers
- `read_buffer(RGBufferHandle, RGBufferUsage)` / `write_buffer(RGBufferHandle, RGBufferUsage)`.
- Convenience import: `read_buffer(VkBuffer, RGBufferUsage, size, name)` and `write_buffer(VkBuffer, ...)` dedup by raw handle.
See `src/render/rg_builder.h:39` and impl in `src/render/rg_builder.cpp:20`.
### Resource Model (`RGResourceRegistry`)
- Imported vs transient resources are tracked uniformly with lifetime indices (`firstUse/lastUse`).
- Imports are deduplicated by `VkImage`/`VkBuffer` and keep initial layout/stage/access as the starting state.
- Transients are created via `ResourceManager` and autodestroyed at end of frame using the frame deletion queue.
- See `src/render/rg_resources.h:11` and `src/render/rg_resources.cpp:1`.
### Synchronization and Layouts
- For each pass, `compile()` compares previous state with desired usage and, if needed, adds a prepass barrier:
- Images: `VkImageMemoryBarrier2` with stage/access/layout derived from `RGImageUsage`.
- Buffers: `VkBufferMemoryBarrier2` with stage/access derived from `RGBufferUsage`.
- Initial state comes from the imported descriptor; if unknown, buffers default to `TOP_OF_PIPE`.
- Format/usage checks:
- Warns if binding a depth format as color (and viceversa).
- Warns if a transient resource is used with flags it wasnt created with.
Image usage → layout/stage examples (subset):
- `SampledFragment` → `SHADER_READ_ONLY_OPTIMAL`, `FRAGMENT_SHADER`.
- `ColorAttachment` → `COLOR_ATTACHMENT_OPTIMAL`, `COLOR_ATTACHMENT_OUTPUT` (read|write).
- `DepthAttachment` → `DEPTH_ATTACHMENT_OPTIMAL`, `EARLY|LATE_FRAGMENT_TESTS`.
- `TransferDst` → `TRANSFER_DST_OPTIMAL`, `TRANSFER`.
- `Present` → `PRESENT_SRC_KHR`, `BOTTOM_OF_PIPE`.
Buffer usage → stage/access examples:
- `IndexRead` → `INDEX_INPUT`, `INDEX_READ`.
- `VertexRead` → `VERTEX_INPUT`, `VERTEX_ATTRIBUTE_READ`.
- `UniformRead` → `ALL_GRAPHICS|COMPUTE`, `UNIFORM_READ`.
- `StorageReadWrite` → `COMPUTE|FRAGMENT`, `SHADER_STORAGE_READ|WRITE`.
### BuiltIn Pass Wiring (Current)
- Resource uploads (if any) → Background (compute) → Geometry (GBuffer) → Lighting (deferred) → Transparent → CopyToSwapchain → ImGui → PreparePresent.
- See registrations: `src/core/vk_engine.cpp:321``src/core/vk_engine.cpp:352`.
### Notes & Limits
- No aliasing or transient pooling yet; images created via `create_*` are released endofframe.
- Graph scheduling uses a topological order by data dependency; it does not parallelize across queues.
- Load/store control for attachments is minimal (`clearOnLoad`, `store` on `RGAttachmentInfo`).
- Render area is the min of all declared attachment extents and `EngineContext::drawExtent`.
### Debugging
- Each pass is wrapped with a debug label (`RG: <name>`).
- Compile prints warnings for suspicious usages or format mismatches.

90
docs/RenderPasses.md Normal file
View File

@@ -0,0 +1,90 @@
## Render Passes: Background → Geometry → Lighting → Transparent → ImGui
Pass classes (`IRenderPass`) define initialization and recording logic, but execution is now driven by the Render Graph. Each pass exposes a `register_graph(...)` method to declare dependencies and render targets; the graph handles barriers, layouts, and dynamic rendering.
### Overview
- Interface: Each pass implements `IRenderPass { init(context); execute(cmd); cleanup(); getName(); }`. Today, `execute()` is unused for built-in passes; work is recorded via the Render Graph record callback.
- Manager: `RenderPassManager::init()` creates and stores built-in passes: `BackgroundPass` (compute), `GeometryPass` (G-Buffer), `LightingPass` (deferred), `TransparentPass`, plus optional `ImGuiPass`.
- Render graph: Passes call `register_graph(graph, ...)` to declare image/buffer access and attachments. The graph inserts barriers and begins/ends dynamic rendering.
- Shared targets: Passes coordinate through `SwapchainManager` images: `drawImage`, `gBufferPosition/Normal/Albedo`, `depthImage` (imported into the graph each frame).
- Hot reload: Fetch graphics pipeline/layout by key each frame through `PipelineManager` in the record callback.
### Quick Start — Add a New Pass (Render Graph)
```c++
class MyPass : public IRenderPass {
public:
void init(EngineContext* ctx) override {
_ctx = ctx;
GraphicsPipelineCreateInfo info{};
info.vertexShaderPath = _ctx->getAssets()->shaderPath("fullscreen.vert.spv");
info.fragmentShaderPath = _ctx->getAssets()->shaderPath("my_pass.frag.spv");
info.setLayouts = { _ctx->getDescriptorLayouts()->gpuSceneDataLayout() };
info.configure = [this](PipelineBuilder& b){
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none(); b.disable_depthtest();
b.set_color_attachment_format(_ctx->getSwapchain()->drawImage().imageFormat);
};
_ctx->pipelines->createGraphicsPipeline("my_pass", info);
}
void register_graph(RenderGraph* graph, RGImageHandle draw, RGImageHandle depth) {
graph->add_pass(
"MyPass",
RGPassType::Graphics,
[draw, depth](RGPassBuilder& b, EngineContext*) {
b.write_color(draw);
b.write_depth(depth, false);
},
[this](VkCommandBuffer cmd, const RGPassResources&, EngineContext* ctx){
VkPipeline p{}; VkPipelineLayout l{};
ctx->pipelines->getGraphics("my_pass", p, l);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
VkViewport vp{0,0,(float)ctx->getDrawExtent().width,(float)ctx->getDrawExtent().height,0,1};
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D sc{{0,0}, ctx->getDrawExtent()};
vkCmdSetScissor(cmd, 0, 1, &sc);
vkCmdDraw(cmd, 3, 1, 0, 0);
}
);
}
void execute(VkCommandBuffer) override {} // unused with Render Graph
void cleanup() override {}
const char* getName() const override { return "MyPass"; }
private:
EngineContext* _ctx{};
};
// Register in RenderPassManager::init()
auto myPass = std::make_unique<MyPass>();
myPass->init(context);
addPass(std::move(myPass));
```
### Built-in Passes
- Background (compute): Declares `ComputeWrite(drawImage)` and dispatches a selected effect instance.
- Geometry (G-Buffer): Declares 3 color attachments and `DepthAttachment`, plus buffer reads for shared index/vertex buffers.
- Lighting (deferred): Reads GBuffer as sampled images and writes to `drawImage`.
- Transparent (forward): Writes to `drawImage` with depth test against `depthImage` after lighting.
- ImGui: Inserted just before present to draw on the swapchain image.
### API Summary
- `RenderPassManager::addPass(unique_ptr<IRenderPass>)`: Register a new pass (storage/ownership only).
- `RenderPassManager::setImGuiPass(...)`: Configure the optional ImGui pass.
- `IRenderPass::register_graph(...)` (per pass class): Declare resources and recording callbacks for the Render Graph.
### Tips
- Dont call `vkCmdBeginRendering` or add manual transitions for declared attachments; the Render Graph handles it.
- Re-fetch pipeline and layout by key each frame to pick up hot-reloaded shaders.
- Allocate transient descriptor sets from `currentFrame->_frameDescriptors`; free pass-owned layouts in `cleanup()`.
- Use `EngineContext::getDrawExtent()` for viewport/scissor.
See also: `docs/RenderGraph.md` for the builder API and synchronization details.

49
docs/ResourceManager.md Normal file
View File

@@ -0,0 +1,49 @@
## Resource Manager: Buffers, Images, Uploads, and Lifetime
Central allocator and uploader built on VMA. Provides creation helpers, an immediate submit path, and a deferred upload queue that is converted into a Render Graph transfer pass each frame.
### Responsibilities
- Create/destroy GPU buffers and images (with mapped memory for CPUtoGPU when requested).
- Stage and upload mesh/texture data either immediately or via a perframe deferred path.
- Integrate with `FrameResources` deletion queues to match lifetimes to the frame.
- Expose a Render Graph pass that batches all pending uploads.
### Key APIs (src/core/vk_resource.h)
- Creation
- `AllocatedBuffer create_buffer(size, usage, memUsage)`
- `AllocatedImage create_image(extent3D, format, usage[, mipmapped])`
- `AllocatedImage create_image(data, extent3D, format, usage[, mipmapped])`
- Destroy with `destroy_buffer`, `destroy_image`.
- Uploads
- Deferred mode toggle: `set_deferred_uploads(bool)`; when true, mesh/texture uploads enqueue staging work.
- Query queues: `pending_buffer_uploads()`, `pending_image_uploads()`; clear via `clear_pending_uploads()`.
- Immediate path: `process_queued_uploads_immediate()` or `immediate_submit(lambda)` for custom commands.
- Render Graph integration: `register_upload_pass(RenderGraph&, FrameResources&)` builds a single `Transfer` pass that:
- Imports staging buffers and destination resources into the graph.
- Inserts the appropriate `TransferSrc/Dst` declarations.
- Records `vkCmdCopyBuffer` / `vkCmdCopyBufferToImage` and optional mip generation.
- Schedules deletion of staging buffers at end of frame.
- Mesh upload convenience
- `GPUMeshBuffers uploadMesh(span<uint32_t> indices, span<Vertex> vertices)` — returns device buffers and device address.
### PerFrame Lifetime
- `FrameResources::_deletionQueue` owns perframe cleanups for transient buffers/images created during rendering passes.
- The upload pass registers cleanups for staging buffers on the frame queue.
### Render Graph Interaction
- `register_upload_pass` is called during frame build before other passes (see `src/core/vk_engine.cpp:315`).
- It uses graph `import_buffer` / `import_image` to deduplicate external resources and attach initial stage/layout.
- Barriers and final layouts for uploaded images are handled in the pass recording (`generate_mipmaps` path transitions to `SHADER_READ_ONLY_OPTIMAL`).
### Guidelines
- Prefer deferred uploads (`set_deferred_uploads(true)`) for framecoherent synchronization under the Render Graph.
- For tooling and oneoff setup, use `immediate_submit(lambda)` to avoid perframe queuing.
- When creating transient images/buffers used only inside a pass, prefer the Render Graphs `create_*` so destruction is automatic at frame end.

54
docs/Scene.md Normal file
View File

@@ -0,0 +1,54 @@
## Scene System: Cameras, DrawContext, and Instances
Thin scene layer that produces `RenderObject`s for the renderer. It gathers opaque/transparent surfaces, maintains the main camera, and exposes simple runtime instance APIs.
### Components
- `SceneManager` (src/scene/vk_scene.h/.cpp)
- Owns the main `Camera`, `GPUSceneData`, and `DrawContext`.
- Loads GLTF scenes via `AssetManager`/`LoadedGLTF` and creates dynamic mesh/GLTF instances.
- Updates perframe transforms, camera, and `GPUSceneData` (`view/proj/viewproj`, sun/ambient).
- `DrawContext`
- Two lists: `OpaqueSurfaces` and `TransparentSurfaces` of `RenderObject`.
- Populated by scene graph traversal and dynamic instances each frame.
- `RenderObject`
- Geometry: `indexBuffer`, `vertexBuffer` (for RG tracking), `vertexBufferAddress` (device address used by shaders).
- Material: `MaterialInstance* material` with bound set and pipeline.
- Transform and bounds for optional culling.
### Frame Flow
1. `SceneManager::update_scene()` clears the draw lists and rebuilds them by drawing all active scene/instance nodes.
2. Renderer consumes the lists:
- Geometry pass sorts opaque by material and index buffer to improve locality.
- Transparent pass sorts backtofront against camera and blends to the HDR target.
3. Uniforms: Passes allocate a small perframe UBO (`GPUSceneData`) and bind it via a shared layout.
### Sorting / Culling
- Opaque (geometry): stable sort by `material` then `indexBuffer` (see `src/render/vk_renderpass_geometry.cpp`).
- Transparent: sort by cameraspace depth far→near (see `src/render/vk_renderpass_transparent.cpp`).
- An example frustum test exists in `vk_renderpass_geometry.cpp` (`is_visible`) and can be enabled to cull meshes.
### Dynamic Instances
- Mesh instances
- `addMeshInstance(name, mesh, transform)`, `removeMeshInstance(name)`, `clearMeshInstances()`.
- Useful for spawning primitives or asset meshes at runtime.
- GLTF instances
- `addGLTFInstance(name, LoadedGLTF, transform)`, `removeGLTFInstance(name)`, `clearGLTFInstances()`.
### GPU Scene Data
- `GPUSceneData` carries camera matrices and lighting constants for the frame.
- Passes map and fill it into a perframe UBO, bindable with `DescriptorManager::gpuSceneDataLayout()`.
### Tips
- Treat `DrawContext` as immutable during rendering; build it fully in `update_scene()`.
- Keep `RenderObject` small; use device addresses for vertex data to avoid perdraw vertex buffer binds.
- For custom sorting/culling, modify only the scene layer; render passes stay simple.

159
docs/asset_manager.md Normal file
View File

@@ -0,0 +1,159 @@
## Asset Manager
Centralized asset path resolution, glTF loading, and runtime mesh creation (including simple materials and primitives). Avoids scattered relative paths and duplicates by resolving roots at runtime and caching results.
### Path Resolution
- Environment root: Honors `VKG_ASSET_ROOT` (expected to contain `assets/` and/or `shaders/`).
- Upward search: If unset, searches upward from the current directory for folders named `assets` and `shaders`.
- Fallbacks: Tries `./assets`, `../assets` and `./shaders`, `../shaders`.
- Methods: `shaderPath(name)`, `assetPath(name)`, and `modelPath(name)` (alias of `assetPath`). Relative or absolute input is returned if already valid; otherwise resolution is attempted as above.
Access the manager anywhere via `EngineContext`:
```c++
auto *assets = context->getAssets();
auto spv = assets->shaderPath("mesh.vert.spv");
auto chairPath = assets->modelPath("models/chair.glb");
```
### API Summary
- Paths
- `std::string shaderPath(std::string_view)`
- `std::string assetPath(std::string_view)` / `modelPath(std::string_view)`
- glTF
- `std::optional<std::shared_ptr<LoadedGLTF>> loadGLTF(std::string_view nameOrPath)` — cached by canonical absolute path
- Meshes
- `std::shared_ptr<MeshAsset> createMesh(const MeshCreateInfo &info)`
- `std::shared_ptr<MeshAsset> createMesh(const std::string &name, std::span<Vertex> v, std::span<uint32_t> i, std::shared_ptr<GLTFMaterial> material = {})`
- `std::shared_ptr<MeshAsset> getMesh(const std::string &name) const`
- `std::shared_ptr<MeshAsset> getPrimitive(std::string_view name) const` (returns existing default primitives if created)
- `bool removeMesh(const std::string &name)`
- `void cleanup()` — releases meshes, material buffers, and any images owned by the manager
### Mesh Creation Model
Use either the convenience descriptor (`MeshCreateInfo`) or the direct overload with vertex/index spans.
```c++
struct AssetManager::MaterialOptions {
std::string albedoPath; // resolved through AssetManager
std::string metalRoughPath; // resolved through AssetManager
bool albedoSRGB = true; // VK_FORMAT_R8G8B8A8_SRGB when true
bool metalRoughSRGB = false; // VK_FORMAT_R8G8B8A8_UNORM when false
GLTFMetallic_Roughness::MaterialConstants constants{};
MaterialPass pass = MaterialPass::MainColor; // or Transparent
};
struct AssetManager::MeshGeometryDesc {
enum class Type { Provided, Cube, Sphere };
Type type = Type::Provided;
std::span<Vertex> vertices{}; // when Provided
std::span<uint32_t> indices{}; // when Provided
int sectors = 16; // for Sphere
int stacks = 16; // for Sphere
};
struct AssetManager::MeshMaterialDesc {
enum class Kind { Default, Textured };
Kind kind = Kind::Default;
MaterialOptions options{}; // used when Textured
};
struct AssetManager::MeshCreateInfo {
std::string name; // cache key; reused if already created
MeshGeometryDesc geometry; // Provided / Cube / Sphere
MeshMaterialDesc material; // Default or Textured
};
```
Behavior and lifetime:
- Default material: If no material is given, a white material is created (2× white textures, per-mesh UBO with sane defaults).
- Textured material: When `MeshMaterialDesc::Textured`, images are loaded via `stb_image` and uploaded; per-mesh UBO is allocated and filled from `constants`.
- Ownership: Material buffers and any images created by the AssetManager are tracked and destroyed on `removeMesh(name)` or `cleanup()`.
- Caching: Meshes are cached by `name`. Re-creating with the same name returns the existing mesh (no new uploads).
### Examples
Create a simple plane and render it (default material):
```c++
std::vector<Vertex> v = {
{{-0.5f, 0.0f, -0.5f}, 0.0f, {0,1,0}, 0.0f, {1,1,1,1}},
{{ 0.5f, 0.0f, -0.5f}, 1.0f, {0,1,0}, 0.0f, {1,1,1,1}},
{{-0.5f, 0.0f, 0.5f}, 0.0f, {0,1,0}, 1.0f, {1,1,1,1}},
{{ 0.5f, 0.0f, 0.5f}, 1.0f, {0,1,0}, 1.0f, {1,1,1,1}},
};
std::vector<uint32_t> i = { 0,1,2, 2,1,3 };
auto plane = ctx->getAssets()->createMesh("plane", v, i); // default white material
glm::mat4 xform = glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
ctx->scene->addMeshInstance("ground", plane, xform);
```
Generate primitives via `MeshCreateInfo`:
```c++
AssetManager::MeshCreateInfo ci{};
ci.name = "cubeA";
ci.geometry.type = AssetManager::MeshGeometryDesc::Type::Cube;
ci.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
auto cube = ctx->getAssets()->createMesh(ci);
ctx->scene->addMeshInstance("cube.instance", cube,
glm::translate(glm::mat4(1.f), glm::vec3(-2.f, 0.f, -2.f)));
AssetManager::MeshCreateInfo si{};
si.name = "sphere48x24";
si.geometry.type = AssetManager::MeshGeometryDesc::Type::Sphere;
si.geometry.sectors = 48; si.geometry.stacks = 24;
si.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
auto sphere = ctx->getAssets()->createMesh(si);
ctx->scene->addMeshInstance("sphere.instance", sphere,
glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f)));
```
Textured primitive (albedo + metal-rough):
```c++
AssetManager::MeshCreateInfo ti{};
ti.name = "ground.textured";
// provide vertices/indices for a plane (see first example)
ti.geometry.type = AssetManager::MeshGeometryDesc::Type::Provided;
ti.geometry.vertices = std::span<Vertex>(v.data(), v.size());
ti.geometry.indices = std::span<uint32_t>(i.data(), i.size());
ti.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured;
ti.material.options.albedoPath = "textures/ground_albedo.png"; // sRGB
ti.material.options.metalRoughPath = "textures/ground_mr.png"; // UNORM
// ti.material.options.pass = MaterialPass::Transparent; // optional
auto texturedPlane = ctx->getAssets()->createMesh(ti);
glm::mat4 tx = glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
ctx->scene->addMeshInstance("ground.textured", texturedPlane, tx);
```
Textured cube/sphere via options is analogous — set `geometry.type` to `Cube` or `Sphere` and fill `material.options`.
Runtime glTF spawning:
```c++
auto chair = ctx->getAssets()->loadGLTF("models/chair.glb");
if (chair)
{
glm::mat4 t = glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, -3.f));
ctx->scene->addGLTFInstance("chair01", *chair, t);
}
// Move / overwrite
ctx->scene->addGLTFInstance("chair01", *chair,
glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.5f, -3.f)));
// Remove
ctx->scene->removeGLTFInstance("chair01");
```
### Notes
- Default primitives: The engine creates default Cube/Sphere meshes via `AssetManager` and registers them as dynamic scene instances.
- Reuse by name: `createMesh("name", ...)` returns the cached mesh if it already exists. Use a unique name or call `removeMesh(name)` to replace.
- sRGB/UNORM: Albedo is sRGB by default, metal-rough is UNORM by default. Adjust via `MaterialOptions`.
- Hot reload: Shaders are resolved via `shaderPath()`; pipeline hot reload is handled by the pipeline manager, not the AssetManager.
- Normal maps: Not wired into the default GLTF PBR material in this branch. Adding them would require descriptor and shader updates.