mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Magma: wire clears defer into the next pass's load op and pass-begin barriers coalesce (passes 11.5 -> 4.3, barriers 15.5 -> 9.0 per frame; inproc apply 1.41 -> 1.24 ms; red-once: no materialize fails 4 ClearOrder cases, a dropped fold fails 2).
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
#include "RenderPassGuard.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "VkBufferManager.h"
|
||||
#include "VkSamplerManager.h"
|
||||
@@ -66,6 +67,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
// The image-write epoch the last full memory barrier made visible (see m_wireImageBarrierEpoch),
|
||||
// and the note a caller that just recorded that same barrier makes.
|
||||
Uint64 WireImageBarrierEpoch() const { return m_wireImageBarrierEpoch; }
|
||||
void NoteWireImageBarrier() const { m_wireImageBarrierEpoch = WireImageWriteEpoch(); }
|
||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||
@@ -532,7 +537,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_samplerResolveMemoHighWater = binding + 1;
|
||||
}
|
||||
}
|
||||
// WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible.
|
||||
// WireImageWriteEpoch() when ResolveWireImageDescriptor, or a new draw pass's attachment
|
||||
// barrier (VulkanRenderer::TransitionWireImage), last made image writes visible.
|
||||
mutable Uint64 m_wireImageBarrierEpoch = 0;
|
||||
// P14: the wire arm's sampler memo, one entry per texture unit. The sampler a unit resolves
|
||||
// to is a function of the sampler CSO it names (immutable for a {slot, gen, Serial} within
|
||||
|
||||
@@ -1881,6 +1881,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
lookup = {key, m_resourceEraseEpoch, &it->second};
|
||||
}
|
||||
TextureResource& resource = *lookup.resource;
|
||||
// P15: an image with deferred clears runs them before any use that is not an attachment
|
||||
// resolve (PendingClearHold), and before a shape or texel change of its own even then:
|
||||
// a mip-chain growth copies the content, and an upload lands after the clear it followed.
|
||||
if (!resource.pendingClears.empty() && pVulkanRenderer != nullptr) {
|
||||
const Bool current = resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial &&
|
||||
record.PendingUploads.empty() &&
|
||||
(!requireStorage || (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) != 0);
|
||||
if (m_pendingClearHold == 0 || !current) pVulkanRenderer->MaterializeWirePendingClears(resource);
|
||||
}
|
||||
if (resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial &&
|
||||
record.PendingUploads.empty() && (!requireStorage || (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) != 0)) {
|
||||
return &resource;
|
||||
|
||||
@@ -114,6 +114,26 @@ public:
|
||||
// manager keys its per-draw fast path on this so an attachment's image recreation
|
||||
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
||||
// P15 (wire deferred clears): while one of these lives, SyncTextureResourceByHandle leaves an
|
||||
// image's pending clears pending - its caller is resolving the image as an ATTACHMENT, and
|
||||
// either folds the clears into the pass it begins (LOAD_OP_CLEAR) or replaces them with a
|
||||
// newer clear. Every other caller (sampling, blits, copies, readbacks, uploads, mip
|
||||
// generation) materializes them first, so a use that forgets the hold is merely slower.
|
||||
class PendingClearHold {
|
||||
public:
|
||||
explicit PendingClearHold(VkTextureManager& manager) : m_manager(manager) { ++m_manager.m_pendingClearHold; }
|
||||
~PendingClearHold() { --m_manager.m_pendingClearHold; }
|
||||
PendingClearHold(const PendingClearHold&) = delete;
|
||||
PendingClearHold& operator=(const PendingClearHold&) = delete;
|
||||
private:
|
||||
VkTextureManager& m_manager;
|
||||
};
|
||||
// A whole-subresource clear that was recorded but has not run yet (VulkanRenderer::DeferWireClear).
|
||||
struct WirePendingClear {
|
||||
Uint32 level = 0, layer = 0, layers = 1;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
VkClearValue value{};
|
||||
};
|
||||
// Bumped whenever any tracked texture resource is erased; cached
|
||||
// TextureResource pointers are valid only while this is unchanged.
|
||||
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
||||
@@ -338,6 +358,9 @@ public:
|
||||
SharedPtr<const void> yuvOwner;
|
||||
SharedPtr<void> yuvSource;
|
||||
Uint64 yuvConvertedFrame = 0;
|
||||
// P15: clears of this image still to run (WirePendingClear). Part of the image, so a
|
||||
// respecify (which moves a new image in, below) drops them with the old one.
|
||||
Vector<WirePendingClear> pendingClears;
|
||||
|
||||
TextureResource() = default;
|
||||
TextureResource(const TextureResource&) = delete;
|
||||
@@ -383,6 +406,7 @@ public:
|
||||
std::swap(this->yuvOwner, that.yuvOwner);
|
||||
std::swap(this->yuvSource, that.yuvSource);
|
||||
std::swap(this->yuvConvertedFrame, that.yuvConvertedFrame);
|
||||
std::swap(this->pendingClears, that.pendingClears);
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
@@ -638,6 +662,7 @@ public:
|
||||
private:
|
||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||
Uint64 m_textureImageEpoch = 1;
|
||||
Uint32 m_pendingClearHold = 0; // PendingClearHold's depth
|
||||
// See AdvanceRecordingGeneration. Starts above every resource's default
|
||||
// stamp of 0 so a fresh resource counts as untouched.
|
||||
Uint64 m_recordingGeneration = 1;
|
||||
|
||||
@@ -162,6 +162,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// queue submission order already puts every earlier draw's access to the old texels
|
||||
// ahead of the copy. The preserve arm keeps the wait: it retires the old image.
|
||||
// (rd12 Magma: the wait was ~1.8 ms/frame of fence stall, one per lightmap update.)
|
||||
// P15 (WireFramebuffer.inc): runs an image's deferred clears now, outside any pass.
|
||||
void MaterializeWirePendingClears(VkTextureManager::TextureResource& resource);
|
||||
Bool FlushWirePendingCommandsForTextureUpload() {
|
||||
return !HasPendingRecordedWork() || FlushPendingCommands();
|
||||
}
|
||||
@@ -587,7 +589,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
|
||||
Bool isWriteTarget = false);
|
||||
void TransitionWireImage(WireImage& image, VkImageLayout layout);
|
||||
void TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier = false);
|
||||
// P15: WIRE DEFERRED CLEARS (WireFramebuffer.inc). A clear of images the open pass
|
||||
// holds is recorded inside it; a whole-image clear of others waits on the image
|
||||
// (WirePendingClear) for the next pass that draws into it (LOAD_OP_CLEAR) or the first
|
||||
// other use (MaterializeWirePendingClears).
|
||||
GLbitfield ClearWireAttachmentsOfOpenPass(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer);
|
||||
GLbitfield DeferWireClear(const MG_Pipe::MGPFramebufferState& fbo, const ClearAttachmentPayload& payload,
|
||||
GLint drawbuffer);
|
||||
// An attachment of the pass being prepared whose image carries pending clears; taken
|
||||
// when the pass begins (BeginWireDrawPassFolding) or applied inside it if it is open.
|
||||
struct WireFoldClear {
|
||||
Uint32 attachment = 0;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
VkTextureManager::TextureResource* resource = nullptr;
|
||||
Uint32 level = 0, layer = 0, layers = 1;
|
||||
};
|
||||
Vector<WireFoldClear> m_wireFoldClears;
|
||||
static Bool WireImageCanDeferClear(const WireImage& image);
|
||||
static void NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects, const VkClearValue& value);
|
||||
VkRenderPass WireClearVariant(RenderPassEntry& begun, Uint64 variantKey);
|
||||
Bool BeginWireDrawPassFolding(FrameContext::FrameData& frame, RenderPassEntry& begun, Uint64 variantKey,
|
||||
const VkClearValue* values);
|
||||
void ApplyWireFoldClearsInPass(FrameContext::FrameData& frame, RenderPassEntry& pass);
|
||||
void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer = -1);
|
||||
void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
@@ -829,6 +854,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry);
|
||||
Vector<WireDrawPassCacheEntry> m_wireDrawPassCache;
|
||||
WireDrawPassKey m_wireDrawPassKey;
|
||||
// P15: the open pass's attachment that is `image` (WireFramebuffer.inc's deferred clears).
|
||||
static Uint32 OpenPassAttachmentOf(const WireImage& image, const Vector<WireDrawAttachmentKey>& attachments);
|
||||
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
|
||||
// The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet.
|
||||
// A draw that continues the pass (its WireDrawPassKey compares equal, which covers every
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
const MG_Pipe::MGPFramebufferState& fbo, WireDrawScratch& scratch, Bool passContinues,
|
||||
Bool& continuing) {
|
||||
continuing = true;
|
||||
m_wireFoldClears.clear();
|
||||
if (passContinues) {
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) {
|
||||
@@ -312,7 +313,13 @@
|
||||
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);
|
||||
// An attachment resolve: its deferred clears stay pending, to become this pass's load
|
||||
// op (BeginWireDrawPassFolding) - WireFramebuffer.inc's deferred-clear section.
|
||||
WireImage image;
|
||||
{
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
image = ResolveWireImage(fbo, surface, aspect, /*isWriteTarget=*/true);
|
||||
}
|
||||
if (!image.image) return VK_ATTACHMENT_UNUSED;
|
||||
m_textureManager->FlushPendingUploads();
|
||||
m_wirePassMemo.attachments.push_back(
|
||||
@@ -345,6 +352,9 @@
|
||||
description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
description.initialLayout = description.finalLayout = layout;
|
||||
attachments.push_back(description);
|
||||
if (image.resource != nullptr && !image.resource->pendingClears.empty())
|
||||
m_wireFoldClears.push_back({static_cast<Uint32>(attachments.size() - 1), info.subresourceRange.aspectMask,
|
||||
image.resource, image.level, image.layer, image.layers});
|
||||
extent.width = std::min(extent.width, image.extent.width);
|
||||
extent.height = std::min(extent.height, image.extent.height);
|
||||
layers = std::min(layers, image.layers);
|
||||
@@ -399,7 +409,8 @@
|
||||
// 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);
|
||||
for (auto& image : sameLayoutAttachments)
|
||||
TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL, /*coalesceWithLastFullBarrier=*/true);
|
||||
}
|
||||
if (!continuing) {
|
||||
RetireWireDrawPass();
|
||||
@@ -541,6 +552,8 @@
|
||||
// pass's attachments are provably the ones this draw would resolve (WirePassMemo).
|
||||
Bool passContinues = WirePassMemoHolds(*fbo, frame);
|
||||
if (!passContinues) {
|
||||
// Attachment resolves: deferred clears stay pending for the pass (PendingClearHold).
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
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);
|
||||
@@ -794,6 +807,11 @@
|
||||
if (passContinues) passContinues = WirePassMemoHolds(*fbo, frame);
|
||||
// A memo that holds is the open pass itself (WirePassMemoHolds compared the active pass).
|
||||
Bool continuing = true;
|
||||
// FROM HERE TO THE PASS BEGIN, EVERY IMAGE RESOLVE IS AN ATTACHMENT'S (the pass key walk,
|
||||
// the pipeline's attachment formats): their deferred clears must reach the pass as its
|
||||
// load op. Run anywhere in between, a clear would land after the walk's barriers and right
|
||||
// before the pass that loads the image (a read-after-write hazard).
|
||||
VkTextureManager::PendingClearHold passHold(*m_textureManager);
|
||||
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.
|
||||
@@ -873,7 +891,8 @@
|
||||
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)) {
|
||||
if (open && !m_wireFoldClears.empty()) ApplyWireFoldClearsInPass(frame, pass);
|
||||
if (!open && !BeginWireDrawPassFolding(frame, pass, 0, nullptr)) {
|
||||
MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun");
|
||||
return false;
|
||||
}
|
||||
@@ -930,6 +949,8 @@
|
||||
// recording) before the pass is chosen, unless the open pass provably holds them.
|
||||
Bool passContinues = WirePassMemoHolds(fbo, frame);
|
||||
if (!passContinues) {
|
||||
// Attachment resolves: deferred clears stay pending for the pass (PendingClearHold).
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
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);
|
||||
@@ -943,6 +964,8 @@
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (passContinues) passContinues = WirePassMemoHolds(fbo, frame);
|
||||
Bool continuing = true;
|
||||
// As in SetupWireDraw: attachment resolves only, from the walk to the pass begin.
|
||||
VkTextureManager::PendingClearHold passHold(*m_textureManager);
|
||||
if (!PrepareWireDrawPass(frame, fbo, m_wireDrawScratch, passContinues, continuing)) return remaining;
|
||||
const auto& key = m_wireDrawPassKey;
|
||||
auto& pass = *m_wireDrawPass;
|
||||
@@ -1061,40 +1084,136 @@
|
||||
values[a].depthStencil = clear.clearValue.depthStencil;
|
||||
}
|
||||
}
|
||||
VkRenderPass variant = VK_NULL_HANDLE;
|
||||
for (const auto& entry : begun.clearVariants)
|
||||
if (entry.first == variantKey) variant = entry.second;
|
||||
if (variant == VK_NULL_HANDLE) {
|
||||
auto descriptions = begun.wireAttachments;
|
||||
for (Uint32 a = 0; a < attachmentCount; ++a) {
|
||||
if (variantKey >> (2 * a) & 1) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
if (variantKey >> (2 * a + 1) & 1) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
}
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = static_cast<Uint32>(begun.wireColors.size());
|
||||
subpass.pColorAttachments = begun.wireColors.data();
|
||||
subpass.pDepthStencilAttachment =
|
||||
begun.wireDepth.attachment != VK_ATTACHMENT_UNUSED ? &begun.wireDepth : nullptr;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = attachmentCount;
|
||||
info.pAttachments = descriptions.data();
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &info, nullptr, &variant));
|
||||
begun.clearVariants.push_back({variantKey, variant});
|
||||
}
|
||||
if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun, variant, values, attachmentCount))
|
||||
return remaining;
|
||||
if (!BeginWireDrawPassFolding(frame, begun, variantKey, values)) return remaining;
|
||||
return remaining & ~handled;
|
||||
}
|
||||
if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
if (!open && !VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return remaining;
|
||||
if (open && !m_wireFoldClears.empty()) ApplyWireFoldClearsInPass(frame, pass);
|
||||
if (!open && !BeginWireDrawPassFolding(frame, pass, 0, nullptr)) return remaining;
|
||||
const VkClearRect rect{area, 0, key.layers};
|
||||
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
|
||||
return remaining & ~handled;
|
||||
}
|
||||
|
||||
// The draw pass `begun` with LOAD_OP_CLEAR for the attachments `variantKey` names (bit 2a:
|
||||
// attachment a's colour or depth, bit 2a+1: its stencil). Built once per key and kept on the
|
||||
// pass, like the pass itself.
|
||||
VkRenderPass VulkanRenderer::WireClearVariant(RenderPassEntry& begun, Uint64 variantKey) {
|
||||
for (const auto& entry : begun.clearVariants)
|
||||
if (entry.first == variantKey) return entry.second;
|
||||
const Uint32 attachmentCount = static_cast<Uint32>(begun.wireAttachments.size());
|
||||
auto descriptions = begun.wireAttachments;
|
||||
for (Uint32 a = 0; a < attachmentCount; ++a) {
|
||||
if (variantKey >> (2 * a) & 1) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
if (variantKey >> (2 * a + 1) & 1) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
}
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = static_cast<Uint32>(begun.wireColors.size());
|
||||
subpass.pColorAttachments = begun.wireColors.data();
|
||||
subpass.pDepthStencilAttachment =
|
||||
begun.wireDepth.attachment != VK_ATTACHMENT_UNUSED ? &begun.wireDepth : nullptr;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = attachmentCount;
|
||||
info.pAttachments = descriptions.data();
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
VkRenderPass variant = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &info, nullptr, &variant));
|
||||
begun.clearVariants.push_back({variantKey, variant});
|
||||
return variant;
|
||||
}
|
||||
|
||||
// P15: EVERY DRAW PASS BEGINS HERE. The pending clears of its attachments
|
||||
// (m_wireFoldClears, gathered by PrepareWireDrawPass) become their load op and leave the
|
||||
// images; a clear the caller already put in `variantKey` (its own glClear, newer than any
|
||||
// pending one) wins over a pending clear of the same attachment, which is then simply gone.
|
||||
Bool VulkanRenderer::BeginWireDrawPassFolding(FrameContext::FrameData& frame, RenderPassEntry& begun,
|
||||
Uint64 variantKey, const VkClearValue* values) {
|
||||
const Uint32 attachmentCount = static_cast<Uint32>(begun.wireAttachments.size());
|
||||
constexpr Uint32 kMaxAttachments = MG_Pipe::kMGPipeMaxColorAttachments + 1;
|
||||
VkClearValue merged[kMaxAttachments]{};
|
||||
if (values != nullptr)
|
||||
for (Uint32 a = 0; a < attachmentCount && a < kMaxAttachments; ++a) merged[a] = values[a];
|
||||
for (const auto& fold : m_wireFoldClears) {
|
||||
if (fold.attachment >= attachmentCount || fold.attachment >= kMaxAttachments || fold.resource == nullptr)
|
||||
continue;
|
||||
const Uint32 a = fold.attachment;
|
||||
auto& pending = fold.resource->pendingClears;
|
||||
for (SizeT i = 0; i < pending.size();) {
|
||||
auto& entry = pending[i];
|
||||
if (entry.level != fold.level || entry.layer != fold.layer || entry.layers != fold.layers) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const VkImageAspectFlags taken = entry.aspects & fold.aspects;
|
||||
if ((taken & VK_IMAGE_ASPECT_COLOR_BIT) && !(variantKey >> (2 * a) & 1)) {
|
||||
variantKey |= 1ull << (2 * a);
|
||||
merged[a].color = entry.value.color;
|
||||
}
|
||||
if ((taken & VK_IMAGE_ASPECT_DEPTH_BIT) && !(variantKey >> (2 * a) & 1)) {
|
||||
variantKey |= 1ull << (2 * a);
|
||||
merged[a].depthStencil.depth = entry.value.depthStencil.depth;
|
||||
}
|
||||
if ((taken & VK_IMAGE_ASPECT_STENCIL_BIT) && !(variantKey >> (2 * a + 1) & 1)) {
|
||||
variantKey |= 1ull << (2 * a + 1);
|
||||
merged[a].depthStencil.stencil = entry.value.depthStencil.stencil;
|
||||
}
|
||||
entry.aspects &= ~taken;
|
||||
if (entry.aspects == 0) pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
else ++i;
|
||||
}
|
||||
}
|
||||
m_wireFoldClears.clear();
|
||||
if (variantKey == 0) return VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun);
|
||||
return VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun, WireClearVariant(begun, variantKey),
|
||||
merged, attachmentCount);
|
||||
}
|
||||
|
||||
// The same pending clears when the pass is already open (a continued pass whose attachments
|
||||
// gained a pending clear since it began - the pass-key walk found them): recorded inside it.
|
||||
void VulkanRenderer::ApplyWireFoldClearsInPass(FrameContext::FrameData& frame, RenderPassEntry& pass) {
|
||||
VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
|
||||
Uint32 count = 0;
|
||||
for (const auto& fold : m_wireFoldClears) {
|
||||
if (fold.resource == nullptr) continue;
|
||||
auto& pending = fold.resource->pendingClears;
|
||||
for (SizeT i = 0; i < pending.size();) {
|
||||
auto& entry = pending[i];
|
||||
if (entry.level != fold.level || entry.layer != fold.layer || entry.layers != fold.layers) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const VkImageAspectFlags taken = entry.aspects & fold.aspects;
|
||||
if ((taken & VK_IMAGE_ASPECT_COLOR_BIT) && count < MG_Pipe::kMGPipeMaxColorAttachments + 1) {
|
||||
for (Uint32 c = 0; c < pass.wireColors.size(); ++c) {
|
||||
if (pass.wireColors[c].attachment != fold.attachment) continue;
|
||||
auto& clear = clears[count++];
|
||||
clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
clear.colorAttachment = c;
|
||||
clear.clearValue.color = entry.value.color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const VkImageAspectFlags depthStencil = taken & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
if (depthStencil && pass.wireDepth.attachment == fold.attachment &&
|
||||
count < MG_Pipe::kMGPipeMaxColorAttachments + 1) {
|
||||
auto& clear = clears[count++];
|
||||
clear.aspectMask = depthStencil;
|
||||
clear.clearValue.depthStencil = entry.value.depthStencil;
|
||||
}
|
||||
entry.aspects &= ~taken;
|
||||
if (entry.aspects == 0) pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
else ++i;
|
||||
}
|
||||
}
|
||||
m_wireFoldClears.clear();
|
||||
if (count == 0) return;
|
||||
const VkClearRect rect{{{0, 0}, {static_cast<Uint32>(pass.extent.x()), static_cast<Uint32>(pass.extent.y())}},
|
||||
0, pass.layers};
|
||||
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) {
|
||||
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier) {
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
@@ -208,15 +208,31 @@
|
||||
// tracked layout - and CopyWireDepthStencilBlit moves the two aspects in two legs.
|
||||
if (WireFormatIsPackedDepthStencil(image.format))
|
||||
barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
// P15: A DRAW PASS'S ATTACHMENTS SHARE ONE FULL BARRIER, and the sampled-image
|
||||
// barrier shares it too. The memory barrier below is global (ALL_COMMANDS, every write
|
||||
// made visible), the same one ResolveWireImageDescriptor records, so a second one with no
|
||||
// image written since the last (WireImageWriteEpoch unchanged) orders nothing. Only the
|
||||
// new-pass caller (PrepareWireDrawPass) opts in: its attachments are written inside the
|
||||
// pass it is about to begin, and that pass's end moves the epoch. A transfer caller
|
||||
// writes right after this call with the epoch already moved, so it must neither skip nor
|
||||
// note - noting there would let a later barrier be skipped past its write.
|
||||
// FCL Magma: 27.5 barriers and 12.6 render passes per frame against dev's 3.4 and 4.1,
|
||||
// most of them a second barrier per attachment and a pass closed and reopened at the
|
||||
// first sampled texture after every pass begin.
|
||||
if (image.layout == layout) {
|
||||
// GENERAL->GENERAL is not a layout transition, but the next load,
|
||||
// sample or blit still depends on earlier attachment/shader writes.
|
||||
// The old per-verb queue idle must not be its only ordering edge.
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
const Bool covered = coalesceWithLastFullBarrier && m_uniformManager &&
|
||||
WireImageWriteEpoch() == m_uniformManager->WireImageBarrierEpoch();
|
||||
if (!covered) {
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
if (coalesceWithLastFullBarrier && m_uniformManager) m_uniformManager->NoteWireImageBarrier();
|
||||
}
|
||||
}
|
||||
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout,
|
||||
stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access,
|
||||
@@ -230,13 +246,337 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- P15: WIRE DEFERRED CLEARS -------------------------------------------------------------
|
||||
//
|
||||
// Minecraft (1.21.5) clears its targets through framebuffers of their own - a depth-only one
|
||||
// for the shared depth texture, a colour+depth one per target - and then draws through the
|
||||
// main framebuffer. A clear that begins a LOAD_OP_CLEAR pass of its own made every such clear a
|
||||
// pass that did nothing but clear, closed by the very next draw: 11.5 passes a frame against
|
||||
// dev's 4.1 (whose clear manager folded the same clears into the next pass's load op), each
|
||||
// one an extra full load/store of the attachments on a tiler.
|
||||
//
|
||||
// So a clear of an image the open pass holds is recorded inside that pass, and a whole-image
|
||||
// clear of any other image WAITS ON THE IMAGE (WirePendingClear, kept on the image's own
|
||||
// resource with its level and layers - never keyed by a GL name). The next draw pass that has
|
||||
// it as an attachment begins with LOAD_OP_CLEAR for it (PrepareWireDrawPass /
|
||||
// BeginWireDrawPassFolding); any other use reaches the image through
|
||||
// SyncTextureResourceByHandle, which runs it first (MaterializeWirePendingClears). The default
|
||||
// framebuffer never defers: its in-pass clear is a known Adreno problem (see
|
||||
// ClearWireFramebufferInPass), and it is presented, not sampled.
|
||||
|
||||
// Whether `image` can carry a deferred clear: a plain single-sample 2D(-array) image of this
|
||||
// session with a float colour or a depth/stencil format, clearable by a transfer command.
|
||||
Bool VulkanRenderer::WireImageCanDeferClear(const WireImage& image) {
|
||||
const auto* resource = image.resource;
|
||||
if (resource == nullptr || image.isDefault || image.is3D || image.samples != VK_SAMPLE_COUNT_1_BIT) return false;
|
||||
if (resource->sharedImageId != 0 || resource->yuvImageId != 0) return false;
|
||||
if ((resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0) return false;
|
||||
if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
|
||||
(vkuFormatIsSRGB(image.format) || vkuFormatIsSINT(image.format) || vkuFormatIsUINT(image.format) ||
|
||||
vkuFormatIsCompressed(image.format)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::MaterializeWirePendingClears(VkTextureManager::TextureResource& resource) {
|
||||
if (resource.pendingClears.empty()) return;
|
||||
if (resource.image == VK_NULL_HANDLE) {
|
||||
resource.pendingClears.clear();
|
||||
return;
|
||||
}
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
EndActiveRenderPassOn(frame.commandBuffer);
|
||||
// After every earlier access to the image (the clears were issued after them).
|
||||
if (resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, resource.image, resource.layout,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
resource.layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_WRITE_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource.aspect, 0, resource.mipLevels))
|
||||
MagmaWireFatal("pending-clear-transition");
|
||||
} else {
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
for (const auto& pending : resource.pendingClears) {
|
||||
const VkImageSubresourceRange range{pending.aspects, pending.level, 1, pending.layer, pending.layers};
|
||||
if (pending.aspects & VK_IMAGE_ASPECT_COLOR_BIT)
|
||||
vkCmdClearColorImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
|
||||
&pending.value.color, 1, &range);
|
||||
else
|
||||
vkCmdClearDepthStencilImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
|
||||
&pending.value.depthStencil, 1, &range);
|
||||
}
|
||||
resource.pendingClears.clear();
|
||||
// Recorded writes: the next sampled-image or attachment barrier must cover them.
|
||||
BumpWireImageWriteEpoch();
|
||||
}
|
||||
|
||||
// Records `aspects` of `image` cleared to `value` as pending, replacing whatever an earlier
|
||||
// pending clear of the same subresource said for those aspects.
|
||||
void VulkanRenderer::NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects,
|
||||
const VkClearValue& value) {
|
||||
auto& pending = image.resource->pendingClears;
|
||||
for (SizeT i = 0; i < pending.size();) {
|
||||
auto& entry = pending[i];
|
||||
if (entry.level == image.level && entry.layer == image.layer && entry.layers == image.layers) {
|
||||
entry.aspects &= ~aspects;
|
||||
if (entry.aspects == 0) {
|
||||
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
VkTextureManager::WirePendingClear entry{};
|
||||
entry.level = image.level;
|
||||
entry.layer = image.layer;
|
||||
entry.layers = image.layers;
|
||||
entry.aspects = aspects;
|
||||
entry.value = value;
|
||||
pending.push_back(entry);
|
||||
}
|
||||
|
||||
// The open wire draw pass's attachment that IS `image` (same image, level and layers), or
|
||||
// VK_ATTACHMENT_UNUSED.
|
||||
Uint32 VulkanRenderer::OpenPassAttachmentOf(const WireImage& image,
|
||||
const Vector<WireDrawAttachmentKey>& attachments) {
|
||||
for (Uint32 a = 0; a < attachments.size(); ++a) {
|
||||
const auto& key = attachments[a];
|
||||
if (key.image == image.image && key.level == image.level && key.layer == image.layer &&
|
||||
key.layers == image.layers)
|
||||
return a;
|
||||
}
|
||||
return VK_ATTACHMENT_UNUSED;
|
||||
}
|
||||
|
||||
// A clear whose images the OPEN draw pass holds - through another framebuffer than the
|
||||
// pass's own (the depth-only clear framebuffer) - is recorded inside that pass. Returns the
|
||||
// buffers it did not take.
|
||||
GLbitfield VulkanRenderer::ClearWireAttachmentsOfOpenPass(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 || fbo.IsDefault || m_wirePreparationDepth != 0 || !m_wireDrawPass) return remaining;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash)
|
||||
return remaining;
|
||||
const auto& key = m_wireDrawPassKey;
|
||||
const RenderPassEntry* pass = m_wireDrawPass.get();
|
||||
const VkCommandBuffer passBuffer = active->commandBuffer;
|
||||
const Uint64 passHash = active->hash;
|
||||
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; // the standalone path answers an empty box
|
||||
area = {{x0, y0}, {static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0)}};
|
||||
}
|
||||
// The image must be exactly the pass's extent, or the pass's render area is not its area.
|
||||
const auto fits = [&](const WireImage& image) {
|
||||
return image.extent.width == key.width && image.extent.height == key.height;
|
||||
};
|
||||
VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
|
||||
Uint32 count = 0;
|
||||
GLbitfield handled = 0;
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
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 slot = fbo.DrawBuffers[i];
|
||||
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
|
||||
if (fbo.Color[slot].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() || payload.colorEncoding != ClearColorEncoding::Float) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
const WireImage image = ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT, true);
|
||||
const Uint32 a = image.image ? OpenPassAttachmentOf(image, key.attachments) : VK_ATTACHMENT_UNUSED;
|
||||
Uint32 reference = VK_ATTACHMENT_UNUSED;
|
||||
for (Uint32 c = 0; a != VK_ATTACHMENT_UNUSED && c < key.colorReferences.size(); ++c)
|
||||
if (key.colorReferences[c] == a) reference = c;
|
||||
if (reference == VK_ATTACHMENT_UNUSED || !fits(image) || vkuFormatIsSRGB(image.format) ||
|
||||
vkuFormatIsSRGB(key.attachments[a].format) || vkuFormatIsSINT(image.format) ||
|
||||
vkuFormatIsUINT(image.format)) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
auto& clear = colorClears[colorCount++];
|
||||
clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
clear.colorAttachment = reference;
|
||||
clear.clearValue.color = MakeVkClearColorValue(payload, false);
|
||||
}
|
||||
if (all) {
|
||||
for (Uint32 c = 0; c < colorCount; ++c) clears[count++] = colorClears[c];
|
||||
handled |= GL_COLOR_BUFFER_BIT;
|
||||
}
|
||||
}
|
||||
VkImageAspectFlags depthStencilAspects = 0;
|
||||
const auto depthStencilHeld = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) {
|
||||
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
|
||||
return image.image && fits(image) && key.depthReference != VK_ATTACHMENT_UNUSED &&
|
||||
OpenPassAttachmentOf(image, key.attachments) == key.depthReference &&
|
||||
(key.attachments[key.depthReference].aspect & aspect) != 0;
|
||||
};
|
||||
if (remaining & GL_DEPTH_BUFFER_BIT) {
|
||||
if (!MG_Pipe::gPipeInputs.GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
||||
handled |= GL_DEPTH_BUFFER_BIT;
|
||||
} else if (depthStencilHeld(fbo.Depth, 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 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
} else if (mask == 255u && depthStencilHeld(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
||||
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};
|
||||
}
|
||||
// A resolve above may have submitted (a lazy upload) or run another image's deferred clear,
|
||||
// closing the pass: then nothing is recorded and nothing is taken.
|
||||
active = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (active == nullptr || active->commandBuffer != passBuffer || active->hash != passHash ||
|
||||
m_wireDrawPass.get() != pass)
|
||||
return remaining;
|
||||
if (count > 0) {
|
||||
const VkClearRect rect{area, 0, key.layers};
|
||||
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
|
||||
}
|
||||
return remaining & ~handled;
|
||||
}
|
||||
|
||||
// A whole-image clear of images the open pass does not hold waits on the images (see the
|
||||
// section comment). Returns the buffers it did not take.
|
||||
GLbitfield VulkanRenderer::DeferWireClear(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 || fbo.IsDefault || drawbuffer >= 0 || m_wirePreparationDepth != 0) return remaining;
|
||||
const Bool scissor = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest);
|
||||
const auto wholeImage = [&](const WireImage& image) {
|
||||
if (!scissor) return true;
|
||||
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
|
||||
return box.x() <= 0 && box.y() <= 0 && box.x() + box.z() >= static_cast<Int32>(image.extent.width) &&
|
||||
box.y() + box.w() >= static_cast<Int32>(image.extent.height);
|
||||
};
|
||||
const Bool passOpen = VkRenderPassManager::GetActiveRenderPass() != nullptr && m_wireDrawPass;
|
||||
const auto heldByOpenPass = [&](const WireImage& image) {
|
||||
return passOpen && OpenPassAttachmentOf(image, m_wireDrawPassKey.attachments) != VK_ATTACHMENT_UNUSED;
|
||||
};
|
||||
struct Target {
|
||||
WireImage image;
|
||||
VkImageAspectFlags aspects;
|
||||
VkClearValue value;
|
||||
Bool texture;
|
||||
};
|
||||
Target targets[MG_Pipe::kMGPipeMaxColorAttachments + 2]{};
|
||||
Uint32 targetCount = 0;
|
||||
GLbitfield handled = 0;
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
if ((remaining & GL_COLOR_BUFFER_BIT) && payload.colorEncoding == ClearColorEncoding::Float) {
|
||||
Bool all = true;
|
||||
const Uint32 first = targetCount;
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
|
||||
const Int32 slot = fbo.DrawBuffers[i];
|
||||
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
|
||||
const auto& surface = fbo.Color[slot];
|
||||
if (surface.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;
|
||||
}
|
||||
const WireImage image = ResolveWireImage(fbo, surface, VK_IMAGE_ASPECT_COLOR_BIT, true);
|
||||
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
auto& target = targets[targetCount++];
|
||||
target.image = image;
|
||||
target.aspects = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
target.value.color = MakeVkClearColorValue(payload, false);
|
||||
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
|
||||
}
|
||||
if (all) handled |= GL_COLOR_BUFFER_BIT;
|
||||
else targetCount = first;
|
||||
}
|
||||
const auto depthStencil = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Bool {
|
||||
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
|
||||
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) return false;
|
||||
for (Uint32 t = 0; t < targetCount; ++t) {
|
||||
auto& target = targets[t];
|
||||
if (target.image.image == image.image && target.image.level == image.level &&
|
||||
target.image.layer == image.layer && target.image.layers == image.layers &&
|
||||
!(target.aspects & VK_IMAGE_ASPECT_COLOR_BIT)) {
|
||||
target.aspects |= aspect; // a combined depth/stencil image: one entry, both aspects
|
||||
return true;
|
||||
}
|
||||
}
|
||||
auto& target = targets[targetCount++];
|
||||
target.image = image;
|
||||
target.aspects = aspect;
|
||||
target.value.depthStencil = {payload.depth, payload.stencil};
|
||||
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
|
||||
return true;
|
||||
};
|
||||
if (remaining & GL_DEPTH_BUFFER_BIT) {
|
||||
if (!MG_Pipe::gPipeInputs.GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
handled |= GL_DEPTH_BUFFER_BIT;
|
||||
else if (depthStencil(fbo.Depth, 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 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
else if (mask == 255u && depthStencil(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT))
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
}
|
||||
m_textureManager->FlushPendingUploads();
|
||||
for (Uint32 t = 0; t < targetCount; ++t) {
|
||||
const auto& target = targets[t];
|
||||
NoteWirePendingClear(target.image, target.aspects, target.value);
|
||||
// A GPU write as far as the CPU shadow is concerned, as a recorded clear is.
|
||||
if (target.texture)
|
||||
m_textureManager->MarkWireTextureGpuWritten(target.image.storage, target.image.level,
|
||||
target.image.layer, target.image.layers);
|
||||
}
|
||||
return remaining & ~handled;
|
||||
}
|
||||
|
||||
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
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.
|
||||
// P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds
|
||||
// its images (another framebuffer's), deferred on the images when it covers them whole,
|
||||
// and only then a pass of its own (the in-pass clear again, which begins one) or the
|
||||
// standalone path.
|
||||
ClearAttachmentPayload payload = clearRequest;
|
||||
payload.mask = ClearWireFramebufferInPass(fbo, clearRequest, drawbuffer);
|
||||
if (m_wirePreparationDepth == 0 && WirePassMemoHolds(fbo, m_frameContext.GetCurrent()))
|
||||
payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
|
||||
if (payload.mask) payload.mask = ClearWireAttachmentsOfOpenPass(fbo, payload, drawbuffer);
|
||||
if (payload.mask) payload.mask = DeferWireClear(fbo, payload, drawbuffer);
|
||||
if (payload.mask) payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
|
||||
if (!payload.mask) return;
|
||||
RetireWireDrawPass();
|
||||
// A stream that neither presents nor finishes (an off-screen loop) never submits, so its
|
||||
|
||||
@@ -195,7 +195,9 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DefaultFramebufferAcrossSwapScenario.cpp
|
||||
Scenarios/ScenarioIsolationScenario.cpp
|
||||
Scenarios/UploadOrderScenario.cpp
|
||||
Scenarios/PassOrderScenario.cpp
|
||||
Scenarios/DefaultScissorFollowsSurfaceScenario.cpp
|
||||
Scenarios/ClearOrderScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -31,6 +31,7 @@ set(MGL_P8A_AMBIENT_CASES
|
||||
AdvertisedLimitsScenario.IndexedBufferBindingsAreReportedVerbatimOnBothWidths
|
||||
BufferTextureScenario.*
|
||||
ClearTexImageUndefinedLevelZeroScenario.*
|
||||
ClearOrderScenario.*
|
||||
ClipDistanceScenario.*
|
||||
CopyImageLevelRangeScenario.*
|
||||
CopyImagePacked16Scenario.*
|
||||
@@ -84,6 +85,7 @@ set(MGL_P8A_AMBIENT_CASES
|
||||
OrientationScenario.*
|
||||
P4aFinalFixScenario.*
|
||||
P4aSeamAuditScenario.*
|
||||
PassOrderScenario.*
|
||||
PipelineFailureScenario.*
|
||||
PointSizeDemotionScenario.*
|
||||
ProgramPipelineScenario.ASamplerUnitRewrittenBetweenDrawsKeepsPaintingTheRightTexture
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearOrderScenario.cpp
|
||||
// Copyright (c) 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 TAKES EFFECT WHERE THE APPLICATION ISSUED IT, WHICHEVER FRAMEBUFFER IT WENT THROUGH.
|
||||
//
|
||||
// Minecraft clears its targets through framebuffers of their own (a depth-only one for the shared
|
||||
// depth texture, one per colour target) and draws through another framebuffer holding the same
|
||||
// textures. Magma keeps such a clear on the image and folds it into the load op of the next pass
|
||||
// that draws into the image, records it inside the open pass when that pass holds the image, and
|
||||
// runs it before any other use (sampling, blit, readback, upload, respecify). Each case below is
|
||||
// one of those paths, with no readback between the clear and the work that depends on it:
|
||||
// - a depth clear through a depth-only framebuffer, then a depth-tested draw through the main one
|
||||
// (folded into the next pass), and the same while the main pass is open (inside it);
|
||||
// - a colour clear through a framebuffer of its own, then a draw covering half the texture
|
||||
// through another framebuffer holding it (the other half keeps the clear colour);
|
||||
// - a cleared texture sampled, blitted from, uploaded into, read back and re-specified before
|
||||
// any pass draws into it;
|
||||
// - a scissored clear, which clears only its box.
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#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 {
|
||||
using Pixel = std::array<GLubyte, 4>;
|
||||
constexpr Pixel red{255, 0, 0, 255}, green{0, 255, 0, 255}, blue{0, 0, 255, 255}, black{0, 0, 0, 255};
|
||||
constexpr int kSize = 8, kTargetWidth = 16;
|
||||
constexpr const char* kVertex = R"(#version 330 core
|
||||
uniform float z;
|
||||
void main() {
|
||||
vec2 p[3] = vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1,3));
|
||||
gl_Position = vec4(p[gl_VertexID], z, 1);
|
||||
})";
|
||||
constexpr const char* kSample = R"(#version 330 core
|
||||
uniform sampler2D src;
|
||||
layout(location=0) out vec4 color;
|
||||
void main() { color = texelFetch(src, ivec2(gl_FragCoord.xy) % 8, 0); })";
|
||||
constexpr const char* kTint = R"(#version 330 core
|
||||
uniform vec4 tint;
|
||||
layout(location=0) out vec4 color;
|
||||
void main() { color = tint; })";
|
||||
|
||||
class ClearOrderScenario : public ScenarioTest {
|
||||
protected:
|
||||
GLuint target = 0, sample = 0, tint = 0, vao = 0;
|
||||
std::vector<GLuint> textures, fbos;
|
||||
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glDepthMask(GL_TRUE);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
target = NewFramebuffer(NewColor(kTargetWidth, kSize, black), 0);
|
||||
sample = Link(kSample);
|
||||
tint = Link(kTint);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClearDepth(1.0);
|
||||
if (vao) glDeleteVertexArrays(1, &vao);
|
||||
if (!fbos.empty()) glDeleteFramebuffers(GLsizei(fbos.size()), fbos.data());
|
||||
if (!textures.empty()) glDeleteTextures(GLsizei(textures.size()), textures.data());
|
||||
if (sample) glDeleteProgram(sample);
|
||||
if (tint) glDeleteProgram(tint);
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
GLuint NewColor(int width, int height, Pixel color) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
textures.push_back(texture);
|
||||
const std::vector<Pixel> pixels(size_t(width) * size_t(height), color);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint NewDepth(int width, int height) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// `color` and/or `depth` may be 0.
|
||||
GLuint NewFramebuffer(GLuint color, GLuint depth) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
fbos.push_back(fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
if (color) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
|
||||
if (depth) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth, 0);
|
||||
if (!color) {
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
}
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
return fbo;
|
||||
}
|
||||
|
||||
static GLuint Link(const char* fragment) {
|
||||
const GLuint linked = glCreateProgram();
|
||||
const std::array<GLenum, 2> kinds{GL_VERTEX_SHADER, GL_FRAGMENT_SHADER};
|
||||
const std::array<const char*, 2> sources{kVertex, fragment};
|
||||
for (size_t i = 0; i < kinds.size(); ++i) {
|
||||
const GLuint shader = glCreateShader(kinds[i]);
|
||||
glShaderSource(shader, 1, &sources[i], nullptr);
|
||||
glCompileShader(shader);
|
||||
glAttachShader(linked, shader);
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
glLinkProgram(linked);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetProgramiv(linked, GL_LINK_STATUS, &ok);
|
||||
EXPECT_EQ(ok, GL_TRUE);
|
||||
return linked;
|
||||
}
|
||||
|
||||
static void ClearColor(GLuint fbo, Pixel color) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glClearColor(color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, color[3] / 255.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
// Draws `color` over (x, y, w, h) of `fbo` at depth `z` (NDC).
|
||||
void Fill(GLuint fbo, Pixel color, int x, int y, int w, int h, float z = 0.0f) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glUseProgram(tint);
|
||||
glUniform1f(glGetUniformLocation(tint, "z"), z);
|
||||
glUniform4f(glGetUniformLocation(tint, "tint"), color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f,
|
||||
color[3] / 255.0f);
|
||||
glViewport(x, y, w, h);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
// Samples `texture` into the 8x8 tile `tile` of the target.
|
||||
void SampleInto(GLuint texture, int tile) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target);
|
||||
glUseProgram(sample);
|
||||
glUniform1f(glGetUniformLocation(sample, "z"), 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glViewport(tile * kSize, 0, kSize, kSize);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
void ExpectPixel(GLuint fbo, int x, int y, Pixel expected, const char* why) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
Pixel actual{};
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, actual.data());
|
||||
for (size_t channel = 0; channel < actual.size(); ++channel)
|
||||
EXPECT_EQ(int(actual[channel]), int(expected[channel])) << why << " channel=" << channel;
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << why;
|
||||
}
|
||||
|
||||
// Main framebuffer (colour + depth) and a depth-only clear framebuffer sharing its depth.
|
||||
struct Pair {
|
||||
GLuint color, depth, main, depthOnly;
|
||||
};
|
||||
Pair NewPair() {
|
||||
Pair pair{};
|
||||
pair.color = NewColor(kSize, kSize, black);
|
||||
pair.depth = NewDepth(kSize, kSize);
|
||||
pair.main = NewFramebuffer(pair.color, pair.depth);
|
||||
pair.depthOnly = NewFramebuffer(0, pair.depth);
|
||||
Prime(pair.main);
|
||||
Prime(pair.depthOnly);
|
||||
return pair;
|
||||
}
|
||||
|
||||
// Uses `fbo` once - a clear and a draw - so the cases below run on images whose records have
|
||||
// settled, the steady state a game frame sees (a first use republishes the attachments' records,
|
||||
// and Magma then runs a pending clear at once rather than folding it into the next pass).
|
||||
void Prime(GLuint fbo) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_ALWAYS);
|
||||
Fill(fbo, black, 0, 0, kSize, kSize);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glFinish();
|
||||
}
|
||||
|
||||
// Draws one pixel of the target nothing checks: whatever pass held the case's images is then
|
||||
// closed, so a clear of them is not taken inside it.
|
||||
void SwitchPassAway() { Fill(target, black, kTargetWidth - 1, kSize - 1, 1, 1); }
|
||||
};
|
||||
|
||||
TEST_F(ClearOrderScenario, ADepthClearThroughADepthOnlyFramebufferReachesTheNextPass) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const Pair pair = NewPair();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, pair.main);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // depth 0: nothing passes LESS
|
||||
glFinish();
|
||||
SwitchPassAway();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, pair.depthOnly);
|
||||
glClearDepth(1.0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT); // through the other framebuffer
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
Fill(pair.main, green, 0, 0, kSize, kSize, 0.0f);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ExpectPixel(pair.main, 4, 4, green, "the draw after the depth-only clear sees the cleared depth");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, ADepthClearThroughADepthOnlyFramebufferLandsInsideTheOpenPass) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const Pair pair = NewPair();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, pair.main);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glFinish();
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
Fill(pair.main, red, 0, 0, kSize, kSize, -0.5f); // the main pass is open, depth now -0.5 -> 0.25
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, pair.depthOnly);
|
||||
glClearDepth(1.0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT); // the open pass holds this depth texture
|
||||
Fill(pair.main, green, 0, 0, kSize, kSize, 0.0f); // behind the red one unless the clear landed
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ExpectPixel(pair.main, 4, 4, green, "the draw after the clear passes the cleared depth");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, AColourClearThroughItsOwnFramebufferReachesAnotherFramebufferHoldingTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, black);
|
||||
const GLuint own = NewFramebuffer(texture, 0);
|
||||
const GLuint depth = NewDepth(kSize, kSize);
|
||||
const GLuint other = NewFramebuffer(texture, depth);
|
||||
Prime(own);
|
||||
Prime(other);
|
||||
SwitchPassAway();
|
||||
ClearColor(own, red);
|
||||
Fill(other, blue, 0, 0, kSize / 2, kSize); // the left half, through the other framebuffer
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(target, 1, 4, blue, "the draw through the other framebuffer lands over the clear");
|
||||
ExpectPixel(target, 6, 4, red, "the clear holds where the draw did not reach");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, AClearedTextureIsSampledBeforeAnyPassDrawsIntoIt) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, green);
|
||||
const GLuint fbo = NewFramebuffer(texture, 0);
|
||||
glFinish();
|
||||
ClearColor(fbo, red);
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(target, 4, 4, red, "the sample sees the clear");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, AClearedTextureIsBlittedFrom) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, green);
|
||||
const GLuint fbo = NewFramebuffer(texture, 0);
|
||||
glFinish();
|
||||
ClearColor(fbo, red);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target);
|
||||
glBlitFramebuffer(0, 0, kSize, kSize, 0, 0, kSize, kSize, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
ExpectPixel(target, 4, 4, red, "the blit reads the cleared texels");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, AnUploadAfterAClearLandsOverIt) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, black);
|
||||
const GLuint fbo = NewFramebuffer(texture, 0);
|
||||
glFinish();
|
||||
ClearColor(fbo, red);
|
||||
const std::vector<Pixel> patch(4, green);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, patch.data());
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(target, 1, 1, green, "the upload after the clear is not overwritten by it");
|
||||
ExpectPixel(target, 6, 6, red, "the clear holds outside the upload");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, AClearedTextureIsReadBack) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, green);
|
||||
const GLuint fbo = NewFramebuffer(texture, 0);
|
||||
glFinish();
|
||||
ClearColor(fbo, red);
|
||||
ExpectPixel(fbo, 4, 4, red, "the readback sees the clear");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, ARespecifiedTextureKeepsItsNewTexelsAndNotAnEarlierClear) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, green);
|
||||
const GLuint fbo = NewFramebuffer(texture, 0);
|
||||
glFinish();
|
||||
ClearColor(fbo, red);
|
||||
const std::vector<Pixel> fresh(size_t(kSize) * kSize, blue);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, fresh.data());
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(target, 4, 4, blue, "the re-specification replaces the cleared texels");
|
||||
}
|
||||
|
||||
TEST_F(ClearOrderScenario, AScissoredClearClearsOnlyItsBox) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewColor(kSize, kSize, green);
|
||||
const GLuint fbo = NewFramebuffer(texture, 0);
|
||||
glFinish();
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, kSize / 2, kSize);
|
||||
ClearColor(fbo, red);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(target, 1, 4, red, "inside the scissor box");
|
||||
ExpectPixel(target, 6, 4, green, "outside the scissor box");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,251 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PassOrderScenario.cpp
|
||||
// Copyright (c) 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 - WORK ON AN IMAGE IN ONE PASS IS ORDERED AGAINST THE NEXT PASS THAT TOUCHES IT.
|
||||
//
|
||||
// Magma records a draw pass per framebuffer and orders a new pass after earlier image writes with
|
||||
// a memory barrier, shared between the pass's attachments and the sampled images of its draws.
|
||||
// Each case below hands an image from one piece of work to the next with no readback in between,
|
||||
// so only that barrier orders them:
|
||||
// - render into a texture, then sample it in the next pass (read after write);
|
||||
// - render into a texture, render elsewhere, render into the texture again (write after write);
|
||||
// - blit into a texture, then sample it (a render-pass blit, then read);
|
||||
// - copy into a texture with glCopyImageSubData, then sample it (an out-of-pass transfer write,
|
||||
// then read - the shape whose barrier must not be skipped on the strength of a barrier
|
||||
// recorded between the transfer's pass close and its write);
|
||||
// - blit one half of a texture onto its other half, then sample it (the one transfer whose
|
||||
// image stays in GENERAL, so a memory barrier and no layout transition orders it);
|
||||
// - sample a texture, then render into it (write after read).
|
||||
// A software rasterizer executes in order and renders these right without the barrier, so the
|
||||
// gate that goes red when the barrier is skipped is synchronization validation (submit-time),
|
||||
// run over these cases; the pixel checks hold the semantics.
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#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 {
|
||||
using Pixel = std::array<GLubyte, 4>;
|
||||
constexpr Pixel red{255, 0, 0, 255}, green{0, 255, 0, 255}, blue{0, 0, 255, 255}, black{0, 0, 0, 255};
|
||||
constexpr int kWidth = 16, kHeight = 8, kSize = 8;
|
||||
constexpr const char* kVertex = R"(#version 330 core
|
||||
void main() {
|
||||
vec2 p[3] = vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1,3));
|
||||
gl_Position = vec4(p[gl_VertexID], 0, 1);
|
||||
})";
|
||||
constexpr const char* kSample = R"(#version 330 core
|
||||
uniform sampler2D src;
|
||||
layout(location=0) out vec4 color;
|
||||
void main() { color = texelFetch(src, ivec2(gl_FragCoord.xy) % 8, 0); })";
|
||||
constexpr const char* kTint = R"(#version 330 core
|
||||
uniform vec4 tint;
|
||||
layout(location=0) out vec4 color;
|
||||
void main() { color = tint; })";
|
||||
|
||||
class PassOrderScenario : public ScenarioTest {
|
||||
protected:
|
||||
GLuint target = 0, sample = 0, tint = 0, vao = 0;
|
||||
GLint tintLocation = -1;
|
||||
std::vector<GLuint> textures, fbos;
|
||||
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
target = NewFramebuffer(NewTexture(kWidth, kHeight, black));
|
||||
sample = Link(kSample);
|
||||
tint = Link(kTint);
|
||||
tintLocation = glGetUniformLocation(tint, "tint");
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (vao) glDeleteVertexArrays(1, &vao);
|
||||
if (!fbos.empty()) glDeleteFramebuffers(GLsizei(fbos.size()), fbos.data());
|
||||
if (!textures.empty()) glDeleteTextures(GLsizei(textures.size()), textures.data());
|
||||
if (sample) glDeleteProgram(sample);
|
||||
if (tint) glDeleteProgram(tint);
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
GLuint NewTexture(int width, int height, Pixel color) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
textures.push_back(texture);
|
||||
const std::vector<Pixel> pixels(size_t(width) * size_t(height), color);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint NewFramebuffer(GLuint texture) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
fbos.push_back(fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
return fbo;
|
||||
}
|
||||
|
||||
static GLuint Link(const char* fragment) {
|
||||
const GLuint linked = glCreateProgram();
|
||||
const std::array<GLenum, 2> kinds{GL_VERTEX_SHADER, GL_FRAGMENT_SHADER};
|
||||
const std::array<const char*, 2> sources{kVertex, fragment};
|
||||
for (size_t i = 0; i < kinds.size(); ++i) {
|
||||
const GLuint shader = glCreateShader(kinds[i]);
|
||||
glShaderSource(shader, 1, &sources[i], nullptr);
|
||||
glCompileShader(shader);
|
||||
glAttachShader(linked, shader);
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
glLinkProgram(linked);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetProgramiv(linked, GL_LINK_STATUS, &ok);
|
||||
EXPECT_EQ(ok, GL_TRUE);
|
||||
return linked;
|
||||
}
|
||||
|
||||
// Fills the viewport (x, y, w, h) of `fbo` with `color` by drawing.
|
||||
void Fill(GLuint fbo, Pixel color, int x, int y, int w, int h) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glUseProgram(tint);
|
||||
glUniform4f(tintLocation, color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, color[3] / 255.0f);
|
||||
glViewport(x, y, w, h);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
// Samples `texture` into the 8x8 tile `tile` of the target.
|
||||
void SampleInto(GLuint texture, int tile) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target);
|
||||
glUseProgram(sample);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glViewport(tile * kSize, 0, kSize, kHeight);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
void ExpectPixel(int x, int y, Pixel expected, const char* why) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target);
|
||||
Pixel actual{};
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, actual.data());
|
||||
for (size_t channel = 0; channel < actual.size(); ++channel)
|
||||
EXPECT_EQ(int(actual[channel]), int(expected[channel])) << why << " channel=" << channel;
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << why;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PassOrderScenario, ATextureRenderedInOnePassIsSampledByTheNext) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewTexture(kSize, kSize, black);
|
||||
const GLuint fbo = NewFramebuffer(texture);
|
||||
glFinish();
|
||||
Fill(fbo, green, 0, 0, kSize, kSize);
|
||||
SampleInto(texture, 0);
|
||||
SampleInto(texture, 1);
|
||||
ExpectPixel(4, 4, green, "the next pass samples what the previous pass rendered");
|
||||
ExpectPixel(12, 4, green, "a second draw in the same pass samples it too");
|
||||
}
|
||||
|
||||
TEST_F(PassOrderScenario, ATextureRenderedAgainAfterAnotherPassKeepsBothWrites) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewTexture(kSize, kSize, black);
|
||||
const GLuint fbo = NewFramebuffer(texture);
|
||||
glFinish();
|
||||
Fill(fbo, red, 0, 0, kSize, kSize);
|
||||
Fill(target, black, 0, 0, kWidth, kHeight); // another pass in between
|
||||
Fill(fbo, blue, 0, 0, kSize / 2, kSize); // the left half again
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(1, 4, blue, "the second write to the texture lands over the first");
|
||||
ExpectPixel(6, 4, red, "the first write survives where the second did not reach");
|
||||
}
|
||||
|
||||
TEST_F(PassOrderScenario, ATextureBlittedIntoIsSampledByTheNextDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint source = NewTexture(kSize, kSize, green);
|
||||
const GLuint sourceFbo = NewFramebuffer(source);
|
||||
const GLuint texture = NewTexture(kSize, kSize, black);
|
||||
const GLuint fbo = NewFramebuffer(texture);
|
||||
glFinish();
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glBlitFramebuffer(0, 0, kSize, kSize, 0, 0, kSize, kSize, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(4, 4, green, "the draw after the blit samples the blitted texels");
|
||||
}
|
||||
|
||||
TEST_F(PassOrderScenario, ATextureCopiedIntoIsSampledByTheNextDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint source = NewTexture(kSize, kSize, green);
|
||||
const GLuint texture = NewTexture(kSize, kSize, black);
|
||||
glFinish();
|
||||
SampleInto(texture, 1); // a pass is open on the target when the copy arrives
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0, kSize, kSize, 1);
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(4, 4, green, "the draw after the copy samples the copied texels");
|
||||
ExpectPixel(12, 4, black, "the draw before the copy samples the old texels");
|
||||
}
|
||||
|
||||
TEST_F(PassOrderScenario, ATextureBlittedWithinItselfIsSampledByTheNextDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
// GL allows a blit between non-overlapping regions of one image; ES 3.x makes identical read
|
||||
// and draw buffers GL_INVALID_OPERATION, and DirectGLES forwards the blit to the driver as is,
|
||||
// so the blit is dropped there (a DirectGLES conformance gap of its own, not this case's subject).
|
||||
if (Gl().BackendName() == "DirectGLES") GTEST_SKIP() << "DirectGLES does not emulate a same-image blit";
|
||||
const GLuint texture = NewTexture(kSize, kSize, red);
|
||||
const GLuint fbo = NewFramebuffer(texture);
|
||||
glFinish();
|
||||
Fill(fbo, green, 0, 0, kSize / 2, kSize);
|
||||
SampleInto(texture, 1); // a pass is open on the target when the blit arrives
|
||||
// The left half onto the right half of the same image: one image is both ends of the
|
||||
// transfer, so it stays in GENERAL and its only ordering edge is the memory barrier.
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glBlitFramebuffer(0, 0, kSize / 2, kSize, kSize / 2, 0, kSize, kSize, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
SampleInto(texture, 0);
|
||||
ExpectPixel(6, 4, green, "the draw after the blit samples the blitted half");
|
||||
ExpectPixel(14, 4, red, "the draw before the blit samples the right half's old texels");
|
||||
}
|
||||
|
||||
TEST_F(PassOrderScenario, ATextureSampledBeforeItIsRenderedIntoKeepsTheOldTexelsForThatDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const GLuint texture = NewTexture(kSize, kSize, red);
|
||||
const GLuint fbo = NewFramebuffer(texture);
|
||||
glFinish();
|
||||
SampleInto(texture, 0);
|
||||
Fill(fbo, blue, 0, 0, kSize, kSize);
|
||||
SampleInto(texture, 1);
|
||||
ExpectPixel(4, 4, red, "the draw before the render samples the old texels");
|
||||
ExpectPixel(12, 4, blue, "the draw after the render samples the new texels");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user