// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.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 "RenderPassGuard.h" #include "UniformManager.h" #include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h" #include "MG_State/GLState/Core.h" #include // P5c ev: the GPU-write announcement routes through the reverse channel (R2). #include #include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/TextureState/TextureObject1D.h" #include "MG_State/GLState/TextureState/TextureObject2D.h" #include "MG_State/GLState/TextureState/TextureObject2DCube.h" #include "MG_State/GLState/TextureState/TextureObject3D.h" #include "MG_State/GLState/TextureState/TextureObjectBuffer.h" #include "MG_State/GLState/TextureState/TextureObjectStubs.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Metrics/PipeStats.h" #include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/ShaderTranspiler/Types.h" #include #include // P7 wave 0: the seam WireDescriptorFatal dies through. See MG_Pipe/PipeSessionFail.h. #include // P7 wave 2 package B3: rule I's tally for this file's silent wire-draw exits. #include "WireDeclineTally.h" #include #include #include #include #include #include namespace MobileGL::MG_Backend::DirectVulkan { SizeT UniformManager::WireImageViewKeyHash::operator()(const WireImageViewKey& key) const { SizeT hash = std::hash{}(key.image); const auto mix = [&hash](Uint64 value) { hash ^= std::hash{}(value) + static_cast(0x9e3779b97f4a7c15ULL) + (hash << 6) + (hash >> 2); }; mix(key.root.Slot); mix(key.root.Gen); mix(key.imageEpoch); mix(key.flags); mix(key.type); mix(key.format); mix(key.components.r); mix(key.components.g); mix(key.components.b); mix(key.components.a); mix(key.range.aspectMask); mix(key.range.baseMipLevel); mix(key.range.levelCount); mix(key.range.baseArrayLayer); mix(key.range.layerCount); return hash; } // P7 wave 0, the descriptor half of WireFramebuffer.inc's MagmaWireFatal - same message, // same seam, same reason. Four of the fifteen P7-marked refusals die here. [[noreturn]] static void WireDescriptorFatal(const char* detail) { MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); } static Bool ResolveWireRange(Uint64 offset, Uint64 declaredSize, VkDeviceSize bufferSize, VkDeviceSize& start, VkDeviceSize& size) { if (offset >= bufferSize) return false; start = static_cast(offset); const VkDeviceSize remaining = bufferSize - start; size = declaredSize == MG_Pipe::kMGPipeWholeBuffer ? remaining : std::min(declaredSize, remaining); return size != 0; } static VkImageViewType WireViewType(MG_Pipe::MGPipeResourceTarget target) { using T = MG_Pipe::MGPipeResourceTarget; switch (target) { case T::Tex1D: return VK_IMAGE_VIEW_TYPE_1D; case T::Tex1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; case T::Tex3D: return VK_IMAGE_VIEW_TYPE_3D; case T::TexCube: return VK_IMAGE_VIEW_TYPE_CUBE; case T::TexCubeArray: return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; case T::Tex2DArray: case T::Tex2DMSArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; default: return VK_IMAGE_VIEW_TYPE_2D; } } // GL ignores Layer/Layered for a target with no layers. In particular image1D stays a 1D // view, and an ignored nonzero Layer must not address another slice. static Bool WireTargetHasLayers(MG_Pipe::MGPipeResourceTarget target) { using T = MG_Pipe::MGPipeResourceTarget; return target == T::Tex1DArray || target == T::Tex2DArray || target == T::Tex2DMSArray || target == T::Tex3D || target == T::TexCube || target == T::TexCubeArray; } // GL 4.6 core 8.26: an access through an image unit whose texture does not HAVE the // (level, layer) the unit names is INVALID. A load returns zero, a store does nothing, an // atomic updates nothing - and none of it is an error, neither at bind time (the bind only // checks the level's and the layer's signs) nor at the draw. It is the observable an empty // unit has, so the answer is the empty unit's (ResolveWirePlaceholderImage): a NULL storage // descriptor where the device enables VK_EXT_robustness2 nullDescriptor, else a storage // placeholder private to the unit (per shape), cleared before each use, of the dimensionality // the SHADER declared. // // KHR-GL46.shader_image_load_store.incomplete_textures is the shape that found it: level 2 // of a texture that defines only level 0 (MAX_LEVEL 7, a mipmapping filter). The same // question comes up empty for a level past an immutable texture's storage, a layer past an // array's last slice, a texture that has no storage at all, and a level past a texture // view's own window even where its owner has one. The record carries exactly what // glBindImageTexture was given, so it is decided here, by the walk every wire consumer uses // to find a texel's storage, and from that walk's "not there" answer ONLY: a null for any // other reason (a dead record, a stale view CSO) stays the descriptor resolve's refusal. // // A level that IS in storage but lies outside [BASE_LEVEL, MAX_LEVEL], or of a texture that // is not mipmap-complete, is decided by the client, which sends it as // kMGPipeImageLevelNone - a level no texture has, so this walk answers it too. static Bool WireShaderImageNamesNoTexel(VkTextureManager& textures, const MG_Pipe::MGPImageView& image) { const auto& state = MG_Pipe::MGPipeApplier(); if (MG_Pipe::MGPipeHandleIsNull(image.Res) || image.Res.Slot >= state.TextureResources.size()) return false; const auto& record = state.TextureResources[image.Res.Slot]; if (!record.Live || record.Gen != image.Res.Gen) return false; const Bool selectsLayer = WireTargetHasLayers(static_cast(record.Desc.Target)) && !image.Layered; Uint32 level = image.Level; Uint32 layer = selectsLayer ? image.Layer : 0; Bool outsideWindow = false; (void)textures.ResolveWireTextureStorage(image.Res, level, layer, nullptr, nullptr, &outsideWindow); return outsideWindow; } static VkComponentSwizzle WireSwizzle(Uint8 value) { switch (static_cast(value)) { case TextureSwizzleParam::Red: return VK_COMPONENT_SWIZZLE_R; case TextureSwizzleParam::Green: return VK_COMPONENT_SWIZZLE_G; case TextureSwizzleParam::Blue: return VK_COMPONENT_SWIZZLE_B; case TextureSwizzleParam::Alpha: return VK_COMPONENT_SWIZZLE_A; case TextureSwizzleParam::Zero: return VK_COMPONENT_SWIZZLE_ZERO; case TextureSwizzleParam::One: return VK_COMPONENT_SWIZZLE_ONE; default: WireDescriptorFatal("sampler-swizzle"); } } Bool UniformManager::ResolveWireImageDescriptor( VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, Bool storage, VkDescriptorImageInfo& out, VkTextureManager::TextureResource** outResource) const { out = {}; if (outResource != nullptr) *outResource = nullptr; auto& state = MG_Pipe::MGPipeApplier(); const Int baseLocation = programObj.samplerUniformLocationByBinding[binding]; const Int location = baseLocation + static_cast(element); if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) return false; const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); if (unit < 0 || static_cast(unit) >= (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; // An EMPTY image unit is the same 8.26 invalid access as the named-but-missing texel // below, so it takes the same answer: a null storage descriptor where the device has // one, else the unit-private placeholder (codex closeout finding 2). if (MG_Pipe::MGPipeHandleIsNull(handle)) return ResolveWirePlaceholderImage(commandBuffer, program, programObj, binding, storage, out, VK_FORMAT_UNDEFINED, static_cast(unit)); if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("image-record"); const auto& record = state.TextureResources[handle.Slot]; if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("image-record-generation"); // Before the sync: a texture with no storage at all is one of the shapes, and syncing // it would refuse rather than answer. if (storage && WireShaderImageNamesNoTexel(*m_textureManager, state.BoundShaderImages[unit])) { const VkFormat bound = MG_Util::ConvertTextureInternalFormatToVkEnum( MG_Util::ConvertGLEnumToTextureInternalFormat(state.BoundShaderImages[unit].InternalFormat)); return ResolveWirePlaceholderImage(commandBuffer, program, programObj, binding, storage, out, bound, static_cast(unit)); } auto* resource = m_textureManager->SyncTextureResourceByHandle(handle, false, storage); if (!resource) WireDescriptorFatal("image-resource"); m_textureManager->FlushPendingUploads(); if (outResource != nullptr) *outResource = resource; const auto target = static_cast(record.Desc.Target); const Bool layerable = WireTargetHasLayers(target); Uint32 level = storage ? state.BoundShaderImages[unit].Level : record.Params.BaseLevel; // GL ignores Layer/Layered for non-layerable targets (WireTargetHasLayers). Uint32 layer = storage && layerable && !state.BoundShaderImages[unit].Layered ? state.BoundShaderImages[unit].Layer : 0; const Uint32 localLevel = level; const Uint32 localLayer = layer; Uint32 layers = 0; VkFormat aliasFormat = VK_FORMAT_UNDEFINED; const auto root = m_textureManager->ResolveWireTextureStorage(handle, level, layer, &aliasFormat, &layers); if (MG_Pipe::MGPipeHandleIsNull(root)) WireDescriptorFatal("image-view-window"); VkImageViewType type = WireViewType(target); if (storage && layerable && !state.BoundShaderImages[unit].Layered) { layers = 1; type = target == MG_Pipe::MGPipeResourceTarget::Tex1DArray ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; } if (type == VK_IMAGE_VIEW_TYPE_3D) layers = 1; Uint32 levels = 1; if (!storage) { if (record.Params.MaxLevel < localLevel || record.Desc.Levels <= localLevel) return false; levels = std::min(record.Desc.Levels - localLevel, record.Params.MaxLevel - localLevel + 1); const auto viewHandle = state.BoundSamplerViews[unit].View; if (!MG_Pipe::MGPipeHandleIsNull(viewHandle) && viewHandle.Slot < state.SamplerViewCsos.size()) { const auto& view = state.SamplerViewCsos[viewHandle.Slot]; if (!view.Live || view.Gen != viewHandle.Gen || view.View.Texture != handle) WireDescriptorFatal("sampler-view-record"); // Ordinary texture views can encode an unrestricted window with zero // counts; only an actual restriction narrows the resource/parameter range. if (view.View.NumLevels != 0) { if (view.View.NumLevels <= localLevel) WireDescriptorFatal("sampler-view-level"); levels = std::min(levels, view.View.NumLevels - localLevel); } } else if (!MG_Pipe::MGPipeHandleIsNull(viewHandle)) { WireDescriptorFatal("sampler-view-record"); } levels = std::min(levels, resource->mipLevels - level); } VkFormat format = aliasFormat == VK_FORMAT_UNDEFINED ? resource->format : aliasFormat; const auto domain = programObj.samplerNumericDomainByBinding[binding]; if (storage) { format = ResolveStorageImageViewFormat(programObj.storageImageFormatByBinding[binding], state.BoundShaderImages[unit].InternalFormat, format, programObj.storageImageUsesBindingFormatByBinding[binding]); } else if (resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT) { format = VkTextureManager::ResolveSampledImageViewFormat(format, domain); } if (format == VK_FORMAT_UNDEFINED) return false; VkImageAspectFlags aspect = resource->aspect; if (!storage && (aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) aspect = record.Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; // All wire image descriptors use GENERAL. A texture sampled and image-bound in // the same shader then has one truthful layout, including views of the same owner. if (!VkTextureManager::TransitionImageLayout(commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource->aspect, 0, resource->mipLevels)) WireDescriptorFatal("image-descriptor-transition"); // A second descriptor/draw can keep GENERAL after a shader write. A layout // equality fast return is not a memory dependency for that prior write. // Only when something may have written an image since the last such barrier // (WireImageWriteEpoch): with nothing written in between, consecutive draws keep their // render pass open instead of closing it for a barrier that orders nothing. if (WireImageWriteEpoch() != m_wireImageBarrierEpoch) { VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; EndActiveRenderPassOn(commandBuffer); vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr); m_wireImageBarrierEpoch = WireImageWriteEpoch(); } VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; info.image = resource->image; info.viewType = type; info.format = format; info.subresourceRange = {aspect, level, levels, layer, layers}; if (!storage) { // An X-format shared image carries undefined alpha bytes, exactly like an expanded RGB. const Bool alphaIsOne = resource->sharedImageAlphaOne || ResolveTextureFormatInfo( static_cast(record.Desc.InternalFormat)).expandRgbToRgba; const auto swizzle = [&](Uint32 channel) { const auto value = record.Params.Swizzle[channel]; return alphaIsOne && static_cast(value) == TextureSwizzleParam::Alpha ? VK_COMPONENT_SWIZZLE_ONE : WireSwizzle(value); }; info.components = {swizzle(0), swizzle(1), swizzle(2), swizzle(3)}; } VkImageView view = VK_NULL_HANDLE; const WireImageViewKey key{root, info.image, m_textureManager->GetTextureImageEpoch(), info.flags, info.viewType, info.format, info.components, info.subresourceRange}; const auto& frame = m_frames[m_wireFrameIndex]; const auto cached = frame.wireImageViewCache.find(key); if (cached != frame.wireImageViewCache.end()) { view = cached->second; } else { if (vkCreateImageView(m_device, &info, nullptr, &view) != VK_SUCCESS) return false; frame.wireImageViews.push_back(view); frame.wireImageViewCache.emplace(key, view); } // Only view creation is memoized. Layout/memory dependencies above and // write tracking below still run for every descriptor, including hits. out.imageView = view; out.imageLayout = VK_IMAGE_LAYOUT_GENERAL; if (storage) { if (state.BoundShaderImages[unit].Access != kMGPipeImageAccessReadOnly) m_textureManager->MarkWireTextureGpuWritten(handle, localLevel, localLayer, layers); } else { auto samplerHandle = state.BoundSamplerStates[unit]; if (MG_Pipe::MGPipeHandleIsNull(samplerHandle)) samplerHandle = record.Params.BuiltinSampler; // A texture's built-in sampler CSO can be missing or gone while the texture still names // it: sampler CSOs are content-addressed and recycled at capacity, and the texture's // record is only re-pointed by its next set_texture_params (a texture whose params never // crossed names none at all). Sample with GL's default parameters rather than end the // session - the DirectGLES arm declines the push in both cases. using SamplerCsoRecord = std::decay_t; static const decltype(SamplerCsoRecord::Params) kDefaultSamplerParams{}; const Bool known = !MG_Pipe::MGPipeHandleIsNull(samplerHandle) && samplerHandle.Slot < state.SamplerCsos.size(); const SamplerCsoRecord* sampler = known ? &state.SamplerCsos[samplerHandle.Slot] : nullptr; const Bool stale = sampler == nullptr || !sampler->Live || sampler->Gen != samplerHandle.Gen; if (stale) { MGLOG_E_ONCE("Magma wire: sampler CSO {%u, %u} named by texture unit %u is missing or no longer " "live; sampling with default parameters", samplerHandle.Slot, samplerHandle.Gen, unit); } const Bool forceNearest = domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger; static_assert(kWireSamplerMemoUnits == MG_Pipe::kMGPipeMaxTextureUnits, "the wire sampler memo has one entry per texture unit"); auto& memo = m_wireSamplerMemo[static_cast(unit)]; const Bool memoHit = !stale && memo.valid && memo.slot == samplerHandle.Slot && memo.gen == samplerHandle.Gen && memo.serial == sampler->Serial && memo.contextSerial == state.ContextSerial && memo.format == record.Desc.InternalFormat && memo.viewLevelCount == levels && memo.forceNearest == forceNearest && memo.frameBoundary == m_samplerManager->FrameBoundaryCount() && memo.destroyEpoch == m_samplerManager->DestroyEpoch(); if (memoHit) { out.sampler = memo.sampler; } else { out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters( stale ? kDefaultSamplerParams : sampler->Params, static_cast(record.Desc.InternalFormat), forceNearest, levels); memo.valid = !stale && out.sampler != VK_NULL_HANDLE; if (memo.valid) { memo.slot = samplerHandle.Slot; memo.gen = samplerHandle.Gen; memo.serial = sampler->Serial; memo.contextSerial = state.ContextSerial; memo.format = record.Desc.InternalFormat; memo.viewLevelCount = levels; memo.forceNearest = forceNearest; memo.frameBoundary = m_samplerManager->FrameBoundaryCount(); memo.destroyEpoch = m_samplerManager->DestroyEpoch(); memo.sampler = out.sampler; } } if (!out.sampler) return false; } return true; } namespace { // The R32 member of each numeric class. Every one of the three is a MANDATORY-support // format for uniform texel buffers, storage texel buffers and storage images alike // (Vulkan 1.0, "Required Format Support"), which is what makes them a fallback that // cannot itself fail for want of device features. VkFormat PlaceholderFormatForNumericDomain(SamplerNumericDomain numericDomain) { switch (numericDomain) { case SamplerNumericDomain::Float: return VK_FORMAT_R32_SFLOAT; case SamplerNumericDomain::SignedInteger: return VK_FORMAT_R32_SINT; case SamplerNumericDomain::UnsignedInteger: return VK_FORMAT_R32_UINT; case SamplerNumericDomain::Unknown: break; } return VK_FORMAT_UNDEFINED; } Bool BufferFormatSupportsFeature(VkPhysicalDevice physicalDevice, VkFormat format, VkFormatFeatureFlags requiredFeature) { if (physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) { return false; } VkFormatProperties properties{}; vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &properties); return (properties.bufferFeatures & requiredFeature) == requiredFeature; } } #include "WirePlaceholderImages.inc" // descriptorCount this binding declares in the descriptor set layout (1 for everything that // is not an array). Kept in one place because the layout, the scratch reservation and the // per-element write loops must all agree on it. static Uint32 BindingDescriptorCount(const ProgramFactory::VkProgramObject& programObj, Uint32 binding) { return binding < programObj.bindingDescriptorCounts.size() ? std::max(1u, programObj.bindingDescriptorCounts[binding]) : 1u; } VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, VkFormat resourceFormat, Bool useBindingFormat) { if (useBindingFormat) { const TextureInternalFormat bindingInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat); return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat); } return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat; } Bool UniformManager::Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager, ProgramFactory* programFactory, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, Uint32 maxBindings, Uint32 setsPerFrame, VkTextureManager* textureManager, VkSamplerManager* samplerManager) { Shutdown(); MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "UniformDescriptorBinder::Initialize requires valid VkDevice"); MOBILEGL_ASSERT(physicalDevice != VK_NULL_HANDLE, "UniformDescriptorBinder::Initialize requires valid VkPhysicalDevice"); MOBILEGL_ASSERT(bufferManager != nullptr, "UniformDescriptorBinder::Initialize requires valid buffer manager"); MOBILEGL_ASSERT(programFactory != nullptr, "UniformDescriptorBinder::Initialize requires valid program factory"); MOBILEGL_ASSERT(frameCount > 0, "UniformDescriptorBinder::Initialize requires frameCount > 0"); MOBILEGL_ASSERT(maxBindings > 0, "UniformDescriptorBinder::Initialize requires maxBindings > 0"); MOBILEGL_ASSERT(setsPerFrame > 0, "UniformDescriptorBinder::Initialize requires setsPerFrame > 0"); MOBILEGL_ASSERT(textureManager != nullptr, "UniformDescriptorBinder::Initialize requires valid texture manager"); MOBILEGL_ASSERT(samplerManager != nullptr, "UniformDescriptorBinder::Initialize requires valid sampler manager"); m_device = device; m_physicalDevice = physicalDevice; m_bufferManager = bufferManager; m_programFactory = programFactory; m_minDynamicOffsetAlignment = std::max(1, minUniformBufferOffsetAlignment); VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(m_physicalDevice, &properties); m_wireStorageOffsetAlignment = std::max(1, properties.limits.minStorageBufferOffsetAlignment); m_wireTexelOffsetAlignment = std::max(1, properties.limits.minTexelBufferOffsetAlignment); m_wireMaxUniformRange = properties.limits.maxUniformBufferRange; m_wireMaxStorageRange = properties.limits.maxStorageBufferRange; m_wireMaxTexelElements = properties.limits.maxTexelBufferElements; m_frameCount = frameCount; m_maxBindings = maxBindings; m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{}); // Every entry is freshly constructed (all-invalid), so nothing needs sweeping until // a resolve writes one. m_samplerResolveMemoHighWater = 0; m_setsPerFrame = setsPerFrame; m_peakDescriptorSetsObserved = 0; m_trimQuietEpochs = MG_Config::Features.MagmaDescriptorTrimFrames; // The renderer records its first frame into slot 0 before any BeginFrame runs. m_epochSlot = 0; m_epochOpen = true; m_descriptorSlotTrims = 0; m_textureManager = textureManager; m_samplerManager = samplerManager; m_frames.resize(m_frameCount); for (Uint32 frameIndex = 0; frameIndex < m_frameCount; ++frameIndex) { auto& frame = m_frames[frameIndex]; frame.activeDescriptorPoolIndex = 0; frame.allocatedSetsThisFrame = 0; frame.peakAllocatedSetsThisFrame = 0; frame.capacitySets = m_setsPerFrame; frame.quietEpochs = 0; frame.descriptorPools.clear(); VkDescriptorPool initialPool = VK_NULL_HANDLE; if (!CreateDescriptorPool(m_setsPerFrame, false, initialPool)) { MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u", frameIndex); Shutdown(); return false; } frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0, false}); MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex, m_setsPerFrame); } return true; } void UniformManager::Shutdown() { // Before the per-frame loop, because these views are NOT owned by any frame slot (see // m_unboundTexelBufferViews) and the loop below is what clears m_device. if (m_device != VK_NULL_HANDLE) { for (const auto& viewEntry : m_unboundTexelBufferViews) { if (viewEntry.second != VK_NULL_HANDLE) { vkDestroyBufferView(m_device, viewEntry.second, nullptr); } } } m_unboundTexelBufferViews.clear(); for (auto& frame : m_frames) { if (m_device != VK_NULL_HANDLE) { frame.wireImageViewCache.clear(); for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr); frame.wireImageViews.clear(); for (auto& view : frame.texelBufferViews) { if (view != VK_NULL_HANDLE) { vkDestroyBufferView(m_device, view, nullptr); } } frame.texelBufferViews.clear(); frame.descriptorSetCacheByLayout.clear(); for (auto& bucket : frame.descriptorPools) { if (bucket.handle != VK_NULL_HANDLE) { vkDestroyDescriptorPool(m_device, bucket.handle, nullptr); bucket.handle = VK_NULL_HANDLE; } } } frame.descriptorPools.clear(); frame.activeDescriptorPoolIndex = 0; frame.allocatedSetsThisFrame = 0; frame.peakAllocatedSetsThisFrame = 0; frame.capacitySets = 0; frame.quietEpochs = 0; } m_frames.clear(); for (auto& entry : m_wirePlaceholderImages) DestroyWirePlaceholderImage(entry.second); m_wirePlaceholderImages.clear(); m_wirePrivateStoragePlaceholders = 0; m_wireInvalidStorageImagesBindNull = false; m_epochSlot = 0; m_epochOpen = false; m_bufferManager = nullptr; m_programFactory = nullptr; m_device = VK_NULL_HANDLE; m_physicalDevice = VK_NULL_HANDLE; m_minDynamicOffsetAlignment = 1; m_frameCount = 0; m_maxBindings = 0; m_samplerResolveMemo.clear(); m_samplerResolveMemoHighWater = 0; m_setsPerFrame = 0; m_peakDescriptorSetsObserved = 0; m_textureManager = nullptr; m_samplerManager = nullptr; } void UniformManager::BeginFrame(Uint32 frameIndex) { MOBILEGL_ASSERT(frameIndex < m_frames.size(), "UniformDescriptorBinder::BeginFrame invalid frame index"); auto& frame = m_frames[frameIndex]; m_wireFrameIndex = frameIndex; frame.wireImageViewCache.clear(); for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr); frame.wireImageViews.clear(); for (auto& view : frame.texelBufferViews) { if (view != VK_NULL_HANDLE) { vkDestroyBufferView(m_device, view, nullptr); } } frame.texelBufferViews.clear(); if (frame.peakAllocatedSetsThisFrame > m_peakDescriptorSetsObserved) { m_peakDescriptorSetsObserved = frame.peakAllocatedSetsThisFrame; MGLOG_D( "UniformDescriptorBinder: new descriptor set peak observed=%u (base setsPerFrame=%u, frame=%u, pools=%zu)", m_peakDescriptorSetsObserved, m_setsPerFrame, frameIndex, frame.descriptorPools.size()); } // The epoch that just ended is the one of the slot begun last - this slot again on the // drain path, the previous frame's slot on the Present path. Its peak is still unreset. if (m_epochOpen && m_epochSlot < m_frames.size()) { NoteEndedEpochForTrim(m_frames[m_epochSlot].peakAllocatedSetsThisFrame); } m_epochSlot = frameIndex; m_epochOpen = true; // No pending command buffer references this slot's sets here (see TrimFrameDescriptorPools // for why that holds at both of BeginFrame's call sites), so a quiet grown slot can be // handed back whole. if (m_trimQuietEpochs != 0 && frame.descriptorPools.size() > 1 && frame.capacitySets >= kDescriptorTrimMinCapacity && frame.quietEpochs >= m_trimQuietEpochs) { TrimFrameDescriptorPools(frame, frameIndex); } frame.activeDescriptorPoolIndex = 0; frame.allocatedSetsThisFrame = 0; frame.peakAllocatedSetsThisFrame = 0; for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) { cacheEntryPair.second.cursor = 0; } // The frame's descriptor sets are recycled above, so last frame's reuse targets // are gone: start the per-draw descriptor-reuse cache fresh this frame. InvalidateDescriptorSetMemos(); // Re-fingerprint the bound sampler set fresh this frame so any GL object address // reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a // resolve has actually written can be valid, so the high-water mark bounds the // sweep - the vector itself is sized to the device's binding cap (256 here), which // is ~30x more entries than any program declares. const Uint32 touchedBindings = std::min(m_samplerResolveMemoHighWater, static_cast(m_samplerResolveMemo.size())); for (Uint32 binding = 0; binding < touchedBindings; ++binding) { m_samplerResolveMemo[binding].valid = false; m_samplerResolveMemo[binding].infoValid = false; } } Bool UniformManager::WireDescriptorSetBudgetReached(Uint32 frameIndex) const { MOBILEGL_ASSERT(frameIndex < m_frames.size(), "WireDescriptorSetBudgetReached invalid frame index"); return m_frames[frameIndex].allocatedSetsThisFrame >= kWireDescriptorSetBudget; } SizeT UniformManager::RewindWireDescriptorSets(Uint32 frameIndex) { MOBILEGL_ASSERT(frameIndex < m_frames.size(), "RewindWireDescriptorSets invalid frame index"); auto& frame = m_frames[frameIndex]; // The caller proved that the last graphics submit using these sets has // retired and that no unsubmitted command buffer still references them. // Keep the pools and sets: only the per-layout write cursors rewind. // Image and buffer views stay alive until the regular frame boundary. m_peakDescriptorSetsObserved = std::max(m_peakDescriptorSetsObserved, frame.peakAllocatedSetsThisFrame); SizeT cachedSets = 0; for (auto& entry : frame.descriptorSetCacheByLayout) { cachedSets += entry.second.sets.size(); entry.second.cursor = 0; } frame.allocatedSetsThisFrame = 0; // The PEAK is not reset: a rewind ends a window of the epoch, not the epoch. The trim scores // an epoch by the most sets the slot held at once in it (NoteEndedEpochForTrim at the next // BeginFrame), and on this arm that is the largest window - resetting here would score a // burst frame by its last window alone and trim a slot that re-grows every frame. InvalidateDescriptorSetMemos(); const Uint32 touchedBindings = std::min(m_samplerResolveMemoHighWater, static_cast(m_samplerResolveMemo.size())); for (Uint32 binding = 0; binding < touchedBindings; ++binding) { m_samplerResolveMemo[binding].valid = false; m_samplerResolveMemo[binding].infoValid = false; } return cachedSets; } void UniformManager::InvalidateDescriptorSetMemos() { // Every VkDescriptorSet handle held outside the per-layout caches - in this backend they all // live in this class. A trim can hand a destroyed set's handle value to a fresh allocation, // so the bind-dedup shadow must drop too, not only the two reuse memos. for (auto& entry : m_descriptorReuseMemo) { entry.valid = false; } m_wireDescriptorRebind.valid = false; m_lastBindValid = false; } void UniformManager::NoteEndedEpochForTrim(Uint32 epochPeakSets) { for (auto& slot : m_frames) { if (static_cast(epochPeakSets) * kDescriptorTrimHeadroom > slot.capacitySets) { slot.quietEpochs = 0; } else if (slot.quietEpochs < std::numeric_limits::max()) { ++slot.quietEpochs; } } } void UniformManager::TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex) { const SizeT poolsBefore = frame.descriptorPools.size(); const Uint64 capacityBefore = frame.capacitySets; // Holders first: the per-layout cache and every memo that may name one of these sets. frame.descriptorSetCacheByLayout.clear(); InvalidateDescriptorSetMemos(); for (SizeT i = 1; i < frame.descriptorPools.size(); ++i) { if (frame.descriptorPools[i].handle != VK_NULL_HANDLE) { vkDestroyDescriptorPool(m_device, frame.descriptorPools[i].handle, nullptr); } } frame.descriptorPools.resize(1); auto& base = frame.descriptorPools[0]; // Frees every set allocated from it; the spec defines no failure for vkResetDescriptorPool. vkResetDescriptorPool(m_device, base.handle, 0); base.allocatedSets = 0; frame.activeDescriptorPoolIndex = 0; frame.capacitySets = base.maxSets; frame.quietEpochs = 0; ++m_descriptorSlotTrims; MGLOG_D("UniformDescriptorBinder: frame %u trimmed its descriptor pools (%zu pools / %llu sets -> 1 / %u)", frameIndex, poolsBefore, static_cast(capacityBefore), base.maxSets); } DescriptorPoolCensus UniformManager::GetDescriptorPoolCensus() const { DescriptorPoolCensus census; census.available = true; census.frameSlots = static_cast(m_frames.size()); for (const auto& frame : m_frames) { const auto poolCount = static_cast(frame.descriptorPools.size()); census.pools += poolCount; census.maxPoolsInOneSlot = std::max(census.maxPoolsInOneSlot, poolCount); census.capacitySets += frame.capacitySets; for (const auto& cacheEntryPair : frame.descriptorSetCacheByLayout) { census.cachedSets += cacheEntryPair.second.sets.size(); } } census.slotTrims = m_descriptorSlotTrims; return census; } void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) { SizeT purgedSets = 0; for (auto& frame : m_frames) { const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout); if (it == frame.descriptorSetCacheByLayout.end()) { continue; } // Free the sets back to their pools and credit the bucket accounting, so // program churn recycles pool capacity instead of abandoning the slots. // GPU-safe: the layout only dies after >1024 idle frame boundaries, so no // in-flight command buffer references these sets. for (const auto& cached : it->second.sets) { if (cached.set == VK_NULL_HANDLE) { continue; } vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set); const auto bucket = std::find_if( frame.descriptorPools.begin(), frame.descriptorPools.end(), [&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; }); if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) { --bucket->allocatedSets; } } purgedSets += it->second.sets.size(); frame.descriptorSetCacheByLayout.erase(it); } if (purgedSets > 0) { // The per-draw reuse memo folds the layout handle into its signature; drop // every entry so a recycled handle value cannot revive a purged set mid-frame. for (auto& entry : m_descriptorReuseMemo) { entry.valid = false; } m_wireDescriptorRebind.valid = false; MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets); } } Bool UniformManager::ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, Bool storage, VkBufferView& out) { out = VK_NULL_HANDLE; if (binding >= programObj.samplerUniformLocationByBinding.size() || binding >= programObj.samplerNumericDomainByBinding.size() || frameIndex >= m_frames.size()) return false; const Int location = programObj.samplerUniformLocationByBinding[binding]; if (location < 0) return false; const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); const auto& state = MG_Pipe::MGPipeApplier(); if (unit < 0 || static_cast(unit) >= (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; if (storage && binding >= programObj.storageImageFormatByBinding.size()) return false; const VkFormat declaredFormat = storage ? programObj.storageImageFormatByBinding[binding] : VK_FORMAT_UNDEFINED; const auto placeholder = [&] { out = AcquireUnboundTexelBufferView(declaredFormat, programObj.samplerNumericDomainByBinding[binding], storage); return out != VK_NULL_HANDLE; }; const auto texture = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; if (MG_Pipe::MGPipeHandleIsNull(texture)) return placeholder(); if (texture.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record"); const auto& record = state.TextureResources[texture.Slot]; if (!record.Live || record.Gen != texture.Gen) WireDescriptorFatal("texel-buffer-generation"); if (record.Desc.Target != static_cast(MG_Pipe::MGPipeResourceTarget::TexBuffer)) WireDescriptorFatal("texel-buffer-target"); const auto buffer = record.Desc.BufferForTexBuffer; if (MG_Pipe::MGPipeHandleIsNull(buffer)) return placeholder(); BufferSlice slice{}; if (!m_bufferManager->AcquireWireSlice(BufferKind::TextureBuffer, buffer, slice) || !slice.IsValid()) return false; const auto internalFormat = static_cast(record.Desc.InternalFormat); VkFormat format = declaredFormat; if (storage && format == VK_FORMAT_UNDEFINED && state.BoundShaderImages[unit].InternalFormat != 0) format = MG_Util::ConvertTextureInternalFormatToVkEnum(MG_Util::ConvertGLEnumToTextureInternalFormat( state.BoundShaderImages[unit].InternalFormat)); if (format == VK_FORMAT_UNDEFINED) format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat); const auto feature = storage ? VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT : VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT; if (format == VK_FORMAT_UNDEFINED || !BufferFormatSupportsFeature(m_physicalDevice, format, feature)) { // P7 A.5, a decline (rule I (a)). GL's buffer-texture format list is wider than what // Vulkan mandates for texel buffers - GL_RGB32{F,I,UI} and the 16-bit UNORM members // are the realistic misses on a real driver - so this is a device capability gap, not // anything the application did wrong, and nothing it could have sent differently. // // The placeholder is the RIGHT answer here, where it was not for the unaligned range // above, and the difference is whether a correct answer exists at all. There, the // bytes are addressable and only the descriptor cannot name them, so a placeholder // would have hidden a fixable gap behind silently dropped writes. Here the device // cannot represent this format as a texel buffer in any form - so the honest // observable is monolith's for a missing view: the fetch reads zeros and the draw // survives. AcquireUnboundTexelBufferView falls back to the R32 member of the // shader's numeric class, whose texel-buffer support is mandatory, so the fallback // itself cannot fail for want of device features. MGLOG_E_ONCE("ResolveWireTexelBufferDescriptor: binding %u wants VkFormat %d as a %s texel buffer and " "this device does not support it; the binding falls back to the zero placeholder and the " "fetch reads zeros (texel-buffer-native-format)", binding, static_cast(format), storage ? "storage" : "uniform"); return placeholder(); } const VkDeviceSize texelSize = MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat); VkDeviceSize start = 0, size = 0; if (texelSize == 0 || !ResolveWireRange(record.Desc.BufOffset, record.Desc.BufSize, slice.size, start, size)) return false; size = std::min(size / texelSize, static_cast(m_wireMaxTexelElements)) * texelSize; if (size == 0) return false; if (start > std::numeric_limits::max() - slice.offset) WireDescriptorFatal("texel-buffer-offset-overflow"); if ((slice.offset + start) % m_wireTexelOffsetAlignment != 0) { // P7 A.4, a named decline (rule I (a)). Not reachable through the public API: // glTexBufferRange already refuses an offset that is not a multiple of // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT (GL_Texture.cpp), and that cap is published // from this same minTexelBufferOffsetAlignment. It stays a decline rather than a // copy for the reason below. const Bool writable = storage && state.BoundShaderImages[unit].Access != kMGPipeImageAccessReadOnly; MGLOG_E_ONCE("ResolveWireTexelBufferDescriptor: binding %u is bound at offset %llu, which is not a " "multiple of this device's minTexelBufferOffsetAlignment (%llu); the %s view is DECLINED " "(unaligned-texel-buffer-range)", binding, static_cast(slice.offset + start), static_cast(m_wireTexelOffsetAlignment), writable ? "writable" : "read-only"); // A read-only fetch can still be served the way an unbound buffer texture is - // zeros, which is what a missing view gives in monolith - and that keeps the draw. // A writable imageBuffer must not: the placeholder is shared by every unbound // binding, so absorbing real stores into it would alias other bindings AND lose // the application's writes without a word. Copying into an aligned transient slice // is not the answer either: the transient arena carries no // {UNIFORM,STORAGE}_TEXEL_BUFFER usage (VkBufferManager::InitializeTransientArenas), // so no VkBufferView can be made over it, and widening a shared per-frame // allocation to buy an unreachable path is the wrong trade. See notes/p7/magma-a.md. return writable ? false : placeholder(); } VkBufferViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO}; viewInfo.buffer = slice.buffer; viewInfo.format = format; viewInfo.offset = slice.offset + start; viewInfo.range = size; if (vkCreateBufferView(m_device, &viewInfo, nullptr, &out) != VK_SUCCESS || out == VK_NULL_HANDLE) return false; m_frames[frameIndex].texelBufferViews.push_back(out); if (storage && state.BoundShaderImages[unit].Access != kMGPipeImageAccessReadOnly) m_bufferManager->MarkWireBufferGpuWritten(buffer, start, size); return true; } Bool UniformManager::ResolveTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, false, outBufferView); } // GLSL `imageBuffer`. The one image uniform whose Vulkan descriptor is a VkBufferView rather // than a VkImageView, so it is half ResolveStorageImageDescriptor (the resource comes from an // IMAGE unit, i.e. from glBindImageTexture, not from a texture unit) and half // ResolveTexelBufferDescriptor (the descriptor is a buffer view over the GL buffer the // texture is attached to). // // Before this existed the descriptor kind reflected as SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_- // TEXEL_BUFFER and fell into ReflectDescriptorTypeToBindingKind's `default:`, whose only // complaint is an assert that compiles out above DEBUG - so a release build declared no // binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline // creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the // case that carries it. Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, true, outBufferView); } Bool UniformManager::ResolveStorageBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const { outBufferInfo = {}; MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null"); MOBILEGL_ASSERT(MG_Pipe::gPipeInputs->IsLive(), "ResolveStorageBufferDescriptor: GL context is null"); MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(), "ResolveStorageBufferDescriptor: binding %u out of range", binding); if ((binding >= programObj.storageBlockIndexByBinding.size() || binding >= programObj.storageBlockNameByBinding.size())) WireDescriptorFatal("storage-buffer-reflection-binding"); const Int blockIndex = programObj.storageBlockIndexByBinding[binding]; MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u", binding); if (blockIndex < 0) WireDescriptorFatal("storage-buffer-reflection-block"); // An atomic counter is not an SSBO the application ever declared: glslang lowers every // atomic_uint onto a synthesized gl_AtomicCounterBlock_ storage block, where N is the // GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary // storage-block slot, so resolving it the SSBO way looked up GL_SHADER_STORAGE_BUFFER // point N' - which is never where glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, N, ...) put // the buffer. The counter therefore never reached the shader (KHR-GL43 // shader_atomic_counters.advanced-usage-*), and when the application also bound an SSBO at // the colliding slot the descriptor silently aliased it, so the dispatch wrote over the // application's own buffer. DirectGLES has always taken this branch explicitly // (SyncAtomicCounterBuffers); this is the same rule in Magma's descriptor resolution. // // Only the SOURCE of the handle differs. The per-counter layout(offset=) is already folded // into the block's SPIR-V member offsets on this path (FlattenAtomicCounterBlockPass is // DirectGLES-only), so everything below - residency, the glBindBufferRange window, the // descriptor fill - is target-agnostic and stays exactly as it was. const String& blockName = programObj.storageBlockNameByBinding[binding]; const Int atomicCounterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(blockName); const Bool isAtomicCounterBlock = atomicCounterBinding >= 0; const BufferTarget bufferTarget = isAtomicCounterBlock ? BufferTarget::AtomicCounter : BufferTarget::ShaderStorage; // A block instance array declares one block whose elements take consecutive GL binding // points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole // array to that one block - so the element index IS the offset from its binding. glslang // synthesizes one counter block per GL binding, so a counter block is never an instance // array and `element` is always 0 there; the +element rule stays with the SSBO case. const GLuint frontendBinding = isAtomicCounterBlock ? static_cast(atomicCounterBinding) : program.GetShaderStorageBlockBinding(static_cast(blockIndex)) + element; const auto& state = MG_Pipe::MGPipeApplier(); const Uint32 cls = isAtomicCounterBlock ? MG_Pipe::kMGPipeShaderBufferClassAtomicCounter : MG_Pipe::kMGPipeShaderBufferClassShaderStorage; if (frontendBinding >= MG_Pipe::kMGPipeMaxBufferBindingPoints) WireDescriptorFatal("storage-buffer-binding-point"); // The applier retains entries outside the last partial update window. Count // describes that update, not the upper bound of the complete binding table. const auto& range = state.BoundShaderBuffers[cls][frontendBinding]; if (MG_Pipe::MGPipeHandleIsNull(range.Res)) { const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor(); if (!placeholder.IsValid()) return false; outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size}; return true; } BufferSlice slice{}; if (!m_bufferManager->AcquireWireSlice(BufferKind::ShaderStorage, range.Res, slice) || !slice.IsValid()) return false; VkDeviceSize start = 0, size = 0; if (!ResolveWireRange(range.Offset, range.Size, slice.size, start, size)) return false; if (start > std::numeric_limits::max() - slice.offset) WireDescriptorFatal("storage-buffer-offset-overflow"); if ((slice.offset + start) % m_wireStorageOffsetAlignment != 0) { // P7 A.4, a named decline (rule I (a)), and the one shape in this cluster an // application can actually reach. GL has no queryable atomic-counter buffer // offset alignment, so glBindBufferRange only enforces offset % 4 for // GL_ATOMIC_COUNTER_BUFFER (GL 4.6 core 6.1.1, GL_Buffer.cpp // ValidateBufferRangeOffsetAndSize); glslang lowers the counter block onto a // storage buffer, whose descriptor offset must be a multiple of // minStorageBufferOffsetAlignment - 16 on lavapipe, 64 on Adreno. Offset 4 is // therefore a legal bind this backend cannot express. The plain SSBO half is // unreachable by comparison: GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is // published from that same Vulkan limit, so the frontend already refused it // with GL_INVALID_VALUE. // // DECLINED RATHER THAN COPIED, deliberately. Copying the window into an aligned // transient slice reads correctly and then silently DROPS every shader write, // and this site cannot tell a writable block from a read-only one: there is no // readonly bit in the storage-block reflection (ProgramFactory.h carries only // name and index), an atomic counter is written by definition, and // MarkWireBufferGpuWritten below already assumes any binding here may be // written. A correct copy needs a post-dispatch copy-back from the transient // slice to the unaligned source, ordered with a barrier - and the place to // record that is the draw/dispatch tail in VulkanRenderer.cpp / WireDraw.inc, // which belong to wave-2 packages B and C, not to this file. Losing the // dispatch with a named line in the role log beats returning stale counters // with no error at all. notes/p7/magma-a.md carries the recipe. MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' is bound at offset %llu, which is not a " "multiple of this device's minStorageBufferOffsetAlignment (%llu); %s ranges cannot be " "expressed as a descriptor and the shader's writes would be dropped by a read-only " "copy, so the binding is DECLINED (unaligned-%s-buffer-range)", blockName.c_str(), static_cast(slice.offset + start), static_cast(m_wireStorageOffsetAlignment), isAtomicCounterBlock ? "atomic-counter" : "shader-storage", isAtomicCounterBlock ? "atomic" : "storage"); return false; } if (size > m_wireMaxStorageRange) { // P7 A.3: clamp, do not refuse. The monolith arm binds whatever the GL range // asked for and never consults maxStorageBufferRange at all (see the resident // path below), so refusing here was the wire arm inventing a death the other // backend does not have. A clamped binding keeps every byte the device can // name reachable; past it the shader is out of the descriptor and // robustBufferAccess answers zero, which beats losing the draw outright. MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' bound %llu bytes, past this device's " "maxStorageBufferRange of %llu; the binding is clamped to the limit", blockName.c_str(), static_cast(size), static_cast(m_wireMaxStorageRange)); size = m_wireMaxStorageRange; } outBufferInfo = {slice.buffer, slice.offset + start, size}; // Shader writes stay in the canonical server VkBuffer. The event marks the // client's shadow stale; neither this path nor later acquires seed it back. m_bufferManager->MarkWireBufferGpuWritten(range.Res, start, size); return true; } Bool UniformManager::ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo) const { return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, true, outImageInfo); } VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain, Bool storage) { MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null"); const VkFormatFeatureFlags requiredFeature = storage ? VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT : VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT; const VkFormat fallbackFormat = PlaceholderFormatForNumericDomain(numericDomain); VkFormat format = declaredFormat; if (format == VK_FORMAT_UNDEFINED || !BufferFormatSupportsFeature(m_physicalDevice, format, requiredFeature)) { // The declared format is what a shader that WRITES through this descriptor is // validated against, so it is tried first and kept whenever the device can use it. // Falling back is for the two cases where it cannot be: a sampled texel buffer, which // declares no format at all, and a device that does not list the declared one as a // texel buffer. The fallback stays inside the shader's numeric class, which is the // part the descriptor is checked on for a formatless declaration - and the R32 // members of the three classes are mandatory-support formats, so this cannot fail for // want of device features. format = fallbackFormat; } if (format == VK_FORMAT_UNDEFINED || !BufferFormatSupportsFeature(m_physicalDevice, format, requiredFeature)) { MGLOG_E_ONCE("AcquireUnboundTexelBufferView: no usable placeholder format (declared=%d fallback=%d " "storage=%s)", static_cast(declaredFormat), static_cast(fallbackFormat), storage ? "true" : "false"); return VK_NULL_HANDLE; } const Uint64 key = (static_cast(format) << 1) | (storage ? 1ull : 0ull); const auto cached = m_unboundTexelBufferViews.find(key); if (cached != m_unboundTexelBufferViews.end()) { return cached->second; } const BufferSlice placeholder = m_bufferManager->AcquireUnboundTexelBufferDescriptor(); if (!placeholder.IsValid()) { MGLOG_E_ONCE("AcquireUnboundTexelBufferView: placeholder buffer unavailable"); return VK_NULL_HANDLE; } // A buffer view's range must be a whole number of texels of its own format, and the // placeholder is sized for the largest of them - so floor rather than assume. const VkDeviceSize texelSize = std::max(1, vkuFormatTexelBlockSize(format)); const VkDeviceSize range = (placeholder.size / texelSize) * texelSize; if (range == 0) { MGLOG_E_ONCE("AcquireUnboundTexelBufferView: placeholder holds no whole texel of format=%d", static_cast(format)); return VK_NULL_HANDLE; } VkBufferViewCreateInfo viewInfo{}; viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; viewInfo.buffer = placeholder.buffer; viewInfo.format = format; viewInfo.offset = placeholder.offset; viewInfo.range = range; VkBufferView view = VK_NULL_HANDLE; const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &view); if (result != VK_SUCCESS || view == VK_NULL_HANDLE) { MGLOG_E_ONCE("AcquireUnboundTexelBufferView: vkCreateBufferView failed result=%d format=%d", result, static_cast(format)); return VK_NULL_HANDLE; } m_unboundTexelBufferViews.emplace(key, view); MGLOG_D("AcquireUnboundTexelBufferView: created placeholder view format=%d storage=%s", static_cast(format), storage ? "true" : "false"); return view; } Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, GLint imageLevel, GLenum imageAccess) { return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel; } Bool UniformManager::ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex, Uint32 bindingPoint, UboBindResult& out) const { if (bindingPoint >= MG_Pipe::kMGPipeMaxBufferBindingPoints) WireDescriptorFatal("uniform-buffer-binding-point"); VkDeviceSize blockSize = program.GetUBOSizeAt(blockIndex); if (blockSize == 0) return false; if (blockSize > m_wireMaxUniformRange) { // P7 A.3: clamp, do not refuse. A conformant program cannot get here - // GL_MAX_UNIFORM_BLOCK_SIZE is published straight from this same // maxUniformBufferRange (BackendLoaders/Vulkan/Loader.cpp), so a block this big // fails to link long before a draw - which makes refusing it a Fatal for a shape // nothing can produce. If the two ever disagree, the honest answer is the bytes // the device CAN name: everything past the clamp is outside the descriptor and // reads back as zero under robustBufferAccess, which is inside GL's "undefined" // for a block the implementation never promised to hold. Clamping keeps the range // constant across draws, so the descriptor-set reuse hash is unaffected. MGLOG_E_ONCE("ResolveWireUniformBufferPayload: reflected uniform block %u is %llu bytes, past this " "device's maxUniformBufferRange of %llu; the binding is clamped to the limit and the " "remainder reads zero", blockIndex, static_cast(blockSize), static_cast(m_wireMaxUniformRange)); blockSize = m_wireMaxUniformRange; } const auto& range = MG_Pipe::MGPipeApplier().BoundShaderBuffers[ MG_Pipe::kMGPipeShaderBufferClassUniform][bindingPoint]; BufferSlice source{}; VkDeviceSize start = 0, available = 0; const Bool bound = !MG_Pipe::MGPipeHandleIsNull(range.Res); if (bound) { if (!m_bufferManager->AcquireWireSlice(BufferKind::Uniform, range.Res, source) || !source.IsValid()) return false; if (!ResolveWireRange(range.Offset, range.Size, source.size, start, available)) return false; if (start > std::numeric_limits::max() - source.offset) WireDescriptorFatal("uniform-buffer-offset-overflow"); const VkDeviceSize absoluteOffset = source.offset + start; if (available >= blockSize && absoluteOffset % m_minDynamicOffsetAlignment == 0 && absoluteOffset <= std::numeric_limits::max()) { out.directBindable = true; out.buffer = source.buffer; out.range = blockSize; out.dynamicOffset = absoluteOffset; return true; } } // Preserve the existing short-range compatibility policy without sampling the // client's shadow: zero the complete reflected block, then GPU-copy only bytes // inside the GL binding range. A readback here could retire the command buffer // whose value BindProgramUniformBuffers is currently holding. const VkDeviceSize copied = std::min(available, blockSize); static thread_local Vector zero; zero.assign(static_cast(blockSize), 0); BufferSlice padded{}; if (!m_bufferManager->UploadTransient(BufferKind::Uniform, m_wireFrameIndex, zero.data(), blockSize, std::max(4, m_minDynamicOffsetAlignment), padded) || !padded.IsValid()) return false; if (bound && copied != 0) { // P7 A.1 retires `uniform-buffer-byte-tail`. The window is word-aligned at both // ends on nearly every bind - glBindBufferRange already forces the offset onto // GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, and that is a multiple of four on every // device - but GL 4.6 core 6.1.1 puts no such rule on the SIZE, so `copied` is free // to end mid-word and the Redmi Minecraft run lands there // (notes/p5f/magma-inproc-fix.md #5). An unaligned start is not reachable through // the public API and only a protocol-level offset can produce one; the sub-word arm // handles it anyway rather than leaving a second shape to trip over later. Both arms // copy the same bytes to the same place: the sub-word one pays one extra transient // slice and one extra region copy, and nothing outside [start, start + copied) // reaches the block, which is what makes the padded zeros the visible tail. const Bool wordWindow = ((start | copied) & 3u) == 0; if (!(wordWindow ? m_bufferManager->CopyWireBufferRangeToSlice(range.Res, start, copied, padded) : m_bufferManager->CopyWireBufferSubWordRangeToSlice(range.Res, start, copied, m_wireFrameIndex, padded, 0))) return false; } if (padded.offset > std::numeric_limits::max()) { // P7 A.2, a decline (rule I (a)), not a session death. A Vulkan dynamic offset is a // uint32_t, so a transient uniform ring that grew past 4 GiB inside one frame has // nowhere to put this block - which is our ring's problem, not a protocol fault, and // nothing the peer could have sent differently. One binding is skipped and the draw // is lost; the session keeps running and the next frame's rewound ring resolves it. // The monolith arm has the same ceiling and truncates silently on the way out // (ResolveDynamicUboDescriptor's static_cast), i.e. it binds a WRONG window // rather than none, so declining is the strictly better observable of the two. MGLOG_E_ONCE("ResolveWireUniformBufferPayload: the transient uniform ring reached offset " "%llu, past the 4 GiB a Vulkan dynamic offset can name; uniform binding " "point %u is declined for this draw", static_cast(padded.offset), bindingPoint); return false; } out.directBindable = true; out.buffer = padded.buffer; out.range = blockSize; out.dynamicOffset = padded.offset; return true; } Bool UniformManager::ResolveUniformBufferPayload(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const { const void* outData = nullptr; VkDeviceSize outSize = 0; MOBILEGL_ASSERT(MG_Pipe::gPipeInputs->IsLive(), "ResolveUniformBufferPayload: GL context is null"); MOBILEGL_ASSERT(binding < programObj.bindingKinds.size(), "ResolveUniformBufferPayload: binding %u out of range", binding); MOBILEGL_ASSERT(programObj.bindingKinds[binding] == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic, "ResolveUniformBufferPayload: binding %u is not a uniform buffer descriptor", binding); if (programObj.globalUboBinding == static_cast(binding)) { outData = program.GetUBOData(); outSize = static_cast(program.GetUBOSize()); static const Array emptyGlobalUbo{}; if (outData == nullptr || outSize == 0) { outData = emptyGlobalUbo.data(); outSize = static_cast(emptyGlobalUbo.size()); } // The global UBO is CPU uniform data, not an app buffer -> always UploadTransient. out.payload = outData; out.payloadSize = outSize; return outData != nullptr && outSize > 0; } MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(), "ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding); if (binding >= programObj.uniformBlockIndexByBinding.size()) WireDescriptorFatal("uniform-buffer-reflection-binding"); Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; if (arrayElement > 0) { const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding); const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() && arrayElement < arrayIt->second.size(); MOBILEGL_ASSERT(elementValid, "ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding, arrayElement); if (!elementValid) { return false; } blockIndex = arrayIt->second[arrayElement]; } MOBILEGL_ASSERT(blockIndex >= 0, "ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding); const Uint32 activeUniformBlockCount = static_cast(program.GetActiveUniformBlocksCount()); MOBILEGL_ASSERT(static_cast(blockIndex) < activeUniformBlockCount, "ResolveUniformBufferPayload: uniform block index %d out of range (count=%u)", blockIndex, activeUniformBlockCount); if ((blockIndex < 0 || static_cast(blockIndex) >= activeUniformBlockCount)) WireDescriptorFatal("uniform-buffer-reflection-block"); const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast(blockIndex)); return ResolveWireUniformBufferPayload(program, static_cast(blockIndex), frontendBinding, out); } Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const { outPool = VK_NULL_HANDLE; if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) { return false; } // Sized from what a real program declares, not from the 256-binding cap. A GL program's // single descriptor set holds the bindings shader reflection found - typically 2 to 8 - so // scaling by m_maxBindings declared 5 x 64 x 256 = 81,920 descriptors per pool and 245,760 // across the three frames in flight, which drivers that reserve backing store proportional // to the declared count pay for at init. An outlier program is absorbed by the existing // VK_ERROR_OUT_OF_POOL_MEMORY -> GrowFrameDescriptorPool path: pool sizes are aggregate // budgets rather than per-set limits, and vkAllocateDescriptorSets is spec-required to // report that error rather than fail hard. static constexpr Uint32 kEstimatedBindingsPerSet = 8; const Uint64 descriptorCount64 = static_cast(maxSets) * static_cast(std::min(m_maxBindings, kEstimatedBindingsPerSet)); if (descriptorCount64 > static_cast(std::numeric_limits::max())) { MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow"); return false; } const Uint32 descriptorCount = static_cast(descriptorCount64); VkDescriptorPoolSize poolSizes[6]{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; poolSizes[0].descriptorCount = descriptorCount; poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; poolSizes[1].descriptorCount = descriptorCount; poolSizes[2].type = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; poolSizes[2].descriptorCount = descriptorCount; poolSizes[3].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; poolSizes[3].descriptorCount = descriptorCount; poolSizes[4].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; poolSizes[4].descriptorCount = descriptorCount; poolSizes[5].type = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; poolSizes[5].descriptorCount = descriptorCount; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; // FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back // (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity. // The cost is on set allocation only, which happens when a layout's per-frame // cache grows - never on the per-draw reuse path. poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT | (updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0); poolInfo.maxSets = maxSets; poolInfo.poolSizeCount = static_cast(std::size(poolSizes)); poolInfo.pPoolSizes = poolSizes; const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool); if (result != VK_SUCCESS) { MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d", result); return false; } return true; } Uint32 UniformManager::NextDescriptorPoolMaxSets(const Vector& pools, Bool updateAfterBind, Uint32 initialMaxSets) { const Uint32 minSets = std::max(1, initialMaxSets); const Uint32 maxSets = std::max(minSets, kMaxDescriptorPoolSets); Uint32 largest = 0; for (const auto& pool : pools) { if (pool.updateAfterBind == updateAfterBind) { largest = std::max(largest, pool.maxSets); } } if (largest == 0) { return minSets; } const Uint64 doubled = static_cast(largest) * 2; return static_cast(std::clamp(doubled, minSets, maxSets)); } Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) { if (frame.descriptorPools.empty()) { return false; } Uint32 grownMaxSets = NextDescriptorPoolMaxSets(frame.descriptorPools, updateAfterBind, m_setsPerFrame); VkDescriptorPool grownPool = VK_NULL_HANDLE; // A large pool is a large driver allocation; if the device refuses one, settle for less // rather than failing the draw - halving down to the base size, where the old policy lived. while (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) { if (grownMaxSets <= m_setsPerFrame) { MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u sets)", grownMaxSets); return false; } grownMaxSets = std::max(m_setsPerFrame, grownMaxSets / 2); } frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind}); frame.activeDescriptorPoolIndex = static_cast(frame.descriptorPools.size() - 1); frame.capacitySets += grownMaxSets; // Growing is the opposite of quiet: a trim must wait a full quiet run from here. frame.quietEpochs = 0; MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool exhausted, grew a %u-set pool (updateAfterBind=%d), " "poolCount=%zu", frameIndex, grownMaxSets, updateAfterBind ? 1 : 0, frame.descriptorPools.size()); return true; } VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) { auto& frame = m_frames[frameIndex]; const Bool updateAfterBind = programObj.usesUpdateAfterBind; if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size() || frame.descriptorPools[frame.activeDescriptorPoolIndex].updateAfterBind != updateAfterBind || frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >= frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) { const auto availableBucket = std::find_if( frame.descriptorPools.begin(), frame.descriptorPools.end(), [updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind && candidate.allocatedSets < candidate.maxSets; }); if (availableBucket == frame.descriptorPools.end()) { outDescriptorSet = VK_NULL_HANDLE; return VK_ERROR_OUT_OF_POOL_MEMORY; } frame.activeDescriptorPoolIndex = static_cast(std::distance(frame.descriptorPools.begin(), availableBucket)); } auto& bucket = frame.descriptorPools[frame.activeDescriptorPoolIndex]; if (bucket.allocatedSets >= bucket.maxSets) { outDescriptorSet = VK_NULL_HANDLE; return VK_ERROR_OUT_OF_POOL_MEMORY; } VkDescriptorSetAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocInfo.descriptorSetCount = 1; allocInfo.pSetLayouts = &programObj.descriptorSetLayout; allocInfo.descriptorPool = bucket.handle; VkResult result = vkAllocateDescriptorSets(m_device, &allocInfo, &outDescriptorSet); if (result == VK_SUCCESS) { ++bucket.allocatedSets; } return result; } VkResult UniformManager::AcquireDescriptorSet(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) { auto& frame = m_frames[frameIndex]; auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout]; if (cache.cursor < cache.sets.size()) { outDescriptorSet = cache.sets[cache.cursor++].set; } else { VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet); if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) { if (!GrowFrameDescriptorPool(frame, frameIndex, programObj.usesUpdateAfterBind)) { MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed"); return allocResult; } allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet); } if (allocResult != VK_SUCCESS || outDescriptorSet == VK_NULL_HANDLE) { return allocResult; } // The successful allocation came from the bucket the alloc helper left // active; record it so a layout-destroyed purge can free the set back. cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle}); ++cache.cursor; MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex, cache.sets.size()); } ++frame.allocatedSetsThisFrame; frame.peakAllocatedSetsThisFrame = std::max(frame.peakAllocatedSetsThisFrame, frame.allocatedSetsThisFrame); return VK_SUCCESS; } Bool UniformManager::ResolveDynamicUboDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer, VkDeviceSize& outRange, Uint32& outDynamicOffset) { UboBindResult ubo{}; const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, arrayElement, ubo); if (!hasPayload) return false; MOBILEGL_ASSERT(hasPayload && (ubo.directBindable || (ubo.payload != nullptr && ubo.payloadSize > 0)), "UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: missing UBO payload on binding %u element %u", binding, arrayElement); if (ubo.directBindable) { // Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy. outBuffer = ubo.buffer; outRange = ubo.range; outDynamicOffset = static_cast(ubo.dynamicOffset); return true; } // Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged // uniform bytes re-use the slice already uploaded this frame. const Bool isGlobalUbo = programObj.globalUboBinding == static_cast(binding) && arrayElement == 0; const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial(); const Uint64 uboProgramLifetimeId = program.GetLifetimeId(); const Uint32 uboContentVersion = program.GetUBOContentVersion(); if (isGlobalUbo) { for (const auto& memo : m_globalUboMemo) { if (memo.buffer != VK_NULL_HANDLE && memo.programLifetimeId == uboProgramLifetimeId && memo.frameSerial == uboFrameSerial && memo.uboContentVersion == uboContentVersion && memo.range == static_cast(ubo.payloadSize)) { outBuffer = memo.buffer; outRange = memo.range; outDynamicOffset = static_cast(memo.offset); return true; } } } BufferSlice slice{}; if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) { MOBILEGL_ASSERT(false, "UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: UBO upload failed on binding %u element %u", binding, arrayElement); return false; } outBuffer = slice.buffer; outRange = ubo.payloadSize; outDynamicOffset = static_cast(slice.offset); if (isGlobalUbo && MG_Util::PipeStats::Enabled()) { // Magma's half of stage-ubo-global, so the class means the same on both // backends. The memo hit above returns before this, so a frame that reuses the // slice correctly contributes nothing. MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal, static_cast(ubo.payloadSize)); } if (isGlobalUbo) { m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion, slice.buffer, slice.offset, static_cast(ubo.payloadSize)}; m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize; } return true; } void UniformManager::BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint, VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet, const Vector& dynamicOffsets) { // Skip the driver call when this exact binding is already live on the // command buffer (see the bind-dedup shadow in the header). const Uint32 offsetCount = static_cast(dynamicOffsets.size()); Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet && m_lastBindLayout == pipelineLayout && m_lastBindPoint == bindPoint && m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets; if (identicalBind) { for (Uint32 i = 0; i < offsetCount; ++i) { if (m_lastBindOffsets[i] != dynamicOffsets[i]) { identicalBind = false; break; } } } if (!identicalBind) { vkCmdBindDescriptorSets(commandBuffer, bindPoint, pipelineLayout, 0, 1, &descriptorSet, offsetCount, dynamicOffsets.data()); if (offsetCount <= kMaxShadowedDynamicOffsets) { m_lastBindValid = true; m_lastBindSet = descriptorSet; m_lastBindLayout = pipelineLayout; m_lastBindPoint = bindPoint; m_lastBindOffsetCount = offsetCount; std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets); } else { m_lastBindValid = false; } } } Bool UniformManager::PrepareWireTextureResources(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { if (programObj.declinedDescriptors || m_textureManager == nullptr) { MGL_WIRE_DECLINE_AT(DescriptorDeclined, "the program carries a descriptor MobileGL could not resolve"); return false; } const auto& state = MG_Pipe::MGPipeApplier(); // Resolve writable images first: aliases sampled by the same draw must // see the final STORAGE-capable allocation before any view is built. for (const Bool storage : {true, false}) { const auto wanted = storage ? ProgramFactory::DescriptorBindingKind::StorageImage : ProgramFactory::DescriptorBindingKind::CombinedImageSampler; for (const Uint32 binding : programObj.activeBindings) { if (binding >= m_maxBindings) break; if (programObj.bindingKinds[binding] != wanted) continue; const Int baseLocation = programObj.samplerUniformLocationByBinding[binding]; const Uint32 count = BindingDescriptorCount(programObj, binding); for (Uint32 element = 0; element < count; ++element) { const Int location = baseLocation + static_cast(element); if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) { MGL_WIRE_DECLINE_AT(SamplerLocationAlias, "binding %u element %u does not alias one sampler uniform", binding, element); return false; } const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); if (unit < 0 || static_cast(unit) >= (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) { MGL_WIRE_DECLINE_AT(SamplerUnitRange, "sampler/image unit %d is out of range", unit); return false; } const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; // Null is the wire's unbound/incomplete binding. Its native // placeholder needs no resource upload or frontend allocation. if (MG_Pipe::MGPipeHandleIsNull(handle)) continue; // Nor does an image unit naming a (level, layer) its texture lacks: the // descriptor resolve binds that same placeholder (GL 4.6 core 8.26), and // preparing the texture would decline a storage-less one and lose the pass. if (storage && WireShaderImageNamesNoTexel(*m_textureManager, state.BoundShaderImages[unit])) continue; const auto* resource = m_textureManager->SyncTextureResourceByHandle(handle, false, storage); if (resource == nullptr) return false; // Another session writes it: the renderer acquires it before this use. if (resource->sharedImageId != 0 || resource->yuvImageId != 0) m_textureManager->NoteSharedImageUse(*resource); } } } m_textureManager->FlushPendingUploads(); return true; } Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex, VkPipelineBindPoint bindPoint) { // This program has a descriptor MobileGL could not resolve (see // VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the // binding is still declared in the layout, so the pipeline is consistent with the shader // and creating it is safe - what must not happen is the draw, because the descriptor // behind that binding can never be written. The draw setup skips the draw on a false // return. ReflectLayout already said why, once, at MGLOG_I. if (programObj.declinedDescriptors) { MGLOG_D("UniformDescriptorBinder::BindProgramUniformBuffers: refusing a program whose descriptor layout " "was declined at reflection"); return false; } auto& frame = m_frames[frameIndex]; if (frame.descriptorPools.empty()) { MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid"); return false; } if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) { frame.activeDescriptorPoolIndex = 0; } if (TryRebindWireDescriptorSet(commandBuffer, program, programObj, frameIndex, bindPoint)) return true; // What the full resolve below proves for the next draw's rebind (WireDescriptorRebind). Bool rebindEligible = true; auto& rebindResources = m_wireRebindResourcesScratch; rebindResources.clear(); // The descriptor set is chosen AFTER the writes are built (below), so a draw // whose resolved descriptor content matches the previous draw can reuse that // set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets. VkDescriptorSet descriptorSet = VK_NULL_HANDLE; MOBILEGL_ASSERT(m_textureManager != nullptr, "BindProgramUniformBuffers: texture manager is null"); MOBILEGL_ASSERT(m_samplerManager != nullptr, "BindProgramUniformBuffers: sampler manager is null"); MOBILEGL_ASSERT(m_bufferManager != nullptr, "BindProgramUniformBuffers: buffer manager is null"); auto& writes = m_writesScratch; auto& bufferInfos = m_bufferInfosScratch; auto& imageInfos = m_imageInfosScratch; auto& texelBufferViews = m_texelBufferViewsScratch; auto& dynamicOffsets = m_dynamicOffsetsScratch; writes.clear(); bufferInfos.clear(); imageInfos.clear(); texelBufferViews.clear(); dynamicOffsets.clear(); // Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for // the worst case so the pBufferInfo pointers taken below never dangle on reallocation. // Arrayed SSBO bindings contribute extra buffer infos too (but no dynamic offsets). Uint32 uboArrayExtra = 0; for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) { uboArrayExtra += static_cast(arrayEntry.second.size()) - 1u; } // Surplus descriptors over "one per binding", summed across EVERY arrayed binding // whatever its kind - storage blocks, image arrays and sampler arrays all land here. // One number for all of them because each container below is bounded by the same total. Uint32 arrayDescriptorExtra = 0; for (const Uint16 count : programObj.bindingDescriptorCounts) { if (count > 1) arrayDescriptorExtra += static_cast(count) - 1u; } writes.reserve(m_maxBindings); bufferInfos.reserve(m_maxBindings + uboArrayExtra + arrayDescriptorExtra); // Every binding pushes at most descriptorCount image infos, so bindings + surplus is the // worst case. Reserving only m_maxBindings here was exact while every binding pushed // exactly one - and reallocates under an image or sampler array, dangling every // pImageInfo already recorded in `writes` before vkUpdateDescriptorSets reads them. That // is reachable wherever m_maxBindings is small (it clamps to ~16 on Adreno and Mali), // which is exactly where a 7-element CTS sampler array does not fit the slack. imageInfos.reserve(m_maxBindings + arrayDescriptorExtra); // Exact, and safe only because it is: BOTH texel kinds (samplerBuffer and imageBuffer) // refuse descriptor arrays at program creation, so each contributes at most one view and // the total cannot exceed the binding count. The branches below take the address of // back(), so making a texel kind array-capable without also giving this the surplus // imageInfos gets would dangle every pTexelBufferView already recorded in `writes`. texelBufferViews.reserve(m_maxBindings); dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra); // Iterate only the bindings this program declares. The old walk covered all 256 slots of // bindingKinds on every draw to find the 1-8 a real program uses. for (const Uint32 binding : programObj.activeBindings) { if (binding >= m_maxBindings) { break; // ascending, so nothing past the cap can follow } const auto kind = programObj.bindingKinds[binding]; if (kind != ProgramFactory::DescriptorBindingKind::UniformBufferDynamic && kind != ProgramFactory::DescriptorBindingKind::StorageBuffer && kind != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) rebindEligible = false; VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; write.dstSet = descriptorSet; write.dstBinding = binding; write.dstArrayElement = 0; write.descriptorCount = 1; if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); const SizeT firstBufferInfoIndex = bufferInfos.size(); for (Uint32 element = 0; element < descriptorCount; ++element) { VkDescriptorBufferInfo bufferInfo{}; // Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo // is stable across draws and the descriptor-set reuse cache keeps hitting. bufferInfo.offset = 0; Uint32 dynOffset = 0; if (!ResolveDynamicUboDescriptor(program, programObj, binding, element, frameIndex, bufferInfo.buffer, bufferInfo.range, dynOffset)) { return false; } bufferInfos.push_back(bufferInfo); // Dynamic offsets are consumed in binding order, then array element order, // matching Vulkan's dynamic-offset consumption rules. dynamicOffsets.push_back(dynOffset); } write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; write.descriptorCount = descriptorCount; write.pBufferInfo = &bufferInfos[firstBufferInfoIndex]; writes.push_back(write); } else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) { VkBufferView bufferView = VK_NULL_HANDLE; if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) || bufferView == VK_NULL_HANDLE) { MGLOG_E_ONCE( "UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor", binding); return false; } texelBufferViews.push_back(bufferView); write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; write.pTexelBufferView = &texelBufferViews.back(); writes.push_back(write); } else if (kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer) { // Shares texelBufferViews with the sampled kind above, and may do so safely for // the same reason: neither kind can be an array, so each contributes exactly one // element and the reserve of m_maxBindings cannot be outrun - which is what keeps // the &back() below from dangling when a later binding pushes. VkBufferView bufferView = VK_NULL_HANDLE; if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) || bufferView == VK_NULL_HANDLE) { MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u " "has no valid descriptor", binding); return false; } texelBufferViews.push_back(bufferView); write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; write.pTexelBufferView = &texelBufferViews.back(); writes.push_back(write); } else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) { // One write per binding, but `descriptorCount` buffer infos: a GLSL block // instance array occupies a single binding whose elements each come from their // own GL binding point. const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); const SizeT firstBufferInfoIndex = bufferInfos.size(); for (Uint32 element = 0; element < descriptorCount; ++element) { VkDescriptorBufferInfo bufferInfo{}; if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) { MGLOG_E_ONCE( "UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u " "element %u has no valid descriptor", binding, element); return false; } bufferInfos.push_back(bufferInfo); } write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; write.descriptorCount = descriptorCount; write.pBufferInfo = &bufferInfos[firstBufferInfoIndex]; writes.push_back(write); } else if (kind == ProgramFactory::DescriptorBindingKind::StorageImage) { // One write per binding, but `descriptorCount` image infos: an ARRAY of image // uniforms is a single binding whose elements each carry their own image unit. // Writing only element 0 - which is all this used to do - left elements 1..N // never written at all, and a shader that indexes them reads an undefined // descriptor (lavapipe faults inside the shader; a real driver is free to do // anything). const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); const SizeT firstImageInfoIndex = imageInfos.size(); for (Uint32 element = 0; element < descriptorCount; ++element) { VkDescriptorImageInfo imageInfo{}; if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element, imageInfo)) { MGLOG_E_ONCE( "UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u " "element %u has no valid descriptor", binding, element); return false; } imageInfos.push_back(imageInfo); } write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; write.descriptorCount = descriptorCount; write.pImageInfo = &imageInfos[firstImageInfoIndex]; writes.push_back(write); } else { // One write per binding, but `descriptorCount` image infos: a sampler ARRAY is a // single binding whose elements each carry their own texture unit. Writing only // element 0 - which is all this used to do - left elements 1..N never written, // so a shader indexing them sampled a descriptor nobody had filled in // (KHR-GL42.shading_language_420pack.binding_sampler_array). const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); const SizeT firstImageInfoIndex = imageInfos.size(); for (Uint32 element = 0; element < descriptorCount; ++element) { VkDescriptorImageInfo imageInfo{}; VkTextureManager::TextureResource* sampledResource = nullptr; const Bool hasImage = ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, imageInfo, &sampledResource); // A placeholder (an empty unit) has no resource to re-check: full resolve. if (sampledResource == nullptr) rebindEligible = false; else rebindResources.push_back(sampledResource); if (!hasImage) { MGLOG_E_ONCE( "UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u " "has no valid texture descriptor", binding, element); return false; } if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) { MGLOG_E_ONCE( "UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u " "has null sampler or imageView", binding, element); return false; } imageInfos.push_back(imageInfo); } write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.descriptorCount = descriptorCount; write.pImageInfo = &imageInfos[firstImageInfoIndex]; writes.push_back(write); } } // Reuse a recent draw's descriptor set when the resolved content is // byte-identical (only the bind-time dynamic offsets differ). The signature // covers the descriptor-set layout + every write's binding/type/count + the // pointed-to buffer/image/texel-buffer infos (all value-initialized, so no // padding noise). Correctness: bindings are re-resolved every draw, so the // signature always reflects the current state and reuse happens only on an // exact match; a reused set is never re-acquired within a frame (the acquire // cursor only advances), so its written contents survive; the layout is part of // the signature so reuse never crosses programs. Sampler overrides (blits) // bypass and invalidate the cache. Uint64 signature = 0xcbf29ce484222325ULL; { const auto mix64 = [&signature](Uint64 word) { signature = (signature ^ word) * 0x100000001b3ULL; }; // The hashed descriptor payloads (VkDescriptorBufferInfo=24B, // VkDescriptorImageInfo=24B, VkBufferView=8B) are all 8-byte-multiple sized // and value-initialized (padding is zero), so hashing 64-bit words at a time // is exact and ~8x cheaper than byte-wise - the signature is recomputed every // draw, so its own cost has to stay small. const auto mixWords = [&mix64](const void* data, SizeT byteSize) { const auto* words = static_cast(data); for (SizeT i = 0; i < byteSize / sizeof(Uint64); ++i) { mix64(words[i]); } }; mixWords(&programObj.descriptorSetLayout, sizeof(programObj.descriptorSetLayout)); // P7 M2 round 2 (ID-P7-43): the buffer infos below name WIRE stores by VkBuffer // handle, and a wire store can be destroyed mid-frame (VkBufferManager:: // DeferredWireRelease) with its handle value re-minted before this frame's memo // is cleared. Folding the destroy epoch in makes every entry taken before such a // destroy miss, so a byte-identical info can never revive a set baked to a dead // store. NOT the slice epoch: that moves on every glBufferSubData and would make // the memo miss on every draw. mix64(m_bufferManager->GetWireStoreDestroyEpoch()); for (const auto& write : writes) { mix64((static_cast(write.dstBinding) << 40) ^ (static_cast(write.descriptorType) << 8) ^ static_cast(write.descriptorCount)); } mixWords(bufferInfos.data(), bufferInfos.size() * sizeof(VkDescriptorBufferInfo)); mixWords(imageInfos.data(), imageInfos.size() * sizeof(VkDescriptorImageInfo)); mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView)); } VkDescriptorSet reusedSet = VK_NULL_HANDLE; for (const auto& entry : m_descriptorReuseMemo) { if (entry.valid && entry.signature == signature) { reusedSet = entry.set; break; } } if (reusedSet != VK_NULL_HANDLE) { descriptorSet = reusedSet; } else { VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet); if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) { MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d", allocResult); return false; } for (auto& write : writes) { write.dstSet = descriptorSet; } if (!writes.empty()) { vkUpdateDescriptorSets(m_device, static_cast(writes.size()), writes.data(), 0, nullptr); } m_descriptorReuseMemo[m_descriptorReuseMemoNext] = DescriptorReuseEntry{signature, descriptorSet, true}; m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize; } BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, descriptorSet, dynamicOffsets); // Stamp what this full resolve depended on, for TryRebindWireDescriptorSet. Taken after // the walk: the walk's own syncs and barriers are part of what it proved. auto& rebind = m_wireDescriptorRebind; rebind.valid = rebindEligible && descriptorSet != VK_NULL_HANDLE && WireImageWriteEpoch() == m_wireImageBarrierEpoch; if (rebind.valid) { const auto& st = MG_Pipe::MGPipeApplier(); rebind.frameIndex = frameIndex; rebind.bindPoint = bindPoint; rebind.programHash = programObj.hash; rebind.layout = programObj.descriptorSetLayout; rebind.pipelineLayout = programObj.pipelineLayout; rebind.programHandle = program.Handle(); rebind.programSerial = program.GetBackendStateVersion(); rebind.programBindingsSerial = program.GetBlockBindingVersion(); rebind.contextSerial = st.ContextSerial; rebind.samplerViewsSerial = st.SamplerViewsSerial; rebind.samplerStatesSerial = st.SamplerStatesSerial; rebind.shaderImagesSerial = st.ShaderImagesSerial; rebind.shaderBuffersSerial = st.ShaderBuffersSerial; rebind.textureShutterSerial = st.TextureShutterSerial; rebind.storeDestroyEpoch = m_bufferManager->GetWireStoreDestroyEpoch(); rebind.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); rebind.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch(); rebind.samplerDestroyEpoch = m_samplerManager->DestroyEpoch(); rebind.samplerFrameBoundary = m_samplerManager->FrameBoundaryCount(); rebind.set = descriptorSet; rebind.sampledResources.assign(rebindResources.begin(), rebindResources.end()); rebind.bufferInfos.assign(bufferInfos.begin(), bufferInfos.end()); } return true; } Bool UniformManager::TryRebindWireDescriptorSet(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex, VkPipelineBindPoint bindPoint) { const auto& rebind = m_wireDescriptorRebind; if (!rebind.valid || rebind.frameIndex != frameIndex || rebind.bindPoint != bindPoint || rebind.programHash != programObj.hash || rebind.layout != programObj.descriptorSetLayout || rebind.pipelineLayout != programObj.pipelineLayout || !(rebind.programHandle == program.Handle()) || rebind.programSerial != program.GetBackendStateVersion() || rebind.programBindingsSerial != program.GetBlockBindingVersion()) return false; const auto& st = MG_Pipe::MGPipeApplier(); if (rebind.contextSerial != st.ContextSerial || rebind.samplerViewsSerial != st.SamplerViewsSerial || rebind.samplerStatesSerial != st.SamplerStatesSerial || rebind.shaderImagesSerial != st.ShaderImagesSerial || rebind.shaderBuffersSerial != st.ShaderBuffersSerial || rebind.textureShutterSerial != st.TextureShutterSerial) return false; if (rebind.storeDestroyEpoch != m_bufferManager->GetWireStoreDestroyEpoch() || rebind.textureImageEpoch != m_textureManager->GetTextureImageEpoch() || rebind.resourceEraseEpoch != m_textureManager->GetResourceEraseEpoch() || rebind.samplerDestroyEpoch != m_samplerManager->DestroyEpoch() || rebind.samplerFrameBoundary != m_samplerManager->FrameBoundaryCount() || WireImageWriteEpoch() != m_wireImageBarrierEpoch) return false; // The resource pointers are good while the erase epoch holds (checked above). A sampled // image that left GENERAL (rendered to, copied into) needs the full resolve's transition. for (const auto* resource : rebind.sampledResources) if (resource->layout != VK_IMAGE_LAYOUT_GENERAL) return false; // The buffer descriptors, in the walk's order, re-resolved: a UBO's dynamic offset is this // draw's; its buffer and range, and every storage buffer's window, must be the set's. auto& infos = m_wireRebindBufferScratch; infos.clear(); auto& dynamicOffsets = m_dynamicOffsetsScratch; dynamicOffsets.clear(); for (const Uint32 binding : programObj.activeBindings) { if (binding >= m_maxBindings) break; const auto kind = programObj.bindingKinds[binding]; const Uint32 count = BindingDescriptorCount(programObj, binding); if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { for (Uint32 element = 0; element < count; ++element) { VkDescriptorBufferInfo info{}; Uint32 offset = 0; if (!ResolveDynamicUboDescriptor(program, programObj, binding, element, frameIndex, info.buffer, info.range, offset)) return false; infos.push_back(info); dynamicOffsets.push_back(offset); } } else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) { for (Uint32 element = 0; element < count; ++element) { VkDescriptorBufferInfo info{}; if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, info)) return false; infos.push_back(info); } } } if (infos.size() != rebind.bufferInfos.size()) return false; for (SizeT i = 0; i < infos.size(); ++i) { const auto& a = infos[i]; const auto& b = rebind.bufferInfos[i]; if (a.buffer != b.buffer || a.offset != b.offset || a.range != b.range) return false; } BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, rebind.set, dynamicOffsets); return true; } } // namespace MobileGL::MG_Backend::DirectVulkan