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:
2026-10-09 04:50:17 -04:00
parent 65d4b808c1
commit 533a256482
10 changed files with 1172 additions and 38 deletions
@@ -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