mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
1725 lines
98 KiB
C++
1725 lines
98 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.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
|
|
|
|
#include "VkRenderPassManager.h"
|
|
|
|
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
|
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
|
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
|
// GL promises "at least the requested samples", so a non-power-of-two
|
|
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
|
if (requestedSamples <= 1) {
|
|
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
return true;
|
|
}
|
|
if (requestedSamples > 64) {
|
|
return false;
|
|
}
|
|
Uint32 bit = 1;
|
|
while (bit < static_cast<Uint32>(requestedSamples)) {
|
|
bit <<= 1;
|
|
}
|
|
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
|
|
return true;
|
|
}
|
|
|
|
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
|
|
switch (format) {
|
|
case VK_FORMAT_D16_UNORM:
|
|
case VK_FORMAT_X8_D24_UNORM_PACK32:
|
|
case VK_FORMAT_D32_SFLOAT:
|
|
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
case VK_FORMAT_S8_UINT:
|
|
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
case VK_FORMAT_D16_UNORM_S8_UINT:
|
|
case VK_FORMAT_D24_UNORM_S8_UINT:
|
|
case VK_FORMAT_D32_SFLOAT_S8_UINT:
|
|
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
default:
|
|
return VK_IMAGE_ASPECT_COLOR_BIT;
|
|
}
|
|
}
|
|
|
|
static Bool ColorFormatLacksAlpha(const MG_State::GLState::ITextureObject* texture) {
|
|
return texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3;
|
|
}
|
|
|
|
[[maybe_unused]] static Float ResolveColorClearAlpha(const MG_State::GLState::ITextureObject* texture, Float requestedAlpha) {
|
|
if (texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3) {
|
|
return 1.0f;
|
|
}
|
|
return requestedAlpha;
|
|
}
|
|
|
|
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
|
return target >= TextureUploadTarget::CubeMapPositiveX &&
|
|
target <= TextureUploadTarget::CubeMapNegativeZ;
|
|
}
|
|
|
|
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
|
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
|
|
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
|
|
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
|
|
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
|
|
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
|
|
// they resolve the layer through their own copies of this helper, which do shift.
|
|
const auto* texture = attachment.GetTexture().get();
|
|
if (attachment.IsLayered()) {
|
|
return ToStorageArrayLayer(texture, 0);
|
|
}
|
|
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
|
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
|
|
return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
|
|
}
|
|
const Int face =
|
|
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
|
|
return ToStorageArrayLayer(texture, face);
|
|
}
|
|
|
|
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
|
|
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
|
|
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
|
|
|
|
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
|
|
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
|
|
// type is not a legal answer for several of the targets GL can attach, and returning it
|
|
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
|
|
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
|
|
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
|
|
// or dereferenced - by the driver inside vkCreateFramebuffer.
|
|
//
|
|
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
|
|
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
|
|
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
|
|
//
|
|
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
|
|
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
|
|
// the only honest answer would be to decline the attachment - and every driver this has run on,
|
|
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
|
|
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
|
|
static VkImageViewType ResolveAttachmentViewType(
|
|
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
|
const VkTextureManager::TextureResource& resource) {
|
|
if (attachment.IsLayered()) {
|
|
switch (resource.viewType) {
|
|
case VK_IMAGE_VIEW_TYPE_3D:
|
|
case VK_IMAGE_VIEW_TYPE_CUBE:
|
|
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
|
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
|
default:
|
|
return resource.viewType;
|
|
}
|
|
}
|
|
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
|
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
|
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
|
|
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
|
// produces a non-layered cube attachment without a face upload target - and is kept for
|
|
// symmetry with CUBE_ARRAY.
|
|
//
|
|
// 3D belongs in the same list and was missing from it, which is why the "per-slice
|
|
// attachment view is a 2D view whose array layer is the slice" branch in
|
|
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
|
|
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
|
|
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
|
|
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
|
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
|
|
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
|
|
return VK_IMAGE_VIEW_TYPE_2D;
|
|
}
|
|
return resource.viewType;
|
|
}
|
|
|
|
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
|
|
const MG_State::GLState::FramebufferObject& fbo,
|
|
FramebufferAttachmentType attachmentType,
|
|
Uint32 drawBufferIndex) {
|
|
if (attachmentType == FramebufferAttachmentType::None) {
|
|
return nullptr;
|
|
}
|
|
|
|
const auto& attachment = fbo.GetAttachment(attachmentType);
|
|
if (!attachment.IsTexture()) {
|
|
if (attachment.IsEmpty()) {
|
|
MGLOG_D("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has no bound color attachment; using VK_ATTACHMENT_UNUSED",
|
|
drawBufferIndex,
|
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
|
fbo.GetExternalIndex());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (!attachment.IsComplete()) {
|
|
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
|
drawBufferIndex,
|
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
|
fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
|
|
auto* texture = attachment.GetTexture().get();
|
|
if (texture == nullptr) {
|
|
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
|
drawBufferIndex,
|
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
|
fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
DepthStencilAttachmentLoadInfo ResolveDepthStencilAttachmentLoadInfo(
|
|
VkImageLayout trackedLayout, Bool clearDepth, Bool clearStencil) {
|
|
DepthStencilAttachmentLoadInfo info{};
|
|
info.depthLoadOp = clearDepth
|
|
? VK_ATTACHMENT_LOAD_OP_CLEAR
|
|
: (trackedLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
|
: VK_ATTACHMENT_LOAD_OP_LOAD);
|
|
info.stencilLoadOp = clearStencil
|
|
? VK_ATTACHMENT_LOAD_OP_CLEAR
|
|
: (trackedLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
|
: VK_ATTACHMENT_LOAD_OP_LOAD);
|
|
info.initialLayout =
|
|
(trackedLayout == VK_IMAGE_LAYOUT_UNDEFINED || (clearDepth && clearStencil)) ? VK_IMAGE_LAYOUT_UNDEFINED
|
|
: trackedLayout;
|
|
return info;
|
|
}
|
|
|
|
IntVec2 ResolveRenderPassFramebufferExtent(Bool isDefaultFbo, const TextureSize& attachmentExtent,
|
|
VkExtent2D swapchainExtent) {
|
|
if (isDefaultFbo) {
|
|
return {static_cast<Int>(swapchainExtent.width), static_cast<Int>(swapchainExtent.height)};
|
|
}
|
|
return {attachmentExtent.x(), attachmentExtent.y()};
|
|
}
|
|
|
|
void VkRenderPassManager::RenderbufferResource::Destroy(VkDevice device, VmaAllocator allocator) {
|
|
if (view != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(device, view, nullptr);
|
|
}
|
|
if (unormTwinView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(device, unormTwinView, nullptr);
|
|
}
|
|
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
|
vmaDestroyImage(allocator, image, allocation);
|
|
}
|
|
renderbuffer.reset();
|
|
image = VK_NULL_HANDLE;
|
|
allocation = nullptr;
|
|
view = VK_NULL_HANDLE;
|
|
unormTwinView = VK_NULL_HANDLE;
|
|
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
format = VK_FORMAT_UNDEFINED;
|
|
aspect = VK_IMAGE_ASPECT_NONE;
|
|
extent = {0, 0};
|
|
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
internalFormat = TextureInternalFormat::Unknown;
|
|
samples = 0;
|
|
deadSinceFrame = kNeverObservedDead;
|
|
}
|
|
|
|
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
|
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
|
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject):
|
|
m_device(device), m_physicalDevice(physicalDevice), m_allocator(allocator), m_config(config),
|
|
m_clearManager(clearManager), m_textureManager(textureManager), m_swapchainObject(swapchainObject) {
|
|
RenderPassEntry::s_device = m_device;
|
|
s_clearManager = &m_clearManager;
|
|
s_textureManager = &m_textureManager;
|
|
s_swapchainObject = &m_swapchainObject;
|
|
s_renderPassManager = this;
|
|
}
|
|
|
|
VkRenderPassManager::~VkRenderPassManager() {}
|
|
|
|
Bool VkRenderPassManager::Initialize() {
|
|
return true;
|
|
}
|
|
|
|
void VkRenderPassManager::Shutdown() {
|
|
m_renderPasses.clear();
|
|
for (auto& [_, resource] : m_renderbufferResources) {
|
|
resource.Destroy(m_device, m_allocator);
|
|
}
|
|
m_renderbufferResources.clear();
|
|
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
|
m_pendingRenderbufferClears.clear();
|
|
RenderPassEntry::s_textureResourcesScratch.clear();
|
|
s_activeRenderPass = {};
|
|
s_hasActiveRenderPass = false;
|
|
m_rpFastValid = false;
|
|
}
|
|
|
|
Uint64 VkRenderPassManager::RetireAgeFrames() const {
|
|
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
|
|
// recording-to-submit gap and one because OnPresent runs ahead of Present's
|
|
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
|
|
// still releasing multi-MB attachment memory promptly (the render-pass cache's
|
|
// 1024-frame retirement would pin it for no additional safety).
|
|
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
|
|
}
|
|
|
|
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
|
|
// The superseded backing may still be referenced by in-flight command buffers
|
|
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
|
|
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
|
|
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
m_deferredRenderbufferReleases.push_back(
|
|
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
|
|
resource.image = VK_NULL_HANDLE;
|
|
resource.allocation = nullptr;
|
|
resource.view = VK_NULL_HANDLE;
|
|
resource.unormTwinView = VK_NULL_HANDLE;
|
|
}
|
|
|
|
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
|
if (m_deferredRenderbufferReleases.empty()) {
|
|
return;
|
|
}
|
|
const Uint64 retireAgeFrames = RetireAgeFrames();
|
|
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
|
|
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
|
|
return false;
|
|
}
|
|
if (release.view != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(m_device, release.view, nullptr);
|
|
}
|
|
if (release.unormTwinView != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
|
|
}
|
|
if (release.image != VK_NULL_HANDLE) {
|
|
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
|
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by
|
|
// command buffers submitted up to frames-in-flight frames ago (it was legally
|
|
// attached and drawn right up to its deletion), so the first observation of an
|
|
// expired weak reference only stamps the current frame counter; Destroy runs once
|
|
// enough frame boundaries have passed that the stamping frame's submission fence
|
|
// has provably been waited (see RetireAgeFrames).
|
|
const Uint64 retireAgeFrames = RetireAgeFrames();
|
|
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
|
|
auto& resource = it->second;
|
|
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
|
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) {
|
|
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
|
++it;
|
|
continue;
|
|
}
|
|
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) {
|
|
resource.deadSinceFrame = m_frameCounter;
|
|
++it;
|
|
continue;
|
|
}
|
|
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) {
|
|
++it;
|
|
continue;
|
|
}
|
|
m_pendingRenderbufferClears.erase(it->first);
|
|
resource.Destroy(m_device, m_allocator);
|
|
it = m_renderbufferResources.erase(it);
|
|
}
|
|
}
|
|
|
|
VkRenderPassManager::RenderbufferResource* VkRenderPassManager::GetOrCreateRenderbufferResource(
|
|
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {
|
|
if (!renderbuffer || !renderbuffer->IsAllocated()) {
|
|
return nullptr;
|
|
}
|
|
|
|
CollectRenderbufferGarbage();
|
|
|
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
|
renderbuffer->GetSamples(),
|
|
renderbuffer->GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
|
|
const auto internalFormat = renderbuffer->GetInternalFormat();
|
|
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
|
|
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
|
|
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
|
|
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
|
|
//
|
|
// This used to be a hand-maintained second copy of that table, and it was missing exactly
|
|
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
|
|
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
|
|
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
|
|
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
|
|
// a size-incompatible vkCmdCopyImage.
|
|
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
|
|
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
|
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
|
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
|
// BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization.
|
|
const VkImageUsageFlags imageUsage =
|
|
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
|
|
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
|
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
|
|
// GL allows the implementation to allocate more samples than requested
|
|
// (glRenderbufferStorageMultisample only promises "at least"), and devices
|
|
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
|
|
// nearest supported count for this format.
|
|
if (renderbuffer->GetSamples() > 0) {
|
|
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
|
|
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
|
|
VkImageFormatProperties formatProperties{};
|
|
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
|
|
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
|
|
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
|
|
&formatProperties) == VK_SUCCESS) {
|
|
supported = formatProperties.sampleCounts;
|
|
}
|
|
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
|
|
}
|
|
const VkSampleCountFlags supported = supportedIt->second;
|
|
if ((supported & sampleCount) == 0) {
|
|
// Smallest supported count above the request, else the largest below it.
|
|
Uint32 rounded = 0;
|
|
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
|
|
if ((supported & bit) != 0) {
|
|
rounded = bit;
|
|
break;
|
|
}
|
|
}
|
|
if (rounded == 0) {
|
|
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
|
|
if ((supported & bit) != 0) {
|
|
rounded = bit;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (rounded != 0) {
|
|
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
|
const Bool needsCreate =
|
|
resource.image == VK_NULL_HANDLE ||
|
|
resource.format != format ||
|
|
resource.extent.width != static_cast<Uint32>(renderbuffer->GetWidth()) ||
|
|
resource.extent.height != static_cast<Uint32>(renderbuffer->GetHeight()) ||
|
|
resource.sampleCount != sampleCount ||
|
|
resource.internalFormat != internalFormat ||
|
|
resource.samples != renderbuffer->GetSamples();
|
|
if (!needsCreate) {
|
|
resource.renderbuffer = renderbuffer;
|
|
// A new renderbuffer at a recycled address may adopt a compatible entry that
|
|
// was already stamped dead; it is alive again, so cancel the aging.
|
|
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
|
return &resource;
|
|
}
|
|
|
|
// Respecify: park the old backing for aged destruction instead of destroying
|
|
// inline - it may still be referenced by in-flight command buffers.
|
|
DeferRenderbufferBackingRelease(resource);
|
|
resource.Destroy(m_device, m_allocator);
|
|
resource.renderbuffer = renderbuffer;
|
|
|
|
VkImageCreateInfo imageInfo{};
|
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
|
imageInfo.extent.width = static_cast<Uint32>(renderbuffer->GetWidth());
|
|
imageInfo.extent.height = static_cast<Uint32>(renderbuffer->GetHeight());
|
|
imageInfo.extent.depth = 1;
|
|
imageInfo.mipLevels = 1;
|
|
imageInfo.arrayLayers = 1;
|
|
imageInfo.format = format;
|
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
imageInfo.usage = imageUsage;
|
|
imageInfo.samples = sampleCount;
|
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
|
|
// is disabled, which needs a format-reinterpreting second view.
|
|
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
|
|
if (hasUnormTwin) {
|
|
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
|
}
|
|
|
|
VkImageFormatProperties imageFormatProperties{};
|
|
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
|
&imageFormatProperties);
|
|
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
|
static_cast<Int>(format),
|
|
static_cast<Int>(sampleCount),
|
|
renderbuffer->GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
|
|
VmaAllocationCreateInfo allocationInfo{};
|
|
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
|
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
|
|
"vmaCreateImage(renderbuffer)");
|
|
++m_renderbufferImageEpoch; // a new attachment image invalidates cached render passes
|
|
|
|
VkImageViewCreateInfo viewInfo{};
|
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
viewInfo.image = resource.image;
|
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
viewInfo.format = format;
|
|
viewInfo.components = {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G,
|
|
VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A};
|
|
viewInfo.subresourceRange.aspectMask = aspect;
|
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
|
viewInfo.subresourceRange.levelCount = 1;
|
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
|
viewInfo.subresourceRange.layerCount = 1;
|
|
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
|
|
"vkCreateImageView(renderbuffer)");
|
|
if (hasUnormTwin) {
|
|
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
|
|
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
|
|
"vkCreateImageView(renderbuffer unorm twin)");
|
|
}
|
|
|
|
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
resource.format = format;
|
|
resource.aspect = aspect;
|
|
resource.extent = {imageInfo.extent.width, imageInfo.extent.height};
|
|
resource.sampleCount = sampleCount;
|
|
resource.internalFormat = internalFormat;
|
|
resource.samples = renderbuffer->GetSamples();
|
|
return &resource;
|
|
}
|
|
|
|
Bool VkRenderPassManager::GetPendingRenderbufferClear(
|
|
MG_State::GLState::RenderbufferObject* renderbuffer, ClearAttachmentPayload& outPayload) const {
|
|
if (renderbuffer == nullptr) {
|
|
return false;
|
|
}
|
|
auto it = m_pendingRenderbufferClears.find(renderbuffer);
|
|
if (it == m_pendingRenderbufferClears.end()) {
|
|
return false;
|
|
}
|
|
const auto liveRenderbuffer = it->second.renderbuffer.lock();
|
|
if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
|
|
return false;
|
|
}
|
|
outPayload = it->second.payload;
|
|
return outPayload.mask != 0;
|
|
}
|
|
|
|
Bool VkRenderPassManager::HasPendingRenderbufferClear(
|
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) const {
|
|
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) {
|
|
return false;
|
|
}
|
|
ClearAttachmentPayload payload{};
|
|
return GetPendingRenderbufferClear(attachment.GetRenderbuffer().get(), payload);
|
|
}
|
|
|
|
void VkRenderPassManager::QueueRenderbufferClear(
|
|
const ClearAttachmentPayload& clearPayload,
|
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
|
if (clearPayload.mask == 0 || !attachment.IsRenderbuffer() || !attachment.IsComplete()) {
|
|
return;
|
|
}
|
|
const auto renderbuffer = attachment.GetRenderbuffer();
|
|
if (!renderbuffer) {
|
|
return;
|
|
}
|
|
auto& pending = m_pendingRenderbufferClears[renderbuffer.get()];
|
|
pending.renderbuffer = renderbuffer;
|
|
pending.payload.mask |= clearPayload.mask;
|
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
|
// The whole colour description, not just the float vector: an integer clear keeps its
|
|
// value in colorInt/colorUint, and dropping the encoding here would leave the pending
|
|
// clear reading as an all-zero float one.
|
|
pending.payload.color = clearPayload.color;
|
|
pending.payload.colorEncoding = clearPayload.colorEncoding;
|
|
pending.payload.colorInt = clearPayload.colorInt;
|
|
pending.payload.colorUint = clearPayload.colorUint;
|
|
}
|
|
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
|
pending.payload.depth = clearPayload.depth;
|
|
}
|
|
if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
|
pending.payload.stencil = clearPayload.stencil;
|
|
}
|
|
}
|
|
|
|
void VkRenderPassManager::QueueRenderbufferClear(
|
|
GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
|
const MG_State::GLState::FramebufferObject& drawFbo) {
|
|
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
|
// Color renderbuffer draw buffers take the framebuffer-level clear too; texture
|
|
// attachments are skipped by the per-attachment overload's IsRenderbuffer guard.
|
|
for (const auto attachmentType : drawFbo.GetDrawBuffers()) {
|
|
if (attachmentType == FramebufferAttachmentType::None) {
|
|
continue;
|
|
}
|
|
QueueRenderbufferClear(
|
|
ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color},
|
|
drawFbo.GetAttachment(attachmentType));
|
|
}
|
|
}
|
|
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
|
QueueRenderbufferClear(
|
|
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
|
|
drawFbo.GetAttachment(FramebufferAttachmentType::Depth));
|
|
}
|
|
if ((mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
|
QueueRenderbufferClear(
|
|
ClearAttachmentPayload{.mask = GL_STENCIL_BUFFER_BIT, .stencil = clearPayload.stencil},
|
|
drawFbo.GetAttachment(FramebufferAttachmentType::Stencil));
|
|
}
|
|
}
|
|
|
|
void VkRenderPassManager::PopPendingRenderbufferClear(
|
|
MG_State::GLState::RenderbufferObject* renderbuffer) {
|
|
if (renderbuffer != nullptr) {
|
|
m_pendingRenderbufferClears.erase(renderbuffer);
|
|
}
|
|
}
|
|
|
|
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
|
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
|
Bool includeDefaultFboDepthStencil) {
|
|
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
|
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
|
if (isDefaultFbo) {
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
|
}
|
|
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
|
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
|
const Bool framebufferSrgbEnabled =
|
|
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
|
auto& drawBuffers = fbo.GetDrawBuffers();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
|
auto readBuffer = fbo.GetReadBuffer();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &readBuffer, sizeof(FramebufferAttachmentType)));
|
|
Int validDrawBufCount = 0;
|
|
for (Int i = 0; i < drawBuffers.size(); ++i) {
|
|
auto drawbuf = drawBuffers[i];
|
|
if (drawbuf != FramebufferAttachmentType::None)
|
|
validDrawBufCount = std::max(validDrawBufCount, i + 1);
|
|
}
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &validDrawBufCount, sizeof(validDrawBufCount)));
|
|
|
|
auto combineFramebufferAttachmentObjHash = [&](FramebufferAttachmentType attachment) {
|
|
auto& att = fbo.GetAttachment(attachment);
|
|
|
|
Int type = 0;
|
|
if (att.IsEmpty()) type = 0;
|
|
else if (att.IsTexture()) type = 1;
|
|
else if (att.IsRenderbuffer()) type = 2;
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &type, sizeof(type)));
|
|
void* contentPtr = nullptr;
|
|
if (att.IsTexture())
|
|
contentPtr = att.GetTexture().get();
|
|
else if (att.IsRenderbuffer())
|
|
contentPtr = att.GetRenderbuffer().get();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &contentPtr, sizeof(contentPtr)));
|
|
if (att.IsTexture()) {
|
|
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
|
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
|
|
att.GetTextureLevel()));
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
|
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
|
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
|
|
att.GetTextureLayer()));
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
|
const Bool textureLayered = att.IsLayered();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
|
|
|
Uint64 imageIdentity = 0;
|
|
auto* texture = att.GetTexture().get();
|
|
auto* resource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
|
if (resource != nullptr) {
|
|
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount)));
|
|
} else {
|
|
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
|
}
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity)));
|
|
}
|
|
|
|
if (includePendingClear && att.IsTexture()) {
|
|
auto* texture = att.GetTexture().get();
|
|
const auto pendingClearKey = VkClearManager::MakePendingClearKey(att);
|
|
auto hasClear = m_clearManager.HasPendingClear(pendingClearKey);
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear)));
|
|
if (hasClear) {
|
|
ClearAttachmentPayload clearPayload{};
|
|
Bool hasPayload = m_clearManager.GetPendingClear(pendingClearKey, clearPayload);
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload)));
|
|
if (hasPayload) {
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask)));
|
|
}
|
|
}
|
|
|
|
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
if (isDefaultFbo) {
|
|
const Bool isDefaultColorAttachment =
|
|
attachment == FramebufferAttachmentType::Color0 ||
|
|
(attachment >= FramebufferAttachmentType::FrontLeft &&
|
|
attachment <= FramebufferAttachmentType::BackRight);
|
|
if (isDefaultColorAttachment) {
|
|
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
|
// Content validity feeds the attachment's loadOp (see the
|
|
// creation path), so it must key the cache as well.
|
|
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
|
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
}
|
|
} else if (attachment == FramebufferAttachmentType::Depth ||
|
|
attachment == FramebufferAttachmentType::Stencil) {
|
|
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
|
|
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
|
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
}
|
|
}
|
|
} else {
|
|
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
|
if (textureResource != nullptr) {
|
|
currentLayout = textureResource->layout;
|
|
}
|
|
}
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout)));
|
|
}
|
|
if (att.IsRenderbuffer() && att.GetRenderbuffer()) {
|
|
const auto& renderbuffer = att.GetRenderbuffer();
|
|
const auto internalFormat = renderbuffer->GetInternalFormat();
|
|
const Int width = renderbuffer->GetWidth();
|
|
const Int height = renderbuffer->GetHeight();
|
|
const Int samples = renderbuffer->GetSamples();
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &internalFormat, sizeof(internalFormat)));
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &width, sizeof(width)));
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &height, sizeof(height)));
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &samples, sizeof(samples)));
|
|
|
|
Uint64 imageIdentity = 0;
|
|
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
auto* resource = GetOrCreateRenderbufferResource(renderbuffer);
|
|
if (resource != nullptr) {
|
|
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
|
currentLayout = resource->layout;
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount)));
|
|
} else {
|
|
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
|
}
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity)));
|
|
|
|
if (includePendingClear) {
|
|
const Bool hasClear = HasPendingRenderbufferClear(att);
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear)));
|
|
if (hasClear) {
|
|
ClearAttachmentPayload clearPayload{};
|
|
const Bool hasPayload = GetPendingRenderbufferClear(renderbuffer.get(), clearPayload);
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload)));
|
|
if (hasPayload) {
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask)));
|
|
}
|
|
}
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout)));
|
|
}
|
|
}
|
|
};
|
|
|
|
for (Int i = 0; i < validDrawBufCount; ++i) {
|
|
auto drawbuf = drawBuffers[i];
|
|
combineFramebufferAttachmentObjHash(drawbuf);
|
|
}
|
|
|
|
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
|
// entirely, so it must hash differently from the depth-full flavor.
|
|
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
|
|
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
|
if (depthStencilIncluded) {
|
|
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
|
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
|
}
|
|
|
|
return XXH64_digest(m_hashState);
|
|
}
|
|
|
|
RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
|
Uint32 swapchainImageIndex,
|
|
Bool drawUsesDepthStencil) {
|
|
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
|
// depth attachment when the caller needs it, when a depth/stencil clear is
|
|
// pending, or when the active pass already carries it (escalate-only, so
|
|
// alternating depth-less draws never split an established depth pass).
|
|
Bool includeDefaultFboDepthStencil = true;
|
|
if (fbo.IsDefaultFramebuffer()) {
|
|
Bool activeDefaultHasDepthStencil = false;
|
|
if (const auto* active = GetActiveRenderPass()) {
|
|
Bool activeIsSwapchainPass = false;
|
|
Bool activeHasSwapchainDepthStencil = false;
|
|
for (const auto& tracked : active->trackedAttachmentLayouts) {
|
|
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
|
|
activeHasSwapchainDepthStencil |=
|
|
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
|
|
}
|
|
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
|
|
}
|
|
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
|
|
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
|
|
const Bool pendingDepthStencilClear =
|
|
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
|
|
HasPendingRenderbufferClear(defaultDepthAtt) ||
|
|
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
|
|
HasPendingRenderbufferClear(defaultStencilAtt);
|
|
includeDefaultFboDepthStencil =
|
|
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
|
|
}
|
|
|
|
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
|
|
const auto& drawBuffers = fbo.GetDrawBuffers();
|
|
for (auto attachment : drawBuffers) {
|
|
if (attachment == FramebufferAttachmentType::None) {
|
|
continue;
|
|
}
|
|
|
|
const auto& att = fbo.GetAttachment(attachment);
|
|
if (att.IsTexture() && m_clearManager.HasPendingClear(att)) {
|
|
return true;
|
|
}
|
|
if (HasPendingRenderbufferClear(att)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const auto& depthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
|
|
if (depthAtt.IsTexture() && m_clearManager.HasPendingClear(depthAtt)) {
|
|
return true;
|
|
}
|
|
if (HasPendingRenderbufferClear(depthAtt)) {
|
|
return true;
|
|
}
|
|
|
|
const auto& stencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
|
|
if (stencilAtt.IsTexture() && m_clearManager.HasPendingClear(stencilAtt)) {
|
|
return true;
|
|
}
|
|
if (HasPendingRenderbufferClear(stencilAtt)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// retrieve from cache first
|
|
auto* activeRenderPass = GetActiveRenderPass();
|
|
|
|
// Dirty-flag state tracking: when the framebuffer state is provably unchanged since the
|
|
// render pass was last resolved, the active render pass is still valid -> skip the
|
|
// expensive per-draw ComputeHash (XXH64 over every attachment + a SyncTexture per
|
|
// attachment). Correctness signals: same FBO object + GetObjectVersion (attachment /
|
|
// draw-buffer / read-buffer changes bump it), same swapchain image, no attachment VkImage
|
|
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
|
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
|
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
|
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
|
|
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
|
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
|
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
|
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
|
|
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
|
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
|
if (activeIt != m_renderPasses.end()) {
|
|
activeIt->second.lastUsedFrame = m_frameCounter;
|
|
return &activeIt->second;
|
|
}
|
|
}
|
|
|
|
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil);
|
|
if (activeRenderPass != nullptr &&
|
|
activeRenderPass->CompatibleWith(compatibilityHash) &&
|
|
!hasPendingClearOnFramebuffer()) {
|
|
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
|
MOBILEGL_ASSERT(activeIt != m_renderPasses.end(),
|
|
"GetOrCreateRenderPass: active render pass hash=0x%llx is missing from cache",
|
|
static_cast<unsigned long long>(activeRenderPass->hash));
|
|
// Populate the fast-path memo so subsequent unchanged draws skip ComputeHash. Read the
|
|
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
|
m_rpFastValid = true;
|
|
m_rpFastFbo = &fbo;
|
|
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
|
|
m_rpFastFboVersion = fbo.GetObjectVersion();
|
|
m_rpFastSwapchainIndex = swapchainImageIndex;
|
|
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
|
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
|
m_rpFastRenderPassHash = activeRenderPass->hash;
|
|
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
|
activeIt->second.lastUsedFrame = m_frameCounter;
|
|
return &activeIt->second;
|
|
}
|
|
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
|
auto it = m_renderPasses.find(hash);
|
|
if (it != m_renderPasses.end()) {
|
|
it->second.lastUsedFrame = m_frameCounter;
|
|
return &it->second;
|
|
}
|
|
|
|
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
|
// Color attachment
|
|
auto& drawbufs = fbo.GetDrawBuffers();
|
|
const Uint32 colorAttachmentSlotCount = static_cast<Uint32>(drawbufs.size());
|
|
|
|
const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
|
|
// Default framebuffer attachments are frontend placeholders; Vulkan framebuffer extent must match the swapchain.
|
|
const IntVec2 defaultFramebufferExtent =
|
|
ResolveRenderPassFramebufferExtent(isDefaultFbo, {0, 0, 0}, swapchainExtent);
|
|
Int width = defaultFramebufferExtent.x();
|
|
Int height = defaultFramebufferExtent.y();
|
|
Vector<VkAttachmentDescription> attachmentDescriptions;
|
|
attachmentDescriptions.reserve(colorAttachmentSlotCount + 1);
|
|
// Keep the full GL draw buffer slot span so fragment outputs targeting GL_NONE map to VK_ATTACHMENT_UNUSED.
|
|
Vector<VkAttachmentReference> colorAttachmentRefs(colorAttachmentSlotCount);
|
|
for (auto& attachmentRef : colorAttachmentRefs) {
|
|
attachmentRef.attachment = VK_ATTACHMENT_UNUSED;
|
|
attachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
}
|
|
Vector<PendingClearAttachmentInfo> pendingClearAttachments;
|
|
pendingClearAttachments.reserve(colorAttachmentSlotCount + 1);
|
|
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
|
|
trackedAttachmentLayouts.reserve(colorAttachmentSlotCount + 1);
|
|
auto& textureResources = RenderPassEntry::s_textureResourcesScratch;
|
|
textureResources.clear();
|
|
textureResources.reserve(colorAttachmentSlotCount + 1);
|
|
Vector<VkImageView> attachmentViews;
|
|
attachmentViews.reserve(colorAttachmentSlotCount + 1);
|
|
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
Bool hasRenderPassSampleCount = false;
|
|
Uint32 framebufferLayers = 1;
|
|
const auto adoptRenderPassSampleCount = [&](VkSampleCountFlagBits sampleCount,
|
|
const char* attachmentKind,
|
|
Int attachmentId) {
|
|
if (!hasRenderPassSampleCount) {
|
|
renderPassSampleCount = sampleCount;
|
|
hasRenderPassSampleCount = true;
|
|
return;
|
|
}
|
|
MOBILEGL_ASSERT(renderPassSampleCount == sampleCount,
|
|
"GetOrCreateRenderPass: mismatched sample count %d on %s attachment %d (expected %d)",
|
|
static_cast<Int>(sampleCount), attachmentKind, attachmentId,
|
|
static_cast<Int>(renderPassSampleCount));
|
|
};
|
|
// This should automatically work on default & offscreen FBO
|
|
// assuming default FBO has the right param
|
|
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
|
|
auto drawbuf = drawbufs[i];
|
|
|
|
// Renderbuffer color attachments mirror the texture path below, with the
|
|
// resource (image/view/format/layout) coming from the render-pass manager's
|
|
// renderbuffer store instead of the texture manager.
|
|
if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) {
|
|
const auto& rbAtt = fbo.GetAttachment(drawbuf);
|
|
if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) {
|
|
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
|
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
|
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
|
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
|
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
|
continue;
|
|
}
|
|
|
|
const Uint32 rbAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
|
attachmentDescriptions.emplace_back();
|
|
VkAttachmentDescription& rbDesc = attachmentDescriptions.back();
|
|
|
|
ClearAttachmentPayload rbClearPayload{};
|
|
Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) &&
|
|
(rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0;
|
|
if (rbHasClear &&
|
|
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
|
|
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
|
ForceOpaqueClearAlpha(rbClearPayload);
|
|
}
|
|
|
|
const VkImageLayout trackedRbLayout = rbResource->layout;
|
|
const Bool rbFramebufferSrgb =
|
|
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
|
const VkFormat rbAttachmentFormat =
|
|
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
|
rbDesc.flags = 0;
|
|
rbDesc.format = rbAttachmentFormat;
|
|
rbDesc.samples = rbResource->sampleCount;
|
|
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
|
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
|
: VK_ATTACHMENT_LOAD_OP_LOAD);
|
|
rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
|
|
VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout;
|
|
rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
adoptRenderPassSampleCount(rbResource->sampleCount, "color",
|
|
static_cast<Int>(renderbuffer->GetExternalIndex()));
|
|
|
|
if (rbHasClear) {
|
|
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
|
.attachmentIndex = rbAttachmentIndex,
|
|
.colorAttachmentSlot = i,
|
|
.renderbuffer = renderbuffer.get(),
|
|
.hasInlinePayload = true,
|
|
.inlinePayload = rbClearPayload,
|
|
});
|
|
}
|
|
|
|
if (width == 0)
|
|
width = static_cast<Int>(rbResource->extent.width);
|
|
if (height == 0)
|
|
height = static_cast<Int>(rbResource->extent.height);
|
|
|
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
|
.target = TrackedAttachmentTarget::Renderbuffer,
|
|
.renderbuffer = renderbuffer,
|
|
.finalLayout = rbDesc.finalLayout,
|
|
});
|
|
textureResources.emplace_back(nullptr);
|
|
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
|
: rbResource->view);
|
|
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
|
|
"%u on FBO %u; declining the render pass",
|
|
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
|
|
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
|
|
if (texture == nullptr)
|
|
continue;
|
|
|
|
auto& att = fbo.GetAttachment(drawbuf);
|
|
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
|
|
const auto textureTarget = texture->GetTarget();
|
|
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
|
attachmentDescriptions.emplace_back();
|
|
|
|
// Color attachment description
|
|
VkAttachmentDescription& desc = attachmentDescriptions.back();
|
|
switch (textureTarget) {
|
|
case TextureTarget::Texture1D:
|
|
case TextureTarget::Texture1DArray:
|
|
case TextureTarget::Texture2D:
|
|
case TextureTarget::Texture2DArray:
|
|
case TextureTarget::Texture2DMultisample:
|
|
case TextureTarget::Texture2DMultisampleArray:
|
|
case TextureTarget::Texture3D:
|
|
case TextureTarget::TextureCubeMap:
|
|
case TextureTarget::TextureCubeMapArray:
|
|
case TextureTarget::TextureRectangle: {
|
|
desc.flags = 0;
|
|
desc.format = isDefaultFbo ?
|
|
m_swapchainObject.GetSurfaceFormat().format :
|
|
MG_Util::ConvertTextureInternalFormatToVkEnum(
|
|
texture->GetFormat());
|
|
VkSampleCountFlagBits attachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
ClearAttachmentPayload clearPayload{};
|
|
Bool hasClear = m_clearManager.GetPendingClear(att, clearPayload);
|
|
VkImageLayout trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
desc.loadOp = hasClear ?
|
|
VK_ATTACHMENT_LOAD_OP_CLEAR :
|
|
VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
desc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
desc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
desc.finalLayout = isDefaultFbo ?
|
|
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR :
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
if (hasClear) {
|
|
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
|
.attachmentIndex = attachmentIndex,
|
|
.colorAttachmentSlot = i,
|
|
.key = VkClearManager::MakePendingClearKey(att)
|
|
});
|
|
}
|
|
// Same remap as ResolveAttachmentLayerCount, for the same reason: a
|
|
// 1D-array attachment's GL height is its layer count, and using it as the
|
|
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
|
|
// image it is built over.
|
|
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
|
|
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
|
|
if (width == 0)
|
|
width = attachmentExtent.x();
|
|
if (height == 0)
|
|
height = attachmentExtent.y();
|
|
|
|
if (isDefaultFbo) {
|
|
const auto& swapchainViews = m_swapchainObject.GetImageViews();
|
|
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
|
|
"GetOrCreateRenderPass: swapchain image index out of range");
|
|
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
|
// EGL: a presented color buffer's content is undefined when its
|
|
// image comes back around (EGL_BUFFER_DESTROYED, the default
|
|
// swap behaviour) - skip the tile load instead of reloading
|
|
// stale pixels nobody may rely on.
|
|
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
|
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
}
|
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
|
.target = TrackedAttachmentTarget::SwapchainColor,
|
|
.swapchainImageIndex = swapchainImageIndex,
|
|
.finalLayout = desc.finalLayout,
|
|
});
|
|
attachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
textureResources.emplace_back(nullptr);
|
|
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
|
|
} else {
|
|
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
|
if (textureResource == nullptr) {
|
|
// SyncTextureResource legitimately declines - an unsupported format,
|
|
// sample count or image-flag combination, or a vkCreateImage the driver
|
|
// refused. There is no image to attach, so there is no render pass.
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
|
|
"attachment %u on FBO %u; declining the render pass",
|
|
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
textureResources.emplace_back(textureResource);
|
|
desc.format = ResolveSrgbAttachmentWriteFormat(
|
|
textureResource->format,
|
|
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
|
attachmentSampleCount = textureResource->sampleCount;
|
|
trackedColorLayout = textureResource->layout;
|
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
|
.target = TrackedAttachmentTarget::Texture,
|
|
.texture = att.GetTexture(),
|
|
.textureRaw = att.GetTexture().get(),
|
|
.textureMipLevel = attachmentMipLevel,
|
|
.finalLayout = desc.finalLayout,
|
|
});
|
|
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(att);
|
|
const Uint32 layerCount = ResolveAttachmentLayerCount(att);
|
|
framebufferLayers = std::max(framebufferLayers, layerCount);
|
|
const VkImageViewType attachmentViewType = ResolveAttachmentViewType(att, *textureResource);
|
|
attachmentViews.emplace_back(
|
|
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
|
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
|
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
|
|
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
|
|
"render pass",
|
|
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
|
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
|
|
fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
}
|
|
desc.samples = attachmentSampleCount;
|
|
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
|
|
|
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
|
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
|
"using LOAD_OP_DONT_CARE",
|
|
texture->GetExternalIndex());
|
|
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
}
|
|
desc.initialLayout = (hasClear || trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
|
|
VK_IMAGE_LAYOUT_UNDEFINED :
|
|
trackedColorLayout;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
MOBILEGL_ASSERT(false, "Unsupported texture target");
|
|
}
|
|
|
|
// Attachment reference
|
|
VkAttachmentReference& attachmentRef = colorAttachmentRefs[i];
|
|
attachmentRef.attachment = attachmentIndex;
|
|
attachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
}
|
|
|
|
// Depth/stencil attachment description
|
|
auto& depthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
|
|
auto& stencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
|
|
VkAttachmentDescription depthAttachmentDescription;
|
|
VkAttachmentReference depthAttachmentRef;
|
|
depthAttachmentRef.attachment = VK_ATTACHMENT_UNUSED;
|
|
depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
VkTextureManager::TextureResource* depthTextureResource = nullptr;
|
|
RenderbufferResource* depthRenderbufferResource = nullptr;
|
|
const auto isUsableDepthStencilAttachment = [](const auto& attachment) {
|
|
return attachment.IsComplete() && (attachment.IsTexture() || attachment.IsRenderbuffer());
|
|
};
|
|
const auto sameDepthStencilAttachmentObject = [](const auto& a, const auto& b) {
|
|
if (a.IsTexture() && b.IsTexture()) {
|
|
return a.GetTexture().get() == b.GetTexture().get() &&
|
|
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
|
|
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
|
|
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
|
|
}
|
|
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
|
|
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
|
|
}
|
|
return false;
|
|
};
|
|
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
|
|
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
|
|
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
|
|
// and their content is undefined anyway (EGL swap), so drop the attachment
|
|
// and its whole tile load + store.
|
|
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
|
|
selectedDepthStencilAttachment = nullptr;
|
|
}
|
|
const Bool hasDistinctDepthAndStencilAttachments =
|
|
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
|
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
|
if (hasDistinctDepthAndStencilAttachments) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
|
fbo.GetExternalIndex());
|
|
}
|
|
if (selectedDepthStencilAttachment != nullptr) {
|
|
const Uint32 depthAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
|
ClearAttachmentPayload clearPayload{};
|
|
Bool hasClear = selectedDepthStencilAttachment->IsTexture()
|
|
? m_clearManager.GetPendingClear(*selectedDepthStencilAttachment, clearPayload)
|
|
: GetPendingRenderbufferClear(selectedDepthStencilAttachment->GetRenderbuffer().get(), clearPayload);
|
|
Bool clearDepth = hasClear && (clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0;
|
|
Bool clearStencil = hasClear && (clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0;
|
|
VkImageLayout trackedDepthLayout = isDefaultFbo ?
|
|
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
|
|
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
|
|
// undefined after a swap, so the first default-FBO pass of a frame can
|
|
// skip the depth/stencil tile load outright.
|
|
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
|
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
}
|
|
depthAttachmentDescription.flags = 0;
|
|
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
Int depthAttachmentId = 0;
|
|
IntVec2 attachmentExtent = {0, 0};
|
|
if (isDefaultFbo) {
|
|
depthAttachmentDescription.format = m_swapchainObject.GetDepthStencilFormat();
|
|
depthAttachmentId = 0;
|
|
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
|
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
|
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
|
|
if (depthTextureResource == nullptr) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
|
|
"attachment of FBO %u; declining the render pass",
|
|
texture.GetExternalIndex(), fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
trackedDepthLayout = depthTextureResource->layout;
|
|
depthAttachmentDescription.format = depthTextureResource->format;
|
|
depthAttachmentSampleCount = depthTextureResource->sampleCount;
|
|
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
|
|
attachmentExtent = ResolveRenderPassFramebufferExtent(
|
|
isDefaultFbo,
|
|
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
|
|
swapchainExtent);
|
|
} else {
|
|
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
|
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
|
|
if (depthRenderbufferResource == nullptr) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
|
|
"attachment of FBO %u; declining the render pass",
|
|
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
trackedDepthLayout = depthRenderbufferResource->layout;
|
|
depthAttachmentDescription.format = depthRenderbufferResource->format;
|
|
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
|
|
depthAttachmentId = static_cast<Int>(renderbuffer->GetExternalIndex());
|
|
attachmentExtent = {static_cast<Int>(depthRenderbufferResource->extent.width),
|
|
static_cast<Int>(depthRenderbufferResource->extent.height)};
|
|
}
|
|
depthAttachmentDescription.samples = depthAttachmentSampleCount;
|
|
adoptRenderPassSampleCount(depthAttachmentSampleCount, "depth/stencil", depthAttachmentId);
|
|
const auto loadInfo =
|
|
ResolveDepthStencilAttachmentLoadInfo(trackedDepthLayout, clearDepth, clearStencil);
|
|
depthAttachmentDescription.loadOp = loadInfo.depthLoadOp;
|
|
depthAttachmentDescription.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
depthAttachmentDescription.stencilLoadOp = loadInfo.stencilLoadOp;
|
|
depthAttachmentDescription.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
|
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
|
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
|
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
|
depthAttachmentId);
|
|
}
|
|
if (hasClear) {
|
|
if (selectedDepthStencilAttachment->IsTexture()) {
|
|
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
|
.attachmentIndex = depthAttachmentIndex,
|
|
.key = VkClearManager::MakePendingClearKey(*selectedDepthStencilAttachment)
|
|
});
|
|
} else {
|
|
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
|
.attachmentIndex = depthAttachmentIndex,
|
|
.renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer().get(),
|
|
.hasInlinePayload = true,
|
|
.inlinePayload = clearPayload
|
|
});
|
|
}
|
|
}
|
|
if (isDefaultFbo) {
|
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
|
.target = TrackedAttachmentTarget::SwapchainDepthStencil,
|
|
.swapchainImageIndex = swapchainImageIndex,
|
|
.finalLayout = depthAttachmentDescription.finalLayout,
|
|
});
|
|
attachmentViews.emplace_back(m_swapchainObject.GetDepthStencilImageView(swapchainImageIndex));
|
|
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
|
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
|
const Uint32 attachmentMipLevel =
|
|
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
|
|
selectedDepthStencilAttachment->GetTextureLevel());
|
|
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
|
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
|
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
|
|
texture.GetExternalIndex());
|
|
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
|
depthAttachmentDescription.stencilLoadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
|
"GetOrCreateRenderPass: stencil attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
|
|
texture.GetExternalIndex());
|
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
|
.target = TrackedAttachmentTarget::Texture,
|
|
.texture = selectedDepthStencilAttachment->GetTexture(),
|
|
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
|
|
.textureMipLevel = attachmentMipLevel,
|
|
.finalLayout = depthAttachmentDescription.finalLayout,
|
|
});
|
|
textureResources.emplace_back(depthTextureResource);
|
|
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(*selectedDepthStencilAttachment);
|
|
const Uint32 layerCount = ResolveAttachmentLayerCount(*selectedDepthStencilAttachment);
|
|
framebufferLayers = std::max(framebufferLayers, layerCount);
|
|
const VkImageViewType attachmentViewType =
|
|
ResolveAttachmentViewType(*selectedDepthStencilAttachment, *depthTextureResource);
|
|
attachmentViews.emplace_back(
|
|
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
|
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
|
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
|
|
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
|
|
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
|
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
|
|
fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
if (width == 0 || height == 0) {
|
|
width = attachmentExtent.x();
|
|
height = attachmentExtent.y();
|
|
}
|
|
} else {
|
|
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
|
MOBILEGL_ASSERT(depthRenderbufferResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
|
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
|
"GetOrCreateRenderPass: depth renderbuffer %u has undefined tracked layout with LOAD_OP_LOAD",
|
|
renderbuffer->GetExternalIndex());
|
|
MOBILEGL_ASSERT(depthRenderbufferResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
|
depthAttachmentDescription.stencilLoadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
|
"GetOrCreateRenderPass: stencil renderbuffer %u has undefined tracked layout with LOAD_OP_LOAD",
|
|
renderbuffer->GetExternalIndex());
|
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
|
.target = TrackedAttachmentTarget::Renderbuffer,
|
|
.renderbuffer = renderbuffer,
|
|
.finalLayout = depthAttachmentDescription.finalLayout,
|
|
});
|
|
textureResources.emplace_back(nullptr);
|
|
attachmentViews.emplace_back(depthRenderbufferResource->view);
|
|
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
|
|
"attachment of FBO %u; declining the render pass",
|
|
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
if (width == 0 || height == 0) {
|
|
width = attachmentExtent.x();
|
|
height = attachmentExtent.y();
|
|
}
|
|
}
|
|
attachmentDescriptions.emplace_back(depthAttachmentDescription);
|
|
depthAttachmentRef.attachment = depthAttachmentIndex;
|
|
}
|
|
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
|
|
|
|
// Declare only the used colour-reference span. The GL draw-buffer array
|
|
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
|
|
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
|
|
// per-pixel render-backend/export path from the DECLARED count, so every
|
|
// fragment of every pass paid the 8-target export cost (measured on
|
|
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
|
|
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
|
|
// Interior GL_NONE holes keep their slots so fragment-output locations
|
|
// still line up; a fragment output at a location past the trimmed count
|
|
// is discarded, which is exactly GL's semantic for writing to a draw
|
|
// buffer set to GL_NONE.
|
|
while (!colorAttachmentRefs.empty() &&
|
|
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
|
|
colorAttachmentRefs.pop_back();
|
|
}
|
|
|
|
// Subpass
|
|
VkSubpassDescription subpassDesc;
|
|
subpassDesc.flags = 0;
|
|
subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
subpassDesc.inputAttachmentCount = 0;
|
|
subpassDesc.pInputAttachments = nullptr;
|
|
subpassDesc.colorAttachmentCount = colorAttachmentRefs.size();
|
|
subpassDesc.pColorAttachments = colorAttachmentRefs.data();
|
|
subpassDesc.pResolveAttachments = nullptr;
|
|
subpassDesc.pDepthStencilAttachment = hasDepthStencilAttachment ? &depthAttachmentRef : VK_NULL_HANDLE;
|
|
subpassDesc.preserveAttachmentCount = 0;
|
|
subpassDesc.pPreserveAttachments = nullptr;
|
|
|
|
// External subpass dependencies. Without them there is NO execution/memory
|
|
// dependency between consecutive render passes (or a pass and a transfer)
|
|
// touching the same attachments when the image layout does not change — the
|
|
// layout-transition helper no-ops on identical layouts and no other barrier
|
|
// exists. Tile-based GPUs then race tile loads against the previous pass's
|
|
// stores (multi-pass FBO chains like MC 26.3's OIT flicker on Adreno).
|
|
// Conservative both-ways dependencies: prior writes (attachment output,
|
|
// depth/stencil, transfer, shader) are made visible to this pass's loads,
|
|
// and this pass's attachment writes to subsequent sampling/transfer/loads.
|
|
VkSubpassDependency subpassDependencies[2];
|
|
subpassDependencies[0] = {};
|
|
subpassDependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
subpassDependencies[0].dstSubpass = 0;
|
|
subpassDependencies[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
|
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
subpassDependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
|
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
subpassDependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
|
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
subpassDependencies[0].dstAccessMask =
|
|
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
|
VK_ACCESS_SHADER_READ_BIT;
|
|
subpassDependencies[0].dependencyFlags = 0;
|
|
subpassDependencies[1] = {};
|
|
subpassDependencies[1].srcSubpass = 0;
|
|
subpassDependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
|
subpassDependencies[1].srcStageMask =
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
|
subpassDependencies[1].srcAccessMask =
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
|
subpassDependencies[1].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
|
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
subpassDependencies[1].dstAccessMask =
|
|
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
|
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
subpassDependencies[1].dependencyFlags = 0;
|
|
|
|
// Render Pass
|
|
VkRenderPassCreateInfo renderPassCreateInfo;
|
|
renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
|
renderPassCreateInfo.pNext = VK_NULL_HANDLE;
|
|
renderPassCreateInfo.flags = 0;
|
|
renderPassCreateInfo.attachmentCount = attachmentDescriptions.size();
|
|
renderPassCreateInfo.pAttachments = attachmentDescriptions.data();
|
|
renderPassCreateInfo.subpassCount = 1;
|
|
renderPassCreateInfo.pSubpasses = &subpassDesc;
|
|
renderPassCreateInfo.dependencyCount = 2;
|
|
renderPassCreateInfo.pDependencies = subpassDependencies;
|
|
|
|
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
|
|
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
|
|
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
|
|
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
|
|
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
|
|
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
|
|
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
|
|
// line on every draw for the life of the process - a decline caches nothing, so every
|
|
// later draw to the same framebuffer re-enters this path and fails again.
|
|
VkRenderPass renderPass = VK_NULL_HANDLE;
|
|
const VkResult renderPassResult =
|
|
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
|
|
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
|
|
"render pass",
|
|
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
|
|
fbo.GetExternalIndex());
|
|
return nullptr;
|
|
}
|
|
|
|
// Framebuffer
|
|
VkFramebufferCreateInfo framebufferCreateInfo;
|
|
framebufferCreateInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
|
framebufferCreateInfo.pNext = nullptr;
|
|
framebufferCreateInfo.flags = 0;
|
|
framebufferCreateInfo.renderPass = renderPass;
|
|
framebufferCreateInfo.attachmentCount = attachmentViews.size();
|
|
framebufferCreateInfo.pAttachments = attachmentViews.data();
|
|
framebufferCreateInfo.width = width;
|
|
framebufferCreateInfo.height = height;
|
|
framebufferCreateInfo.layers = framebufferLayers;
|
|
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
|
|
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
|
const VkResult framebufferResult =
|
|
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
|
|
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
|
|
// The render pass has no entry to own it yet, so it is destroyed here rather than
|
|
// leaked - RenderPassEntry's destructor is the only other thing that would.
|
|
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
|
|
"%u attachments, %u layers); declining the render pass",
|
|
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
|
|
fbo.GetExternalIndex(), width, height,
|
|
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
|
|
vkDestroyRenderPass(m_device, renderPass, nullptr);
|
|
return nullptr;
|
|
}
|
|
IntVec2 extent = {width, height};
|
|
RenderPassEntry renderPassEntry {
|
|
hash,
|
|
renderPass,
|
|
framebuffer,
|
|
compatibilityHash,
|
|
Move(pendingClearAttachments),
|
|
Move(trackedAttachmentLayouts),
|
|
static_cast<Uint32>(attachmentViews.size()),
|
|
static_cast<Uint32>(colorAttachmentRefs.size()),
|
|
hasDepthStencilAttachment,
|
|
renderPassSampleCount,
|
|
extent,
|
|
framebufferLayers };
|
|
MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d",
|
|
static_cast<unsigned long long>(hash),
|
|
static_cast<unsigned long long>(compatibilityHash),
|
|
renderPassEntry.attachmentCount,
|
|
renderPassEntry.colorAttachmentCount,
|
|
static_cast<Int>(renderPassEntry.sampleCount),
|
|
extent.x(),
|
|
extent.y());
|
|
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
|
insertedIt->second.lastUsedFrame = m_frameCounter;
|
|
return &insertedIt->second;
|
|
}
|
|
|
|
void VkRenderPassManager::OnPresent() {
|
|
++m_frameCounter;
|
|
|
|
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
|
|
// is O(#renderbuffer resources) — single digits in practice — and per-frame
|
|
// invocation keeps dead-resource reclaim latency at the aging bound instead of
|
|
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
|
|
// site never runs again once an app stops using renderbuffers).
|
|
CollectRenderbufferGarbage();
|
|
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
|
|
|
|
// Sweep occasionally; evict entries whose last use is far past every
|
|
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
|
// safely (RenderPassEntry's destructor releases the handles).
|
|
constexpr Uint64 kSweepInterval = 256;
|
|
constexpr Uint64 kRetireAgeFrames = 1024;
|
|
if ((m_frameCounter % kSweepInterval) != 0) {
|
|
return;
|
|
}
|
|
|
|
// Collect the dying handles and notify once after the loop: pipelines hashed
|
|
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
|
|
// by draws that hit those entries), so the observer may destroy them
|
|
// immediately - and a single batched notification costs one pipeline-cache
|
|
// scan instead of one per evicted pass.
|
|
Vector<VkRenderPass> destroyedRenderPasses;
|
|
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
|
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
|
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
|
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
|
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
|
m_rpFastValid = false;
|
|
}
|
|
destroyedRenderPasses.push_back(it->second.renderPass);
|
|
it = m_renderPasses.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
|
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
|
}
|
|
}
|
|
|
|
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
|
// TODO: Transition all the attachments into proper layout before starting the render pass
|
|
VkRenderPassBeginInfo renderPassBeginInfo;
|
|
renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
|
renderPassBeginInfo.pNext = nullptr;
|
|
renderPassBeginInfo.renderPass = renderPassEntry.renderPass;
|
|
renderPassBeginInfo.framebuffer = renderPassEntry.framebuffer;
|
|
renderPassBeginInfo.renderArea.offset = { 0, 0 };
|
|
renderPassBeginInfo.renderArea.extent = {
|
|
(Uint32)renderPassEntry.extent.x(), (Uint32)renderPassEntry.extent.y() };
|
|
|
|
Vector<VkClearValue> clearValues(renderPassEntry.attachmentCount);
|
|
for (auto& clearValue: clearValues) {
|
|
clearValue.color = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
clearValue.depthStencil = {1.0f, 0};
|
|
}
|
|
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
|
if (pending.attachmentIndex >= clearValues.size()) {
|
|
continue;
|
|
}
|
|
ClearAttachmentPayload clearPayload{};
|
|
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
|
if (pending.hasInlinePayload) {
|
|
// The inline payload was snapshotted when the entry was CREATED, but the
|
|
// clear VALUE is not part of the entry's hash - a cache hit with a newer
|
|
// glClear would replay the creation-time value and drop the new one (the
|
|
// texture path below is immune because it re-reads the live payload).
|
|
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
|
|
// pending clear, fall back to the snapshot only when none is queued.
|
|
if (s_renderPassManager != nullptr &&
|
|
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
|
|
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
|
|
3) {
|
|
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
|
ForceOpaqueClearAlpha(clearPayload);
|
|
}
|
|
} else {
|
|
clearPayload = pending.inlinePayload;
|
|
}
|
|
} else {
|
|
if (pending.key.texture == nullptr ||
|
|
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
|
continue;
|
|
}
|
|
}
|
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
|
clearValues[pending.attachmentIndex].color =
|
|
MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(liveTexture.get()));
|
|
}
|
|
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
|
clearValues[pending.attachmentIndex].depthStencil.depth = clearPayload.depth;
|
|
}
|
|
if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
|
clearValues[pending.attachmentIndex].depthStencil.stencil = clearPayload.stencil;
|
|
}
|
|
}
|
|
|
|
renderPassBeginInfo.clearValueCount = static_cast<Uint32>(clearValues.size());
|
|
renderPassBeginInfo.pClearValues = clearValues.data();
|
|
|
|
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
|
// Pre-pass stream bookkeeping: this pass's attachment images are now
|
|
// referenced by the open frame recording.
|
|
if (s_textureManager != nullptr) {
|
|
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
|
|
if (tracked.target == TrackedAttachmentTarget::Texture) {
|
|
if (const auto texture = tracked.texture.lock()) {
|
|
s_textureManager->StampTextureRecordingUse(texture.get());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
|
if (pending.hasInlinePayload) {
|
|
if (s_renderPassManager != nullptr) {
|
|
s_renderPassManager->PopPendingRenderbufferClear(pending.renderbuffer);
|
|
}
|
|
} else {
|
|
s_clearManager->PopPendingClear(pending.key);
|
|
}
|
|
}
|
|
s_activeRenderPass.hash = renderPassEntry.hash;
|
|
s_activeRenderPass.compatibilityHash = renderPassEntry.compatibilityHash;
|
|
s_activeRenderPass.trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts;
|
|
s_activeRenderPass.extent = renderPassEntry.extent;
|
|
s_hasActiveRenderPass = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
Bool VkRenderPassManager::EndRenderPass(VkCommandBuffer commandBuffer) {
|
|
auto* activeRenderPass = GetActiveRenderPass();
|
|
vkCmdEndRenderPass(commandBuffer);
|
|
// The fast-path memo reuses the ACTIVE render pass; once the pass ends it must not carry
|
|
// over (the next span may be a different FBO resolved before its render pass is begun).
|
|
if (s_renderPassManager != nullptr) {
|
|
s_renderPassManager->m_rpFastValid = false;
|
|
}
|
|
if (activeRenderPass != nullptr && !activeRenderPass->trackedAttachmentLayouts.empty()) {
|
|
for (const auto& trackedAttachment : activeRenderPass->trackedAttachmentLayouts) {
|
|
switch (trackedAttachment.target) {
|
|
case TrackedAttachmentTarget::Texture:
|
|
MOBILEGL_ASSERT(s_textureManager != nullptr, "EndRenderPass: texture manager is null");
|
|
if (const auto texture = trackedAttachment.texture.lock()) {
|
|
s_textureManager->UpdateTrackedImageLayoutAfterAttachmentWrite(
|
|
commandBuffer,
|
|
texture.get(),
|
|
trackedAttachment.textureMipLevel,
|
|
trackedAttachment.finalLayout);
|
|
}
|
|
break;
|
|
case TrackedAttachmentTarget::Renderbuffer:
|
|
MOBILEGL_ASSERT(s_renderPassManager != nullptr, "EndRenderPass: render pass manager is null");
|
|
if (const auto renderbuffer = trackedAttachment.renderbuffer.lock()) {
|
|
auto resourceIt =
|
|
s_renderPassManager->m_renderbufferResources.find(renderbuffer.get());
|
|
if (resourceIt != s_renderPassManager->m_renderbufferResources.end()) {
|
|
resourceIt->second.layout = trackedAttachment.finalLayout;
|
|
}
|
|
}
|
|
break;
|
|
case TrackedAttachmentTarget::SwapchainColor:
|
|
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
|
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
|
// The pass stored into the attachment: its content is defined
|
|
// until the image is next presented.
|
|
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
|
break;
|
|
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
|
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
|
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
|
trackedAttachment.finalLayout);
|
|
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
|
break;
|
|
default:
|
|
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
|
static_cast<Int>(trackedAttachment.target));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
s_activeRenderPass = {};
|
|
s_hasActiveRenderPass = false;
|
|
return true;
|
|
}
|
|
|
|
ActiveRenderPassInfo* VkRenderPassManager::GetActiveRenderPass() {
|
|
return s_hasActiveRenderPass ? &s_activeRenderPass : nullptr;
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|