[Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): a sampler uniform array is one binding with many descriptors too - Magma wrote only element zero, and declines the multi-dimensional shape loudly

This commit is contained in:
2026-08-12 00:31:37 -04:00
parent a020de76e3
commit d2a36d65a3
4 changed files with 346 additions and 162 deletions
@@ -1837,9 +1837,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// A descriptor ARRAY occupies one binding with descriptorCount = N, and is // A descriptor ARRAY occupies one binding with descriptorCount = N, and is
// supported for exactly the kinds that have a per-element resolve path in // supported for exactly the kinds that have a per-element resolve path in
// UniformManager::BindProgramUniformBuffers: UBO instance arrays // UniformManager::BindProgramUniformBuffers: UBO instance arrays
// (uniform Block {...} b[N];), storage-block instance arrays, and image // (uniform Block {...} b[N];), storage-block instance arrays, image uniform
// uniform arrays. Anything else must fail program creation cleanly rather // arrays, and combined-image-sampler arrays (uniform sampler2D s[N];).
// than continue with corrupt state. // Anything else - a uniform TEXEL buffer array is the one remaining kind -
// must fail program creation cleanly rather than continue with corrupt state.
// //
// Getting listed here is not cosmetic: a kind that is rejected leaves // Getting listed here is not cosmetic: a kind that is rejected leaves
// GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and // GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and
@@ -1848,12 +1849,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unification and the set->0 normalisation this function exists to do. A // unification and the set->0 normalisation this function exists to do. A
// program with an image array plus any second descriptor got aliased // program with an image array plus any second descriptor got aliased
// bindings out of that, and a DEBUG build trapped on the same program. // bindings out of that, and a DEBUG build trapped on the same program.
// Which is also why the message below is MGLOG_I: MGLOG_E is compiled out
// of an INFO build, so a refusal that only said MGLOG_E said nothing at all
// in the builds that ship.
const Bool arraySupportedForKind = const Bool arraySupportedForKind =
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic || kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer || kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageImage; kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
if (binding->count != 1 && !arraySupportedForKind) { if (binding->count != 1 && !arraySupportedForKind) {
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor " MGLOG_I("ProgramFactory: descriptor arrays are unsupported for this descriptor "
"kind (name='%s' count=%u type=%d)", "kind (name='%s' count=%u type=%d)",
binding->name ? binding->name : "<null>", binding->count, binding->name ? binding->name : "<null>", binding->count,
static_cast<Int>(binding->descriptor_type)); static_cast<Int>(binding->descriptor_type));
@@ -2386,6 +2391,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
// How many descriptors to declare for an ARRAY of opaque uniforms (samplers, images) at
// one binding - or 0, meaning this binding cannot be described and must be declined.
//
// Two separate things have to hold, and neither is checkable from the SPIR-V alone:
//
// * the count has to fit a VkDescriptorSetLayoutBinding this device will accept, and fit
// the Uint16 it is stored in (65536 would narrow to 0) and the scratch the bind path
// reserves from it;
// * the frontend reflection has to have RESERVED that many consecutive uniform locations
// for this uniform, because the per-element resolve paths address element k as
// baseLocation + k. SPIRV-Reflect's `count` is the FLATTENED element count, while GL
// locations follow the OUTER dimension only (ProgramObject::GetUniformArraySizeByTIndex
// answers TType::getOuterArraySize()). For a one-dimensional array the two agree; for
// `uniform sampler2D g[2][3]` SPIR-V says 6 where the reflection reserved 2, and
// elements 2..5 would silently resolve onto whichever uniform got the next locations.
//
// Asking the reflection whether baseLocation and baseLocation + count - 1 are slots of the
// SAME uniform tests exactly that precondition, without this code having to model how
// glslang chooses to lay an array of arrays out - if a future glslang reserves all six, the
// check passes and the per-element paths are right for it too.
static Uint32 DescriptorCountForOpaqueUniformArray(const MG_State::GLState::ProgramObject& program,
const String& uniformName, Uint32 binding, Int baseLocation,
Uint32 reflectedCount, Uint32 maxBindings,
const char* kindLabel) {
const Uint32 count = std::max<Uint32>(1u, reflectedCount);
if (count == 1) {
return 1u;
}
if (count > maxBindings) {
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
"this device can describe - declining the program",
kindLabel, uniformName.c_str(), binding, count, maxBindings);
return 0u;
}
if (baseLocation < 0 ||
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
"neighbouring uniform",
kindLabel, uniformName.c_str(), binding, count, baseLocation);
return 0u;
}
return count;
}
void ProgramFactory::ReflectLayout(const MG_State::GLState::ProgramObject& program, void ProgramFactory::ReflectLayout(const MG_State::GLState::ProgramObject& program,
const Vector<Vector<Uint>>& spirv, VkProgramObject& entry) const { const Vector<Vector<Uint>>& spirv, VkProgramObject& entry) const {
// Initialize layout vectors // Initialize layout vectors
@@ -2608,6 +2659,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int location = program.GetUniformLocation(uniformName); const Int location = program.GetUniformLocation(uniformName);
if (location < 0) { if (location < 0) {
// A uniform with no location is ordinarily one GL never made active, and
// dropping it is routine. An ARRAY reaching here is not routine: it is the
// multi-dimensional case. `uniform sampler2D g[2][3]` arrives from
// SPIRV-Reflect as one binding of 6 descriptors named "g", while the frontend
// reflection keys an array of arrays by its full "[0]"-terminated spelling
// ("g[0][0]"), so no base location resolves and the per-element paths have
// nothing to count from. Declining is the honest answer - but it has to SAY
// so at a level that survives a release build, because dropping the binding
// leaves the shader reading a descriptor the layout never declared.
if (sampler->count > 1) {
MGLOG_I("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
"descriptor array with no frontend uniform location (a multi-dimensional array "
"of samplers or images is the known cause)",
uniformName.c_str(), binding, sampler->count);
}
entry.bindingKinds[binding] = DescriptorBindingKind::None; entry.bindingKinds[binding] = DescriptorBindingKind::None;
continue; continue;
} }
@@ -2624,15 +2690,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a storage BLOCK array, whose elements take consecutive GL binding points // a storage BLOCK array, whose elements take consecutive GL binding points
// from the declared one, each element of an image array carries its own // from the declared one, each element of an image array carries its own
// independently assigned image unit - see ResolveStorageImageDescriptor. // independently assigned image unit - see ResolveStorageImageDescriptor.
// Bounds-checked like the UBO array path above: descriptorCount goes // Bounds- and extent-checked like the UBO array path above; see
// straight into a VkDescriptorSetLayoutBinding, and the bind path reserves // DescriptorCountForOpaqueUniformArray for what "declined" costs and why
// scratch from it, so an absurd array size has to be refused here rather // the reflection's reserved extent - not SPIRV-Reflect's flattened count -
// than narrowed into a Uint16 (where 65536 would become 0). // is what the per-element resolve can actually address.
const Uint32 imageArrayCount = std::max<Uint32>(1u, sampler->count); const Uint32 imageArrayCount = DescriptorCountForOpaqueUniformArray(
if (imageArrayCount > m_maxBindings) { program, uniformName, binding, location, sampler->count, m_maxBindings, "image");
MGLOG_E("ProgramFactory::ReflectLayout: image array '%s' at binding %u has %u elements, " if (imageArrayCount == 0) {
"past the %u this device can describe",
uniformName.c_str(), binding, imageArrayCount, m_maxBindings);
entry.bindingKinds[binding] = DescriptorBindingKind::None; entry.bindingKinds[binding] = DescriptorBindingKind::None;
continue; continue;
} }
@@ -2668,6 +2732,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'", "ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
uniformName.c_str()); uniformName.c_str());
if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) { if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) {
// An ARRAY of sampler uniforms is ONE binding carrying `count` descriptors,
// exactly like the image array above, and for the same reason: GLSL 4.20
// gives `layout(binding = 1) uniform sampler2D goku[4]` one declaration
// spanning texture units 1..4, each element with its own glUniform1i-assigned
// unit. Leaving descriptorCount at 1 declared a single-descriptor binding
// while the shader indexed descriptors 1..3 of it, and the bind path wrote
// only element 0 - so elements 1..N read a descriptor nobody had written
// (KHR-GL42.shading_language_420pack.binding_sampler_array; lavapipe faults
// inside the JIT-ed shader rather than reporting).
const Uint32 samplerArrayCount = DescriptorCountForOpaqueUniformArray(
program, uniformName, binding, location, sampler->count, m_maxBindings, "sampler");
if (samplerArrayCount == 0) {
entry.bindingKinds[binding] = DescriptorBindingKind::None;
continue;
}
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(samplerArrayCount);
const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType); const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType);
MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown, MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain " "ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain "
@@ -68,6 +68,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
// 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 MG_State::GLState::ProgramObject& 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 MG_State::GLState::ProgramObject& program, Int location, Uint32 binding) { static Int ResolveSamplerUnitIndex(const MG_State::GLState::ProgramObject& program, Int location, Uint32 binding) {
MOBILEGL_ASSERT(location >= -1, "ResolveSamplerUnitIndex: invalid sampler location for binding %u", binding); MOBILEGL_ASSERT(location >= -1, "ResolveSamplerUnitIndex: invalid sampler location for binding %u", binding);
if (location < 0) { if (location < 0) {
@@ -268,15 +291,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, VkDescriptorImageInfo& outImageInfo, Uint32 binding, Uint32 element,
VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint) const { Bool trustUnchangedHint) const {
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null"); MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null");
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler 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 // 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 - // last full resolve (which also filled the cache), so the whole chain below -
// texture/sampler resolution, completeness probe, sync, layout handling, sampler // texture/sampler resolution, completeness probe, sync, layout handling, sampler
// and view lookups - would recompute the identical descriptor. // and view lookups - would recompute the identical descriptor.
if (trustUnchangedHint && binding < m_samplerResolveMemo.size() && if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
m_samplerResolveMemo[binding].infoValid) { m_samplerResolveMemo[binding].infoValid) {
outImageInfo = m_samplerResolveMemo[binding].info; outImageInfo = m_samplerResolveMemo[binding].info;
return true; return true;
@@ -286,9 +314,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Raw-pointer resolve to skip the SharedPtr atomic refcount churn: the bound texture stays // 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 // 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. // keep the fallback texture alive for the rest of this call.
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding); MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding, element);
const Int location = programObj.samplerUniformLocationByBinding[binding]; // Per ELEMENT: 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.
const Int location =
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
if (location < 0 && element > 0) {
MGLOG_I("ResolveSamplerDescriptor: binding %u element %u is past the end of its sampler array", binding,
element);
return false;
}
const Int unit = ResolveSamplerUnitIndex(program, location, binding); const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const auto& samplerOverride = textureUnit.GetSamplerObject(); const auto& samplerOverride = textureUnit.GetSamplerObject();
@@ -459,7 +497,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (outImageInfo.sampler == VK_NULL_HANDLE) { if (outImageInfo.sampler == VK_NULL_HANDLE) {
return false; return false;
} }
if (binding < m_samplerResolveMemo.size()) { // 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 (descriptorMemoUsable && binding < m_samplerResolveMemo.size()) {
m_samplerResolveMemo[binding].info = outImageInfo; m_samplerResolveMemo[binding].info = outImageInfo;
m_samplerResolveMemo[binding].infoValid = true; m_samplerResolveMemo[binding].infoValid = true;
NoteSamplerResolveMemoTouched(binding); NoteSamplerResolveMemoTouched(binding);
@@ -505,37 +546,45 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue; continue;
} }
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding); // The rewrite this gates is program-wide, so EVERY sampler the program can read has
if (texture == nullptr) return false; // to qualify - including every element of a sampler array, each of which reaches a
const auto& levelRange = texture->GetLevelRange(); // different texture through its own unit.
if (levelRange.x() != levelRange.y()) return false; const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
for (Uint32 element = 0; element < descriptorCount; ++element) {
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 // 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 // filtering - which a single-level view can still have. Resolve the sampler exactly
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply. // the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
const Int location = programObj.samplerUniformLocationByBinding[binding]; const Int location = ResolveDescriptorElementLocation(
const Int unit = ResolveSamplerUnitIndex(program, location, binding); program, programObj.samplerUniformLocationByBinding[binding], element);
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject(); if (location < 0 && element > 0) return false;
const auto* effectiveSampler = const Int unit = ResolveSamplerUnitIndex(program, location, binding);
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get(); const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
if (effectiveSampler == nullptr) return false; const auto* effectiveSampler =
if (effectiveSampler->GetMaxAnisotropy() > 1.0f && samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear && if (effectiveSampler == nullptr) return false;
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) { if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
return false; 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 // 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 // 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 // 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. // filters are the same and the choice cannot be observed.
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
? 0.0f ? 0.0f
: effectiveSampler->GetMaxLod(); : effectiveSampler->GetMaxLod();
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) { if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
return false; return false;
}
sawSampler = true;
} }
sawSampler = true;
} }
return sawSampler; return sawSampler;
} }
@@ -568,14 +617,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw( MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj,
Uint32 binding) { Uint32 binding, Uint32 element) {
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTextureRaw: GL context is null"); MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTextureRaw: GL context is null");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveSamplerTextureRaw: sampler location binding %u out of range", binding); "ResolveSamplerTextureRaw: sampler location binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(), MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
"ResolveSamplerTextureRaw: sampler target binding %u out of range", binding); "ResolveSamplerTextureRaw: sampler target binding %u out of range", binding);
const Int location = programObj.samplerUniformLocationByBinding[binding]; const Int location =
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
const Int unit = ResolveSamplerUnitIndex(program, location, binding); const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
@@ -860,7 +910,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture, MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const { const MG_State::GLState::SamplerObject*& outSampler) const {
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the // Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
@@ -871,7 +921,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ResolveSampledBinding: sampler location binding %u out of range", binding); "ResolveSampledBinding: sampler location binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(), MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
"ResolveSampledBinding: sampler target binding %u out of range", binding); "ResolveSampledBinding: sampler target binding %u out of range", binding);
const Int location = programObj.samplerUniformLocationByBinding[binding]; const Int location =
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
if (location < 0 && element > 0) {
return false;
}
const Int unit = ResolveSamplerUnitIndex(program, location, binding); const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
@@ -915,19 +969,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
MG_State::GLState::ITextureObject* texture = nullptr; // Every ELEMENT of a sampler array reaches its own texture through its own unit,
const MG_State::GLState::SamplerObject* sampler = nullptr; // so every element has to be in the sampled set: this walk is what gets those
if (!ResolveSampledBinding(program, programObj, binding, texture, sampler)) { // textures synced and transitioned to a sampled layout BEFORE the render pass
continue; // opens, and a missed element would first be touched by the descriptor resolve
} // inside an active pass.
if (outBindingRecords != nullptr) { const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
outBindingRecords->push_back({texture != nullptr ? texture->GetLifetimeId() : 0, for (Uint32 element = 0; element < descriptorCount; ++element) {
sampler != nullptr ? sampler->GetLifetimeId() : 0}); 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); auto found = std::find(outTextures.begin(), outTextures.end(), texture);
if (found == outTextures.end()) { if (found == outTextures.end()) {
outTextures.push_back(texture); outTextures.push_back(texture);
}
} }
} }
return true; return true;
@@ -948,18 +1010,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue; continue;
} }
MG_State::GLState::ITextureObject* texture = nullptr; // Element-for-element, in the same order CollectSampledTextures recorded them -
const MG_State::GLState::SamplerObject* sampler = nullptr; // the two walks have to visit the identical descriptor sequence or the positional
if (!ResolveSampledBinding(program, programObj, binding, texture, sampler)) { // comparison below drifts.
continue; const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
} for (Uint32 element = 0; element < descriptorCount; ++element) {
if (recordIndex >= previousRecords.size()) { MG_State::GLState::ITextureObject* texture = nullptr;
return false; const MG_State::GLState::SamplerObject* sampler = nullptr;
} if (!ResolveSampledBinding(program, programObj, binding, element, texture, sampler)) {
const SampledBindingRecord& record = previousRecords[recordIndex++]; continue;
if (record.textureLifetimeId != (texture != nullptr ? texture->GetLifetimeId() : 0) || }
record.samplerLifetimeId != (sampler != nullptr ? sampler->GetLifetimeId() : 0)) { if (recordIndex >= previousRecords.size()) {
return false; 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(); return recordIndex == previousRecords.size();
@@ -984,26 +1052,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
const Int location = programObj.samplerUniformLocationByBinding[binding]; const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) { if (baseLocation < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding); MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
return false; return false;
} }
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location)); // Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { // binding whose elements each carry their own image unit, so each reaches its own
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u", // texture. This walk is what puts those textures into the pre-pass sync and layout
imageUnit, binding); // transition; collecting only element 0 left elements 1..N to be first touched by
return false; // 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("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("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
imageUnit, binding, element);
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get(); auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) { if (texture == nullptr) {
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u", MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
imageUnit, binding); imageUnit, binding, element);
return false; return false;
} }
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) { if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
outTextures.push_back(texture); outTextures.push_back(texture);
}
} }
} }
return true; return true;
@@ -1437,19 +1519,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) { for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u; uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
} }
Uint32 ssboArrayExtra = 0; // 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) { for (const Uint16 count : programObj.bindingDescriptorCounts) {
if (count > 1) ssboArrayExtra += static_cast<Uint32>(count) - 1u; if (count > 1) arrayDescriptorExtra += static_cast<Uint32>(count) - 1u;
} }
writes.reserve(m_maxBindings); writes.reserve(m_maxBindings);
bufferInfos.reserve(m_maxBindings + uboArrayExtra + ssboArrayExtra); bufferInfos.reserve(m_maxBindings + uboArrayExtra + arrayDescriptorExtra);
// ssboArrayExtra sums EVERY arrayed binding's surplus, image arrays included, so it is // Every binding pushes at most descriptorCount image infos, so bindings + surplus is the
// the right worst case for this container too now that a storage-image binding pushes // worst case. Reserving only m_maxBindings here was exact while every binding pushed
// one info per element. Reserving only m_maxBindings here was exact while every binding // exactly one - and reallocates under an image or sampler array, dangling every
// pushed exactly one - and would have let the vector reallocate under an image array, // pImageInfo already recorded in `writes` before vkUpdateDescriptorSets reads them. That
// dangling every pImageInfo already recorded in `writes` (including the sampler // is reachable wherever m_maxBindings is small (it clamps to ~16 on Adreno and Mali),
// branch's &imageInfos.back()) before vkUpdateDescriptorSets reads them. // which is exactly where a 7-element CTS sampler array does not fit the slack.
imageInfos.reserve(m_maxBindings + ssboArrayExtra); imageInfos.reserve(m_maxBindings + arrayDescriptorExtra);
texelBufferViews.reserve(m_maxBindings); texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra); dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
@@ -1477,10 +1562,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.descriptorCount = 1; write.descriptorCount = 1;
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
const Uint32 descriptorCount = const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
binding < programObj.bindingDescriptorCounts.size()
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
: 1u;
dynamicUboDescriptorCount += descriptorCount; dynamicUboDescriptorCount += descriptorCount;
fastRebindUboBinding = binding; fastRebindUboBinding = binding;
const SizeT firstBufferInfoIndex = bufferInfos.size(); const SizeT firstBufferInfoIndex = bufferInfos.size();
@@ -1523,10 +1605,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// One write per binding, but `descriptorCount` buffer infos: a GLSL block // One write per binding, but `descriptorCount` buffer infos: a GLSL block
// instance array occupies a single binding whose elements each come from their // instance array occupies a single binding whose elements each come from their
// own GL binding point. // own GL binding point.
const Uint32 descriptorCount = const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
binding < programObj.bindingDescriptorCounts.size()
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
: 1u;
const SizeT firstBufferInfoIndex = bufferInfos.size(); const SizeT firstBufferInfoIndex = bufferInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) { for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorBufferInfo bufferInfo{}; VkDescriptorBufferInfo bufferInfo{};
@@ -1552,10 +1631,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// never written at all, and a shader that indexes them reads an undefined // 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 // descriptor (lavapipe faults inside the shader; a real driver is free to do
// anything). // anything).
const Uint32 descriptorCount = const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
binding < programObj.bindingDescriptorCounts.size()
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
: 1u;
const SizeT firstImageInfoIndex = imageInfos.size(); const SizeT firstImageInfoIndex = imageInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) { for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorImageInfo imageInfo{}; VkDescriptorImageInfo imageInfo{};
@@ -1575,32 +1651,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.pImageInfo = &imageInfos[firstImageInfoIndex]; write.pImageInfo = &imageInfos[firstImageInfoIndex];
writes.push_back(write); writes.push_back(write);
} else { } else {
VkDescriptorImageInfo imageInfo{}; // One write per binding, but `descriptorCount` image infos: a sampler ARRAY is a
Bool hasImage = false; // single binding whose elements each carry their own texture unit. Writing only
if (samplerBindingOverride != nullptr && // element 0 - which is all this used to do - left elements 1..N never written,
samplerBindingOverride->binding == binding && // so a shader indexing them sampled a descriptor nobody had filled in
samplerBindingOverride->texture != nullptr && // (KHR-GL42.shading_language_420pack.binding_sampler_array).
samplerBindingOverride->sampler != nullptr) { const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo); // Overrides come only from MobileGL's own blit and depth-mipmap programs, whose
} else { // samplers are scalars; the override replaces THE descriptor at its binding, so
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, imageInfo, // there is no element for it to mean on an arrayed one.
samplerDescriptorsUnchangedHint); 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{};
Bool hasImage = false;
if (overrideThisBinding && element == 0) {
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
} else {
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element,
imageInfo, samplerDescriptorsUnchangedHint);
}
if (!hasImage) {
MGLOG_E(
"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(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
"has null sampler or imageView",
binding, element);
return false;
}
imageInfos.push_back(imageInfo);
} }
if (!hasImage) { if (descriptorCount > 1) {
MGLOG_E( // The dynamic-offset-only rebind replays a whole descriptor set on the
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u has no valid texture descriptor", // strength of the sampler hint alone, and its eligibility probe was written
binding); // for bindings that carry one descriptor each. An arrayed sampler binding
return false; // also bypasses the per-binding descriptor memo, so there is nothing for it
// to win here either.
fastRebindKindsEligible = false;
} }
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
MGLOG_E(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u has null sampler or imageView",
binding);
return false;
}
imageInfos.push_back(imageInfo);
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imageInfos.back(); write.descriptorCount = descriptorCount;
write.pImageInfo = &imageInfos[firstImageInfoIndex];
writes.push_back(write); writes.push_back(write);
} }
} }
@@ -53,10 +53,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// caches - a live layout's entry must never be purged (its sets would be // caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here. // unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout); void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// One record per visited CombinedImageSampler binding (post fallback substitution, // One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
// in binding order): the resolved texture and effective sampler, as never-reused // in binding order, and within a binding in array-element order): the resolved texture
// lifetime ids so a freed-and-reallocated object at the same heap address can only // and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo). // at the same heap address can only MISS a comparison, never false-hit it (same ABA
// rule as SamplerResolveMemo). An arrayed binding contributes one record per element -
// element granularity is required, or swapping the textures of two elements of the same
// array would leave the record list identical and the fast path would keep a stale set.
struct SampledBindingRecord { struct SampledBindingRecord {
Uint64 textureLifetimeId = 0; Uint64 textureLifetimeId = 0;
Uint64 samplerLifetimeId = 0; Uint64 samplerLifetimeId = 0;
@@ -143,8 +146,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// texture after the fallback substitution (may still be null when no fallback // texture after the fallback substitution (may still be null when no fallback
// exists), effective sampler = unit override else the texture's own sampler. // exists), effective sampler = unit override else the texture's own sampler.
// False = the binding is skipped (unbound with a non-2D fallback target). // False = the binding is skipped (unbound with a non-2D fallback target).
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program, Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture, MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const; const MG_State::GLState::SamplerObject*& outSampler) const;
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures): // Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
@@ -152,14 +156,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only need the pointer skip the SharedPtr copy's atomic refcount churn. // only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw( static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding); const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const; SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
// independently assigned GL texture unit, so it selects the texture, the sampler
// override and the fallback separately from its neighbours.
//
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright // trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof // (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// obligations the caller carries). // obligations the caller carries). The cache is keyed by binding alone, so it is
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo, Uint32 element, VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const; Bool trustUnchangedHint = false) const;
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride, Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const; VkDescriptorImageInfo& outImageInfo) const;
@@ -346,6 +355,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// proves every resolve input unchanged; cleared with the per-frame reset // proves every resolve input unchanged; cleared with the per-frame reset
// (the cached VkSampler outlives a frame only via a fresh resolve, which // (the cached VkSampler outlives a frame only via a fresh resolve, which
// also re-stamps it against VkSamplerManager's frame-boundary sweep). // also re-stamps it against VkSamplerManager's frame-boundary sweep).
//
// This one field is keyed by binding but describes ONE descriptor, so it is
// written and read only for single-descriptor bindings. A sampler ARRAY's
// elements share the binding and would overwrite each other here - the last
// element resolved would then be handed to element 0 on the next hinted draw.
// Every other field above is self-validating (each compares its full key
// before reuse, and the view-format entry is a pure function of format and
// numeric domain), so an arrayed binding may keep using those.
VkDescriptorImageInfo info{}; VkDescriptorImageInfo info{};
Bool infoValid = false; Bool infoValid = false;
}; };
@@ -233,19 +233,6 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
return image.At(gl.Width() / 2, gl.Height() / 2); return image.At(gl.Width() / 2, gl.Height() / 2);
} }
// Magma turns a sampler array into ONE descriptor with descriptorCount = N, and
// ProgramFactory::ReflectLayout refuses any descriptor array that is not a
// dynamic UBO (MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp - "descriptor
// arrays are unsupported for this descriptor kind"), so program creation fails
// and the draw samples descriptors that were never written. On a hardware driver
// that reads back as wrong pixels; under lavapipe it is a segfault in the
// rasterizer thread. Supporting it means carrying an element dimension through
// UniformManager's per-binding location tables, which is a feature, not a fix -
// so this case is SCOPED rather than disabled, because the frontend half it also
// covers (the seeded units) is real on both backends and is asserted below
// before the draw.
bool SamplerArrayDescriptorsAreSupported() const { return Gl().BackendName() != "DirectVulkan"; }
// Same shape, different gap: with the compile fixed, this shader now links on // Same shape, different gap: with the compile fixed, this shader now links on
// both backends but paints nothing on Magma - the atomic counter becomes a // both backends but paints nothing on Magma - the atomic counter becomes a
// buffer descriptor there and that half is not wired up yet (the conformance // buffer descriptor there and that half is not wired up yet (the conformance
@@ -300,11 +287,6 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
} }
glUseProgram(0); glUseProgram(0);
if (!SamplerArrayDescriptorsAreSupported()) {
GTEST_SKIP() << "sampler descriptor arrays are unimplemented on " << Gl().BackendName()
<< "; the seeded units above are the half of this case it can answer";
}
const Rgba8 centre = DrawAndRead(program); const Rgba8 centre = DrawAndRead(program);
EXPECT_EQ(FirstGLError(), 0u); EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(centre.r, 0) << "sampler array elements that read the wrong texture: " << BadElements(centre.r); EXPECT_EQ(centre.r, 0) << "sampler array elements that read the wrong texture: " << BadElements(centre.r);