mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
- M2's defer path destroys a wire store MID-FRAME (the serial-0 park, the sweep's prefix walk, the watermark); before it a wire store only died at the boundary that also clears UniformManager's memos (BeginFrame from Present or a drain). Those memos are keyed on the VkBuffer HANDLE - m_descriptorReuseMemo hashes the VkDescriptorBufferInfo words, FastRebindMemo compares uboBuffer - and on the wire arm the direct-bind UBO and the SSBO descriptor name the store's own handle. A store destroyed at a park, its handle value re-minted by the next Create (a heap pointer under lavapipe, LIFO out of glibc's tcache), and a draw resolving the same (handle, range) reused a set whose descriptor still pointed at the dead store's memory: silent wrong bytes, no Fatal (ID-P7-43, the fable review's must-fix).
- VkBufferManager gains m_wireStoreDestroyEpoch, bumped on every path that destroys a wire store's VkBuffer (DeferWireRelease's immediate destroy, SweepDeferredWireReleases once per sweep that freed anything, DestroyAllDeferredWireReleases, Shutdown's record teardown) and never reset, with GetWireStoreDestroyEpoch(). UniformManager mixes it into the descriptor-reuse signature and records it in FastRebindMemo, refusing the fast rebind when it moved; all under MOBILEGL_BUILD_DISAGGREGATED. Deliberately not m_sliceEpochCounter, which every WriteWireBuffer bumps and would defeat the memo on every glBufferSubData. On the wire arm only the signature is reached (SetupWireDraw passes no sampler hint), the FastRebindMemo field is the defensive half.
- Two comments from the same review: SweepDeferredWireReleases no longer claims GetSyncPointSubmitIndex() never decreases - it steps back by one across an abandoned recording (RecreateSwapchain, the minimized Present), and the prefix walk is conservative across that, not wrong; and the header's "floor is not a proof" keeps its decision on its first reason alone, the second was a stale citation of a fix that is on the integration tree.
- Section 9 partition exception for UniformManager.{h,cpp} granted by the integrator for this fix.
3410 lines
206 KiB
C++
3410 lines
206 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.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 "UniformManager.h"
|
|
|
|
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
|
#include "MG_State/GLState/Core.h"
|
|
#include <MG_Pipe/PipeInputsSwitch.h>
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// P5c ev: the GPU-write announcement routes through the reverse channel (R2).
|
|
#include <MG_Impl/Pipe/ResourceTracker.h>
|
|
#endif
|
|
#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 <Config.h>
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
#include <MG_Pipe/PipeApply.h>
|
|
// P7 wave 0: the seam WireDescriptorFatal dies through. See MG_Pipe/PipeSessionFail.h.
|
|
#include <MG_Pipe/PipeSessionFail.h>
|
|
// P7 wave 2 package B3: rule I's tally for this file's silent wire-draw exits.
|
|
#include "WireDeclineTally.h"
|
|
#endif
|
|
#include <vulkan/utility/vk_format_utils.h>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
SizeT UniformManager::WireImageViewKeyHash::operator()(const WireImageViewKey& key) const {
|
|
SizeT hash = std::hash<VkImage>{}(key.image);
|
|
const auto mix = [&hash](Uint64 value) {
|
|
hash ^= std::hash<Uint64>{}(value) + static_cast<SizeT>(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<VkDeviceSize>(offset);
|
|
const VkDeviceSize remaining = bufferSize - start;
|
|
size = declaredSize == MG_Pipe::kMGPipeWholeBuffer
|
|
? remaining : std::min<VkDeviceSize>(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;
|
|
}
|
|
}
|
|
|
|
static VkComponentSwizzle WireSwizzle(Uint8 value) {
|
|
switch (static_cast<TextureSwizzleParam>(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) const {
|
|
out = {};
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
|
const Int location = baseLocation + static_cast<Int>(element);
|
|
if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) return false;
|
|
const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
if (unit < 0 || static_cast<Uint32>(unit) >=
|
|
(storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false;
|
|
const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture;
|
|
if (MG_Pipe::MGPipeHandleIsNull(handle))
|
|
return ResolveWirePlaceholderImage(commandBuffer, program, programObj, binding, storage, out);
|
|
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");
|
|
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle, false, storage);
|
|
if (!resource) WireDescriptorFatal("image-resource");
|
|
m_textureManager->FlushPendingUploads();
|
|
|
|
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target);
|
|
using Target = MG_Pipe::MGPipeResourceTarget;
|
|
const Bool layerable = target == Target::Tex1DArray || target == Target::Tex2DArray ||
|
|
target == Target::Tex2DMSArray || target == Target::Tex3D ||
|
|
target == Target::TexCube || target == Target::TexCubeArray;
|
|
Uint32 level = storage ? state.BoundShaderImages[unit].Level : record.Params.BaseLevel;
|
|
// GL ignores Layer/Layered for non-layerable targets. In particular image1D
|
|
// stays a 1D view, and an ignored nonzero Layer must not address another slice.
|
|
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<Uint32>(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<Uint32>(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.
|
|
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
0, 1, &memory, 0, nullptr, 0, nullptr);
|
|
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) {
|
|
const Bool alphaIsOne = ResolveTextureFormatInfo(
|
|
static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba;
|
|
const auto swizzle = [&](Uint32 channel) {
|
|
const auto value = record.Params.Swizzle[channel];
|
|
return alphaIsOne && static_cast<TextureSwizzleParam>(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;
|
|
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle) || samplerHandle.Slot >= state.SamplerCsos.size())
|
|
WireDescriptorFatal("sampler-record");
|
|
const auto& sampler = state.SamplerCsos[samplerHandle.Slot];
|
|
if (!sampler.Live || sampler.Gen != samplerHandle.Gen) WireDescriptorFatal("sampler-record-generation");
|
|
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(sampler.Params,
|
|
static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
|
|
domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger,
|
|
levels);
|
|
if (!out.sampler) return false;
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
namespace {
|
|
constexpr Uint kFallbackTexture2DExternalIndex = 0xFFFFFF00u;
|
|
// One id for every storage-image placeholder. They are never reachable through GL - no
|
|
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
|
|
// id only has to stay clear of the application's, exactly like the sampled fallback's.
|
|
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
|
|
// The multisample sampled fallbacks: one per (target, numeric domain), because unlike the
|
|
// single-sampled fallback they cannot be reinterpreted into another domain at view time
|
|
// (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the
|
|
// same reason as the two above - they must not collide with anything glGenTextures can
|
|
// hand out.
|
|
constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u;
|
|
constexpr Uint kFallbackMultisampleExternalIndexCount = 6u;
|
|
|
|
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
|
|
// do reaches one, so anything keyed on the GL object an application bound - image-unit
|
|
// aliasing above all - has to leave them alone.
|
|
Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) {
|
|
if (texture == nullptr) return false;
|
|
const Uint index = static_cast<Uint>(texture->GetExternalIndex());
|
|
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
|
|
(index >= kFallbackMultisampleExternalIndexBase &&
|
|
index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Reverse of MG_Util::ConvertTextureInternalFormatToVkEnum. A placeholder texture is
|
|
// built through the ordinary frontend texture object (that is what gets it an image with
|
|
// STORAGE usage, a GENERAL transition and a view, for free), and that object is described
|
|
// by a GL internal format - while everything upstream of here speaks VkFormat. Scanned
|
|
// rather than tabulated: it runs once per (target, format) placeholder ever created, the
|
|
// enum is ~70 entries, and a second hand-written table is a second thing to drift.
|
|
// Ascending order matters: the sized formats precede the unsized aliases, so a scan
|
|
// answers with the sized one.
|
|
TextureInternalFormat InternalFormatForVkFormat(VkFormat format) {
|
|
if (format == VK_FORMAT_UNDEFINED) {
|
|
return TextureInternalFormat::Unknown;
|
|
}
|
|
for (Int index = 0; index < static_cast<Int>(TextureInternalFormat::TextureInternalFormatCount);
|
|
++index) {
|
|
const auto candidate = static_cast<TextureInternalFormat>(index);
|
|
if (MG_Util::ConvertTextureInternalFormatToVkEnum(candidate) == format) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return TextureInternalFormat::Unknown;
|
|
}
|
|
|
|
// What a 1x1 placeholder of a given target has to allocate for the backend to give it the
|
|
// Vulkan view type that target's image declaration demands (see
|
|
// VkTextureManager's TryResolveTextureShapeInfo, which reads exactly these two things).
|
|
struct PlaceholderShape {
|
|
Array<TextureUploadTarget, 6> uploadTargets{};
|
|
Uint32 uploadTargetCount = 0;
|
|
// The GL depth of the single level: the array length for an array target, the depth
|
|
// for a 3D one, and 6 for a cube map array (one whole cube).
|
|
Int depth = 1;
|
|
Bool valid = false;
|
|
};
|
|
|
|
PlaceholderShape PlaceholderShapeForTarget(TextureTarget target) {
|
|
PlaceholderShape shape{};
|
|
switch (target) {
|
|
case TextureTarget::Texture1D:
|
|
shape = {{TextureUploadTarget::Texture1D}, 1, 1, true};
|
|
break;
|
|
case TextureTarget::Texture2D:
|
|
shape = {{TextureUploadTarget::Texture2D}, 1, 1, true};
|
|
break;
|
|
case TextureTarget::TextureRectangle:
|
|
shape = {{TextureUploadTarget::TextureRectangle}, 1, 1, true};
|
|
break;
|
|
case TextureTarget::Texture3D:
|
|
shape = {{TextureUploadTarget::Texture3D}, 1, 1, true};
|
|
break;
|
|
case TextureTarget::Texture1DArray:
|
|
shape = {{TextureUploadTarget::Texture1DArray}, 1, 1, true};
|
|
break;
|
|
case TextureTarget::Texture2DArray:
|
|
shape = {{TextureUploadTarget::Texture2DArray}, 1, 1, true};
|
|
break;
|
|
case TextureTarget::TextureCubeMap:
|
|
shape = {{TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
|
|
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
|
|
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ},
|
|
6, 1, true};
|
|
break;
|
|
case TextureTarget::TextureCubeMapArray:
|
|
// Layers are cube faces, so the count must be a whole number of cubes.
|
|
shape = {{TextureUploadTarget::CubeMapArray}, 1, 6, true};
|
|
break;
|
|
default:
|
|
// Multisample targets above all: their descriptor needs a multisample view.
|
|
break;
|
|
}
|
|
return shape;
|
|
}
|
|
|
|
// TextureObjectMipmap, not ITextureObject: AllocateStorage and MarkStorageDirty live
|
|
// there, and every placeholder shape above is one of its subclasses.
|
|
SharedPtr<MG_State::GLState::TextureObjectMipmap> MakePlaceholderTextureObject(TextureTarget target,
|
|
Uint index) {
|
|
switch (target) {
|
|
case TextureTarget::Texture1D:
|
|
return MakeShared<MG_State::GLState::TextureObject1D>(index);
|
|
case TextureTarget::Texture2D:
|
|
return MakeShared<MG_State::GLState::TextureObject2D>(index);
|
|
case TextureTarget::TextureRectangle:
|
|
return MakeShared<MG_State::GLState::TextureObjectRectangle>(index);
|
|
case TextureTarget::Texture3D:
|
|
return MakeShared<MG_State::GLState::TextureObject3D>(index);
|
|
case TextureTarget::Texture1DArray:
|
|
return MakeShared<MG_State::GLState::TextureObject1DArray>(index);
|
|
case TextureTarget::Texture2DArray:
|
|
return MakeShared<MG_State::GLState::TextureObject2DArray>(index);
|
|
case TextureTarget::TextureCubeMap:
|
|
return MakeShared<MG_State::GLState::TextureObject2DCube>(index);
|
|
case TextureTarget::TextureCubeMapArray:
|
|
return MakeShared<MG_State::GLState::TextureObjectCubeMapArray>(index);
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
#include "WirePlaceholderImages.inc"
|
|
|
|
static Bool FindFramebufferAttachmentForTexture(const MG_State::GLState::FramebufferObject& framebuffer,
|
|
const MG_State::GLState::ITextureObject& texture,
|
|
FramebufferAttachmentType& outAttachment, Int& outLevel) {
|
|
const auto& attachments = framebuffer.GetAllAttachmentObjects();
|
|
for (SizeT i = 0; i < attachments.size(); ++i) {
|
|
const auto attachmentType = static_cast<FramebufferAttachmentType>(i);
|
|
if (attachmentType == FramebufferAttachmentType::None) {
|
|
continue;
|
|
}
|
|
|
|
const auto& attachment = attachments[i];
|
|
if (!attachment.IsTexture()) {
|
|
continue;
|
|
}
|
|
|
|
auto attachedTexture = attachment.GetTexture();
|
|
if (attachedTexture && attachedTexture.get() == &texture) {
|
|
outAttachment = attachmentType;
|
|
outLevel = static_cast<Int>(ToStorageMipLevel(attachment.GetTexture().get(),
|
|
attachment.GetTextureLevel()));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static Bool IsValidSampledImageLayout(VkImageLayout layout) {
|
|
switch (layout) {
|
|
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
|
case VK_IMAGE_LAYOUT_GENERAL:
|
|
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
|
|
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
|
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Uniform location of ELEMENT `element` of the opaque-uniform array at `baseLocation`, or
|
|
// -1 when the reflection did not reserve that element. DoReflection hands out one location
|
|
// per array element, so the element's location is the base plus its index - bounded by the
|
|
// array's real extent so a descriptorCount that outran the reflection cannot walk onto the
|
|
// next uniform. Element 0 is the ordinary non-array case and costs nothing extra.
|
|
static Int ResolveDescriptorElementLocation(const MagmaProgramSource& program, Int baseLocation,
|
|
Uint32 element) {
|
|
if (baseLocation < 0 || element == 0) {
|
|
return baseLocation;
|
|
}
|
|
const Int location = baseLocation + static_cast<Int>(element);
|
|
return program.UniformLocationsAliasSameUniform(baseLocation, location) ? location : -1;
|
|
}
|
|
|
|
// 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<Uint32>(1u, programObj.bindingDescriptorCounts[binding])
|
|
: 1u;
|
|
}
|
|
|
|
static Int ResolveSamplerUnitIndex(const MagmaProgramSource& program, Int location, Uint32 binding) {
|
|
MOBILEGL_ASSERT(location >= -1, "ResolveSamplerUnitIndex: invalid sampler location for binding %u", binding);
|
|
if (location < 0) {
|
|
return 0;
|
|
}
|
|
const Int uniformUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
MOBILEGL_ASSERT(uniformUnit >= -1,
|
|
"ResolveSamplerUnitIndex: invalid texture unit for binding %u location %d (unit=%d)", binding,
|
|
location, uniformUnit);
|
|
return uniformUnit >= 0 ? uniformUnit : 0;
|
|
}
|
|
|
|
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<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
|
VkPhysicalDeviceProperties properties{};
|
|
vkGetPhysicalDeviceProperties(m_physicalDevice, &properties);
|
|
m_wireStorageOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minStorageBufferOffsetAlignment);
|
|
m_wireTexelOffsetAlignment = std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment);
|
|
m_wireMaxUniformRange = properties.limits.maxUniformBufferRange;
|
|
m_wireMaxStorageRange = properties.limits.maxStorageBufferRange;
|
|
m_wireMaxTexelElements = properties.limits.maxTexelBufferElements;
|
|
}
|
|
#endif
|
|
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_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.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();
|
|
m_unboundStorageImageTextures.clear();
|
|
for (auto& frame : m_frames) {
|
|
if (m_device != VK_NULL_HANDLE) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
frame.wireImageViewCache.clear();
|
|
for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr);
|
|
frame.wireImageViews.clear();
|
|
#endif
|
|
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;
|
|
}
|
|
m_frames.clear();
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
for (auto& entry : m_wirePlaceholderImages) DestroyWirePlaceholderImage(entry.second);
|
|
m_wirePlaceholderImages.clear();
|
|
#endif
|
|
|
|
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;
|
|
m_fallbackTexture2D.reset();
|
|
m_fallbackMultisampleTextures.clear();
|
|
}
|
|
|
|
void UniformManager::BeginFrame(Uint32 frameIndex) {
|
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "UniformDescriptorBinder::BeginFrame invalid frame index");
|
|
auto& frame = m_frames[frameIndex];
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
m_wireFrameIndex = frameIndex;
|
|
frame.wireImageViewCache.clear();
|
|
for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr);
|
|
frame.wireImageViews.clear();
|
|
#endif
|
|
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());
|
|
}
|
|
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.
|
|
for (auto& entry : m_descriptorReuseMemo) {
|
|
entry.valid = false;
|
|
}
|
|
m_fastRebindMemo.valid = false;
|
|
m_lastBindValid = false;
|
|
// 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<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
|
|
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
|
|
m_samplerResolveMemo[binding].valid = false;
|
|
m_samplerResolveMemo[binding].infoValid = false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
// The rebind memo's set may be among the freed ones.
|
|
m_fastRebindMemo.valid = false;
|
|
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
|
}
|
|
}
|
|
|
|
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
|
const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 binding, Uint32 element,
|
|
VkDescriptorImageInfo& outImageInfo,
|
|
Bool trustUnchangedHint) const {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, outImageInfo);
|
|
#endif
|
|
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null");
|
|
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null");
|
|
// The whole-descriptor memo below is keyed by binding alone, so it describes a binding
|
|
// that carries exactly one descriptor. An arrayed binding's elements would overwrite
|
|
// each other in it (see SamplerResolveMemo::info); they re-resolve instead.
|
|
const Bool descriptorMemoUsable = BindingDescriptorCount(programObj, binding) == 1u;
|
|
// The caller proved every input of this binding's resolution unchanged since the
|
|
// last full resolve (which also filled the cache), so the whole chain below -
|
|
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
|
// and view lookups - would recompute the identical descriptor.
|
|
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
|
m_samplerResolveMemo[binding].infoValid &&
|
|
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
|
|
outImageInfo = m_samplerResolveMemo[binding].info;
|
|
return true;
|
|
}
|
|
MOBILEGL_ASSERT(binding < programObj.samplerNameByBinding.size(),
|
|
"ResolveSamplerDescriptor: sampler binding %u name lookup out of range", binding);
|
|
// Per ELEMENT, and resolved BEFORE anything is looked up through it: GLSL 4.20 gives every
|
|
// element of `uniform sampler2D goku[4]` its own texture unit (consecutive from the
|
|
// declared binding, but glUniform1i may scatter them afterwards), so the unit - and with
|
|
// it the bound texture, the unit's sampler override and the fallback decision - is the
|
|
// element's, not the binding's. An element past the array's reserved extent has no unit
|
|
// at all, and must not fall back to resolving unit 0's texture.
|
|
const Int location =
|
|
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
|
|
if (location < 0 && element > 0) {
|
|
MGLOG_D("ResolveSamplerDescriptor: binding %u element %u is past the end of its sampler array", binding,
|
|
element);
|
|
return false;
|
|
}
|
|
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
|
// Raw-pointer resolve to skip the SharedPtr atomic refcount churn: the bound texture stays
|
|
// alive through the draw via GL binding state. Only the fallback path needs a SharedPtr to
|
|
// keep the fallback texture alive for the rest of this call.
|
|
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding, element);
|
|
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
|
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
|
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
|
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
|
|
// A texture that fails the completeness rules for the filter in effect reads
|
|
// (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the
|
|
// same route as a sampler with nothing bound.
|
|
if (texture != nullptr &&
|
|
MG_State::GLState::SamplesAsIncompleteTexture(
|
|
texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) {
|
|
texture = nullptr;
|
|
}
|
|
if (texture == nullptr) {
|
|
// The binding's sampler class, read here rather than through the `numericDomain`
|
|
// local further down (it is declared after this point): the multisample placeholder
|
|
// has to be built in the class the shader will read it in.
|
|
fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]);
|
|
texture = fallbackHolder.get();
|
|
if (texture == nullptr) {
|
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
|
"location=%d unit=%d target=%d",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
|
static_cast<Int>(preferredTarget));
|
|
return false;
|
|
}
|
|
MGLOG_W_ONCE(
|
|
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
|
static_cast<Int>(preferredTarget));
|
|
}
|
|
|
|
const MG_State::GLState::SamplerObject* samplerToUse =
|
|
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
|
if (samplerToUse == nullptr) {
|
|
MGLOG_E_ONCE(
|
|
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
|
|
unit);
|
|
return false;
|
|
}
|
|
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
|
if (resource == nullptr) {
|
|
MGLOG_E_ONCE(
|
|
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
|
static_cast<Int>(texture->GetTarget()), location, unit);
|
|
return false;
|
|
}
|
|
if (!IsValidSampledImageLayout(resource->layout)) {
|
|
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
|
FramebufferAttachmentType attachmentType = FramebufferAttachmentType::None;
|
|
Int attachmentLevel = 0;
|
|
if (drawFbo &&
|
|
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
|
|
MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
|
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
|
|
"trackedLayout=%d)",
|
|
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
|
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
|
attachmentLevel, static_cast<Int>(resource->layout));
|
|
}
|
|
|
|
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
|
|
if (!readyForSampling) {
|
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
|
texture->GetExternalIndex(), binding);
|
|
return false;
|
|
}
|
|
resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
|
MOBILEGL_ASSERT(resource != nullptr,
|
|
"ResolveSamplerDescriptor: failed to resync textureId=%d after sampling transition",
|
|
texture->GetExternalIndex());
|
|
MOBILEGL_ASSERT(IsValidSampledImageLayout(resource->layout),
|
|
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
|
|
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
|
|
}
|
|
|
|
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
|
"ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding);
|
|
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
|
|
// Vulkan forbids linear filtering and anisotropy for integer sampled-image formats.
|
|
// Some desktop GL shader packs deliberately bit-read a mutable float texture through a
|
|
// usampler and still leave the texture's ordinary linear parameters in place; texelFetch
|
|
// ignores filtering, so a nearest VkSampler preserves the operation while keeping the
|
|
// descriptor valid.
|
|
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
|
|
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
|
SamplerResolveMemo* viewFormatMemo =
|
|
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
|
|
// The format this GL texture presents to the shader. For a texture created by
|
|
// glTextureView that is the format the VIEW reinterpreted its storage as (GL 4.6 core
|
|
// 8.18), not the storage image's own - resolving the numeric domain against the latter
|
|
// would pick a sampled view for a format the shader never declared. The probe is behind
|
|
// IsTextureView() so nothing about the ordinary per-draw path changes.
|
|
const VkFormat sampledSourceFormat =
|
|
texture->IsTextureView()
|
|
? m_textureManager->ResolveTextureViewWindow(*texture, *resource).format
|
|
: resource->format;
|
|
VkFormat sampledViewFormat;
|
|
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
|
|
viewFormatMemo->viewFormatSource == sampledSourceFormat &&
|
|
viewFormatMemo->viewFormatDomain == numericDomain) {
|
|
sampledViewFormat = viewFormatMemo->viewFormat;
|
|
} else {
|
|
sampledViewFormat =
|
|
VkTextureManager::ResolveSampledImageViewFormat(sampledSourceFormat, numericDomain);
|
|
if (viewFormatMemo != nullptr) {
|
|
viewFormatMemo->viewFormatSource = sampledSourceFormat;
|
|
viewFormatMemo->viewFormatDomain = numericDomain;
|
|
viewFormatMemo->viewFormat = sampledViewFormat;
|
|
viewFormatMemo->viewFormatValid = true;
|
|
NoteSamplerResolveMemoTouched(binding);
|
|
}
|
|
}
|
|
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
|
"textureId=%d imageFormat=%d numericDomain=%d",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
|
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
|
return false;
|
|
}
|
|
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
|
|
// already produced instead of re-entering GetOrCreateSampledImageView's sync path. A GL
|
|
// texture view is excluded because resource->sampledView belongs to the texture it VIEWS
|
|
// - same image, but the storage texture's level range and depth/stencil aspect, which is
|
|
// exactly the state a view exists to differ on.
|
|
const VkImageView sampledImageView =
|
|
(!texture->IsTextureView() && sampledViewFormat == resource->format)
|
|
? resource->sampledView
|
|
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
|
if (sampledImageView == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
|
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
|
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
|
static_cast<Int>(numericDomain));
|
|
return false;
|
|
}
|
|
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
|
|
// sampler object and texture (both by lifetime id + version) are unchanged from the
|
|
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler
|
|
// handle, are guaranteed identical. Lifetime ids are never reused, so a freed-and-
|
|
// reallocated sampler/texture at the same address gets a fresh id and misses instead
|
|
// of false-hitting. Cached handles live until Shutdown, so the memo can never hand
|
|
// back a destroyed sampler.
|
|
VkSampler resolvedSampler = VK_NULL_HANDLE;
|
|
if (binding < m_samplerResolveMemo.size()) {
|
|
auto& memo = m_samplerResolveMemo[binding];
|
|
const Uint64 samplerLifetimeId = samplerToUse->GetLifetimeId();
|
|
const Uint16 samplerVersion = samplerToUse->GetVersion();
|
|
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
|
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
|
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
|
|
// follows uploads as well as GL parameters - so it belongs in the memo key too.
|
|
const Uint32 viewLevelCount = resource->sampledLevelCount;
|
|
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
|
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
|
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
|
|
resolvedSampler = memo.sampler;
|
|
} else {
|
|
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
|
|
forceNearestFiltering, viewLevelCount);
|
|
memo.samplerLifetimeId = samplerLifetimeId;
|
|
memo.samplerVersion = samplerVersion;
|
|
memo.textureLifetimeId = textureLifetimeId;
|
|
memo.textureParamsVersion = textureParamsVersion;
|
|
memo.forceNearestFiltering = forceNearestFiltering;
|
|
memo.viewLevelCount = viewLevelCount;
|
|
memo.sampler = resolvedSampler;
|
|
memo.valid = true;
|
|
NoteSamplerResolveMemoTouched(binding);
|
|
}
|
|
} else {
|
|
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
|
|
resource->sampledLevelCount);
|
|
}
|
|
outImageInfo = {
|
|
.sampler = resolvedSampler,
|
|
.imageView = sampledImageView,
|
|
.imageLayout = resource->layout,
|
|
};
|
|
if (outImageInfo.sampler == VK_NULL_HANDLE) {
|
|
return false;
|
|
}
|
|
// Only for a binding that carries a single descriptor - an array's elements would
|
|
// publish each other's descriptors here, and the next hinted draw would hand element
|
|
// N-1's texture to element 0.
|
|
if (binding < m_samplerResolveMemo.size()) {
|
|
if (descriptorMemoUsable) {
|
|
m_samplerResolveMemo[binding].info = outImageInfo;
|
|
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
|
m_samplerResolveMemo[binding].infoValid = true;
|
|
} else {
|
|
// An arrayed binding publishes nothing here, and clears what a previous program
|
|
// published at this index. Not strictly required - the hint's proof obligations
|
|
// are program-scoped and the entry is reset every frame - but leaving another
|
|
// program's descriptor sitting in a slot this one never refreshes is the kind of
|
|
// thing the next reader has to re-derive is safe.
|
|
m_samplerResolveMemo[binding].infoValid = false;
|
|
}
|
|
NoteSamplerResolveMemoTouched(binding);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Bool UniformManager::ResolveSamplerDescriptorOverride(
|
|
const SamplerBindingOverride& samplerBindingOverride, VkDescriptorImageInfo& outImageInfo) const {
|
|
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptorOverride: texture manager is null");
|
|
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptorOverride: sampler manager is null");
|
|
MOBILEGL_ASSERT(samplerBindingOverride.texture != nullptr,
|
|
"ResolveSamplerDescriptorOverride: override texture is null for binding %u",
|
|
samplerBindingOverride.binding);
|
|
MOBILEGL_ASSERT(samplerBindingOverride.sampler != nullptr,
|
|
"ResolveSamplerDescriptorOverride: override sampler is null for binding %u",
|
|
samplerBindingOverride.binding);
|
|
|
|
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*samplerBindingOverride.texture);
|
|
MOBILEGL_ASSERT(resource != nullptr,
|
|
"ResolveSamplerDescriptorOverride: failed to sync override texture resource for binding %u textureId=%d",
|
|
samplerBindingOverride.binding, samplerBindingOverride.texture->GetExternalIndex());
|
|
MOBILEGL_ASSERT(IsValidSampledImageLayout(resource->layout),
|
|
"ResolveSamplerDescriptorOverride: invalid layout %d for binding %u textureId=%d",
|
|
static_cast<Int>(resource->layout), samplerBindingOverride.binding,
|
|
samplerBindingOverride.texture->GetExternalIndex());
|
|
|
|
outImageInfo = {
|
|
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
|
|
*samplerBindingOverride.texture,
|
|
samplerBindingOverride.forceNearestFiltering,
|
|
resource->sampledLevelCount),
|
|
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
|
samplerBindingOverride.imageView :
|
|
// Same reason as in ResolveSamplerDescriptor: the resource's own views describe
|
|
// the storage texture, so a view has to be asked for its own.
|
|
(samplerBindingOverride.texture->IsTextureView()
|
|
? m_textureManager->GetOrCreateSampledImageView(*samplerBindingOverride.texture,
|
|
VK_FORMAT_UNDEFINED)
|
|
: (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView)),
|
|
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
|
|
samplerBindingOverride.imageLayout : resource->layout,
|
|
};
|
|
return outImageInfo.sampler != VK_NULL_HANDLE;
|
|
}
|
|
|
|
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
|
|
const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) return false; // Conservative optimization gate; no frontend probe.
|
|
#endif
|
|
// A declined program never draws (see VkProgramObject::declinedDescriptors), and its
|
|
// declined binding has no resolvable uniform location - so there is nothing to prove
|
|
// about the textures it would have sampled.
|
|
if (programObj.declinedDescriptors) {
|
|
return false;
|
|
}
|
|
Bool sawSampler = false;
|
|
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
|
|
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
continue;
|
|
}
|
|
// The rewrite this gates is program-wide, so EVERY sampler the program can read has
|
|
// to qualify - including every element of a sampler array, each of which reaches a
|
|
// different texture through its own unit.
|
|
const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
|
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
|
// The element's own location first, exactly as ResolveSamplerDescriptor resolves
|
|
// it - an element with no location would otherwise be judged on unit 0's texture.
|
|
const Int location = ResolveDescriptorElementLocation(
|
|
program, programObj.samplerUniformLocationByBinding[binding], element);
|
|
if (location < 0 && element > 0) return false;
|
|
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding, element);
|
|
if (texture == nullptr) return false;
|
|
const auto& levelRange = texture->GetLevelRange();
|
|
if (levelRange.x() != levelRange.y()) return false;
|
|
|
|
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
|
|
// filtering - which a single-level view can still have. Resolve the sampler exactly
|
|
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
|
|
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
|
const auto& samplerOverride = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
|
|
const auto* effectiveSampler =
|
|
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
|
if (effectiveSampler == nullptr) return false;
|
|
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
|
|
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
|
|
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
|
|
return false;
|
|
}
|
|
|
|
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
|
|
// min/mag decision. That only matches the implicit form when lambda could not have been
|
|
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
|
|
// filters are the same and the choice cannot be observed.
|
|
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
|
|
? 0.0f
|
|
: effectiveSampler->GetMaxLod();
|
|
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
|
|
return false;
|
|
}
|
|
sawSampler = true;
|
|
}
|
|
}
|
|
return sawSampler;
|
|
}
|
|
|
|
Bool UniformManager::ResolveSamplerTexture(const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
|
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
|
outTexture.reset();
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTexture: GL context is null");
|
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
|
"ResolveSamplerTexture: sampler location binding %u out of range", binding);
|
|
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
|
"ResolveSamplerTexture: sampler target binding %u out of range", binding);
|
|
|
|
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
|
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
|
|
|
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
|
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
|
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
|
|
// The slot always holds at least the target's default texture (name 0). While that
|
|
// default has no image it is unsampleable; report it as "unbound" so callers keep
|
|
// taking their fallback paths instead of trying to sync a storage-less texture.
|
|
if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) {
|
|
outTexture.reset();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw(
|
|
const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 binding, Uint32 element) {
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTextureRaw: GL context is null");
|
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
|
"ResolveSamplerTextureRaw: sampler location binding %u out of range", binding);
|
|
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
|
"ResolveSamplerTextureRaw: sampler target binding %u out of range", binding);
|
|
|
|
const Int location =
|
|
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
|
|
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
|
|
|
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
|
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
|
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
|
|
// touching the refcount (no atomic inc/dec per binding per draw).
|
|
MG_State::GLState::ITextureObject* texture =
|
|
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
|
// The slot always holds at least the target's default texture (name 0). While that
|
|
// default has no image it is unsampleable; report it as "unbound" so the caller
|
|
// substitutes its fallback texture exactly like it did for the old null slot.
|
|
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
|
return nullptr;
|
|
}
|
|
return texture;
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
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<Uint>(location));
|
|
const auto& state = MG_Pipe::MGPipeApplier();
|
|
if (unit < 0 || static_cast<Uint32>(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<Uint8>(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<TextureInternalFormat>(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<Int>(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<VkDeviceSize>(m_wireMaxTexelElements)) * texelSize;
|
|
if (size == 0) return false;
|
|
if (start > std::numeric_limits<VkDeviceSize>::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<unsigned long long>(slice.offset + start),
|
|
static_cast<unsigned long long>(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;
|
|
}
|
|
#endif
|
|
|
|
Bool UniformManager::ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 binding, Uint32 frameIndex,
|
|
VkBufferView& outBufferView) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire())
|
|
return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, false, outBufferView);
|
|
#endif
|
|
outBufferView = VK_NULL_HANDLE;
|
|
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null");
|
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "ResolveTexelBufferDescriptor: frame index out of range");
|
|
|
|
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
|
"ResolveTexelBufferDescriptor: numeric domain binding %u out of range", binding);
|
|
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
|
|
|
|
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
|
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
|
// NOT an error, and not a reason to lose the draw. A texture unit with nothing on it
|
|
// is a legal GL state (4.6 core 8.24): the sampler is incomplete, so a fetch through
|
|
// it returns undefined values - the same answer the sampled path above gives with its
|
|
// fallback texture, which a buffer texture simply cannot use because its descriptor is
|
|
// a VkBufferView. A per-format placeholder view is the equivalent for this kind.
|
|
const VkBufferView placeholder =
|
|
AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, numericDomain, false);
|
|
if (placeholder == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound, and the "
|
|
"placeholder descriptor could not be created", binding,
|
|
programObj.samplerNameByBinding[binding].c_str());
|
|
return false;
|
|
}
|
|
MGLOG_D("ResolveTexelBufferDescriptor: binding %u ('%s') is unbound; using the placeholder descriptor",
|
|
binding, programObj.samplerNameByBinding[binding].c_str());
|
|
outBufferView = placeholder;
|
|
return true;
|
|
}
|
|
|
|
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
|
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
|
MGLOG_E_ONCE(
|
|
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
|
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
|
|
return false;
|
|
}
|
|
|
|
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
|
if (bufferObject == nullptr) {
|
|
// A buffer texture with no buffer object attached is INCOMPLETE, not illegal (GL 4.6
|
|
// core 8.9), and sampling an incomplete texture is undefined - so this too keeps the
|
|
// draw on a placeholder rather than dropping it.
|
|
const VkBufferView placeholder =
|
|
AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, numericDomain, false);
|
|
if (placeholder == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound, "
|
|
"and the placeholder descriptor could not be created", binding,
|
|
programObj.samplerNameByBinding[binding].c_str());
|
|
return false;
|
|
}
|
|
MGLOG_D("ResolveTexelBufferDescriptor: binding %u ('%s') has no attached GL buffer; using the "
|
|
"placeholder descriptor", binding, programObj.samplerNameByBinding[binding].c_str());
|
|
outBufferView = placeholder;
|
|
return true;
|
|
}
|
|
|
|
BufferSlice slice{};
|
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
|
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
|
return false;
|
|
}
|
|
|
|
const auto internalFormat = textureBuffer->GetFormat();
|
|
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
|
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
|
static_cast<Int>(internalFormat));
|
|
return false;
|
|
}
|
|
|
|
const VkDeviceSize texelSize =
|
|
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
|
// glTextureBufferRange addresses a window of the buffer, not all of it; the whole-buffer
|
|
// forms report the buffer's current size here, so both go through the same clamp.
|
|
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
|
|
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
|
|
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
|
|
if (texelSize > 0) {
|
|
viewRange = (viewRange / texelSize) * texelSize;
|
|
}
|
|
if (viewRange == 0) {
|
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
|
return false;
|
|
}
|
|
|
|
VkBufferViewCreateInfo viewInfo{};
|
|
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
|
viewInfo.buffer = slice.buffer;
|
|
viewInfo.format = vkFormat;
|
|
viewInfo.offset = slice.offset + rangeOffset;
|
|
viewInfo.range = viewRange;
|
|
|
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
|
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
|
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
|
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
|
return false;
|
|
}
|
|
|
|
m_frames[frameIndex].texelBufferViews.push_back(bufferView);
|
|
outBufferView = bufferView;
|
|
return true;
|
|
}
|
|
|
|
// 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) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire())
|
|
return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, true, outBufferView);
|
|
#endif
|
|
outBufferView = VK_NULL_HANDLE;
|
|
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
|
|
"ResolveStorageTexelBufferDescriptor: frame index out of range");
|
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
|
"ResolveStorageTexelBufferDescriptor: binding %u out of range", binding);
|
|
|
|
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
|
if (location < 0) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
|
|
programObj.samplerNameByBinding[binding].c_str());
|
|
return false;
|
|
}
|
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
|
|
binding);
|
|
return false;
|
|
}
|
|
|
|
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
|
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
|
|
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
|
"ResolveStorageTexelBufferDescriptor: numeric domain binding %u out of range", binding);
|
|
|
|
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
|
|
const auto& texture = imageBinding.Texture;
|
|
if (texture == nullptr) {
|
|
// An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero
|
|
// and stores are discarded. Declining here took the whole draw or dispatch with it -
|
|
// the same shape as the unbound storage block fixed alongside this. A placeholder view
|
|
// in the shader's own declared format lets the work proceed with the stores landing
|
|
// nowhere anyone can observe, which is what GL asks for.
|
|
const VkBufferView placeholder =
|
|
AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding],
|
|
programObj.samplerNumericDomainByBinding[binding], true);
|
|
if (placeholder == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u, and the "
|
|
"placeholder descriptor could not be created", imageUnit, binding);
|
|
return false;
|
|
}
|
|
MGLOG_D("ResolveStorageTexelBufferDescriptor: image unit %d (binding %u) is unbound; using the "
|
|
"placeholder descriptor", imageUnit, binding);
|
|
outBufferView = placeholder;
|
|
return true;
|
|
}
|
|
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
|
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
|
"unit %d, got textureId=%u target=%d storage=%d",
|
|
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
|
|
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
|
|
static_cast<Int>(texture->GetStorageType()));
|
|
return false;
|
|
}
|
|
|
|
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
|
if (bufferObject == nullptr) {
|
|
// Incomplete buffer texture, same as the sampled path: legal state, undefined data,
|
|
// and no reason to drop the work.
|
|
const VkBufferView placeholder =
|
|
AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding],
|
|
programObj.samplerNumericDomainByBinding[binding], true);
|
|
if (placeholder == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer "
|
|
"bound, and the placeholder descriptor could not be created", imageUnit);
|
|
return false;
|
|
}
|
|
MGLOG_D("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no attached GL buffer; "
|
|
"using the placeholder descriptor", imageUnit);
|
|
outBufferView = placeholder;
|
|
return true;
|
|
}
|
|
|
|
// Unlike the sampled texel buffer, the shader MAY write this one, and those writes land
|
|
// in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
|
|
// GetBufferSubData read. Same two calls, and for the same reason, as the storage-block
|
|
// path above - but only the residency is unconditional. Marking a GL_READ_ONLY binding
|
|
// GPU-written would make the next map or readback wait for a dispatch that could not have
|
|
// changed a byte of it.
|
|
bufferObject->EnsureGpuResidentStorage();
|
|
if (imageBinding.Access != GL_READ_ONLY) {
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// P5c ev (R2, CONTRACT-P5C §4.2): the announcement goes through the reverse
|
|
// channel - the apply thread may not poke the client object directly.
|
|
MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject);
|
|
#else
|
|
bufferObject->MarkGpuWritten();
|
|
#endif
|
|
}
|
|
|
|
BufferSlice slice{};
|
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
|
|
!slice.IsValid()) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
|
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
|
return false;
|
|
}
|
|
|
|
// The format the SHADER declared wins over the one glBindImageTexture named, on the same
|
|
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
|
|
// r32ui whatever the texture's own attachment format says. Falling back, in order:
|
|
// reflected format, then the bind format, then the texture's attached format.
|
|
const auto internalFormat = textureBuffer->GetFormat();
|
|
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
|
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
|
VkFormat vkFormat = reflectedFormat;
|
|
if (vkFormat == VK_FORMAT_UNDEFINED && imageBinding.Format != 0) {
|
|
vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
|
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
|
}
|
|
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
|
vkFormat = resourceFormat;
|
|
}
|
|
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
|
|
static_cast<Int>(internalFormat), imageBinding.Format);
|
|
return false;
|
|
}
|
|
|
|
// Sized from the TEXTURE's attached format even though the view may carry a different
|
|
// one. That is not a shortcut: GL requires the shader's format qualifier, the format
|
|
// passed to glBindImageTexture and the texture's own internal format to belong to the
|
|
// same format CLASS (GL 4.6 core, table 8.27), and every member of a class has the same
|
|
// texel size. So the three can disagree on interpretation and never on bytes - which is
|
|
// what the range below has to be a whole multiple of.
|
|
const VkDeviceSize texelSize =
|
|
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
|
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
|
|
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
|
|
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
|
|
if (texelSize > 0) {
|
|
viewRange = (viewRange / texelSize) * texelSize;
|
|
}
|
|
if (viewRange == 0) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
|
|
texture->GetExternalIndex());
|
|
return false;
|
|
}
|
|
|
|
VkBufferViewCreateInfo viewInfo{};
|
|
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
|
viewInfo.buffer = slice.buffer;
|
|
viewInfo.format = vkFormat;
|
|
viewInfo.offset = slice.offset + rangeOffset;
|
|
viewInfo.range = viewRange;
|
|
|
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
|
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
|
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
|
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
|
return false;
|
|
}
|
|
|
|
m_frames[frameIndex].texelBufferViews.push_back(bufferView);
|
|
outBufferView = bufferView;
|
|
return true;
|
|
}
|
|
|
|
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(MGB_CTX_LIVE, "ResolveStorageBufferDescriptor: GL context is null");
|
|
MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(),
|
|
"ResolveStorageBufferDescriptor: binding %u out of range", binding);
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire() && (binding >= programObj.storageBlockIndexByBinding.size() ||
|
|
binding >= programObj.storageBlockNameByBinding.size()))
|
|
WireDescriptorFatal("storage-buffer-reflection-binding");
|
|
#endif
|
|
|
|
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
|
|
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
|
binding);
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire() && blockIndex < 0) WireDescriptorFatal("storage-buffer-reflection-block");
|
|
#endif
|
|
// An atomic counter is not an SSBO the application ever declared: glslang lowers every
|
|
// atomic_uint onto a synthesized gl_AtomicCounterBlock_<N> 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<GLuint>(atomicCounterBinding)
|
|
:
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
(program.IsWire() ? program.GetShaderStorageBlockBinding(static_cast<GLuint>(blockIndex)) :
|
|
GetShaderStorageBlockBinding(*program.Frontend(), static_cast<GLuint>(blockIndex))) + element;
|
|
#else
|
|
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
|
#endif
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) {
|
|
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<VkDeviceSize>::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<unsigned long long>(slice.offset + start),
|
|
static_cast<unsigned long long>(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<unsigned long long>(size),
|
|
static_cast<unsigned long long>(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;
|
|
}
|
|
#endif
|
|
const Uint32 bindingPointCount =
|
|
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(bufferTarget));
|
|
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
|
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
|
|
frontendBinding, blockName.c_str());
|
|
|
|
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(bufferTarget, frontendBinding);
|
|
const auto& bufferObject = bindingPoint.GetBoundObject();
|
|
if (bufferObject == nullptr) {
|
|
// NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a
|
|
// program declare a shader storage block the application never binds a buffer to:
|
|
// the block simply has no store, so a read is undefined and a write goes nowhere.
|
|
// Refusing here used to take the whole draw or dispatch with it (SetupDraw and
|
|
// DispatchCompute both skip on a false return), which is how AcceleratedRendering's
|
|
// GUI batch lost its backgrounds: its vertex-transform compute shader declares a
|
|
// `Meshes` block it only reads when a vertex comes from a cached server mesh, and a
|
|
// batch of plain GUI blits has no meshes and so binds nothing there. The dispatch
|
|
// never ran, the transformed vertex buffer stayed as it was, and every hotbar and
|
|
// container-screen background quad came out degenerate. A shared zero-filled
|
|
// placeholder puts something legal in the descriptor and lets the draw proceed.
|
|
const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor();
|
|
if (!placeholder.IsValid()) {
|
|
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no buffer bound at frontend binding %u for block "
|
|
"'%s', and the placeholder descriptor could not be created",
|
|
frontendBinding, blockName.c_str());
|
|
return false;
|
|
}
|
|
MGLOG_D("ResolveStorageBufferDescriptor: frontend binding %u ('%s') is unbound; using the placeholder "
|
|
"descriptor",
|
|
frontendBinding, blockName.c_str());
|
|
outBufferInfo.buffer = placeholder.buffer;
|
|
outBufferInfo.offset = placeholder.offset;
|
|
outBufferInfo.range = placeholder.size;
|
|
return true;
|
|
}
|
|
|
|
// The shader may write this buffer, and those writes land in GPU memory behind the
|
|
// frontend's CPU shadow - which is what MapBuffer and GetBufferSubData read.
|
|
// Host-visible coherent GPU residency makes the shadow BE that memory, so the
|
|
// results are visible without a readback path, exactly as for a capture buffer.
|
|
bufferObject->EnsureGpuResidentStorage();
|
|
// ... and the read that follows has to wait for this draw or dispatch to retire.
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// P5c ev (R2, CONTRACT-P5C §4.2): through the reverse channel, not a direct poke of
|
|
// the client object from the apply thread.
|
|
MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject);
|
|
#else
|
|
bufferObject->MarkGpuWritten();
|
|
#endif
|
|
|
|
BufferSlice slice{};
|
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
|
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
|
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
|
|
return false;
|
|
}
|
|
|
|
const auto range = bindingPoint.GetRange();
|
|
const VkDeviceSize bufferSize = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
|
VkDeviceSize rangeStart = static_cast<VkDeviceSize>(std::min(range.start, bufferObject->GetSize()));
|
|
VkDeviceSize rangeEnd = static_cast<VkDeviceSize>(std::min(range.end, bufferObject->GetSize()));
|
|
if (rangeEnd <= rangeStart) {
|
|
rangeStart = 0;
|
|
rangeEnd = bufferSize;
|
|
}
|
|
if (rangeEnd <= rangeStart) {
|
|
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
|
programObj.storageBlockNameByBinding[binding].c_str());
|
|
return false;
|
|
}
|
|
|
|
outBufferInfo.buffer = slice.buffer;
|
|
outBufferInfo.offset = slice.offset + rangeStart;
|
|
outBufferInfo.range = rangeEnd - rangeStart;
|
|
return true;
|
|
}
|
|
|
|
Bool UniformManager::ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
|
const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 binding, Uint32 element,
|
|
VkDescriptorImageInfo& outImageInfo) const {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, true, outImageInfo);
|
|
#endif
|
|
outImageInfo = {};
|
|
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null");
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageImageDescriptor: GL context is null");
|
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
|
"ResolveStorageImageDescriptor: binding %u out of range", binding);
|
|
|
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
|
if (baseLocation < 0) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
|
return false;
|
|
}
|
|
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
|
|
// gives every element of `uniform image2D g_image[4]` its own glUniform1i-assigned image
|
|
// unit, and the four units need not be consecutive or even ordered (the conformance case
|
|
// uses 0, 2, 4, 6). DoReflection reserves one uniform location per array element, so the
|
|
// element's location is the base plus its index - checked against the array's real
|
|
// extent so a descriptorCount that outran the reflection cannot walk onto the next
|
|
// uniform.
|
|
const Int location = baseLocation + static_cast<Int>(element);
|
|
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
|
binding, element);
|
|
return false;
|
|
}
|
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
|
imageUnit, binding);
|
|
return false;
|
|
}
|
|
|
|
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
|
|
if (imageBinding.Texture == nullptr) {
|
|
// Legal GL: an image unit with no texture bound makes loads return zero and discards
|
|
// stores (4.6 core 8.26). It is not a reason to lose the draw, which is what returning
|
|
// false here did - both SetupDraw and DispatchCompute skip everything on it. The
|
|
// placeholder is a 1x1 image of the target and format the shader's declaration asks
|
|
// for, so the descriptor is valid and the stores land where nobody can see them.
|
|
TextureTarget placeholderTarget = TextureTarget::Unknown;
|
|
VkFormat placeholderFormat = VK_FORMAT_UNDEFINED;
|
|
SharedPtr<MG_State::GLState::ITextureObject> placeholder;
|
|
if (ResolveUnboundStorageImagePlaceholder(programObj, binding, placeholderTarget, placeholderFormat)) {
|
|
placeholder = GetUnboundStorageImageTexture(placeholderTarget, placeholderFormat);
|
|
}
|
|
VkImageView placeholderView = VK_NULL_HANDLE;
|
|
if (placeholder != nullptr &&
|
|
m_textureManager->TransitionTextureForStorageImage(commandBuffer, *placeholder)) {
|
|
// layered=true, layer=0: the placeholder's own view type IS the one the shader's
|
|
// image declaration demands, and that is exactly what the layered form asks for
|
|
// (see GetOrCreateStorageImageView, which only narrows the view type when a
|
|
// non-layered binding names a single layer).
|
|
placeholderView =
|
|
m_textureManager->GetOrCreateStorageImageView(*placeholder, 0, placeholderFormat, true, 0);
|
|
}
|
|
if (placeholderView == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u, and no "
|
|
"placeholder descriptor could be built (target=%d format=%d)",
|
|
imageUnit, binding, static_cast<Int>(placeholderTarget),
|
|
static_cast<Int>(placeholderFormat));
|
|
return false;
|
|
}
|
|
MGLOG_D("ResolveStorageImageDescriptor: image unit %d (binding %u) is unbound; using the placeholder "
|
|
"descriptor", imageUnit, binding);
|
|
outImageInfo.sampler = VK_NULL_HANDLE;
|
|
outImageInfo.imageView = placeholderView;
|
|
outImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
return true;
|
|
}
|
|
|
|
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
|
if (!ready) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
|
imageBinding.Texture->GetExternalIndex(), imageUnit);
|
|
return false;
|
|
}
|
|
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*imageBinding.Texture);
|
|
if (resource == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
|
|
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
|
"ResolveStorageImageDescriptor: storage image format binding %u out of range", binding);
|
|
MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(),
|
|
"ResolveStorageImageDescriptor: storage image format policy binding %u out of range",
|
|
binding);
|
|
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
|
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
|
|
// The storage's own VkFormat is the wrong reference for a GL texture view: the view
|
|
// reinterprets it (GL 4.6 core table 8.21), and it is the VIEW's format the shader's
|
|
// image declaration was written against. Same correction the sampled path makes above.
|
|
const VkFormat storageImageSourceFormat =
|
|
imageBinding.Texture->IsTextureView()
|
|
? m_textureManager->ResolveTextureViewWindow(*imageBinding.Texture, *resource).format
|
|
: resource->format;
|
|
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
|
reflectedFormat, imageBinding.Format, storageImageSourceFormat, useBindingFormat);
|
|
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
|
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
|
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
|
useBindingFormat ? "true" : "false");
|
|
return false;
|
|
}
|
|
// glBindImageTexture named a level and a layer of the bound texture; on a GL texture
|
|
// view both are relative to the view, and the storage image is what the descriptor
|
|
// actually points at (see ToStorageMipLevel).
|
|
const Int32 storageImageLayer =
|
|
imageBinding.Layered != GL_FALSE
|
|
? imageBinding.Layer
|
|
: static_cast<Int32>(ToStorageArrayLayer(imageBinding.Texture.get(), imageBinding.Layer));
|
|
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
|
*imageBinding.Texture, ToStorageMipLevel(imageBinding.Texture.get(), static_cast<Int>(mipLevel)),
|
|
viewFormat, imageBinding.Layered != GL_FALSE, storageImageLayer);
|
|
if (view == VK_NULL_HANDLE) {
|
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
|
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
|
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
|
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
|
static_cast<Int>(viewFormat),
|
|
useBindingFormat ? "true" : "false");
|
|
return false;
|
|
}
|
|
outImageInfo.sampler = VK_NULL_HANDLE;
|
|
outImageInfo.imageView = view;
|
|
outImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
return outImageInfo.imageView != VK_NULL_HANDLE;
|
|
}
|
|
|
|
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(
|
|
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
|
// A multisample sampler cannot be served by the single-sampled 2D image below - its
|
|
// descriptor demands a multisample view - so it gets its own placeholder rather than no
|
|
// placeholder at all. Without one, ResolveSamplerDescriptor declined and
|
|
// BindProgramUniformBuffers dropped the WHOLE draw, which is how every
|
|
// sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a
|
|
// sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture
|
|
// unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an
|
|
// incomplete texture is undefined, not fatal, so the draw has to happen.
|
|
if (target == TextureTarget::Texture2DMultisample ||
|
|
target == TextureTarget::Texture2DMultisampleArray) {
|
|
return GetFallbackMultisampleTexture(target, numericDomain);
|
|
}
|
|
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
|
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
|
static_cast<Int>(target));
|
|
return nullptr;
|
|
}
|
|
|
|
// The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its
|
|
// image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can
|
|
// hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample
|
|
// image can never carry that bit, which is why the arm above needs one object per domain.
|
|
if (m_fallbackTexture2D == nullptr) {
|
|
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
|
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
|
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
|
{.texelSize = {1, 1, 1}, .byteSize = 4});
|
|
// (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only
|
|
// sensible answer for a sampler with nothing bound.
|
|
static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255};
|
|
fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0,
|
|
{kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)});
|
|
fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true);
|
|
m_fallbackTexture2D = fallbackTexture;
|
|
}
|
|
|
|
return m_fallbackTexture2D;
|
|
}
|
|
|
|
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
|
|
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
|
// ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback.
|
|
//
|
|
// A descriptor whose image format is in a different numeric class than the sampler that
|
|
// reads it needs a format-reinterpreting view, and building one needs
|
|
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it:
|
|
// SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and
|
|
// the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8
|
|
// placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs
|
|
// mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped -
|
|
// the exact outcome the placeholder exists to prevent, just reached later. Matching the
|
|
// image's own format to the sampler's class instead means no reinterpreting view is
|
|
// needed at all.
|
|
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
|
|
TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8;
|
|
Uint32 domainSlot = 0;
|
|
switch (numericDomain) {
|
|
case SamplerNumericDomain::SignedInteger:
|
|
internalFormat = TextureInternalFormat::RGBA8I;
|
|
domainSlot = 1;
|
|
break;
|
|
case SamplerNumericDomain::UnsignedInteger:
|
|
internalFormat = TextureInternalFormat::RGBA8UI;
|
|
domainSlot = 2;
|
|
break;
|
|
case SamplerNumericDomain::Float:
|
|
case SamplerNumericDomain::Unknown:
|
|
default:
|
|
// Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default:
|
|
// a shader whose sampler class could not be reflected is far likelier to be a plain
|
|
// sampler2DMS than an integer one, and a float view is the only one buildable without
|
|
// the mutable bit anyway.
|
|
break;
|
|
}
|
|
const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot;
|
|
auto cached = m_fallbackMultisampleTextures.find(key);
|
|
if (cached != m_fallbackMultisampleTextures.end()) {
|
|
return cached->second;
|
|
}
|
|
|
|
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
|
|
: TextureUploadTarget::Texture2DMultisample;
|
|
const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key;
|
|
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
|
|
if (arrayed) {
|
|
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(externalIndex);
|
|
} else {
|
|
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(externalIndex);
|
|
}
|
|
texture->SetInternalFormat(internalFormat);
|
|
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
|
|
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
|
|
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
|
|
// that re-created it would be worse than none.
|
|
texture->SetSamples(2);
|
|
texture->SetFixedSampleLocations(true);
|
|
// No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be
|
|
// written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike
|
|
// the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined,
|
|
// which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample
|
|
// texture that is not complete. The point of the placeholder is that the DRAW happens.
|
|
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
|
|
texture->TruncateMipmapLevels(uploadTarget, 1);
|
|
texture->MarkStorageDirty(uploadTarget, 0, false);
|
|
// Worth knowing if it ever fires: an integer multisample format can legitimately support
|
|
// no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be
|
|
// VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this
|
|
// placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2)
|
|
// above exists to avoid. It already warns from there; nothing better is available - a
|
|
// one-sample integer image is still a draw, and declining is the outcome this whole
|
|
// placeholder replaced.
|
|
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d",
|
|
static_cast<Int>(target), static_cast<Int>(numericDomain), static_cast<Int>(internalFormat));
|
|
return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second;
|
|
}
|
|
|
|
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<Int>(declaredFormat), static_cast<Int>(fallbackFormat),
|
|
storage ? "true" : "false");
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
|
|
const Uint64 key = (static_cast<Uint64>(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<VkDeviceSize>(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<Int>(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<Int>(format));
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
m_unboundTexelBufferViews.emplace(key, view);
|
|
MGLOG_D("AcquireUnboundTexelBufferView: created placeholder view format=%d storage=%s",
|
|
static_cast<Int>(format), storage ? "true" : "false");
|
|
return view;
|
|
}
|
|
|
|
Bool UniformManager::ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 binding, TextureTarget& outTarget,
|
|
VkFormat& outFormat) const {
|
|
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
|
"ResolveUnboundStorageImagePlaceholder: binding %u out of range", binding);
|
|
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
|
"ResolveUnboundStorageImagePlaceholder: format binding %u out of range", binding);
|
|
outTarget = programObj.samplerTextureTargetByBinding[binding];
|
|
// The shader's own format qualifier, exactly as the bound path prefers it over the one
|
|
// glBindImageTexture named - there is no binding here to name one. A `writeonly` image
|
|
// may carry no qualifier at all; its numeric class is then the only constraint, and the
|
|
// R32 member of that class is what carries it (see AcquireUnboundTexelBufferView).
|
|
outFormat = programObj.storageImageFormatByBinding[binding];
|
|
if (outFormat == VK_FORMAT_UNDEFINED) {
|
|
outFormat = PlaceholderFormatForNumericDomain(programObj.samplerNumericDomainByBinding[binding]);
|
|
}
|
|
return outFormat != VK_FORMAT_UNDEFINED && PlaceholderShapeForTarget(outTarget).valid;
|
|
}
|
|
|
|
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetUnboundStorageImageTexture(
|
|
TextureTarget target, VkFormat format) const {
|
|
const Uint64 key = (static_cast<Uint64>(target) << 32) | static_cast<Uint32>(format);
|
|
const auto cached = m_unboundStorageImageTextures.find(key);
|
|
if (cached != m_unboundStorageImageTextures.end()) {
|
|
return cached->second;
|
|
}
|
|
|
|
const PlaceholderShape shape = PlaceholderShapeForTarget(target);
|
|
if (!shape.valid) {
|
|
// A multisample image uniform is the case with no answer here: its descriptor demands
|
|
// a multisample view, and a single-sampled 1x1 image is invalid Vulkan in that slot,
|
|
// not a degraded picture. The caller declines the binding exactly as it did before.
|
|
MGLOG_D("GetUnboundStorageImageTexture: no placeholder shape for target=%d", static_cast<Int>(target));
|
|
return nullptr;
|
|
}
|
|
const TextureInternalFormat internalFormat = InternalFormatForVkFormat(format);
|
|
if (internalFormat == TextureInternalFormat::Unknown) {
|
|
MGLOG_E_ONCE("GetUnboundStorageImageTexture: no GL internal format matches VkFormat=%d",
|
|
static_cast<Int>(format));
|
|
return nullptr;
|
|
}
|
|
|
|
auto texture = MakePlaceholderTextureObject(target, kUnboundStorageImageExternalIndex);
|
|
if (texture == nullptr) {
|
|
return nullptr;
|
|
}
|
|
texture->SetInternalFormat(internalFormat);
|
|
const SizeT texelBytes = MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat);
|
|
for (Uint32 index = 0; index < shape.uploadTargetCount; ++index) {
|
|
texture->AllocateStorage(shape.uploadTargets[index], 0,
|
|
{.texelSize = {1, 1, shape.depth},
|
|
.byteSize = texelBytes * static_cast<SizeT>(shape.depth)});
|
|
// Not dirty: there is deliberately nothing to upload. The image is created and
|
|
// transitioned to GENERAL by the storage-image preparation pass like any other, and
|
|
// its contents are exactly as undefined as GL says a fetch through an unbound image
|
|
// unit is.
|
|
texture->MarkStorageDirty(shape.uploadTargets[index], 0, false);
|
|
}
|
|
m_unboundStorageImageTextures.emplace(key, texture);
|
|
MGLOG_D("GetUnboundStorageImageTexture: created placeholder target=%d format=%d", static_cast<Int>(target),
|
|
static_cast<Int>(format));
|
|
return texture;
|
|
}
|
|
|
|
Bool UniformManager::ResolveSampledBinding(const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 binding, Uint32 element,
|
|
MG_State::GLState::ITextureObject*& outTexture,
|
|
const MG_State::GLState::SamplerObject*& outSampler) const {
|
|
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
|
|
// texture and the sampler override - this runs per binding per full-path draw,
|
|
// and program-alternating draw streams take the full path on every draw.
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSampledBinding: GL context is null");
|
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
|
"ResolveSampledBinding: sampler location binding %u out of range", binding);
|
|
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
|
"ResolveSampledBinding: sampler target binding %u out of range", binding);
|
|
const Int location =
|
|
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
|
|
if (location < 0 && element > 0) {
|
|
return false;
|
|
}
|
|
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
|
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
|
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
|
MG_State::GLState::ITextureObject* texture =
|
|
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
|
// The sampler in effect, resolved BEFORE the completeness test below rather than after:
|
|
// GL's completeness rules are a property of (texture, sampler in effect), so the test
|
|
// cannot be asked without it.
|
|
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
|
const MG_State::GLState::SamplerObject* effectiveSampler =
|
|
samplerOverride ? samplerOverride.get()
|
|
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
|
// Undefined default texture (name 0, no image) resolves as "unbound", exactly
|
|
// like ResolveSamplerTextureRaw reports it.
|
|
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
|
texture = nullptr;
|
|
}
|
|
// ...and so does a texture that fails the completeness rules for the filter in effect,
|
|
// because that is precisely what ResolveSamplerDescriptor does with it. The two used to
|
|
// disagree: this one asked only whether the default texture was UNDEFINED, so a default
|
|
// texture that had been given a base level but no mip chain - which is what the GL-CTS
|
|
// state reset between test cases leaves behind, and what any application that uploads to
|
|
// texture 0 has - stayed in the sampled set while the descriptor path swapped it for the
|
|
// fallback. SetupDraw then synced a texture no descriptor would use, the sync declined
|
|
// (GL calls it incomplete), and the null it returned was dereferenced one line later.
|
|
// Keeping the two predicates identical is the invariant; CollectSampledTextures exists to
|
|
// pre-sync exactly the textures the descriptors will hold.
|
|
if (MG_State::GLState::SamplesAsIncompleteTexture(texture, effectiveSampler)) {
|
|
texture = nullptr;
|
|
}
|
|
if (texture == nullptr) {
|
|
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
|
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
|
// its first use instead of leaving that work to happen inside an active pass.
|
|
// Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
|
|
// a multisample arm and the two must not drift apart.
|
|
texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get();
|
|
if (texture == nullptr) {
|
|
return false;
|
|
}
|
|
// The substitution changed the texture, so the "no override" arm of the effective
|
|
// sampler has to follow it to the fallback's own.
|
|
if (!samplerOverride) {
|
|
effectiveSampler = texture != nullptr ? texture->GetSamplerObject().get() : nullptr;
|
|
}
|
|
}
|
|
outTexture = texture;
|
|
outSampler = effectiveSampler;
|
|
return true;
|
|
}
|
|
|
|
Bool UniformManager::CollectSampledTextures(const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
|
Vector<SampledBindingRecord>* outBindingRecords) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) { outTextures.clear(); if (outBindingRecords) outBindingRecords->clear(); return true; }
|
|
#endif
|
|
outTextures.clear();
|
|
if (outBindingRecords != nullptr) {
|
|
outBindingRecords->clear();
|
|
}
|
|
// Nothing to prepare for a program the bind path is going to refuse; its declined
|
|
// binding has no uniform location to resolve a texture through either.
|
|
if (programObj.declinedDescriptors) {
|
|
return true;
|
|
}
|
|
|
|
const Uint32 bindingCount =
|
|
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
|
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
|
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
continue;
|
|
}
|
|
|
|
// Every ELEMENT of a sampler array reaches its own texture through its own unit,
|
|
// so every element has to be in the sampled set: this walk is what gets those
|
|
// textures synced and transitioned to a sampled layout BEFORE the render pass
|
|
// opens, and a missed element would first be touched by the descriptor resolve
|
|
// inside an active pass.
|
|
const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
|
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
|
if (!ResolveSampledBinding(program, programObj, binding, element, texture, sampler)) {
|
|
continue;
|
|
}
|
|
if (outBindingRecords != nullptr) {
|
|
outBindingRecords->push_back({texture != nullptr ? texture->GetLifetimeId() : 0,
|
|
sampler != nullptr ? sampler->GetLifetimeId() : 0});
|
|
}
|
|
|
|
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
|
|
if (found == outTextures.end()) {
|
|
outTextures.push_back(texture);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Bool UniformManager::SampledBindingsUnchanged(const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
const Vector<SampledBindingRecord>& previousRecords) const {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) return false;
|
|
#endif
|
|
// A declined program takes the full path every time and is refused there.
|
|
if (programObj.declinedDescriptors) {
|
|
return false;
|
|
}
|
|
SizeT recordIndex = 0;
|
|
// Iterate only the bindings this program declares (ascending), exactly like
|
|
// BindProgramUniformBuffers: this runs per draw whenever the texture bind
|
|
// generation moved, and walking all m_maxBindings slots to find the 1-8 real
|
|
// ones dominated it.
|
|
for (const Uint32 binding : programObj.activeBindings) {
|
|
if (binding >= m_maxBindings) {
|
|
break; // ascending, so nothing past the cap can follow
|
|
}
|
|
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
continue;
|
|
}
|
|
// Element-for-element, in the same order CollectSampledTextures recorded them -
|
|
// the two walks have to visit the identical descriptor sequence or the positional
|
|
// comparison below drifts.
|
|
const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
|
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
|
if (!ResolveSampledBinding(program, programObj, binding, element, texture, sampler)) {
|
|
continue;
|
|
}
|
|
if (recordIndex >= previousRecords.size()) {
|
|
return false;
|
|
}
|
|
const SampledBindingRecord& record = previousRecords[recordIndex++];
|
|
if (record.textureLifetimeId != (texture != nullptr ? texture->GetLifetimeId() : 0) ||
|
|
record.samplerLifetimeId != (sampler != nullptr ? sampler->GetLifetimeId() : 0)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return recordIndex == previousRecords.size();
|
|
}
|
|
|
|
Bool UniformManager::CollectStorageImageTextures(
|
|
const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) { outTextures.clear(); return true; }
|
|
#endif
|
|
outTextures.clear();
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE,
|
|
"CollectStorageImageTextures: GL context is null");
|
|
// Same as the sampled walk: a declined program is refused at bind time, and its declined
|
|
// binding has no uniform location to reach an image unit through.
|
|
if (programObj.declinedDescriptors) {
|
|
return true;
|
|
}
|
|
|
|
const Uint32 bindingCount =
|
|
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
|
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
|
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
|
continue;
|
|
}
|
|
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
|
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
|
return false;
|
|
}
|
|
|
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
|
if (baseLocation < 0) {
|
|
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
|
return false;
|
|
}
|
|
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
|
|
// binding whose elements each carry their own image unit, so each reaches its own
|
|
// texture. This walk is what puts those textures into the pre-pass sync and layout
|
|
// transition; collecting only element 0 left elements 1..N to be first touched by
|
|
// the descriptor resolve, which happens with a render pass already open.
|
|
const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
|
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
|
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
|
|
if (location < 0) {
|
|
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
|
binding, element);
|
|
return false;
|
|
}
|
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
|
imageUnit, binding, element);
|
|
return false;
|
|
}
|
|
|
|
auto* texture = MGB_CTX->GetImageTextureBinding(imageUnit).Texture.get();
|
|
if (texture == nullptr) {
|
|
// ResolveStorageImageDescriptor will substitute the placeholder image for this
|
|
// binding; include it here for the same reason the sampled walk includes the
|
|
// fallback texture - this walk is what gets a storage image created,
|
|
// STORAGE-usage-marked and transitioned to GENERAL BEFORE the render pass
|
|
// opens, and all three of those are illegal once it has. A target with no
|
|
// placeholder shape (multisample) contributes nothing and is declined at
|
|
// resolve time exactly as it was.
|
|
TextureTarget placeholderTarget = TextureTarget::Unknown;
|
|
VkFormat placeholderFormat = VK_FORMAT_UNDEFINED;
|
|
if (!ResolveUnboundStorageImagePlaceholder(programObj, binding, placeholderTarget,
|
|
placeholderFormat)) {
|
|
continue;
|
|
}
|
|
texture = GetUnboundStorageImageTexture(placeholderTarget, placeholderFormat).get();
|
|
if (texture == nullptr) {
|
|
continue;
|
|
}
|
|
}
|
|
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
|
|
outTextures.push_back(texture);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
|
GLint imageLevel, GLenum imageAccess) {
|
|
return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel;
|
|
}
|
|
|
|
Bool UniformManager::CollectSamplerImageFeedback(
|
|
const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Vector<SamplerImageFeedbackBinding>& outBindings) const {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire()) { outBindings.clear(); return true; }
|
|
#endif
|
|
outBindings.clear();
|
|
MOBILEGL_ASSERT(MGB_CTX_LIVE,
|
|
"CollectSamplerImageFeedback: GL context is null");
|
|
if (programObj.declinedDescriptors) return true;
|
|
|
|
for (const Uint32 samplerBinding : programObj.activeBindings) {
|
|
if (samplerBinding >= m_maxBindings ||
|
|
programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
continue;
|
|
}
|
|
const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding);
|
|
for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) {
|
|
MG_State::GLState::ITextureObject* sampledTexture = nullptr;
|
|
const MG_State::GLState::SamplerObject* sampledSampler = nullptr;
|
|
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
|
|
sampledTexture, sampledSampler) ||
|
|
sampledTexture == nullptr || sampledSampler == nullptr ||
|
|
IsPlaceholderTexture(sampledTexture)) {
|
|
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
|
|
// alias the image-unit binding of the original texture. The unbound and
|
|
// incomplete cases both arrive here AS that fallback now that
|
|
// ResolveSampledBinding applies the completeness rule itself, so the test is
|
|
// "is this one of ours" rather than a second completeness check.
|
|
continue;
|
|
}
|
|
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
|
|
// direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches
|
|
// into failed dispatches; a correct snapshot for them needs a same-sample-count path.
|
|
const TextureTarget sampledTarget = sampledTexture->GetTarget();
|
|
if (sampledTarget == TextureTarget::Texture2DMultisample ||
|
|
sampledTarget == TextureTarget::Texture2DMultisampleArray) {
|
|
continue;
|
|
}
|
|
const auto& levelRange = sampledTexture->GetLevelRange();
|
|
Bool aliasesWritableImage = false;
|
|
for (const Uint32 imageBinding : programObj.activeBindings) {
|
|
if (imageBinding >= m_maxBindings ||
|
|
programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
|
continue;
|
|
}
|
|
if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false;
|
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding];
|
|
if (baseLocation < 0) return false;
|
|
const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding);
|
|
for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) {
|
|
const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement);
|
|
if (location < 0) return false;
|
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
return false;
|
|
}
|
|
const auto& image = MGB_CTX->GetImageTextureBinding(imageUnit);
|
|
// A sampler view exposes all layers of its target; equal texture plus an
|
|
// overlapping mip therefore aliases the writable image subresource.
|
|
if (image.Texture.get() == sampledTexture &&
|
|
SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(),
|
|
image.Level, image.Access)) {
|
|
aliasesWritableImage = true;
|
|
break;
|
|
}
|
|
}
|
|
if (aliasesWritableImage) break;
|
|
}
|
|
if (aliasesWritableImage) {
|
|
outBindings.push_back({.samplerBinding = samplerBinding,
|
|
.samplerElement = samplerElement,
|
|
.texture = sampledTexture,
|
|
.sampler = sampledSampler,
|
|
.numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]});
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
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<unsigned long long>(blockSize),
|
|
static_cast<unsigned long long>(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<VkDeviceSize>::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<Uint32>::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<Uint8> zero;
|
|
zero.assign(static_cast<SizeT>(blockSize), 0);
|
|
BufferSlice padded{};
|
|
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, m_wireFrameIndex, zero.data(), blockSize,
|
|
std::max<VkDeviceSize>(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<Uint32>::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<Uint32>), 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<unsigned long long>(padded.offset), bindingPoint);
|
|
return false;
|
|
}
|
|
out.directBindable = true;
|
|
out.buffer = padded.buffer;
|
|
out.range = blockSize;
|
|
out.dynamicOffset = padded.offset;
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
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(MGB_CTX_LIVE, "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<Int>(binding)) {
|
|
outData = program.GetUBOData();
|
|
outSize = static_cast<VkDeviceSize>(program.GetUBOSize());
|
|
static const Array<Uint8, 16> emptyGlobalUbo{};
|
|
if (outData == nullptr || outSize == 0) {
|
|
outData = emptyGlobalUbo.data();
|
|
outSize = static_cast<VkDeviceSize>(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 MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire() && binding >= programObj.uniformBlockIndexByBinding.size())
|
|
WireDescriptorFatal("uniform-buffer-reflection-binding");
|
|
#endif
|
|
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<Uint32>(program.GetActiveUniformBlocksCount());
|
|
MOBILEGL_ASSERT(static_cast<Uint32>(blockIndex) < activeUniformBlockCount,
|
|
"ResolveUniformBufferPayload: uniform block index %d out of range (count=%u)", blockIndex,
|
|
activeUniformBlockCount);
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire() && (blockIndex < 0 || static_cast<Uint32>(blockIndex) >= activeUniformBlockCount))
|
|
WireDescriptorFatal("uniform-buffer-reflection-block");
|
|
#endif
|
|
|
|
const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast<Uint32>(blockIndex));
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire())
|
|
return ResolveWireUniformBufferPayload(program, static_cast<Uint32>(blockIndex), frontendBinding, out);
|
|
#endif
|
|
const Uint32 uniformBindingPointCount =
|
|
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(BufferTarget::Uniform));
|
|
MOBILEGL_ASSERT(frontendBinding < uniformBindingPointCount,
|
|
"ResolveUniformBufferPayload: frontend UBO binding %u out of range for block '%s'",
|
|
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
|
|
|
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
|
|
const auto& bufferObject = bindingPoint.GetBoundObject();
|
|
MOBILEGL_ASSERT(bufferObject != nullptr,
|
|
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
|
|
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
|
bufferObject->SyncPersistentMappedRange();
|
|
|
|
MOBILEGL_ASSERT(bufferObject->MappedData() != nullptr && bufferObject->GetSize() != 0,
|
|
"ResolveUniformBufferPayload: bound UBO data is empty for block '%s'",
|
|
program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
|
|
|
const auto range = bindingPoint.GetRange();
|
|
const VkDeviceSize bufferSize = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
|
const VkDeviceSize rangeStart = static_cast<VkDeviceSize>(range.start);
|
|
MOBILEGL_ASSERT(rangeStart < bufferSize,
|
|
"ResolveUniformBufferPayload: UBO range start %zu exceeds buffer size %zu for block '%s'",
|
|
static_cast<SizeT>(rangeStart), static_cast<SizeT>(bufferSize),
|
|
program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
|
|
|
VkDeviceSize rangeEnd = static_cast<VkDeviceSize>(range.end);
|
|
if (rangeEnd > bufferSize) {
|
|
rangeEnd = bufferSize;
|
|
}
|
|
MOBILEGL_ASSERT(rangeEnd > rangeStart,
|
|
"ResolveUniformBufferPayload: invalid UBO range [%zu, %zu) for block '%s'",
|
|
static_cast<SizeT>(rangeStart), static_cast<SizeT>(rangeEnd),
|
|
program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
|
|
|
const VkDeviceSize blockSize = static_cast<VkDeviceSize>(program.GetUBOSizeAt(static_cast<Uint32>(blockIndex)));
|
|
MOBILEGL_ASSERT(blockSize > 0,
|
|
"ResolveUniformBufferPayload: reflected UBO size is zero for block '%s'",
|
|
program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
|
|
|
const VkDeviceSize available = rangeEnd - rangeStart;
|
|
outSize = blockSize;
|
|
outData = bufferObject->MappedData() + static_cast<SizeT>(rangeStart);
|
|
if (available < blockSize) {
|
|
static thread_local Vector<Uint8> paddedUbo;
|
|
paddedUbo.assign(static_cast<SizeT>(blockSize), 0);
|
|
Memcpy(paddedUbo.data(), outData, static_cast<SizeT>(available));
|
|
outData = paddedUbo.data();
|
|
}
|
|
out.payload = outData;
|
|
out.payloadSize = outSize;
|
|
|
|
// Zero-copy direct bind: for a persistent-mapped coherent app buffer whose full reflected
|
|
// block fits within the aligned bound range, point the descriptor straight at the app's
|
|
// resident VkBuffer (the same buffer the GLES backend binds) with the block range as the
|
|
// dynamic offset - no per-draw copy into a transient ring. Any gate failing keeps the
|
|
// UploadTransient payload above. AcquireResidentSlice does the persistent busy-tracking and
|
|
// (for the persistent case) hits a zero-work fast path returning the whole-buffer slice.
|
|
if (bufferObject->IsBackendPersistentMapped() && available >= blockSize &&
|
|
(rangeStart % m_minDynamicOffsetAlignment) == 0) {
|
|
BufferSlice slice{};
|
|
if (m_bufferManager->AcquireResidentSlice(BufferKind::Uniform, bufferObject, slice) &&
|
|
slice.IsValid() && slice.offset == 0 && slice.size >= rangeStart + blockSize) {
|
|
out.directBindable = true;
|
|
out.buffer = slice.buffer;
|
|
out.range = blockSize;
|
|
out.dynamicOffset = rangeStart;
|
|
}
|
|
}
|
|
if (MG_Util::PipeStats::Enabled() && !out.directBindable) {
|
|
// D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host
|
|
// payload a split build would have to ship with set_shader_buffers. Espryt binds
|
|
// the frontend buffer to the driver and contributes nothing here, which is why
|
|
// the class is named for the payload and not for the call. Counted AFTER the
|
|
// zero-copy direct-bind decision: a direct bind repacks nothing, and counting it
|
|
// here reported a copy that never happened.
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
|
|
static_cast<Uint64>(outSize));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
|
|
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
|
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
|
return false;
|
|
}
|
|
|
|
const Uint32 descriptorCount = static_cast<Uint32>(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<Uint32>(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;
|
|
}
|
|
|
|
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
|
|
if (frame.descriptorPools.empty()) {
|
|
return false;
|
|
}
|
|
|
|
const auto matchingBucket = std::find_if(
|
|
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
|
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
|
|
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
|
|
? std::max<Uint32>(1, matchingBucket->maxSets)
|
|
: m_setsPerFrame;
|
|
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
|
|
: currentMaxSets;
|
|
|
|
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
|
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
|
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
|
currentMaxSets, grownMaxSets);
|
|
return false;
|
|
}
|
|
|
|
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
|
|
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
|
MGLOG_D(
|
|
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
|
|
frameIndex, currentMaxSets, grownMaxSets, 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<Uint32>(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 MOBILEGL_BUILD_DISAGGREGATED
|
|
if (program.IsWire() && !hasPayload) return false;
|
|
#endif
|
|
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<Uint32>(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<Int>(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<VkDeviceSize>(ubo.payloadSize)) {
|
|
outBuffer = memo.buffer;
|
|
outRange = memo.range;
|
|
outDynamicOffset = static_cast<Uint32>(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<Uint32>(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<Uint64>(ubo.payloadSize));
|
|
}
|
|
if (isGlobalUbo) {
|
|
m_globalUboMemo[m_globalUboMemoNext] =
|
|
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
|
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
|
|
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void UniformManager::BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
|
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
|
const Vector<Uint32>& 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<Uint32>(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
Bool UniformManager::PrepareWireTextureResources(const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj) {
|
|
if (!program.IsWire()) return true;
|
|
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<Int>(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<Uint>(location));
|
|
if (unit < 0 || static_cast<Uint32>(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;
|
|
if (!m_textureManager->SyncTextureResourceByHandle(handle, false, storage)) return false;
|
|
}
|
|
}
|
|
}
|
|
m_textureManager->FlushPendingUploads();
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
|
const MagmaProgramSource& program,
|
|
const ProgramFactory::VkProgramObject& programObj,
|
|
Uint32 frameIndex,
|
|
VkPipelineBindPoint bindPoint,
|
|
const SamplerBindingOverride* samplerBindingOverride,
|
|
Bool samplerDescriptorsUnchangedHint,
|
|
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
|
|
// 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;
|
|
}
|
|
|
|
// Dynamic-offset-only rebind (see FastRebindMemo in the header): the last
|
|
// cacheable walk of this exact program selected a set whose contents are
|
|
// provably still what this walk would write - the hint covers every
|
|
// sampler binding, and an unchanged (buffer, range) for the single
|
|
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
|
// the SAME set delivers without any descriptor write.
|
|
const Bool cacheable = samplerBindingOverride == nullptr &&
|
|
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
|
|
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
|
m_fastRebindMemo.frameIndex == frameIndex &&
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// A wire store destroyed since the memo was taken may have handed its handle
|
|
// value to a later mint (see FastRebindMemo): "same VkBuffer" then names a
|
|
// different store, so the memo is refused and the full walk re-records it.
|
|
m_fastRebindMemo.wireStoreDestroyEpoch == m_bufferManager->GetWireStoreDestroyEpoch() &&
|
|
#endif
|
|
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
|
m_fastRebindMemo.programHash == programObj.hash) {
|
|
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
|
VkDeviceSize uboRange = 0;
|
|
Uint32 uboDynamicOffset = 0;
|
|
if (ResolveDynamicUboDescriptor(program, programObj, m_fastRebindMemo.uboBinding, 0, frameIndex,
|
|
uboBuffer, uboRange, uboDynamicOffset) &&
|
|
uboBuffer == m_fastRebindMemo.uboBuffer && uboRange == m_fastRebindMemo.uboRange) {
|
|
auto& fastOffsets = m_dynamicOffsetsScratch;
|
|
fastOffsets.clear();
|
|
fastOffsets.push_back(uboDynamicOffset);
|
|
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout,
|
|
m_fastRebindMemo.set, fastOffsets);
|
|
return true;
|
|
}
|
|
// Any mismatch (arena wrap or growth, direct-bind retarget, upload
|
|
// failure) falls through to the full walk, which re-records the memo.
|
|
}
|
|
|
|
// 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<Uint32>(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<Uint32>(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);
|
|
|
|
// Eligibility probe for FastRebindMemo, filled by this walk: exactly one
|
|
// dynamic-UBO descriptor (no arrayed elements) and otherwise only
|
|
// combined-image samplers, so the whole set's content is pinned by the
|
|
// sampler hint plus one (buffer, range) compare.
|
|
Uint32 dynamicUboDescriptorCount = 0;
|
|
Uint32 fastRebindUboBinding = 0;
|
|
Bool fastRebindKindsEligible = true;
|
|
|
|
// 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];
|
|
|
|
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);
|
|
dynamicUboDescriptorCount += descriptorCount;
|
|
fastRebindUboBinding = 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);
|
|
fastRebindKindsEligible = false;
|
|
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);
|
|
fastRebindKindsEligible = false;
|
|
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);
|
|
}
|
|
|
|
fastRebindKindsEligible = false;
|
|
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);
|
|
}
|
|
fastRebindKindsEligible = false;
|
|
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);
|
|
// Overrides come only from MobileGL's own blit and depth-mipmap programs, whose
|
|
// samplers are scalars; the override replaces THE descriptor at its binding, so
|
|
// there is no element for it to mean on an arrayed one.
|
|
const Bool overrideThisBinding = samplerBindingOverride != nullptr &&
|
|
samplerBindingOverride->binding == binding &&
|
|
samplerBindingOverride->texture != nullptr &&
|
|
samplerBindingOverride->sampler != nullptr;
|
|
MOBILEGL_ASSERT(
|
|
!overrideThisBinding || descriptorCount == 1,
|
|
"BindProgramUniformBuffers: sampler override targets arrayed binding %u (%u descriptors)",
|
|
binding, descriptorCount);
|
|
const SizeT firstImageInfoIndex = imageInfos.size();
|
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
|
VkDescriptorImageInfo imageInfo{};
|
|
const SamplerBindingOverride* overrideForElement =
|
|
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr;
|
|
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) {
|
|
const auto overrideIt = std::find_if(
|
|
samplerBindingOverrides->begin(), samplerBindingOverrides->end(),
|
|
[binding, element](const SamplerBindingOverride& candidate) {
|
|
return candidate.binding == binding && candidate.element == element;
|
|
});
|
|
if (overrideIt != samplerBindingOverrides->end()) {
|
|
overrideForElement = &*overrideIt;
|
|
}
|
|
}
|
|
const Bool hasImage = overrideForElement != nullptr
|
|
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
|
|
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
|
|
element, imageInfo,
|
|
samplerDescriptorsUnchangedHint);
|
|
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);
|
|
}
|
|
if (descriptorCount > 1) {
|
|
// The dynamic-offset-only rebind replays a whole descriptor set on the
|
|
// strength of the sampler hint alone, and its eligibility probe was written
|
|
// for bindings that carry one descriptor each. An arrayed sampler binding
|
|
// also bypasses the per-binding descriptor memo, so there is nothing for it
|
|
// to win here either.
|
|
fastRebindKindsEligible = false;
|
|
}
|
|
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<const Uint64*>(data);
|
|
for (SizeT i = 0; i < byteSize / sizeof(Uint64); ++i) {
|
|
mix64(words[i]);
|
|
}
|
|
};
|
|
mixWords(&programObj.descriptorSetLayout, sizeof(programObj.descriptorSetLayout));
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// 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());
|
|
#endif
|
|
for (const auto& write : writes) {
|
|
mix64((static_cast<Uint64>(write.dstBinding) << 40) ^
|
|
(static_cast<Uint64>(write.descriptorType) << 8) ^
|
|
static_cast<Uint64>(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;
|
|
if (cacheable) {
|
|
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<Uint32>(writes.size()), writes.data(), 0, nullptr);
|
|
}
|
|
if (cacheable) {
|
|
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
|
|
DescriptorReuseEntry{signature, descriptorSet, true};
|
|
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
|
|
} else {
|
|
for (auto& entry : m_descriptorReuseMemo) {
|
|
entry.valid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// (Re)record the dynamic-offset-only rebind memo. Recording on every
|
|
// cacheable walk (allocated or reused set alike - both hold exactly the
|
|
// content just computed) keeps the single slot tracking the most recent
|
|
// program; a non-cacheable override walk drops it alongside the reuse
|
|
// memo above.
|
|
if (cacheable && fastRebindKindsEligible && dynamicUboDescriptorCount == 1) {
|
|
m_fastRebindMemo = FastRebindMemo{
|
|
/*valid=*/true, frameIndex, program.GetLifetimeId(), programObj.hash,
|
|
fastRebindUboBinding, bufferInfos[0].buffer,
|
|
bufferInfos[0].range, descriptorSet,
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
m_bufferManager->GetWireStoreDestroyEpoch(),
|
|
#endif
|
|
};
|
|
} else {
|
|
m_fastRebindMemo.valid = false;
|
|
}
|
|
|
|
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, descriptorSet,
|
|
dynamicOffsets);
|
|
return true;
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|