-
Notifications
You must be signed in to change notification settings - Fork 0
Guides Scene
A small scene with multiple meshes, each with its own model matrix, sharing one pipeline. Plus a clean shutdown order that scales — five meshes or five hundred, the rule doesn't change.
- Guides · Shaders — pipeline + push constants + descriptors.
A renderer with one mesh hides the question of "who owns what". As soon as you add a second mesh sharing the same pipeline, you need a small struct that bundles geometry + transform + draw call. Call it SceneObject. Iterate over a std::vector<SceneObject> and the per-frame loop stays the same shape regardless of N.
#include "VCK.h"
using namespace VCK;
struct SceneObject {
VulkanMesh mesh; // owns the GPU buffers
Mat4 transform; // model matrix; updated by app code
};
std::vector<SceneObject> scene;VulkanMesh is move-only. std::vector over it works as long as you don't shrink-resize after Upload; build the scene up front, drive draws by index.
auto AddCube = [&](Vec3 pos, float size = 1.0f)
{
Primitives::Mesh cpu = Primitives::Cube(size);
struct V { Vec3 p; Vec3 n; };
static_assert(sizeof(V) == 24, "tightly packed");
std::vector<V> verts(cpu.positions.size());
for (std::size_t i = 0; i < verts.size(); ++i)
verts[i] = { cpu.positions[i], cpu.normals[i] };
SceneObject obj;
obj.transform = Translate(pos);
obj.mesh.Upload(device, command,
verts.data(),
static_cast<VkDeviceSize>(sizeof(V) * verts.size()),
static_cast<uint32_t>(verts.size()),
cpu.indices.data(),
static_cast<uint32_t>(cpu.indices.size()));
scene.push_back(std::move(obj));
};
AddCube({-2, 0, 0});
AddCube({ 0, 0, 0}, 1.5f);
AddCube({ 2, 0, 0});
AddCube({ 0, 0, -3}, 0.7f);Primitives::Cube returns CPU-side positions/normals/uvs/indices — VCK never owns the resulting Mesh (rule 22). Pack the streams the way your shader expects (here: interleaved {position, normal}), then upload.
VulkanMesh::Upload's second argument is the total byte count of the vertex buffer, not the per-vertex stride. Pass sizeof(V) * verts.size() — the example file calls this out because it's the obvious thing to get wrong.
void DrawScene(Frame& f)
{
vkCmdBindPipeline(f.PrimaryCmd(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.GetPipeline());
// Per-frame UBO (camera) is bound once; all objects share it.
VkDescriptorSet camSet = camera.GetSet(sync.GetCurrentFrameIndex());
vkCmdBindDescriptorSets(f.PrimaryCmd(), VK_PIPELINE_BIND_POINT_GRAPHICS,
pipeline.GetPipelineLayout(), 0, 1, &camSet, 0, nullptr);
for (SceneObject& obj : scene)
{
pc.Set("model", obj.transform);
pc.Apply(f.PrimaryCmd(), pipeline.GetPipelineLayout(), VK_SHADER_STAGE_VERTEX_BIT);
obj.mesh.RecordDraw(f.PrimaryCmd());
}
}One pipeline bind, one descriptor bind, N pushes + draws. Most renderers stay this shape until they need bucket sorting by material — at which point you sort scene by pipeline / texture and bind only when the bucket changes.
If a draw call fans out to many submission buffers (compute prepasses, async transfers), use f.Submissions().QueueGraphics(cb, info) for each and let the GpuSubmissionBatcher collapse them into one vkQueueSubmit.
This is the rule that breaks in messy codebases. State it explicitly (rule 3):
void Shutdown()
{
vkDeviceWaitIdle(device.GetDevice()); // exactly once, here
scheduler.Shutdown(); // borrows command + sync
for (SceneObject& obj : scene) obj.mesh.Shutdown();
scene.clear();
framebuffers.Shutdown();
// ... any other expansion objects (textures, depth, descriptor pools) ...
sync.Shutdown();
command.Shutdown();
pipeline.Shutdown();
swapchain.Shutdown();
device.Shutdown();
context.Shutdown();
window.Destroy();
}Strict reverse of init. vkDeviceWaitIdle is the only blocking wait that's correct outside of OneTimeCommand and the scheduler's own primitives — and only here, at shutdown (rule 4).
If you forget the order, validation will tell you which VkDevice was destroyed while still owning a VkBuffer. Don't silence those messages; they're correct.
- Guides · Flying Camera — drive the view matrix from yaw / pitch
- Cookbook — recipe 5 (instanced rendering), recipe 9 (material buckets)
- Home
- Quick Start
- Your First App
- Understanding cfg
- Build — Windows / Linux / macOS
Single source of truth for the full API surface is the doc block at the top of VCK.h.