mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
3637 lines
219 KiB
C++
3637 lines
219 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 "RenderPassGuard.h"
|
|
#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;
|
|
}
|
|
}
|
|
|
|
// GL ignores Layer/Layered for a target with no layers. In particular image1D stays a 1D
|
|
// view, and an ignored nonzero Layer must not address another slice.
|
|
static Bool WireTargetHasLayers(MG_Pipe::MGPipeResourceTarget target) {
|
|
using T = MG_Pipe::MGPipeResourceTarget;
|
|
return target == T::Tex1DArray || target == T::Tex2DArray || target == T::Tex2DMSArray ||
|
|
target == T::Tex3D || target == T::TexCube || target == T::TexCubeArray;
|
|
}
|
|
|
|
// GL 4.6 core 8.26: an access through an image unit whose texture does not HAVE the
|
|
// (level, layer) the unit names is INVALID. A load returns zero, a store does nothing, an
|
|
// atomic updates nothing - and none of it is an error, neither at bind time (the bind only
|
|
// checks the level's and the layer's signs) nor at the draw. It is the observable an empty
|
|
// unit has, so the answer is the empty unit's (ResolveWirePlaceholderImage): a NULL storage
|
|
// descriptor where the device enables VK_EXT_robustness2 nullDescriptor, else a storage
|
|
// placeholder private to the unit (per shape), cleared before each use, of the dimensionality
|
|
// the SHADER declared.
|
|
//
|
|
// KHR-GL46.shader_image_load_store.incomplete_textures is the shape that found it: level 2
|
|
// of a texture that defines only level 0 (MAX_LEVEL 7, a mipmapping filter). The same
|
|
// question comes up empty for a level past an immutable texture's storage, a layer past an
|
|
// array's last slice, a texture that has no storage at all, and a level past a texture
|
|
// view's own window even where its owner has one. The record carries exactly what
|
|
// glBindImageTexture was given, so it is decided here, by the walk every wire consumer uses
|
|
// to find a texel's storage, and from that walk's "not there" answer ONLY: a null for any
|
|
// other reason (a dead record, a stale view CSO) stays the descriptor resolve's refusal.
|
|
//
|
|
// NOT DECIDED HERE, deliberately: a level that IS in storage but lies outside
|
|
// [BASE_LEVEL, MAX_LEVEL], or of a texture that is not mipmap-complete. The monolith arm
|
|
// binds those as it finds them too, and the P7 gate 5 comparison is against that arm.
|
|
static Bool WireShaderImageNamesNoTexel(VkTextureManager& textures, const MG_Pipe::MGPImageView& image) {
|
|
const auto& state = MG_Pipe::MGPipeApplier();
|
|
if (MG_Pipe::MGPipeHandleIsNull(image.Res) || image.Res.Slot >= state.TextureResources.size()) return false;
|
|
const auto& record = state.TextureResources[image.Res.Slot];
|
|
if (!record.Live || record.Gen != image.Res.Gen) return false;
|
|
const Bool selectsLayer =
|
|
WireTargetHasLayers(static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target)) && !image.Layered;
|
|
Uint32 level = image.Level;
|
|
Uint32 layer = selectsLayer ? image.Layer : 0;
|
|
Bool outsideWindow = false;
|
|
(void)textures.ResolveWireTextureStorage(image.Res, level, layer, nullptr, nullptr, &outsideWindow);
|
|
return outsideWindow;
|
|
}
|
|
|
|
static VkComponentSwizzle WireSwizzle(Uint8 value) {
|
|
switch (static_cast<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;
|
|
// An EMPTY image unit is the same 8.26 invalid access as the named-but-missing texel
|
|
// below, so it takes the same answer: a null storage descriptor where the device has
|
|
// one, else the unit-private placeholder (codex closeout finding 2).
|
|
if (MG_Pipe::MGPipeHandleIsNull(handle))
|
|
return ResolveWirePlaceholderImage(commandBuffer, program, programObj, binding, storage, out,
|
|
VK_FORMAT_UNDEFINED, static_cast<Uint32>(unit));
|
|
if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("image-record");
|
|
const auto& record = state.TextureResources[handle.Slot];
|
|
if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("image-record-generation");
|
|
// Before the sync: a texture with no storage at all is one of the shapes, and syncing
|
|
// it would refuse rather than answer.
|
|
if (storage && WireShaderImageNamesNoTexel(*m_textureManager, state.BoundShaderImages[unit])) {
|
|
const VkFormat bound = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
|
MG_Util::ConvertGLEnumToTextureInternalFormat(state.BoundShaderImages[unit].InternalFormat));
|
|
return ResolveWirePlaceholderImage(commandBuffer, program, programObj, binding, storage, out, bound,
|
|
static_cast<Uint32>(unit));
|
|
}
|
|
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);
|
|
const Bool layerable = WireTargetHasLayers(target);
|
|
Uint32 level = storage ? state.BoundShaderImages[unit].Level : record.Params.BaseLevel;
|
|
// GL ignores Layer/Layered for non-layerable targets (WireTargetHasLayers).
|
|
Uint32 layer = storage && layerable && !state.BoundShaderImages[unit].Layered
|
|
? state.BoundShaderImages[unit].Layer : 0;
|
|
const Uint32 localLevel = level;
|
|
const Uint32 localLayer = layer;
|
|
Uint32 layers = 0;
|
|
VkFormat aliasFormat = VK_FORMAT_UNDEFINED;
|
|
const auto root = m_textureManager->ResolveWireTextureStorage(handle, level, layer, &aliasFormat, &layers);
|
|
if (MG_Pipe::MGPipeHandleIsNull(root)) WireDescriptorFatal("image-view-window");
|
|
VkImageViewType type = WireViewType(target);
|
|
if (storage && layerable && !state.BoundShaderImages[unit].Layered) {
|
|
layers = 1;
|
|
type = target == MG_Pipe::MGPipeResourceTarget::Tex1DArray ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D;
|
|
}
|
|
if (type == VK_IMAGE_VIEW_TYPE_3D) layers = 1;
|
|
Uint32 levels = 1;
|
|
if (!storage) {
|
|
if (record.Params.MaxLevel < localLevel || record.Desc.Levels <= localLevel) return false;
|
|
levels = std::min<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.
|
|
// Only when something may have written an image since the last such barrier
|
|
// (WireImageWriteEpoch): with nothing written in between, consecutive draws keep their
|
|
// render pass open instead of closing it for a barrier that orders nothing.
|
|
if (WireImageWriteEpoch() != m_wireImageBarrierEpoch) {
|
|
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
EndActiveRenderPassOn(commandBuffer);
|
|
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
0, 1, &memory, 0, nullptr, 0, nullptr);
|
|
m_wireImageBarrierEpoch = WireImageWriteEpoch();
|
|
}
|
|
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
info.image = resource->image;
|
|
info.viewType = type;
|
|
info.format = format;
|
|
info.subresourceRange = {aspect, level, levels, layer, layers};
|
|
if (!storage) {
|
|
// An X-format shared image carries undefined alpha bytes, exactly like an expanded RGB.
|
|
const Bool alphaIsOne = resource->sharedImageAlphaOne || ResolveTextureFormatInfo(
|
|
static_cast<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;
|
|
// A texture's built-in sampler CSO can be missing or gone while the texture still names
|
|
// it: sampler CSOs are content-addressed and recycled at capacity, and the texture's
|
|
// record is only re-pointed by its next set_texture_params (a texture whose params never
|
|
// crossed names none at all). Sample with GL's default parameters rather than end the
|
|
// session - the DirectGLES arm declines the push in both cases.
|
|
using SamplerCsoRecord = std::decay_t<decltype(state.SamplerCsos[0])>;
|
|
static const decltype(SamplerCsoRecord::Params) kDefaultSamplerParams{};
|
|
const Bool known = !MG_Pipe::MGPipeHandleIsNull(samplerHandle) &&
|
|
samplerHandle.Slot < state.SamplerCsos.size();
|
|
const SamplerCsoRecord* sampler = known ? &state.SamplerCsos[samplerHandle.Slot] : nullptr;
|
|
const Bool stale = sampler == nullptr || !sampler->Live || sampler->Gen != samplerHandle.Gen;
|
|
if (stale) {
|
|
MGLOG_E_ONCE("Magma wire: sampler CSO {%u, %u} named by texture unit %u is missing or no longer "
|
|
"live; sampling with default parameters", samplerHandle.Slot, samplerHandle.Gen, unit);
|
|
}
|
|
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(stale ? kDefaultSamplerParams : 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_trimQuietEpochs = MG_Config::Features.MagmaDescriptorTrimFrames;
|
|
// The renderer records its first frame into slot 0 before any BeginFrame runs.
|
|
m_epochSlot = 0;
|
|
m_epochOpen = true;
|
|
m_descriptorSlotTrims = 0;
|
|
m_textureManager = textureManager;
|
|
m_samplerManager = samplerManager;
|
|
|
|
m_frames.resize(m_frameCount);
|
|
for (Uint32 frameIndex = 0; frameIndex < m_frameCount; ++frameIndex) {
|
|
auto& frame = m_frames[frameIndex];
|
|
frame.activeDescriptorPoolIndex = 0;
|
|
frame.allocatedSetsThisFrame = 0;
|
|
frame.peakAllocatedSetsThisFrame = 0;
|
|
frame.capacitySets = m_setsPerFrame;
|
|
frame.quietEpochs = 0;
|
|
frame.descriptorPools.clear();
|
|
|
|
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
|
if (!CreateDescriptorPool(m_setsPerFrame, false, initialPool)) {
|
|
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
|
frameIndex);
|
|
Shutdown();
|
|
return false;
|
|
}
|
|
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0, false});
|
|
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex,
|
|
m_setsPerFrame);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void UniformManager::Shutdown() {
|
|
// Before the per-frame loop, because these views are NOT owned by any frame slot (see
|
|
// m_unboundTexelBufferViews) and the loop below is what clears m_device.
|
|
if (m_device != VK_NULL_HANDLE) {
|
|
for (const auto& viewEntry : m_unboundTexelBufferViews) {
|
|
if (viewEntry.second != VK_NULL_HANDLE) {
|
|
vkDestroyBufferView(m_device, viewEntry.second, nullptr);
|
|
}
|
|
}
|
|
}
|
|
m_unboundTexelBufferViews.clear();
|
|
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;
|
|
frame.capacitySets = 0;
|
|
frame.quietEpochs = 0;
|
|
}
|
|
m_frames.clear();
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
for (auto& entry : m_wirePlaceholderImages) DestroyWirePlaceholderImage(entry.second);
|
|
m_wirePlaceholderImages.clear();
|
|
m_wirePrivateStoragePlaceholders = 0;
|
|
m_wireInvalidStorageImagesBindNull = false;
|
|
#endif
|
|
m_epochSlot = 0;
|
|
m_epochOpen = false;
|
|
|
|
m_bufferManager = nullptr;
|
|
m_programFactory = nullptr;
|
|
m_device = VK_NULL_HANDLE;
|
|
m_physicalDevice = VK_NULL_HANDLE;
|
|
m_minDynamicOffsetAlignment = 1;
|
|
m_frameCount = 0;
|
|
m_maxBindings = 0;
|
|
m_samplerResolveMemo.clear();
|
|
m_samplerResolveMemoHighWater = 0;
|
|
m_setsPerFrame = 0;
|
|
m_peakDescriptorSetsObserved = 0;
|
|
m_textureManager = nullptr;
|
|
m_samplerManager = nullptr;
|
|
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());
|
|
}
|
|
// The epoch that just ended is the one of the slot begun last - this slot again on the
|
|
// drain path, the previous frame's slot on the Present path. Its peak is still unreset.
|
|
if (m_epochOpen && m_epochSlot < m_frames.size()) {
|
|
NoteEndedEpochForTrim(m_frames[m_epochSlot].peakAllocatedSetsThisFrame);
|
|
}
|
|
m_epochSlot = frameIndex;
|
|
m_epochOpen = true;
|
|
// No pending command buffer references this slot's sets here (see TrimFrameDescriptorPools
|
|
// for why that holds at both of BeginFrame's call sites), so a quiet grown slot can be
|
|
// handed back whole.
|
|
if (m_trimQuietEpochs != 0 && frame.descriptorPools.size() > 1 &&
|
|
frame.capacitySets >= kDescriptorTrimMinCapacity && frame.quietEpochs >= m_trimQuietEpochs) {
|
|
TrimFrameDescriptorPools(frame, frameIndex);
|
|
}
|
|
frame.activeDescriptorPoolIndex = 0;
|
|
frame.allocatedSetsThisFrame = 0;
|
|
frame.peakAllocatedSetsThisFrame = 0;
|
|
for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
|
|
cacheEntryPair.second.cursor = 0;
|
|
}
|
|
// The frame's descriptor sets are recycled above, so last frame's reuse targets
|
|
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
|
|
InvalidateDescriptorSetMemos();
|
|
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
|
// reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a
|
|
// resolve has actually written can be valid, so the high-water mark bounds the
|
|
// sweep - the vector itself is sized to the device's binding cap (256 here), which
|
|
// is ~30x more entries than any program declares.
|
|
const Uint32 touchedBindings =
|
|
std::min<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;
|
|
}
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
Bool UniformManager::WireDescriptorSetBudgetReached(Uint32 frameIndex) const {
|
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "WireDescriptorSetBudgetReached invalid frame index");
|
|
return m_frames[frameIndex].allocatedSetsThisFrame >= kWireDescriptorSetBudget;
|
|
}
|
|
|
|
SizeT UniformManager::RewindWireDescriptorSets(Uint32 frameIndex) {
|
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "RewindWireDescriptorSets invalid frame index");
|
|
auto& frame = m_frames[frameIndex];
|
|
// The caller proved that the last graphics submit using these sets has
|
|
// retired and that no unsubmitted command buffer still references them.
|
|
// Keep the pools and sets: only the per-layout write cursors rewind.
|
|
// Image and buffer views stay alive until the regular frame boundary.
|
|
m_peakDescriptorSetsObserved = std::max(m_peakDescriptorSetsObserved, frame.peakAllocatedSetsThisFrame);
|
|
SizeT cachedSets = 0;
|
|
for (auto& entry : frame.descriptorSetCacheByLayout) {
|
|
cachedSets += entry.second.sets.size();
|
|
entry.second.cursor = 0;
|
|
}
|
|
frame.allocatedSetsThisFrame = 0;
|
|
// The PEAK is not reset: a rewind ends a window of the epoch, not the epoch. The trim scores
|
|
// an epoch by the most sets the slot held at once in it (NoteEndedEpochForTrim at the next
|
|
// BeginFrame), and on this arm that is the largest window - resetting here would score a
|
|
// burst frame by its last window alone and trim a slot that re-grows every frame.
|
|
InvalidateDescriptorSetMemos();
|
|
const Uint32 touchedBindings =
|
|
std::min<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;
|
|
}
|
|
return cachedSets;
|
|
}
|
|
#endif
|
|
|
|
void UniformManager::InvalidateDescriptorSetMemos() {
|
|
// Every VkDescriptorSet handle held outside the per-layout caches - in this backend they all
|
|
// live in this class. A trim can hand a destroyed set's handle value to a fresh allocation,
|
|
// so the bind-dedup shadow must drop too, not only the two reuse memos.
|
|
for (auto& entry : m_descriptorReuseMemo) {
|
|
entry.valid = false;
|
|
}
|
|
m_fastRebindMemo.valid = false;
|
|
m_lastBindValid = false;
|
|
}
|
|
|
|
void UniformManager::NoteEndedEpochForTrim(Uint32 epochPeakSets) {
|
|
for (auto& slot : m_frames) {
|
|
if (static_cast<Uint64>(epochPeakSets) * kDescriptorTrimHeadroom > slot.capacitySets) {
|
|
slot.quietEpochs = 0;
|
|
} else if (slot.quietEpochs < std::numeric_limits<Uint32>::max()) {
|
|
++slot.quietEpochs;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UniformManager::TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex) {
|
|
const SizeT poolsBefore = frame.descriptorPools.size();
|
|
const Uint64 capacityBefore = frame.capacitySets;
|
|
// Holders first: the per-layout cache and every memo that may name one of these sets.
|
|
frame.descriptorSetCacheByLayout.clear();
|
|
InvalidateDescriptorSetMemos();
|
|
for (SizeT i = 1; i < frame.descriptorPools.size(); ++i) {
|
|
if (frame.descriptorPools[i].handle != VK_NULL_HANDLE) {
|
|
vkDestroyDescriptorPool(m_device, frame.descriptorPools[i].handle, nullptr);
|
|
}
|
|
}
|
|
frame.descriptorPools.resize(1);
|
|
auto& base = frame.descriptorPools[0];
|
|
// Frees every set allocated from it; the spec defines no failure for vkResetDescriptorPool.
|
|
vkResetDescriptorPool(m_device, base.handle, 0);
|
|
base.allocatedSets = 0;
|
|
frame.activeDescriptorPoolIndex = 0;
|
|
frame.capacitySets = base.maxSets;
|
|
frame.quietEpochs = 0;
|
|
++m_descriptorSlotTrims;
|
|
MGLOG_D("UniformDescriptorBinder: frame %u trimmed its descriptor pools (%zu pools / %llu sets -> 1 / %u)",
|
|
frameIndex, poolsBefore, static_cast<unsigned long long>(capacityBefore), base.maxSets);
|
|
}
|
|
|
|
DescriptorPoolCensus UniformManager::GetDescriptorPoolCensus() const {
|
|
DescriptorPoolCensus census;
|
|
census.available = true;
|
|
census.frameSlots = static_cast<uint32_t>(m_frames.size());
|
|
for (const auto& frame : m_frames) {
|
|
const auto poolCount = static_cast<uint32_t>(frame.descriptorPools.size());
|
|
census.pools += poolCount;
|
|
census.maxPoolsInOneSlot = std::max(census.maxPoolsInOneSlot, poolCount);
|
|
census.capacitySets += frame.capacitySets;
|
|
for (const auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
|
|
census.cachedSets += cacheEntryPair.second.sets.size();
|
|
}
|
|
}
|
|
census.slotTrims = m_descriptorSlotTrims;
|
|
return census;
|
|
}
|
|
|
|
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
|
|
SizeT purgedSets = 0;
|
|
for (auto& frame : m_frames) {
|
|
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
|
|
if (it == frame.descriptorSetCacheByLayout.end()) {
|
|
continue;
|
|
}
|
|
// Free the sets back to their pools and credit the bucket accounting, so
|
|
// program churn recycles pool capacity instead of abandoning the slots.
|
|
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
|
|
// in-flight command buffer references these sets.
|
|
for (const auto& cached : it->second.sets) {
|
|
if (cached.set == VK_NULL_HANDLE) {
|
|
continue;
|
|
}
|
|
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
|
|
const auto bucket = std::find_if(
|
|
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
|
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
|
|
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
|
|
--bucket->allocatedSets;
|
|
}
|
|
}
|
|
purgedSets += it->second.sets.size();
|
|
frame.descriptorSetCacheByLayout.erase(it);
|
|
}
|
|
if (purgedSets > 0) {
|
|
// The per-draw reuse memo folds the layout handle into its signature; drop
|
|
// every entry so a recycled handle value cannot revive a purged set mid-frame.
|
|
for (auto& entry : m_descriptorReuseMemo) {
|
|
entry.valid = false;
|
|
}
|
|
// 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();
|
|
static thread_local Vector<Uint8> paddedUbo;
|
|
if (bufferObject == nullptr) {
|
|
// A block the program declares at a point with nothing bound - never bound, or its
|
|
// buffer was deleted while bound, which unbinds it (UnboundUniformBlockScenario,
|
|
// DeletedBoundBufferScenario). GL leaves what the shader READS undefined and raises no
|
|
// error; this used to dereference the null object. Answer it with a zeroed block of the
|
|
// reflected size, as the wire arm does (ResolveWireUniformBufferPayload).
|
|
const VkDeviceSize blockSize =
|
|
static_cast<VkDeviceSize>(program.GetUBOSizeAt(static_cast<Uint32>(blockIndex)));
|
|
paddedUbo.assign(static_cast<SizeT>(blockSize > 0 ? blockSize : 16), 0);
|
|
out.payload = paddedUbo.data();
|
|
out.payloadSize = static_cast<VkDeviceSize>(paddedUbo.size());
|
|
return true;
|
|
}
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
Uint32 UniformManager::NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
|
|
Uint32 initialMaxSets) {
|
|
const Uint32 minSets = std::max<Uint32>(1, initialMaxSets);
|
|
const Uint32 maxSets = std::max(minSets, kMaxDescriptorPoolSets);
|
|
Uint32 largest = 0;
|
|
for (const auto& pool : pools) {
|
|
if (pool.updateAfterBind == updateAfterBind) {
|
|
largest = std::max(largest, pool.maxSets);
|
|
}
|
|
}
|
|
if (largest == 0) {
|
|
return minSets;
|
|
}
|
|
const Uint64 doubled = static_cast<Uint64>(largest) * 2;
|
|
return static_cast<Uint32>(std::clamp<Uint64>(doubled, minSets, maxSets));
|
|
}
|
|
|
|
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
|
|
if (frame.descriptorPools.empty()) {
|
|
return false;
|
|
}
|
|
|
|
Uint32 grownMaxSets = NextDescriptorPoolMaxSets(frame.descriptorPools, updateAfterBind, m_setsPerFrame);
|
|
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
|
// A large pool is a large driver allocation; if the device refuses one, settle for less
|
|
// rather than failing the draw - halving down to the base size, where the old policy lived.
|
|
while (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
|
if (grownMaxSets <= m_setsPerFrame) {
|
|
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u sets)",
|
|
grownMaxSets);
|
|
return false;
|
|
}
|
|
grownMaxSets = std::max(m_setsPerFrame, grownMaxSets / 2);
|
|
}
|
|
|
|
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
|
|
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
|
frame.capacitySets += grownMaxSets;
|
|
// Growing is the opposite of quiet: a trim must wait a full quiet run from here.
|
|
frame.quietEpochs = 0;
|
|
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool exhausted, grew a %u-set pool (updateAfterBind=%d), "
|
|
"poolCount=%zu",
|
|
frameIndex, grownMaxSets, updateAfterBind ? 1 : 0, frame.descriptorPools.size());
|
|
return true;
|
|
}
|
|
|
|
VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) {
|
|
auto& frame = m_frames[frameIndex];
|
|
const Bool updateAfterBind = programObj.usesUpdateAfterBind;
|
|
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size() ||
|
|
frame.descriptorPools[frame.activeDescriptorPoolIndex].updateAfterBind != updateAfterBind ||
|
|
frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
|
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
|
const auto availableBucket = std::find_if(
|
|
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
|
[updateAfterBind](const DescriptorPoolBucket& candidate) {
|
|
return candidate.updateAfterBind == updateAfterBind && candidate.allocatedSets < candidate.maxSets;
|
|
});
|
|
if (availableBucket == frame.descriptorPools.end()) {
|
|
outDescriptorSet = VK_NULL_HANDLE;
|
|
return VK_ERROR_OUT_OF_POOL_MEMORY;
|
|
}
|
|
frame.activeDescriptorPoolIndex =
|
|
static_cast<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;
|
|
// Nor does an image unit naming a (level, layer) its texture lacks: the
|
|
// descriptor resolve binds that same placeholder (GL 4.6 core 8.26), and
|
|
// preparing the texture would decline a storage-less one and lose the pass.
|
|
if (storage && WireShaderImageNamesNoTexel(*m_textureManager, state.BoundShaderImages[unit]))
|
|
continue;
|
|
const auto* resource = m_textureManager->SyncTextureResourceByHandle(handle, false, storage);
|
|
if (resource == nullptr) return false;
|
|
// Another session writes it: the renderer acquires it before this use.
|
|
if (resource->sharedImageId != 0 || resource->yuvImageId != 0)
|
|
m_textureManager->NoteSharedImageUse(*resource);
|
|
}
|
|
}
|
|
}
|
|
m_textureManager->FlushPendingUploads();
|
|
return true;
|
|
}
|
|
#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
|