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