[Perf] (Magma): wire clears record inside the draw pass, draw passes are cached across frames and blit sets come from per-frame pools (FCL Magma CPU 3.29 -> 2.37 ms/frame).

This commit is contained in:
2026-10-08 06:59:38 -04:00
parent c7360b47e2
commit 4984da923f
8 changed files with 778 additions and 238 deletions
+5
View File
@@ -120,6 +120,11 @@ namespace MobileGL::MG_Config {
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_MAGMA_INPASS_CLEAR: a wire-arm glClear / glClearBuffer* whose targets are the
// attachments of the draw pass for the bound framebuffer is recorded inside that pass
// (vkCmdClearAttachments) rather than in a render pass of its own built for the clear.
// Auto is ON; ForceOff restores the standalone clear for every surface.
QuirkOverride MagmaInPassClear = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
+1
View File
@@ -345,6 +345,7 @@ namespace MobileGL::MG_ConfigLoader {
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
@@ -757,6 +757,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct WireColorBlitResources;
WireColorBlitResources* m_wireColorBlitResources = nullptr;
void DestroyWireColorBlitResources();
void ResetWireColorBlitFramePool(Uint32 frameIndex);
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): the baked depth-mip program (WireDepthMipmap.inc).
// One level of a depth chain, source and destination being two depth subresources of
// the same image; the caller loops. It owns no frontend object, which is what the
@@ -812,13 +813,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
WireDrawPassKey key;
UniquePtr<RenderPassEntry> pass;
Vector<VkImageView> views;
// The last submission that recorded this pass (P15 S0): it may be destroyed only once
// that submission completed.
Uint64 lastUseSubmit = 0;
};
// Objects only: every draw still ends/begins its pass and records the
// existing memory dependencies. Each slot is cleared after its fence.
// existing memory dependencies. P15 S0: ONE cache across frame slots, so a framebuffer
// drawn every frame keeps its render pass, framebuffer and views instead of rebuilding them
// each frame. An entry whose key's image epochs moved can never hit again (the key holds
// them) and is destroyed once its last use completed (ClearWireDrawPassCache).
Bool AcquireCachedWireDrawPass(const WireDrawPassKey& key);
void ClearWireDrawPassCache(Uint32 frameIndex);
void ClearAllWireDrawPassCaches();
Vector<Vector<WireDrawPassCacheEntry>> m_wireDrawPassCaches;
void DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry);
Vector<WireDrawPassCacheEntry> m_wireDrawPassCache;
WireDrawPassKey m_wireDrawPassKey;
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
// The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet.
@@ -872,6 +880,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, const FrameContext::FrameData& frame) const;
Uint64 m_wirePassMarksClearGeneration = 0;
WireDrawScratch m_wireDrawScratch;
Bool PrepareWireDrawPass(FrameContext::FrameData& frame, const MG_Pipe::MGPFramebufferState& fbo,
WireDrawScratch& scratch, Bool passContinues, Bool& continuing);
// P15: glClear / glClearBuffer* into the attachments of the wire draw pass, recorded inside it
// (vkCmdClearAttachments). Returns the GL clear-mask bits it could not take; those go through
// the standalone path.
GLbitfield ClearWireFramebufferInPass(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer);
struct WireRetiredObjects {
Uint64 submitIndex = 0;
UniquePtr<RenderPassEntry> drawPass;
@@ -14,8 +14,19 @@
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
Vector<Target> targets;
// P15 S0: one descriptor pool per frame slot for the blit's set, reset when the slot's
// previous recording is known complete (ClearWireDrawPassCache). A pool created and
// destroyed per blit was ~0.16 ms a blit on Adreno 750. A full pool falls back to that.
static constexpr Uint32 kFramePoolSets = 64;
Vector<VkDescriptorPool> framePools;
};
void VulkanRenderer::ResetWireColorBlitFramePool(Uint32 frameIndex) {
auto& pools = m_wireColorBlitResources->framePools;
if (frameIndex < pools.size() && pools[frameIndex] != VK_NULL_HANDLE)
vkResetDescriptorPool(m_device, pools[frameIndex], 0);
}
void VulkanRenderer::DestroyWireColorBlitResources() {
if (!m_wireColorBlitResources) return;
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit
@@ -25,6 +36,8 @@
vkDestroyPipeline(m_device, target.pipeline, nullptr);
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
}
for (const auto pool : cache.framePools)
if (pool != VK_NULL_HANDLE) vkDestroyDescriptorPool(m_device, pool, nullptr);
vkDestroySampler(m_device, cache.nearest, nullptr);
vkDestroySampler(m_device, cache.linear, nullptr);
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
@@ -328,17 +341,36 @@
// would rewrite the source view of an older queued blit.
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc.descriptorPool = descriptorPool;
alloc.descriptorSetCount = 1;
alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
{
// The frame slot's pool (P15 S0): its sets live until the slot's next frame begins,
// which is after this recording completed.
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
if (cache.framePools.size() <= frameIndex) cache.framePools.resize(frameIndex + 1, VK_NULL_HANDLE);
if (cache.framePools[frameIndex] == VK_NULL_HANDLE) {
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
WireColorBlitResources::kFramePoolSets};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = WireColorBlitResources::kFramePoolSets;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &cache.framePools[frameIndex]));
}
alloc.descriptorPool = cache.framePools[frameIndex];
if (vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet) != VK_SUCCESS) descriptorSet = VK_NULL_HANDLE;
}
if (descriptorSet == VK_NULL_HANDLE) {
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
alloc.descriptorPool = descriptorPool;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
}
// GL_LINEAR filters only a STRETCHED blit; an unscaled one (mirrored or not, on any of the
// four transforms) is a copy and samples NEAREST, which is exact where linear filtering
// at a quantised coordinate is not (WireColorBlitFilter.h). The format check above still
@@ -399,7 +431,7 @@
WireRetiredObjects retired{};
retired.framebuffers.push_back(framebuffer);
retired.imageViews = {sourceView, destinationView};
retired.descriptorPools.push_back(descriptorPool);
if (descriptorPool != VK_NULL_HANDLE) retired.descriptorPools.push_back(descriptorPool);
RetireWireObjects(Move(retired));
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
return true;
@@ -127,28 +127,25 @@
auto& frame = m_frameContext.GetCurrent();
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
if (m_wireDrawPassCaches.size() < m_frameContext.GetFrameCount())
m_wireDrawPassCaches.resize(m_frameContext.GetFrameCount());
auto& cache = m_wireDrawPassCaches[frameIndex];
// Bound a present-less stream of distinct framebuffer configurations.
// Overflow retains the original submit-index retirement path.
constexpr SizeT kMaxCachedWirePassesPerFrame = 32;
if (cache.size() < kMaxCachedWirePassesPerFrame) {
cache.push_back({Move(m_wireDrawPassKey), Move(m_wireDrawPass), Move(m_wireDrawViews)});
return;
}
WireRetiredObjects retired{};
retired.drawPass = Move(m_wireDrawPass);
retired.imageViews = Move(m_wireDrawViews);
RetireWireObjects(Move(retired));
// The pass was recorded into the current recording: its last use is the next submission.
m_wireDrawPassCache.push_back(
{Move(m_wireDrawPassKey), Move(m_wireDrawPass), Move(m_wireDrawViews), GetWireNextSubmitIndex()});
m_wireDrawPassKey = {};
// Bound a stream of distinct framebuffer configurations: the oldest entry goes through the
// submit-indexed retirement, which waits for its last use like every retired object.
constexpr SizeT kMaxCachedWirePasses = 64;
if (m_wireDrawPassCache.size() > kMaxCachedWirePasses) {
auto& oldest = m_wireDrawPassCache.front();
WireRetiredObjects retired{};
retired.drawPass = Move(oldest.pass);
retired.imageViews = Move(oldest.views);
RetireWireObjects(Move(retired));
m_wireDrawPassCache.erase(m_wireDrawPassCache.begin());
}
}
Bool VulkanRenderer::AcquireCachedWireDrawPass(const WireDrawPassKey& key) {
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
if (frameIndex >= m_wireDrawPassCaches.size()) return false;
auto& cache = m_wireDrawPassCaches[frameIndex];
auto& cache = m_wireDrawPassCache;
for (auto it = cache.begin(); it != cache.end(); ++it) {
// Exact values, not a compatibility hash: two FBOs with the same
// formats can name different images, windows or draw-buffer slots.
@@ -162,19 +159,47 @@
return false;
}
void VulkanRenderer::ClearWireDrawPassCache(Uint32 frameIndex) {
if (frameIndex >= m_wireDrawPassCaches.size()) return;
auto& cache = m_wireDrawPassCaches[frameIndex];
for (auto& entry : cache) {
entry.pass.reset();
for (const auto view : entry.views) vkDestroyImageView(m_device, view, nullptr);
}
cache.clear();
void VulkanRenderer::DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry) {
entry.pass.reset();
for (const auto view : entry.views) vkDestroyImageView(m_device, view, nullptr);
entry.views.clear();
}
// At a frame slot's begin, after its fence (Present): the slot's blit sets are free, and every
// cached pass whose image epochs moved - it can never hit again - is dropped before the texture
// manager may release retired images: destroyed now when its last use completed, else retired
// behind that use (which also keeps any image it names alive until then).
void VulkanRenderer::ClearWireDrawPassCache(Uint32 frameIndex) {
if (m_wireColorBlitResources) ResetWireColorBlitFramePool(frameIndex);
const Uint64 imageEpoch = m_textureManager->GetTextureImageEpoch();
const Uint64 eraseEpoch = m_textureManager->GetResourceEraseEpoch();
SizeT kept = 0;
for (SizeT i = 0; i < m_wireDrawPassCache.size(); ++i) {
auto& entry = m_wireDrawPassCache[i];
if (entry.key.textureImageEpoch == imageEpoch && entry.key.resourceEraseEpoch == eraseEpoch) {
if (kept != i) m_wireDrawPassCache[kept] = Move(entry);
++kept;
continue;
}
if (entry.lastUseSubmit <= m_completedSubmitCounter) {
DestroyWireDrawPassCacheEntry(entry);
} else {
WireRetiredObjects retired{};
retired.drawPass = Move(entry.pass);
retired.imageViews = Move(entry.views);
RetireWireObjects(Move(retired));
}
}
m_wireDrawPassCache.resize(kept);
}
// Idle points only (shutdown, swapchain replacement, the device-idle drain): nothing in flight.
void VulkanRenderer::ClearAllWireDrawPassCaches() {
for (Uint32 frameIndex = 0; frameIndex < m_wireDrawPassCaches.size(); ++frameIndex)
ClearWireDrawPassCache(frameIndex);
for (auto& entry : m_wireDrawPassCache) DestroyWireDrawPassCacheEntry(entry);
m_wireDrawPassCache.clear();
if (m_wireColorBlitResources)
for (Uint32 frameIndex = 0; frameIndex < m_frameContext.GetFrameCount(); ++frameIndex)
ResetWireColorBlitFramePool(frameIndex);
}
void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) {
@@ -235,6 +260,217 @@
return true;
}
// P15: the draw pass for `fbo`, continued (passContinues: the WirePassMemo holds) or
// looked up / built from a full attachment walk. Shared by SetupWireDraw and the in-pass
// clear (WireFramebuffer.inc): a clear into the attachments of the open pass is recorded
// inside it instead of closing it and building a pass of its own. Leaves m_wireDrawPass and
// m_wireDrawPassKey describing the pass, `continuing` true when it is the open one.
Bool VulkanRenderer::PrepareWireDrawPass(FrameContext::FrameData& frame,
const MG_Pipe::MGPFramebufferState& fbo, WireDrawScratch& scratch, Bool passContinues,
Bool& continuing) {
continuing = true;
if (passContinues) {
auto& store = MG_Record::ServerStagedTexture();
if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) {
for (const auto& mark : m_wirePassMemo.marks)
m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers);
m_wirePassMarksValid = true;
m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration();
}
} else {
m_wirePassMemo.valid = false;
auto& attachments = scratch.attachments;
auto& viewInfos = scratch.viewInfos;
auto& colors = scratch.colors;
attachments.clear();
viewInfos.clear();
colors.clear();
VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
VkExtent2D extent{~0u, ~0u};
Uint32 layers = ~0u;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
Uint64 passHash = 0;
auto& passKey = scratch.passKey;
passKey.ResetKeepingCapacity();
passKey.isDefault = fbo.IsDefault;
passKey.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
if (fbo.IsDefault) {
passKey.swapchain = m_swapchainObject.GetHandle();
// A drawing pass on the default framebuffer is what makes the freshly acquired
// swapchain image the one GL addresses (see m_defaultFramebufferImageIndex); the
// attachments below resolve the same index, so this key has to be taken after it.
passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true);
}
auto& sameLayoutAttachments = scratch.sameLayoutAttachments;
sameLayoutAttachments.clear();
scratch.gpuWriteMarks.clear();
m_wirePassMemo.attachments.clear();
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
WireImage image = ResolveWireImage(fbo, surface, aspect, /*isWriteTarget=*/true);
if (!image.image) return VK_ATTACHMENT_UNUSED;
m_textureManager->FlushPendingUploads();
m_wirePassMemo.attachments.push_back(
{image.resource, image.storage, surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer});
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT)
image.format = ResolveSrgbAttachmentWriteFormat(image.format,
MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ?
VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL;
// Already in its attachment layout: its barrier (which closes any open pass) waits until
// it is known whether the open pass continues with this very attachment.
if (image.layout == layout) sameLayoutAttachments.push_back(image);
else TransitionWireImage(image, layout);
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = image.image;
info.viewType = image.viewType;
info.format = image.format;
info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers};
if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
viewInfos.push_back(info);
passKey.attachments.push_back({image.storage, static_cast<Uint32>(surface.Kind), image.image,
image.viewType, image.format, info.subresourceRange.aspectMask, aspect,
image.level, image.layer, image.layers, image.levels, image.extent.width, image.extent.height,
image.samples});
VkAttachmentDescription description{};
description.format = image.format;
description.samples = image.samples;
description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
description.initialLayout = description.finalLayout = layout;
attachments.push_back(description);
extent.width = std::min(extent.width, image.extent.width);
extent.height = std::min(extent.height, image.extent.height);
layers = std::min(layers, image.layers);
samples = image.samples;
passHash = XXH64(&description, sizeof(description), passHash);
// Marked below, once the pass is known: a continued pass repeats the same marks.
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
scratch.gpuWriteMarks.push_back({image.storage, image.level, image.layer, image.layers});
if (image.isDefault) {
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
}
return static_cast<Uint32>(attachments.size() - 1);
};
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const Int32 slot = fbo.DrawBuffers[i];
const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
colors.push_back({index, VK_IMAGE_LAYOUT_GENERAL});
}
while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back();
if (fbo.IsDefault || fbo.Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
depth.attachment = add(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
else if (fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
depth.attachment = add(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
if (attachments.empty()) {
MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments");
return false;
}
passKey.width = extent.width;
passKey.height = extent.height;
passKey.layers = layers;
passKey.depthReference = depth.attachment;
for (const auto& color : colors) passKey.colorReferences.push_back(color.attachment);
// A native handle can be recycled after image replacement or death.
// Sample epochs after every lazy resolve, then compare them with the key.
passKey.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
passKey.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch();
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = static_cast<Uint32>(colors.size());
subpass.pColorAttachments = colors.data();
subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr;
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
passInfo.attachmentCount = static_cast<Uint32>(attachments.size());
passInfo.pAttachments = attachments.data();
passInfo.subpassCount = 1;
passInfo.pSubpasses = &subpass;
continuing = m_wireDrawPass && m_wireDrawPassKey == passKey;
{
const auto* active = VkRenderPassManager::GetActiveRenderPass();
const Bool stillOpen = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer;
// A new pass orders itself after earlier writes to its attachments; a continued one
// is the same pass, whose attachment writes are ordered by rasterization.
if (!stillOpen)
for (auto& image : sameLayoutAttachments) TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
}
if (!continuing) {
RetireWireDrawPass();
m_wireDrawPassCompatibilityId = 0;
}
if (!continuing && !AcquireCachedWireDrawPass(passKey)) {
// A copy, not a move: passKey is the scratch's, and a new pass is the rare case.
m_wireDrawPassKey = passKey;
Vector<VkImageView> views;
views.reserve(viewInfos.size());
for (const auto& info : viewInfos) {
VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view));
m_wireDrawViews.push_back(view);
views.push_back(view);
}
m_wireDrawPass = MakeUnique<RenderPassEntry>();
auto& created = *m_wireDrawPass;
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &created.renderPass));
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fbInfo.renderPass = created.renderPass;
fbInfo.attachmentCount = static_cast<Uint32>(views.size());
fbInfo.pAttachments = views.data();
fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers;
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &created.framebuffer));
created.attachmentCount = static_cast<Uint32>(attachments.size());
created.colorAttachmentCount = static_cast<Uint32>(colors.size());
created.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED;
created.sampleCount = samples;
created.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
created.layers = layers;
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
}
// The attachments' GPU-write marks (MarkWireTextureGpuWritten): idempotent while nothing
// clears a GPU-dirty mark, so a draw that continues a pass whose draws already marked the
// same attachments - the pass key compared equal - repeats them only after a clear.
{
auto& store = MG_Record::ServerStagedTexture();
if (!continuing || !m_wirePassMarksValid ||
m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) {
for (const auto& mark : scratch.gpuWriteMarks)
m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers);
m_wirePassMarksValid = true;
m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration();
}
}
{
// What this walk proved, for WirePassMemoHolds on the next draw.
const auto& st = MG_Pipe::MGPipeApplier();
auto& memo = m_wirePassMemo;
memo.valid = !fbo.IsDefault;
memo.framebufferSerial = st.FramebufferSerial;
memo.contextSerial = st.ContextSerial;
memo.drawFramebuffer = st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)];
memo.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
memo.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch();
memo.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
memo.passHash = m_wireDrawPass->hash;
memo.marks.assign(scratch.gpuWriteMarks.begin(), scratch.gpuWriteMarks.end());
}
// The compatibility key is built from this walk's attachments only when the pass is new.
if (m_wireDrawPassCompatibilityId == 0) {
auto& pipelinePassKey = scratch.compatibilityKey;
pipelinePassKey.attachmentFormatsAndSamples.clear();
pipelinePassKey.colorReferences.clear();
for (const auto& attachment : attachments)
pipelinePassKey.attachmentFormatsAndSamples.push_back(
(static_cast<Uint64>(attachment.format) << 32) | static_cast<Uint32>(attachment.samples));
for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment);
pipelinePassKey.depthReference = depth.attachment;
m_wireDrawPassCompatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey);
}
} // !passContinues
return true;
}
Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode,
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) {
auto& state = MG_Pipe::MGPipeApplier();
@@ -533,204 +769,7 @@
if (passContinues) passContinues = WirePassMemoHolds(*fbo, frame);
// A memo that holds is the open pass itself (WirePassMemoHolds compared the active pass).
Bool continuing = true;
if (passContinues) {
auto& store = MG_Record::ServerStagedTexture();
if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) {
for (const auto& mark : m_wirePassMemo.marks)
m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers);
m_wirePassMarksValid = true;
m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration();
}
} else {
m_wirePassMemo.valid = false;
auto& attachments = scratch.attachments;
auto& viewInfos = scratch.viewInfos;
auto& colors = scratch.colors;
attachments.clear();
viewInfos.clear();
colors.clear();
VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
VkExtent2D extent{~0u, ~0u};
Uint32 layers = ~0u;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
Uint64 passHash = 0;
auto& passKey = scratch.passKey;
passKey.ResetKeepingCapacity();
passKey.isDefault = fbo->IsDefault;
passKey.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
if (fbo->IsDefault) {
passKey.swapchain = m_swapchainObject.GetHandle();
// A drawing pass on the default framebuffer is what makes the freshly acquired
// swapchain image the one GL addresses (see m_defaultFramebufferImageIndex); the
// attachments below resolve the same index, so this key has to be taken after it.
passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true);
}
auto& sameLayoutAttachments = scratch.sameLayoutAttachments;
sameLayoutAttachments.clear();
scratch.gpuWriteMarks.clear();
m_wirePassMemo.attachments.clear();
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true);
if (!image.image) return VK_ATTACHMENT_UNUSED;
m_textureManager->FlushPendingUploads();
m_wirePassMemo.attachments.push_back(
{image.resource, image.storage, surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer});
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT)
image.format = ResolveSrgbAttachmentWriteFormat(image.format,
MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ?
VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL;
// Already in its attachment layout: its barrier (which closes any open pass) waits until
// it is known whether the open pass continues with this very attachment.
if (image.layout == layout) sameLayoutAttachments.push_back(image);
else TransitionWireImage(image, layout);
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = image.image;
info.viewType = image.viewType;
info.format = image.format;
info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers};
if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
viewInfos.push_back(info);
passKey.attachments.push_back({image.storage, static_cast<Uint32>(surface.Kind), image.image,
image.viewType, image.format, info.subresourceRange.aspectMask, aspect,
image.level, image.layer, image.layers, image.levels, image.extent.width, image.extent.height,
image.samples});
VkAttachmentDescription description{};
description.format = image.format;
description.samples = image.samples;
description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
description.initialLayout = description.finalLayout = layout;
attachments.push_back(description);
extent.width = std::min(extent.width, image.extent.width);
extent.height = std::min(extent.height, image.extent.height);
layers = std::min(layers, image.layers);
samples = image.samples;
passHash = XXH64(&description, sizeof(description), passHash);
// Marked below, once the pass is known: a continued pass repeats the same marks.
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
scratch.gpuWriteMarks.push_back({image.storage, image.level, image.layer, image.layers});
if (image.isDefault) {
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
}
return static_cast<Uint32>(attachments.size() - 1);
};
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const Int32 slot = fbo->DrawBuffers[i];
const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
colors.push_back({index, VK_IMAGE_LAYOUT_GENERAL});
}
while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back();
if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
if (attachments.empty()) {
MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments");
return false;
}
passKey.width = extent.width;
passKey.height = extent.height;
passKey.layers = layers;
passKey.depthReference = depth.attachment;
for (const auto& color : colors) passKey.colorReferences.push_back(color.attachment);
// A native handle can be recycled after image replacement or death.
// Sample epochs after every lazy resolve, then compare them with the key.
passKey.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
passKey.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch();
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = static_cast<Uint32>(colors.size());
subpass.pColorAttachments = colors.data();
subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr;
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
passInfo.attachmentCount = static_cast<Uint32>(attachments.size());
passInfo.pAttachments = attachments.data();
passInfo.subpassCount = 1;
passInfo.pSubpasses = &subpass;
continuing = m_wireDrawPass && m_wireDrawPassKey == passKey;
{
const auto* active = VkRenderPassManager::GetActiveRenderPass();
const Bool stillOpen = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer;
// A new pass orders itself after earlier writes to its attachments; a continued one
// is the same pass, whose attachment writes are ordered by rasterization.
if (!stillOpen)
for (auto& image : sameLayoutAttachments) TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
}
if (!continuing) {
RetireWireDrawPass();
m_wireDrawPassCompatibilityId = 0;
}
if (!continuing && !AcquireCachedWireDrawPass(passKey)) {
// A copy, not a move: passKey is the scratch's, and a new pass is the rare case.
m_wireDrawPassKey = passKey;
Vector<VkImageView> views;
views.reserve(viewInfos.size());
for (const auto& info : viewInfos) {
VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view));
m_wireDrawViews.push_back(view);
views.push_back(view);
}
m_wireDrawPass = MakeUnique<RenderPassEntry>();
auto& created = *m_wireDrawPass;
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &created.renderPass));
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fbInfo.renderPass = created.renderPass;
fbInfo.attachmentCount = static_cast<Uint32>(views.size());
fbInfo.pAttachments = views.data();
fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers;
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &created.framebuffer));
created.attachmentCount = static_cast<Uint32>(attachments.size());
created.colorAttachmentCount = static_cast<Uint32>(colors.size());
created.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED;
created.sampleCount = samples;
created.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
created.layers = layers;
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
}
// The attachments' GPU-write marks (MarkWireTextureGpuWritten): idempotent while nothing
// clears a GPU-dirty mark, so a draw that continues a pass whose draws already marked the
// same attachments - the pass key compared equal - repeats them only after a clear.
{
auto& store = MG_Record::ServerStagedTexture();
if (!continuing || !m_wirePassMarksValid ||
m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) {
for (const auto& mark : scratch.gpuWriteMarks)
m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers);
m_wirePassMarksValid = true;
m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration();
}
}
{
// What this walk proved, for WirePassMemoHolds on the next draw.
const auto& st = MG_Pipe::MGPipeApplier();
auto& memo = m_wirePassMemo;
memo.valid = !fbo->IsDefault;
memo.framebufferSerial = st.FramebufferSerial;
memo.contextSerial = st.ContextSerial;
memo.drawFramebuffer = st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)];
memo.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
memo.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch();
memo.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
memo.passHash = m_wireDrawPass->hash;
memo.marks.assign(scratch.gpuWriteMarks.begin(), scratch.gpuWriteMarks.end());
}
// The compatibility key is built from this walk's attachments only when the pass is new.
if (m_wireDrawPassCompatibilityId == 0) {
auto& pipelinePassKey = scratch.compatibilityKey;
pipelinePassKey.attachmentFormatsAndSamples.clear();
pipelinePassKey.colorReferences.clear();
for (const auto& attachment : attachments)
pipelinePassKey.attachmentFormatsAndSamples.push_back(
(static_cast<Uint64>(attachment.format) << 32) | static_cast<Uint32>(attachment.samples));
for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment);
pipelinePassKey.depthReference = depth.attachment;
m_wireDrawPassCompatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey);
}
} // !passContinues
if (!PrepareWireDrawPass(frame, *fbo, scratch, passContinues, continuing)) return false;
auto& pass = *m_wireDrawPass;
// Interned once per pass (above): a continued pass has the same compatibility.
const Uint64 compatibilityId = m_wireDrawPassCompatibilityId;
@@ -826,6 +865,150 @@
return true;
}
// P15 S0: A CLEAR INTO THE OPEN PASS'S ATTACHMENTS IS RECORDED INSIDE THE PASS.
//
// The standalone path (ClearWireFramebuffer's per-surface lambda) closes the draw pass, then
// builds an image view, a render pass and a framebuffer for every cleared surface, runs a pass
// of its own and retires all three: about 70 us a clear on Adreno 750, and every following draw
// to the same framebuffer opens a new pass (FCL MC 1.21.5: 6 clears a frame, 0.42 ms). Here the
// clear takes the pass a draw to this framebuffer would use (PrepareWireDrawPass: the open one,
// a cached one, or a new one through the same walk) and becomes a vkCmdClearAttachments in it,
// so the draws after it continue the same pass.
//
// vkCmdClearAttachments inside the subpass is ordered with the pass's draws as a draw is, writes
// through the same attachment views (the UNORM twin of an sRGB image while GL_FRAMEBUFFER_SRGB
// is off, which is exactly GL's raw clear - no pre-compensation), and needs no layout change:
// wire attachments live in GENERAL. What it cannot express goes back to the standalone path,
// per GL buffer bit: a partial colour or stencil write mask (that path warns and skips, as
// before), an attachment whose extent or layer count differs from the pass's (the pass covers the
// common area only), and a stencil clear the pass's depth attachment does not carry.
GLbitfield VulkanRenderer::ClearWireFramebufferInPass(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer) {
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
if (!remaining || MG_Config::Features.MagmaInPassClear == MG_Config::QuirkOverride::ForceOff) return remaining;
// Nested inside a draw's preparation the scratch and the pass belong to that draw.
if (m_wirePreparationDepth != 0) return remaining;
auto& frame = m_frameContext.GetCurrent();
WirePreparationScope preparing(m_wirePreparationDepth);
// As SetupWireDraw: resolve every attachment (a lazy upload or promotion may submit the
// recording) before the pass is chosen, unless the open pass provably holds them.
Bool passContinues = WirePassMemoHolds(fbo, frame);
if (!passContinues) {
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const Int32 slot = fbo.DrawBuffers[i];
if (slot >= 0) (void)ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
}
if (fbo.IsDefault || fbo.Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
(void)ResolveWireImage(fbo, fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
else if (fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
(void)ResolveWireImage(fbo, fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
}
m_textureManager->FlushPendingUploads();
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (passContinues) passContinues = WirePassMemoHolds(fbo, frame);
Bool continuing = true;
if (!PrepareWireDrawPass(frame, fbo, m_wireDrawScratch, passContinues, continuing)) return remaining;
const auto& key = m_wireDrawPassKey;
auto& pass = *m_wireDrawPass;
const auto fits = [&](Uint32 attachment) {
if (attachment >= key.attachments.size()) return false;
const auto& a = key.attachments[attachment];
return a.width == key.width && a.height == key.height && a.layers == key.layers;
};
VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
Uint32 count = 0;
GLbitfield handled = 0;
if (remaining & GL_COLOR_BUFFER_BIT) {
Bool all = true;
Uint32 colorCount = 0;
VkClearAttachment colorClears[MG_Pipe::kMGPipeMaxColorAttachments]{};
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
if (drawbuffer >= 0 && static_cast<Uint32>(drawbuffer) != i) continue;
const Int32 attachment = fbo.DrawBuffers[i];
if (attachment < 0 || attachment >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
if (!fbo.IsDefault && fbo.Color[attachment].Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
const auto mask = MG_Pipe::gPipeInputs.GetColorMaskIndexed(i);
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) { all = false; break; }
if (i >= key.colorReferences.size() || key.colorReferences[i] == VK_ATTACHMENT_UNUSED ||
!fits(key.colorReferences[i])) { all = false; break; }
auto& clear = colorClears[colorCount++];
clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
clear.colorAttachment = i;
clear.clearValue.color = MakeVkClearColorValue(payload, false);
}
if (all) {
for (Uint32 c = 0; c < colorCount; ++c) clears[count++] = colorClears[c];
handled |= GL_COLOR_BUFFER_BIT;
}
}
const WireDrawAttachmentKey* depthKey =
key.depthReference != VK_ATTACHMENT_UNUSED && fits(key.depthReference) ? &key.attachments[key.depthReference]
: nullptr;
VkImageAspectFlags depthStencilAspects = 0;
if (remaining & GL_DEPTH_BUFFER_BIT) {
if (!MG_Pipe::gPipeInputs.GetDepthMask()) {
handled |= GL_DEPTH_BUFFER_BIT; // nothing to write, as on the standalone path
} else if (!fbo.IsDefault && fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
handled |= GL_DEPTH_BUFFER_BIT; // no depth surface: nothing to clear
} else if (depthKey && depthKey->requestedAspect == VK_IMAGE_ASPECT_DEPTH_BIT &&
(depthKey->aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) {
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
handled |= GL_DEPTH_BUFFER_BIT;
}
}
if (remaining & GL_STENCIL_BUFFER_BIT) {
const Uint32 mask = MG_Pipe::gPipeInputs.GetStencilState(StencilFace::Front).WriteMask & 255u;
if (mask == 0) {
handled |= GL_STENCIL_BUFFER_BIT;
} else if (!fbo.IsDefault && fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
handled |= GL_STENCIL_BUFFER_BIT;
} else if (mask == 255u && depthKey && (depthKey->aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) {
// The pass's depth/stencil attachment is the stencil surface itself: a stencil-only
// framebuffer, the default framebuffer, or a depth surface that is the same image
// subresource (a DEPTH_STENCIL attachment point).
const auto& depth = fbo.Depth;
const auto& stencil = fbo.Stencil;
const Bool same = fbo.IsDefault || depthKey->requestedAspect == VK_IMAGE_ASPECT_STENCIL_BIT ||
(depth.Kind == stencil.Kind && depth.Res == stencil.Res &&
depth.Level == stencil.Level && depth.Layer == stencil.Layer && depth.Layered == stencil.Layered);
if (same) {
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
handled |= GL_STENCIL_BUFFER_BIT;
}
}
}
if (depthStencilAspects) {
auto& clear = clears[count++];
clear.aspectMask = depthStencilAspects;
clear.clearValue.depthStencil = {payload.depth, payload.stencil};
}
if (count == 0) return remaining & ~handled;
VkRect2D area{{0, 0}, {key.width, key.height}};
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(key.width));
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(key.height));
if (x0 >= x1 || y0 >= y1) return remaining & ~(handled | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT |
GL_STENCIL_BUFFER_BIT); // an empty box clears nothing
area = {{x0, y0}, {static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0)}};
if (fbo.IsDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1 - x0, y1 - y0, VkExtent2D{key.width, key.height},
m_swapchainObject.GetPreTransform(), &area.offset, &area.extent))
MagmaWireFatal("clear-surface-transform");
}
const auto* active = VkRenderPassManager::GetActiveRenderPass();
const Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer &&
active->hash == pass.hash;
if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
if (!open && !VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return remaining;
const VkClearRect rect{area, 0, key.layers};
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
return remaining & ~handled;
}
// ---- P8-D: THE WIRE ARM'S INDIRECT DRAWS READ THEIR WORDS ON THE GPU ----------------------
//
// The shape the monolith arm records (VulkanRenderer.cpp's MultiDraw{Elements,Arrays}Indirect
@@ -231,7 +231,13 @@
}
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer) {
const ClearAttachmentPayload& clearRequest, GLint drawbuffer) {
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
// P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass,
// WireDraw.inc); only the buffers it hands back take the standalone pass below.
ClearAttachmentPayload payload = clearRequest;
payload.mask = ClearWireFramebufferInPass(fbo, clearRequest, drawbuffer);
if (!payload.mask) return;
RetireWireDrawPass();
// A stream that neither presents nor finishes (an off-screen loop) never submits, so its
// recording and the per-clear objects retired below would grow until host memory runs
@@ -96,6 +96,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClientVertexArrayScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp
# P15 S0: a clear between draws into the same framebuffer (DirectVulkan records it in the pass).
Scenarios/InPassClearScenario.cpp
Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp
@@ -2452,6 +2454,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endmacro()
mgl_itest_register_split_arms("ClearThenReadPixelsScenario.*" "")
mgl_itest_register_split_arms("InPassClearScenario.*" "")
# GL_ARB_shader_objects (P13): ARB-created shader / program objects travel the record arm like
# core-created ones, so the scenario runs on every split arm as well as both monoliths.
mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "")
@@ -0,0 +1,295 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/InPassClearScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A CLEAR BETWEEN DRAWS INTO THE SAME FRAMEBUFFER.
//
// P15 S0: DirectVulkan's wire arm records a glClear / glClearBuffer* whose targets are the
// attachments of the draw pass for the bound framebuffer INSIDE that pass
// (VulkanRenderer::ClearWireFramebufferInPass, vkCmdClearAttachments) instead of closing it and
// running a pass of its own. What that path has to get right, and what each case pins:
//
// * ORDER and SCISSOR: a draw, a scissored clear, a second draw - all in one pass. The clear
// must land after the first draw and before the second, inside the scissor box only.
// * THE DRAW-BUFFER INDEX: VkClearAttachment::colorAttachment is the SUBPASS colour slot, which
// the wire pass lays out by draw-buffer slot, not by framebuffer attachment point. With
// glDrawBuffers({NONE, ATTACHMENT0}) a clear writes attachment 0 and leaves attachment 1 alone;
// glClearBufferfv(GL_COLOR, 1, ...) is the same slot by number.
// * DEPTH and STENCIL: a depth / stencil clear between two depth- / stencil-tested draws decides
// what the second draw passes.
// * sRGB: a clear of an sRGB attachment with GL_FRAMEBUFFER_SRGB off stores the raw value, with it
// on the encoded one - through the pass's attachment view, which is the UNORM twin when off.
//
// DirectGLES is the control (a host GL driver); MOBILEGL_MAGMA_INPASS_CLEAR=0 is the standalone
// path, which the same assertions hold for.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
uniform float uDepth;
void main() { gl_Position = vec4(aPos, uDepth, 1.0); }
)";
constexpr const char* kFS = R"(#version 330 core
uniform vec4 uColor;
layout(location = 0) out vec4 o0;
layout(location = 1) out vec4 o1;
void main() { o0 = uColor; o1 = uColor; }
)";
constexpr int kSize = 64;
class InPassClearScenario : public ScenarioTest {
protected:
unsigned int program = 0;
GLuint vao = 0, vbo = 0;
void Build() {
std::string error;
program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
}
void Quad(float r, float g, float b, float a, float depth = 0.0f) {
glUseProgram(program);
glUniform4f(glGetUniformLocation(program, "uColor"), r, g, b, a);
glUniform1f(glGetUniformLocation(program, "uDepth"), depth);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
void TearDown() override {
if (vbo) glDeleteBuffers(1, &vbo);
if (vao) glDeleteVertexArrays(1, &vao);
if (program) glDeleteProgram(program);
ScenarioTest::TearDown();
}
static GLuint Texture(GLenum internalFormat) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
return texture;
}
static Rgba8 ReadAt(GLenum readBuffer, int x, int y) {
glReadBuffer(readBuffer);
const Image image = ReadPixelsRect(x, y, 1, 1);
return image.At(0, 0);
}
};
::testing::AssertionResult Near(const Rgba8& got, int r, int g, int b, int a, int tolerance = 2) {
const auto close = [&](int v, int want) { return v >= want - tolerance && v <= want + tolerance; };
if (close(got.r, r) && close(got.g, g) && close(got.b, b) && close(got.a, a))
return ::testing::AssertionSuccess();
return ::testing::AssertionFailure()
<< "got (" << int(got.r) << ", " << int(got.g) << ", " << int(got.b) << ", " << int(got.a)
<< "), want (" << r << ", " << g << ", " << b << ", " << a << ")";
}
} // namespace
TEST_F(InPassClearScenario, AScissoredClearBetweenTwoDrawsLandsBetweenThem) {
if (!Ready()) return;
Build();
const GLuint color = Texture(GL_RGBA8);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
Quad(1.0f, 0.0f, 0.0f, 1.0f); // red everywhere
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, kSize / 2, kSize); // left half
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); // green, left half
glScissor(0, 0, kSize, kSize / 4); // bottom quarter
Quad(0.0f, 0.0f, 1.0f, 1.0f); // blue, bottom quarter (scissor clips the draw)
glDisable(GL_SCISSOR_TEST);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 4, kSize * 3 / 4), 0, 255, 0, 255)) << "left top: the clear";
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize * 3 / 4, kSize * 3 / 4), 255, 0, 0, 255))
<< "right top: the first draw, outside the clear's scissor box";
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 4, kSize / 8), 0, 0, 255, 255))
<< "left bottom: the second draw, after the clear";
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize * 3 / 4, kSize / 8), 0, 0, 255, 255));
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &color);
}
TEST_F(InPassClearScenario, AClearWritesTheDrawBufferSlotsAttachmentOnly) {
if (!Ready()) return;
Build();
const GLuint color0 = Texture(GL_RGBA8), color1 = Texture(GL_RGBA8);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color0, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, color1, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
// Both attachments red through a draw to both.
const GLenum both[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, both);
Quad(1.0f, 0.0f, 0.0f, 1.0f);
// Draw buffer 1 is attachment 0 and draw buffer 0 is nothing: a draw + clear in that
// layout touches attachment 0 only.
const GLenum swapped[] = {GL_NONE, GL_COLOR_ATTACHMENT0};
glDrawBuffers(2, swapped);
Quad(1.0f, 1.0f, 0.0f, 1.0f); // yellow into attachment 0 (opens the pass in this layout)
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 0, 0, 255, 255))
<< "attachment 0 is draw buffer 1: cleared to blue";
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT1, kSize / 2, kSize / 2), 255, 0, 0, 255))
<< "attachment 1 is in no draw buffer: still the first draw's red";
// glClearBufferfv names the draw-buffer slot by number.
glDrawBuffers(2, both);
Quad(1.0f, 0.0f, 0.0f, 1.0f);
const float green[] = {0.0f, 1.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 1, green);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 255, 0, 0, 255)) << "slot 0 untouched";
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT1, kSize / 2, kSize / 2), 0, 255, 0, 255)) << "slot 1 cleared";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &color0);
glDeleteTextures(1, &color1);
}
TEST_F(InPassClearScenario, DepthAndStencilClearsBetweenTestedDrawsDecideTheSecondDraw) {
if (!Ready()) return;
Build();
const GLuint color = Texture(GL_RGBA8), depthStencil = Texture(GL_DEPTH24_STENCIL8);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthStencil, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
glDepthMask(GL_TRUE);
glStencilMask(0xff);
glClearDepth(1.0);
glClearStencil(0);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// Depth: a near draw (z = -0.5 -> 0.25 in the window) writes depth; a clear to 1.0
// between it and a farther draw (z = 0.0 -> 0.5) is the only way that draw passes LESS.
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
Quad(1.0f, 0.0f, 0.0f, 1.0f, -0.5f);
glClear(GL_DEPTH_BUFFER_BIT);
Quad(0.0f, 1.0f, 0.0f, 1.0f, 0.0f);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 0, 255, 0, 255))
<< "the depth clear between the draws let the farther draw pass";
// Stencil: a draw that writes 1 everywhere, a stencil clear to 2, then a draw that only
// passes where stencil == 2.
glDisable(GL_DEPTH_TEST);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xff);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
Quad(1.0f, 0.0f, 0.0f, 1.0f);
glClearStencil(2);
glClear(GL_STENCIL_BUFFER_BIT);
glStencilFunc(GL_EQUAL, 2, 0xff);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
Quad(0.0f, 0.0f, 1.0f, 1.0f);
glDisable(GL_STENCIL_TEST);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 0, 0, 255, 255))
<< "the stencil clear between the draws let the EQUAL 2 draw pass";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &color);
glDeleteTextures(1, &depthStencil);
}
TEST_F(InPassClearScenario, AnSrgbClearStoresRawWithEncodingOffAndEncodedWithItOn) {
if (!Ready()) return;
Build();
const GLuint color = Texture(GL_SRGB8_ALPHA8);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_FRAMEBUFFER_SRGB);
Quad(1.0f, 0.0f, 0.0f, 1.0f); // opens the pass
glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 128, 128, 128, 255))
<< "GL_FRAMEBUFFER_SRGB off: the raw value";
glEnable(GL_FRAMEBUFFER_SRGB);
Quad(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_FRAMEBUFFER_SRGB);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 188, 188, 188, 255))
<< "GL_FRAMEBUFFER_SRGB on: linear 0.5 encoded";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &color);
}
} // namespace MGITest