fix(graphics): spec-compliant secondary CB recording with type-enforced ordering
fix(graphics): spec-compliant secondary CB recording with type-enforced ordering Validation layers exposed that the previous secondary command buffer recording violated Vulkan's command-buffer state machine: secondaries were begun without pInheritanceInfo and recorded their own render pass instances, which is illegal for RENDER_PASS_CONTINUE buffers and can hang/reset the GPU driver. Fix by making the legal ordering structural instead of calling-order dependent: - RecordingSecondary (move-only): recording state; only finish() can turn it into ReadySecondary; destructor asserts an abandoned recording. - ReadySecondary (move-only): only executeSecondary()/RenderPassExecutor can access its raw handle, so an unfinished buffer can never reach vkCmdExecuteCommands. - RenderPassExecutor (RAII): opens the primary's render pass (eSecondaryCommandBuffers), collects ReadySecondary, executes them and ends the pass on destruction. - beginSecondary() is the single begin site: inheritance info, subpass, framebuffer and RENDER_PASS_CONTINUE flags are centralized. - Shadow (3 cascades) and G-buffer passes record into secondaries on JobSystem workers, executed per cascade from the primary; the forward scene pass records into its own secondary via currentDrawCb() and is executed at endSceneColorRenderPass. - The frame slot's secondary buffers are pre-created on the render thread before jobs fork (command-pool allocation / vector growth must not race on workers). Verified: full win32-debug build + unit_test link. GPU rendering must be verified on CI (local submodule checkout of VKBuilder does not match the worktree's pinned version, so local GPU runs are not authoritative).
This commit is contained in:
1 parent
11f7b526a0
commit
1291df8c30
2 files changed
+258
-67
No files matched your search
@@ -555,9 +555,9 @@ vk::CommandBuffer &Graphics::currentPresentCb() {
|
||||
return presentRecording.commandBuffer();
|
||||
}
|
||||
|
||||
vk::CommandBuffer &Graphics::currentDrawCb() {
|
||||
if (sceneSecondaryActive && !frameSecondaryCbs.empty())
|
||||
return currentFrameSecondaryCbs().buffers[kForwardSecondary];
|
||||
vk::CommandBuffer Graphics::currentDrawCb() {
|
||||
if (sceneSecondaryActive && forwardRecorder_)
|
||||
return forwardRecorder_.cb();
|
||||
return currentPresentCb();
|
||||
}
|
||||
|
||||
@@ -579,6 +579,66 @@ Graphics::FrameSecondaryCbs &Graphics::currentFrameSecondaryCbs() {
|
||||
return slot;
|
||||
}
|
||||
|
||||
RecordingSecondary::~RecordingSecondary() {
|
||||
// An abandoned recording means the frame's command ordering broke; fail
|
||||
// loudly in debug builds instead of submitting a half-recorded buffer.
|
||||
ASSERT(static_cast<VkCommandBuffer>(cb_) == VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
ReadySecondary RecordingSecondary::finish() {
|
||||
ASSERT(static_cast<VkCommandBuffer>(cb_) != VK_NULL_HANDLE);
|
||||
vk::CommandBuffer done = cb_;
|
||||
cb_ = nullptr;
|
||||
done.end();
|
||||
return ReadySecondary(done);
|
||||
}
|
||||
|
||||
void executeSecondary(vk::CommandBuffer primary, const ReadySecondary &ready) {
|
||||
ASSERT(static_cast<VkCommandBuffer>(ready.cb_) != VK_NULL_HANDLE);
|
||||
primary.executeCommands(1, &ready.cb_);
|
||||
}
|
||||
|
||||
RenderPassExecutor::RenderPassExecutor(vk::CommandBuffer primary, vk::RenderPass renderPass,
|
||||
vk::Framebuffer framebuffer, vk::Rect2D area,
|
||||
const vk::ClearValue *clears, uint32_t clearCount)
|
||||
: primary_(primary) {
|
||||
vk::RenderPassBeginInfo rpBegin{};
|
||||
rpBegin.renderPass = renderPass;
|
||||
rpBegin.framebuffer = framebuffer;
|
||||
rpBegin.renderArea = area;
|
||||
rpBegin.clearValueCount = clearCount;
|
||||
rpBegin.pClearValues = clears;
|
||||
primary_.beginRenderPass(rpBegin, vk::SubpassContents::eSecondaryCommandBuffers);
|
||||
}
|
||||
|
||||
RenderPassExecutor::~RenderPassExecutor() {
|
||||
if (!secondaries_.empty())
|
||||
primary_.executeCommands(uint32_t(secondaries_.size()), secondaries_.data());
|
||||
primary_.endRenderPass();
|
||||
}
|
||||
|
||||
void RenderPassExecutor::add(ReadySecondary ready) {
|
||||
ASSERT(static_cast<VkCommandBuffer>(ready.cb_) != VK_NULL_HANDLE);
|
||||
secondaries_.push_back(ready.cb_);
|
||||
}
|
||||
|
||||
RecordingSecondary Graphics::beginSecondary(uint32_t index, vk::RenderPass renderPass,
|
||||
vk::Framebuffer framebuffer) {
|
||||
auto &slot = currentFrameSecondaryCbs();
|
||||
ASSERT(index < slot.buffers.size());
|
||||
vk::CommandBuffer secondary = slot.buffers[index];
|
||||
vk::CommandBufferInheritanceInfo inheritance{};
|
||||
inheritance.renderPass = renderPass;
|
||||
inheritance.subpass = 0;
|
||||
inheritance.framebuffer = framebuffer;
|
||||
vk::CommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit |
|
||||
vk::CommandBufferUsageFlagBits::eRenderPassContinue;
|
||||
beginInfo.pInheritanceInfo = &inheritance;
|
||||
secondary.begin(beginInfo);
|
||||
return RecordingSecondary(secondary);
|
||||
}
|
||||
|
||||
vkb::FrameSlot Graphics::frameToken() const {
|
||||
if (offscreen3DPassOpen) return vkb::FrameSlot{0};
|
||||
if (presentRecording) return presentRecording.slot();
|
||||
@@ -673,23 +733,10 @@ void Graphics::dropPendingOffscreenPasses() {
|
||||
gbufferPassDraws.clear();
|
||||
}
|
||||
|
||||
void Graphics::recordShadowCascadeSecondary(vk::CommandBuffer secondary, int cascade) {
|
||||
auto &slot = currentShadowMap();
|
||||
ReadySecondary Graphics::recordShadowCascadeSecondary(RecordingSecondary recorder, int cascade) {
|
||||
const uint32_t size = uint32_t(ShadowConfig::kMapSize);
|
||||
vk::CommandBuffer secondary = recorder.cb();
|
||||
|
||||
vk::CommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
|
||||
secondary.begin(beginInfo);
|
||||
|
||||
vk::ClearValue clear{};
|
||||
clear.depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
|
||||
vk::RenderPassBeginInfo rpBegin{};
|
||||
rpBegin.renderPass = shadowRenderPass;
|
||||
rpBegin.framebuffer = slot.framebuffers[cascade];
|
||||
rpBegin.renderArea = vk::Rect2D{{0, 0}, {size, size}};
|
||||
rpBegin.clearValueCount = 1;
|
||||
rpBegin.pClearValues = &clear;
|
||||
secondary.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
|
||||
setViewportAndScissor(secondary, size, size);
|
||||
secondary.bindPipeline(vk::PipelineBindPoint::eGraphics, shadowPipeline);
|
||||
bool alphaBound = false;
|
||||
@@ -715,31 +762,15 @@ void Graphics::recordShadowCascadeSecondary(vk::CommandBuffer secondary, int cas
|
||||
vk::ShaderStageFlagBits::eVertex, 0, sizeof(glm::mat4), &d.mvp);
|
||||
drawIndexedMesh(secondary, *gpuMesh);
|
||||
}
|
||||
secondary.endRenderPass();
|
||||
secondary.end();
|
||||
return recorder.finish();
|
||||
}
|
||||
|
||||
void Graphics::recordGBufferSecondary(vk::CommandBuffer secondary) {
|
||||
ReadySecondary Graphics::recordGBufferSecondary(RecordingSecondary recorder) {
|
||||
auto *slot = currentGBufferSlot();
|
||||
const uint32_t w = uint32_t(gbufferWidth);
|
||||
const uint32_t h = uint32_t(gbufferHeight);
|
||||
vk::CommandBuffer secondary = recorder.cb();
|
||||
|
||||
vk::CommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
|
||||
secondary.begin(beginInfo);
|
||||
|
||||
std::array<vk::ClearValue, 4> clears{};
|
||||
clears[0].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
|
||||
clears[1].color = vk::ClearColorValue(std::array<float, 4>{1, 1, 1, 1});
|
||||
clears[2].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
|
||||
clears[3].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
|
||||
vk::RenderPassBeginInfo rpBegin{};
|
||||
rpBegin.renderPass = gbufferRenderPass;
|
||||
rpBegin.framebuffer = slot->framebuffer;
|
||||
rpBegin.renderArea = vk::Rect2D{{0, 0}, {w, h}};
|
||||
rpBegin.clearValueCount = uint32_t(clears.size());
|
||||
rpBegin.pClearValues = clears.data();
|
||||
secondary.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
|
||||
setViewportAndScissor(secondary, w, h);
|
||||
secondary.bindPipeline(vk::PipelineBindPoint::eGraphics, gbufferPipeline);
|
||||
bool alphaBound = false;
|
||||
@@ -763,8 +794,7 @@ void Graphics::recordGBufferSecondary(vk::CommandBuffer secondary) {
|
||||
0, sizeof(GBufferPush), &d.push);
|
||||
drawIndexedMesh(secondary, *gpuMesh);
|
||||
}
|
||||
secondary.endRenderPass();
|
||||
secondary.end();
|
||||
return recorder.finish();
|
||||
}
|
||||
|
||||
void Graphics::recordDeferredPassesParallel() {
|
||||
@@ -784,40 +814,62 @@ void Graphics::recordDeferredPassesParallel() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &cbs = currentFrameSecondaryCbs();
|
||||
auto &cb = currentPresentCb();
|
||||
|
||||
// Record each deferred pass into its own secondary command buffer on the
|
||||
// JobSystem workers; the primary only chains them with vkCmdExecuteCommands.
|
||||
// JobSystem workers; each job records its own secondary buffer (one buffer
|
||||
// per pass), so beginSecondary()/finish() are never contended. The
|
||||
// ReadySecondary results are only readable by the render thread after
|
||||
// group->wait(), and only RenderPassExecutor can execute them.
|
||||
// The record jobs come from the per-frame arena, so recording allocates no
|
||||
// job control blocks per frame.
|
||||
auto *jobs = thread::Thread::create()->getJobSystem();
|
||||
jobs->beginFrame();
|
||||
// Pre-create this frame slot's secondary buffers on the render thread:
|
||||
// beginSecondary() runs on JobSystem workers, and allocating the command
|
||||
// pool / resizing the slot vector there would race.
|
||||
(void)currentFrameSecondaryCbs();
|
||||
thread::TaskGroup *group = jobs->createFrameTaskGroup();
|
||||
std::array<ReadySecondary, ShadowConfig::kCascades + 2> ready{};
|
||||
if (hasShadow) {
|
||||
for (int c = 0; c < ShadowConfig::kCascades; ++c) {
|
||||
if ((shadowPendingMask & (1u << c)) == 0) continue;
|
||||
const uint32_t idx = kShadowSecondaryBase + uint32_t(c);
|
||||
group->fork([this, &cbs, idx, c] {
|
||||
recordShadowCascadeSecondary(cbs.buffers[idx], c);
|
||||
group->fork([this, idx, c, &ready] {
|
||||
RecordingSecondary recorder =
|
||||
beginSecondary(idx, vk::RenderPass(shadowRenderPass),
|
||||
currentShadowMap().framebuffers[c]);
|
||||
ready[idx] = recordShadowCascadeSecondary(std::move(recorder), c);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hasGBuffer) {
|
||||
group->fork([this, &cbs] { recordGBufferSecondary(cbs.buffers[kGBufferSecondary]); });
|
||||
group->fork([this, &ready] {
|
||||
RecordingSecondary recorder =
|
||||
beginSecondary(kGBufferSecondary, vk::RenderPass(gbufferRenderPass),
|
||||
currentGBufferSlot()->framebuffer);
|
||||
ready[kGBufferSecondary] = recordGBufferSecondary(std::move(recorder));
|
||||
});
|
||||
}
|
||||
group->wait();
|
||||
jobs->endFrame(); // arena-owned group; do not delete
|
||||
|
||||
// Layout transitions stay on the render thread (image layout tracking is
|
||||
// not thread-safe); the secondaries only contain the render-pass instances.
|
||||
// not thread-safe); RenderPassExecutor opens each render pass, executes the
|
||||
// ready secondaries and closes it — one render pass instance per cascade.
|
||||
if (hasShadow) {
|
||||
auto &slot = currentShadowMap();
|
||||
slot.image.beginDepthAttachment();
|
||||
const uint32_t size = uint32_t(ShadowConfig::kMapSize);
|
||||
vk::ClearValue clear{};
|
||||
clear.depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
|
||||
for (int c = 0; c < ShadowConfig::kCascades; ++c) {
|
||||
if ((shadowPendingMask & (1u << c)) == 0) continue;
|
||||
const uint32_t idx = kShadowSecondaryBase + uint32_t(c);
|
||||
cb.executeCommands(1, &cbs.buffers[idx]);
|
||||
RenderPassExecutor pass(cb, vk::RenderPass(shadowRenderPass),
|
||||
slot.framebuffers[c],
|
||||
vk::Rect2D{{0, 0}, {size, size}}, &clear, 1);
|
||||
if (ready[kShadowSecondaryBase + uint32_t(c)])
|
||||
pass.add(std::move(ready[kShadowSecondaryBase + uint32_t(c)]));
|
||||
}
|
||||
slot.image.endSampledLayout();
|
||||
shadowPendingMask = 0;
|
||||
@@ -829,7 +881,18 @@ void Graphics::recordDeferredPassesParallel() {
|
||||
slot->depthColor.beginColorAttachment();
|
||||
slot->albedo.beginColorAttachment();
|
||||
slot->depth.beginDepthAttachment();
|
||||
cb.executeCommands(1, &cbs.buffers[kGBufferSecondary]);
|
||||
const uint32_t w = uint32_t(gbufferWidth);
|
||||
const uint32_t h = uint32_t(gbufferHeight);
|
||||
std::array<vk::ClearValue, 4> clears{};
|
||||
clears[0].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
|
||||
clears[1].color = vk::ClearColorValue(std::array<float, 4>{1, 1, 1, 1});
|
||||
clears[2].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
|
||||
clears[3].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
|
||||
RenderPassExecutor pass(cb, vk::RenderPass(gbufferRenderPass), slot->framebuffer,
|
||||
vk::Rect2D{{0, 0}, {w, h}}, clears.data(),
|
||||
uint32_t(clears.size()));
|
||||
if (ready[kGBufferSecondary])
|
||||
pass.add(std::move(ready[kGBufferSecondary]));
|
||||
slot->normal.endSampledLayout();
|
||||
slot->depthColor.endSampledLayout();
|
||||
slot->albedo.endSampledLayout();
|
||||
@@ -1725,10 +1788,7 @@ void Graphics::createSceneColorResources(int width, int height) {
|
||||
bool Graphics::beginSceneColorRenderPass() {
|
||||
auto *slot = currentSceneColorSlot();
|
||||
if (!slot || !sceneColorRenderPass || !slot->framebuffer) return false;
|
||||
auto &secondary = currentFrameSecondaryCbs().buffers[kForwardSecondary];
|
||||
vk::CommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
|
||||
secondary.begin(beginInfo);
|
||||
auto &cb = currentPresentCb();
|
||||
const bool msaa = sceneColorSamples != vk::SampleCountFlagBits::e1;
|
||||
// A=1 marks sky / far plane so SSGI skips uncleared pixels.
|
||||
if (msaa) {
|
||||
@@ -1748,7 +1808,7 @@ bool Graphics::beginSceneColorRenderPass() {
|
||||
slot->msaaColor.beginColorAttachment();
|
||||
slot->color.beginColorAttachment();
|
||||
slot->depth.beginDepthAttachment();
|
||||
secondary.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
|
||||
cb.beginRenderPass(rpBegin, vk::SubpassContents::eSecondaryCommandBuffers);
|
||||
} else {
|
||||
std::array<vk::ClearValue, 2> clears{};
|
||||
clears[0].color = vk::ClearColorValue(
|
||||
@@ -1763,9 +1823,12 @@ bool Graphics::beginSceneColorRenderPass() {
|
||||
rpBegin.pClearValues = clears.data();
|
||||
slot->color.beginColorAttachment();
|
||||
slot->depth.beginDepthAttachment();
|
||||
secondary.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
|
||||
cb.beginRenderPass(rpBegin, vk::SubpassContents::eSecondaryCommandBuffers);
|
||||
}
|
||||
setViewportAndScissor(secondary, uint32_t(sceneColorWidth), uint32_t(sceneColorHeight));
|
||||
forwardRecorder_ =
|
||||
beginSecondary(kForwardSecondary, vk::RenderPass(sceneColorRenderPass), slot->framebuffer);
|
||||
setViewportAndScissor(forwardRecorder_.cb(), uint32_t(sceneColorWidth),
|
||||
uint32_t(sceneColorHeight));
|
||||
sceneColorPassOpen = true;
|
||||
sceneSecondaryActive = true;
|
||||
return true;
|
||||
@@ -1773,15 +1836,13 @@ bool Graphics::beginSceneColorRenderPass() {
|
||||
|
||||
void Graphics::endSceneColorRenderPass() {
|
||||
if (!sceneColorPassOpen) return;
|
||||
auto &secondary = currentFrameSecondaryCbs().buffers[kForwardSecondary];
|
||||
secondary.endRenderPass();
|
||||
auto &cb = currentPresentCb();
|
||||
ReadySecondary ready = std::move(forwardRecorder_).finish();
|
||||
executeSecondary(cb, ready);
|
||||
cb.endRenderPass();
|
||||
sceneColorPassOpen = false;
|
||||
if (auto *slot = currentSceneColorSlot())
|
||||
slot->color.endSampledLayout();
|
||||
secondary.end();
|
||||
// Execute the forward-pass secondary from the single present command
|
||||
// buffer before the scene resolve / swapchain passes are recorded.
|
||||
currentPresentCb().executeCommands(1, &secondary);
|
||||
sceneSecondaryActive = false;
|
||||
}
|
||||
|
||||
@@ -4277,7 +4338,9 @@ void Graphics::drawVoxelFaceInstances(const uint32_t *packed, int count, float o
|
||||
Texture *atlas, int tilesPerRow, const uint32_t *ao) {
|
||||
ASSERT(initialized);
|
||||
if (!initialized) throw Exception("drawVoxelFaceInstances: graphics not initialized");
|
||||
if (!swapchainPassOpen) throw Exception("drawVoxelFaceInstances: call begin3DFrame first");
|
||||
if (!swapchainPassOpen) {
|
||||
throw Exception("drawVoxelFaceInstances: call begin3DFrame first");
|
||||
}
|
||||
if (!voxelRectPipeline) createVoxelRectPipeline();
|
||||
if (!packed || count <= 0) return;
|
||||
|
||||
@@ -4343,7 +4406,7 @@ void Graphics::drawVoxelFaceInstances(const uint32_t *packed, int count, float o
|
||||
pc.atlasInfo = glm::vec4(float(std::max(1, tilesPerRow)), 0.f, 0.f, 0.f);
|
||||
pc.tint = mesh3dFrameUbo.tint;
|
||||
|
||||
auto &cb = currentDrawCb();
|
||||
auto cb = currentDrawCb();
|
||||
vk::DescriptorSet set = voxelRectSetFor(gpuTex);
|
||||
cb.bindPipeline(vk::PipelineBindPoint::eGraphics, voxelRectPipeline);
|
||||
cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, voxelRectPipelineLayout, 0, 1, &set, 0,
|
||||
@@ -5188,7 +5251,7 @@ void Graphics::drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *textu
|
||||
const float envIntensity = (mesh3dEnvTexture && mesh3dEnvIntensity > 0.f) ? mesh3dEnvIntensity : 0.f;
|
||||
|
||||
const bool useClustered = mesh3dClusteredActive && !shader && mesh3dClusteredPipeline;
|
||||
auto &cb = currentDrawCb();
|
||||
auto cb = currentDrawCb();
|
||||
|
||||
auto makeShadowUbo = [&]() {
|
||||
ShadowUBO s = mesh3dShadows.ubo;
|
||||
|
||||
@@ -20,6 +20,118 @@
|
||||
namespace eve::graphics::vulkan {
|
||||
|
||||
class OffscreenCanvas;
|
||||
class Graphics;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Secondary command buffer recording lifecycle.
|
||||
//
|
||||
// Vulkan's command-buffer state machine (spec 5.2) restricts the legal order
|
||||
// of operations:
|
||||
// Initial -> Recording -> Executable -> Pending -> Executable (after fence)
|
||||
// A secondary command buffer must be fully recorded (vkEndCommandBuffer) and
|
||||
// therefore Executable before vkCmdExecuteCommands may reference it, and a
|
||||
// RENDER_PASS_CONTINUE secondary may only be executed while the primary is
|
||||
// recording the compatible render pass. These types make that ordering
|
||||
// structural instead of "remember to call things in the right order":
|
||||
// * RecordingSecondary can only be turned into ReadySecondary via finish();
|
||||
// * ReadySecondary's raw handle is only reachable through executeSecondary();
|
||||
// * RenderPassExecutor opens the primary's render pass and executes every
|
||||
// added secondary on destruction — a render pass can never be left open,
|
||||
// executed twice, or executed before its secondaries finished recording.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief A fully recorded secondary command buffer (move-only).
|
||||
*
|
||||
* Only obtainable from RecordingSecondary::finish(); only executeSecondary()
|
||||
* and RenderPassExecutor may access its raw handle, so an unfinished or
|
||||
* still-recording buffer can never reach vkCmdExecuteCommands.
|
||||
*/
|
||||
class ReadySecondary {
|
||||
public:
|
||||
ReadySecondary() = default;
|
||||
ReadySecondary(const ReadySecondary &) = delete;
|
||||
ReadySecondary &operator=(const ReadySecondary &) = delete;
|
||||
ReadySecondary(ReadySecondary &&) noexcept = default;
|
||||
ReadySecondary &operator=(ReadySecondary &&) noexcept = default;
|
||||
|
||||
/** @brief Whether this holds a recorded buffer (false when a job failed). */
|
||||
explicit operator bool() const { return cb_ != nullptr; }
|
||||
|
||||
private:
|
||||
friend class RecordingSecondary;
|
||||
friend class RenderPassExecutor;
|
||||
friend void executeSecondary(vk::CommandBuffer primary, const ReadySecondary &ready);
|
||||
explicit ReadySecondary(vk::CommandBuffer cb) : cb_(cb) {}
|
||||
vk::CommandBuffer cb_ = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A secondary command buffer currently being recorded (move-only).
|
||||
*
|
||||
* Created by Graphics::beginSecondary(); the caller records draw commands via
|
||||
* cb() and must call finish() exactly once. The destructor asserts the
|
||||
* recorder was finished, so an abandoned recording is a debug-build failure
|
||||
* instead of silently corrupting the frame.
|
||||
*/
|
||||
class RecordingSecondary {
|
||||
public:
|
||||
RecordingSecondary() = default;
|
||||
~RecordingSecondary();
|
||||
RecordingSecondary(const RecordingSecondary &) = delete;
|
||||
RecordingSecondary &operator=(const RecordingSecondary &) = delete;
|
||||
RecordingSecondary(RecordingSecondary &&) noexcept = default;
|
||||
RecordingSecondary &operator=(RecordingSecondary &&) noexcept = default;
|
||||
|
||||
/** @brief The command buffer being recorded (nullptr when inactive). */
|
||||
vk::CommandBuffer cb() const { return cb_; }
|
||||
explicit operator bool() const { return cb_ != nullptr; }
|
||||
|
||||
/**
|
||||
* @brief End recording and hand the buffer over as ready to execute.
|
||||
* @return ReadySecondary (only executable via executeSecondary()).
|
||||
*/
|
||||
ReadySecondary finish();
|
||||
|
||||
private:
|
||||
friend class Graphics;
|
||||
friend class ReadySecondary;
|
||||
explicit RecordingSecondary(vk::CommandBuffer cb) : cb_(cb) {}
|
||||
vk::CommandBuffer cb_ = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Execute a recorded secondary command buffer from the primary.
|
||||
* @param primary Command buffer currently recording a compatible render pass.
|
||||
* @param ready Fully recorded secondary (from finish()).
|
||||
*/
|
||||
void executeSecondary(vk::CommandBuffer primary, const ReadySecondary &ready);
|
||||
|
||||
/**
|
||||
* @brief RAII render-pass on the primary for secondary command buffers.
|
||||
*
|
||||
* Constructor begins the render pass with SubpassContents::eSecondaryCommandBuffers
|
||||
* (caller must have done the attachment layout transitions and provides the
|
||||
* clears). add() collects ready secondaries; the destructor executes them in
|
||||
* order and ends the render pass. While this object is alive the caller must
|
||||
* not record any other render pass or draw commands on the primary.
|
||||
*/
|
||||
class RenderPassExecutor {
|
||||
public:
|
||||
RenderPassExecutor(vk::CommandBuffer primary, vk::RenderPass renderPass,
|
||||
vk::Framebuffer framebuffer, vk::Rect2D area,
|
||||
const vk::ClearValue *clears, uint32_t clearCount);
|
||||
~RenderPassExecutor();
|
||||
RenderPassExecutor(const RenderPassExecutor &) = delete;
|
||||
RenderPassExecutor &operator=(const RenderPassExecutor &) = delete;
|
||||
|
||||
/** @brief Queue a ready secondary for execution at the end of the pass. */
|
||||
void add(ReadySecondary ready);
|
||||
|
||||
private:
|
||||
vk::CommandBuffer primary_;
|
||||
std::vector<vk::CommandBuffer> secondaries_;
|
||||
};
|
||||
|
||||
struct ColorVertex {
|
||||
glm::vec2 pos;
|
||||
@@ -672,9 +784,25 @@ private:
|
||||
std::vector<FrameSecondaryCbs> frameSecondaryCbs; // per swapchain frame slot
|
||||
bool sceneSecondaryActive = false;
|
||||
FrameSecondaryCbs ¤tFrameSecondaryCbs();
|
||||
/**
|
||||
* @brief Begin recording one of this frame's secondary command buffers.
|
||||
*
|
||||
* Centralizes the inheritance info / RENDER_PASS_CONTINUE begin flags, so
|
||||
* every secondary is begun with the exact state the primary's render pass
|
||||
* expects. Must only be called after the current frame slot's fence was
|
||||
* waited (the acquire in beginPresentCommandBuffer).
|
||||
*
|
||||
* @param index Index into FrameSecondaryCbs::buffers.
|
||||
* @param renderPass Render pass the secondary inherits from.
|
||||
* @param framebuffer Framebuffer of that render pass instance.
|
||||
* @return Recording secondary; call finish() once recording is done.
|
||||
*/
|
||||
RecordingSecondary beginSecondary(uint32_t index, vk::RenderPass renderPass,
|
||||
vk::Framebuffer framebuffer);
|
||||
RecordingSecondary forwardRecorder_;
|
||||
void recordDeferredPassesParallel();
|
||||
void recordShadowCascadeSecondary(vk::CommandBuffer secondary, int cascade);
|
||||
void recordGBufferSecondary(vk::CommandBuffer secondary);
|
||||
ReadySecondary recordShadowCascadeSecondary(RecordingSecondary recorder, int cascade);
|
||||
ReadySecondary recordGBufferSecondary(RecordingSecondary recorder);
|
||||
|
||||
struct SceneColorSlot {
|
||||
vkb::ColorTarget msaaColor;
|
||||
@@ -855,7 +983,7 @@ private:
|
||||
* forward-pass secondary command buffer when one is open, otherwise the
|
||||
* present command buffer (2D / fallback / offscreen paths).
|
||||
*/
|
||||
vk::CommandBuffer ¤tDrawCb();
|
||||
vk::CommandBuffer currentDrawCb();
|
||||
vkb::FrameSlot frameToken() const;
|
||||
/** @brief Drain in-flight frames before mutating a GPU object sampled/read by them. */
|
||||
void waitForSharedGpuResources();
|
||||
|
||||
Reference in new issue
Block a user