mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
11
Commits
4446c861be
...
12df061e0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12df061e0b | ||
|
|
d83a48da5c | ||
|
|
b3100b0de5 | ||
|
|
1cde801a01 | ||
|
|
52c050131e | ||
|
|
ebb8a4cebf | ||
|
|
5398fb4289 | ||
|
|
7f2ca68615 | ||
|
|
b12ef4d717 | ||
|
|
724755d9df | ||
|
|
59f7059bf4 |
@@ -301,6 +301,12 @@ namespace MobileGL {
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
|
||||
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
|
||||
// 64 for storage buffers. Answering the storage query with the uniform value let an
|
||||
// application bind a storage range at an offset the driver cannot address, which it
|
||||
// accepted without error and then wrote somewhere else entirely.
|
||||
SizeT ShaderStorageBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
|
||||
@@ -1323,6 +1323,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
|
||||
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
@@ -5141,6 +5142,164 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrainBlitErrors();
|
||||
}
|
||||
|
||||
// ---- glBlitFramebuffer onto a non-zero array layer -------------------------------------
|
||||
//
|
||||
// Some drivers write to layer 0 whatever layer the DRAW framebuffer's
|
||||
// glFramebufferTextureLayer attachment names, and raise no error doing it (Adreno 830;
|
||||
// SelfTest::ProbeBlitIgnoresDestinationArrayLayer measures it, with the destination-layer-0
|
||||
// case as the control). glCopyImageSubData takes the destination layer as an argument rather
|
||||
// than reading it off an attachment, and honours it on the same driver - so a blit that is a
|
||||
// plain 1:1 copy is issued that way instead.
|
||||
//
|
||||
// ONLY a plain 1:1 copy. glCopyImageSubData cannot scale, flip, convert format or resolve
|
||||
// samples, and it is not clipped by the scissor, so every one of those is a reason to hand
|
||||
// the call back to the driver rather than quietly perform a different operation. Those blits
|
||||
// still land on the wrong layer; a once-per-process line says so rather than leaving it to be
|
||||
// rediscovered.
|
||||
//
|
||||
// Per aspect, not all-or-nothing: the returned mask is the bits this performed itself, and
|
||||
// the caller passes the rest to the driver. A COLOR|DEPTH blit whose colour half scales and
|
||||
// whose depth half does not still gets its depth half repaired.
|
||||
static GLbitfield BlitLayeredDestinationAspects(
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer, GLint srcX0, GLint srcY0,
|
||||
GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) {
|
||||
if (mask == 0 || !readFramebuffer || !drawFramebuffer) return 0;
|
||||
if (!g_GLESFuncs.glCopyImageSubData) return 0;
|
||||
if (!MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer(g_GLESFuncs)) return 0;
|
||||
|
||||
// The default framebuffer has no layers to get wrong, and a blit between the two halves
|
||||
// of the same framebuffer object is not a shape this substitutes for.
|
||||
const Int width = srcX1 - srcX0;
|
||||
const Int height = srcY1 - srcY0;
|
||||
const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height;
|
||||
// The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two
|
||||
// different operations no matter how the rectangles line up.
|
||||
const Bool scissorEnabled =
|
||||
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
|
||||
using MobileGL::FramebufferAttachmentType;
|
||||
struct AspectPlan {
|
||||
GLbitfield bit;
|
||||
FramebufferAttachmentType source;
|
||||
FramebufferAttachmentType destination;
|
||||
};
|
||||
// The colour aspect follows glReadBuffer on the read side and draw buffer 0 on the write
|
||||
// side, which is the only draw buffer a blit onto a layered destination can be pinned to
|
||||
// here: a blit writes EVERY enabled draw buffer, so a framebuffer with more than one is
|
||||
// left to the driver rather than half-repaired.
|
||||
const auto& drawBuffers = drawFramebuffer->GetDrawBuffers();
|
||||
// GL_NONE is what an unwritten draw-buffer slot holds, and it is a different value from
|
||||
// the "no such attachment" one - counting it as enabled made every framebuffer look like
|
||||
// it had eight and sent every colour blit to the driver.
|
||||
//
|
||||
// The buffer is found rather than assumed to be slot 0: a blit writes every ENABLED draw
|
||||
// buffer, and glDrawBuffers(GL_NONE, GL_NONE, GL_NONE, GL_COLOR_ATTACHMENT0) leaves slot
|
||||
// 0 empty while still naming exactly one destination.
|
||||
Int enabledDrawBuffers = 0;
|
||||
FramebufferAttachmentType colorDestination = FramebufferAttachmentType::None;
|
||||
for (const FramebufferAttachmentType buffer : drawBuffers) {
|
||||
if (buffer != FramebufferAttachmentType::Unknown && buffer != FramebufferAttachmentType::None) {
|
||||
++enabledDrawBuffers;
|
||||
if (enabledDrawBuffers == 1) colorDestination = buffer;
|
||||
}
|
||||
}
|
||||
const AspectPlan plans[] = {
|
||||
{GL_COLOR_BUFFER_BIT, readFramebuffer->GetReadBuffer(), colorDestination},
|
||||
{GL_DEPTH_BUFFER_BIT, FramebufferAttachmentType::Depth, FramebufferAttachmentType::Depth},
|
||||
{GL_STENCIL_BUFFER_BIT, FramebufferAttachmentType::Stencil, FramebufferAttachmentType::Stencil},
|
||||
};
|
||||
|
||||
GLbitfield handled = 0;
|
||||
for (const AspectPlan& plan : plans) {
|
||||
if ((mask & plan.bit) == 0) continue;
|
||||
if (plan.source == FramebufferAttachmentType::Unknown ||
|
||||
plan.destination == FramebufferAttachmentType::Unknown ||
|
||||
plan.source == FramebufferAttachmentType::None ||
|
||||
plan.destination == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
const auto& sourceAttachment = readFramebuffer->GetAttachment(plan.source);
|
||||
const auto& destinationAttachment = drawFramebuffer->GetAttachment(plan.destination);
|
||||
// Renderbuffers have no layers, so a destination that is one cannot be hitting this.
|
||||
if (!sourceAttachment.IsTexture() || !destinationAttachment.IsTexture()) continue;
|
||||
// Layer 0 is the case the driver gets right, and a LAYERED attachment (glFramebufferTexture
|
||||
// with no layer) blits its layer 0 by spec - neither is this defect.
|
||||
if (destinationAttachment.GetTextureLayer() == 0) continue;
|
||||
if (destinationAttachment.IsLayered() || sourceAttachment.IsLayered()) continue;
|
||||
|
||||
const auto& sourceTexture = sourceAttachment.GetTexture();
|
||||
const auto& destinationTexture = destinationAttachment.GetTexture();
|
||||
if (!sourceTexture || !destinationTexture) continue;
|
||||
// glCopyImageSubData moves texel blocks: same format both ends, or it is a different
|
||||
// operation. Multisample endpoints would additionally have to agree on sample count,
|
||||
// which is a resolve the driver still owns.
|
||||
if (sourceTexture->GetFormat() != destinationTexture->GetFormat()) continue;
|
||||
if (sourceTexture->GetSamples() > 0 || destinationTexture->GetSamples() > 0) continue;
|
||||
// Copying an image region onto itself is undefined for glCopyImageSubData, and a blit
|
||||
// whose source and destination overlap is undefined for GL too - so this is not a
|
||||
// shape to substitute FOR, it is one to leave exactly as the application wrote it.
|
||||
if (sourceTexture == destinationTexture &&
|
||||
sourceAttachment.GetTextureLevel() == destinationAttachment.GetTextureLevel() &&
|
||||
sourceAttachment.GetTextureLayer() == destinationAttachment.GetTextureLayer()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A combined depth-stencil texture is ONE image to glCopyImageSubData: it carries both
|
||||
// aspects across whether or not the mask asked for both. Taking only GL_DEPTH_BUFFER_BIT
|
||||
// on a DEPTH24_STENCIL8 destination would overwrite a stencil the application asked to
|
||||
// keep, so the copy is only allowed when the mask covers everything the format holds.
|
||||
const TextureInternalFormat format = destinationTexture->GetFormat();
|
||||
const Bool hasDepth = MG_Util::IsDepthFormatInternalFormat(format);
|
||||
const Bool hasStencil = MG_Util::IsStencilFormatInternalFormat(format);
|
||||
if (hasDepth && (mask & GL_DEPTH_BUFFER_BIT) == 0) continue;
|
||||
if (hasStencil && (mask & GL_STENCIL_BUFFER_BIT) == 0) continue;
|
||||
// ... and having carried both, it must be credited with both, or the caller hands the
|
||||
// stencil half to the driver and it lands on layer 0 after all.
|
||||
const GLbitfield aspectBits =
|
||||
hasDepth || hasStencil
|
||||
? static_cast<GLbitfield>((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) |
|
||||
(hasStencil ? GL_STENCIL_BUFFER_BIT : 0))
|
||||
: static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
||||
if ((handled & aspectBits) == aspectBits) continue;
|
||||
|
||||
if (!oneToOne || scissorEnabled || (plan.bit == GL_COLOR_BUFFER_BIT && enabledDrawBuffers != 1)) {
|
||||
MGLOG_E_ONCE("BlitFramebuffer: this driver ignores a non-zero destination array layer and this "
|
||||
"blit cannot be expressed as a copy (%s), so it will land on layer 0",
|
||||
!oneToOne ? "it scales or flips"
|
||||
: scissorEnabled ? "the scissor test is enabled"
|
||||
: "the destination has more than one draw buffer");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto backendSource = TextureImpl::SyncTextureObjectToBackend(sourceTexture);
|
||||
auto backendDestination = TextureImpl::SyncTextureObjectToBackend(destinationTexture);
|
||||
if (!backendSource || !backendDestination) continue;
|
||||
const GLuint sourceName = backendSource->GetBackendTextureId();
|
||||
const GLuint destinationName = backendDestination->GetBackendTextureId();
|
||||
if (sourceName == 0 || destinationName == 0) continue;
|
||||
const GLenum sourceTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(sourceTexture->GetTarget());
|
||||
const GLenum destinationTarget =
|
||||
TextureImpl::ConvertTextureTargetToBackendGLEnum(destinationTexture->GetTarget());
|
||||
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glCopyImageSubData(sourceName, sourceTarget, sourceAttachment.GetTextureLevel(), srcX0, srcY0,
|
||||
sourceAttachment.GetTextureLayer(), destinationName, destinationTarget,
|
||||
destinationAttachment.GetTextureLevel(), dstX0, dstY0,
|
||||
destinationAttachment.GetTextureLayer(), width, height, 1);
|
||||
if (const GLenum error = g_GLESFuncs.glGetError(); error != GL_NO_ERROR) {
|
||||
// The driver blit still runs for this aspect - onto the wrong layer, but the
|
||||
// substitute has to leave the call no worse off than it found it.
|
||||
MGLOG_E_ONCE("BlitFramebuffer: the layered-destination copy substitute failed with %s; the "
|
||||
"driver blit will run instead and land on layer 0",
|
||||
MG_Util::ConvertGLEnumToString(error).c_str());
|
||||
continue;
|
||||
}
|
||||
handled |= aspectBits;
|
||||
}
|
||||
return handled & mask;
|
||||
}
|
||||
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
@@ -5167,7 +5326,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
});
|
||||
MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0,
|
||||
dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
|
||||
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
// A no-op on every driver that honours a non-zero destination array layer, which is all
|
||||
// of them but the probed one. Whatever it performs itself is taken out of the mask.
|
||||
mask &= ~BlitLayeredDestinationAspects(
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
|
||||
srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask);
|
||||
if (mask != 0) {
|
||||
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
}
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
||||
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
@@ -5189,7 +5356,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1,
|
||||
dstX0, dstY0, dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
|
||||
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
// See the DSA-free entry point above: only the probed defect makes this do anything.
|
||||
mask &= ~BlitLayeredDestinationAspects(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0,
|
||||
dstY0, dstX1, dstY1, mask);
|
||||
if (mask != 0) {
|
||||
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
}
|
||||
// Debug-only diagnostics: which GLES depth texture did this blit write?
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
if (mask & GL_DEPTH_BUFFER_BIT) {
|
||||
|
||||
@@ -6341,8 +6341,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// neither std430 nor std140") and the stage never reaches the driver. Collapse the
|
||||
// block into one uint array at offset 0 and re-index each counter to the element
|
||||
// that used to be at its byte offset; the buffer then stays bound whole, which it
|
||||
// has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 32 on this device, so an
|
||||
// 8-byte bind offset is not expressible). BEFORE SetAtomicCounterBlockBindings
|
||||
// has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 64 on Adreno 830 and 32 or
|
||||
// more everywhere else, so an 8-byte bind offset is not expressible on any of them).
|
||||
// BEFORE SetAtomicCounterBlockBindings
|
||||
// below, which only moves the block's BINDING and needs the block intact.
|
||||
//
|
||||
// NO KEY MATERIAL either, for the same reason - and note where the application's
|
||||
|
||||
@@ -855,6 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
|
||||
@@ -2186,6 +2186,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
SamplerNumericDomain ProgramFactory::UniformTypeToImageNumericDomain(GLenum glType) {
|
||||
switch (glType) {
|
||||
case GL_INT_IMAGE_1D:
|
||||
case GL_INT_IMAGE_2D:
|
||||
case GL_INT_IMAGE_3D:
|
||||
case GL_INT_IMAGE_2D_RECT:
|
||||
case GL_INT_IMAGE_CUBE:
|
||||
case GL_INT_IMAGE_BUFFER:
|
||||
case GL_INT_IMAGE_1D_ARRAY:
|
||||
case GL_INT_IMAGE_2D_ARRAY:
|
||||
case GL_INT_IMAGE_CUBE_MAP_ARRAY:
|
||||
case GL_INT_IMAGE_2D_MULTISAMPLE:
|
||||
case GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY:
|
||||
return SamplerNumericDomain::SignedInteger;
|
||||
case GL_UNSIGNED_INT_IMAGE_1D:
|
||||
case GL_UNSIGNED_INT_IMAGE_2D:
|
||||
case GL_UNSIGNED_INT_IMAGE_3D:
|
||||
case GL_UNSIGNED_INT_IMAGE_2D_RECT:
|
||||
case GL_UNSIGNED_INT_IMAGE_CUBE:
|
||||
case GL_UNSIGNED_INT_IMAGE_BUFFER:
|
||||
case GL_UNSIGNED_INT_IMAGE_1D_ARRAY:
|
||||
case GL_UNSIGNED_INT_IMAGE_2D_ARRAY:
|
||||
case GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY:
|
||||
case GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE:
|
||||
case GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY:
|
||||
return SamplerNumericDomain::UnsignedInteger;
|
||||
case GL_IMAGE_1D:
|
||||
case GL_IMAGE_2D:
|
||||
case GL_IMAGE_3D:
|
||||
case GL_IMAGE_2D_RECT:
|
||||
case GL_IMAGE_CUBE:
|
||||
case GL_IMAGE_BUFFER:
|
||||
case GL_IMAGE_1D_ARRAY:
|
||||
case GL_IMAGE_2D_ARRAY:
|
||||
case GL_IMAGE_CUBE_MAP_ARRAY:
|
||||
case GL_IMAGE_2D_MULTISAMPLE:
|
||||
case GL_IMAGE_2D_MULTISAMPLE_ARRAY:
|
||||
return SamplerNumericDomain::Float;
|
||||
default:
|
||||
return SamplerNumericDomain::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||
CompileOptionFlags flags) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
@@ -2956,6 +2999,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<Int>(numericDomain));
|
||||
entry.samplerNumericDomainByBinding[binding] = numericDomain;
|
||||
}
|
||||
// Every other opaque kind records its domain too. Only the combined-image-sampler
|
||||
// path above needs it to pick a sampled view format; the three below need it to
|
||||
// describe the descriptor a binding gets when its unit is UNBOUND, which is legal
|
||||
// GL and must not lose the draw (see UniformManager's Resolve*Descriptor). Left
|
||||
// Unknown, those placeholders would have no way to tell a `samplerBuffer` from a
|
||||
// `usamplerBuffer` - and a texel buffer view whose numeric type disagrees with the
|
||||
// shader's is invalid Vulkan, not merely wrong data.
|
||||
if (descriptorKind == DescriptorBindingKind::UniformTexelBuffer ||
|
||||
descriptorKind == DescriptorBindingKind::StorageTexelBuffer ||
|
||||
descriptorKind == DescriptorBindingKind::StorageImage) {
|
||||
const SamplerNumericDomain opaqueDomain =
|
||||
descriptorKind == DescriptorBindingKind::UniformTexelBuffer
|
||||
? UniformTypeToSamplerNumericDomain(uniformType)
|
||||
: UniformTypeToImageNumericDomain(uniformType);
|
||||
MOBILEGL_ASSERT(opaqueDomain != SamplerNumericDomain::Unknown,
|
||||
"ProgramFactory::ReflectLayout: failed to resolve numeric domain for '%s' "
|
||||
"(uniformType=0x%x)",
|
||||
uniformName.c_str(), uniformType);
|
||||
MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] ==
|
||||
SamplerNumericDomain::Unknown ||
|
||||
entry.samplerNumericDomainByBinding[binding] == opaqueDomain,
|
||||
"ProgramFactory::ReflectLayout: binding %u ('%s') has conflicting numeric "
|
||||
"domains (%d vs %d)",
|
||||
binding, uniformName.c_str(),
|
||||
static_cast<Int>(entry.samplerNumericDomainByBinding[binding]),
|
||||
static_cast<Int>(opaqueDomain));
|
||||
entry.samplerNumericDomainByBinding[binding] = opaqueDomain;
|
||||
}
|
||||
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
|
||||
entry.samplerUniformLocationByBinding[binding] == location,
|
||||
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
|
||||
|
||||
@@ -448,6 +448,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
|
||||
// sampler form above deliberately does not answer. Kept separate rather than folded in
|
||||
// because the two are asked in different places for different reasons: a sampler's domain
|
||||
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
|
||||
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
|
||||
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
|
||||
// format at all, and the numeric domain is then the only thing that constrains it.
|
||||
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
|
||||
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||
|
||||
@@ -11,14 +11,19 @@
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#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/TextureMetrics.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <Config.h>
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -28,6 +33,136 @@
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Bool FindFramebufferAttachmentForTexture(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
@@ -115,7 +250,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
|
||||
}
|
||||
|
||||
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
Bool UniformManager::Initialize(VkDevice device, VkPhysicalDevice physicalDevice,
|
||||
VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings, Uint32 setsPerFrame,
|
||||
@@ -123,6 +259,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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");
|
||||
@@ -135,6 +273,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"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);
|
||||
@@ -173,6 +312,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
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) {
|
||||
for (auto& view : frame.texelBufferViews) {
|
||||
@@ -199,6 +349,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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;
|
||||
@@ -693,11 +844,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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) {
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
// 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 ||
|
||||
@@ -712,9 +881,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
// 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{};
|
||||
@@ -804,12 +985,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
const auto& texture = imageBinding.Texture;
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
||||
binding);
|
||||
return false;
|
||||
// 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) {
|
||||
@@ -824,9 +1024,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
||||
imageUnit);
|
||||
return false;
|
||||
// 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
|
||||
@@ -852,8 +1063,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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.
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
|
||||
const auto internalFormat = textureBuffer->GetFormat();
|
||||
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||
@@ -1062,8 +1271,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
if (imageBinding.Texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||
return false;
|
||||
// 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);
|
||||
@@ -1155,6 +1396,138 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return m_fallbackTexture2D;
|
||||
}
|
||||
|
||||
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 MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, Uint32 element,
|
||||
@@ -1340,9 +1713,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||
imageUnit, binding, element);
|
||||
return false;
|
||||
// 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);
|
||||
|
||||
@@ -42,7 +42,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
};
|
||||
|
||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
|
||||
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
|
||||
// texel buffer, and there is no other route to that answer from here.
|
||||
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
||||
@@ -177,6 +180,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
|
||||
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
|
||||
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
|
||||
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
|
||||
// one shared object, because a descriptor whose format disagrees with the shader's
|
||||
// declaration is invalid Vulkan even when nothing ever reads it.
|
||||
//
|
||||
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
|
||||
// texel buffer, which never carries one, or for a formatless `writeonly` image);
|
||||
// `numericDomain` decides the format when there is no declaration and is the fallback
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) 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.
|
||||
@@ -251,6 +280,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorSet& outDescriptorSet);
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VkBufferManager* m_bufferManager = nullptr;
|
||||
ProgramFactory* m_programFactory = nullptr;
|
||||
Vector<FrameResources> m_frames;
|
||||
@@ -263,6 +293,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
|
||||
// every minStorageBufferOffsetAlignment in the wild, and free.
|
||||
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
|
||||
// See VkBufferManager::AcquireUnboundTexelBufferDescriptor. The same 256 bytes, for the
|
||||
// same reason plus one: a texel buffer view's range must be a whole number of texels of
|
||||
// whatever format the placeholder is asked for, and 256 divides by every texel size in
|
||||
// the GL image-format table (1, 2, 4, 8 and 16 bytes).
|
||||
constexpr VkDeviceSize kUnboundTexelBufferDescriptorBytes = 256;
|
||||
|
||||
// A zero-copy persistent buffer is created once and never recreated (the app holds
|
||||
// its mapped pointer), and may be bound to any role, so it carries every usage.
|
||||
@@ -135,6 +140,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
m_transientUploadArena.Shutdown();
|
||||
m_unboundStorageBuffer.Destroy();
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
DestroyAllDeferredReleases();
|
||||
ReleaseAllLiveResources();
|
||||
m_copyProvider = nullptr;
|
||||
@@ -742,6 +748,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return m_unboundStorageBuffer.GetSlice();
|
||||
}
|
||||
|
||||
BufferSlice VkBufferManager::AcquireUnboundTexelBufferDescriptor() {
|
||||
if (!m_unboundTexelBuffer.IsValid()) {
|
||||
if (m_initInfo.allocator == nullptr) {
|
||||
return {};
|
||||
}
|
||||
// A SECOND placeholder rather than more usage bits on the storage-block one. The two
|
||||
// are independent failure domains: a device that refuses this allocation must not
|
||||
// take the storage-block placeholder - and with it the fix this one is a sibling of -
|
||||
// down with it. Host-visible and zero-filled for the same reason as that one: this is
|
||||
// reached from descriptor resolution, inside an already-open recording, which must
|
||||
// not start a copy of its own.
|
||||
const Bool created = m_unboundTexelBuffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = kUnboundTexelBufferDescriptorBytes,
|
||||
.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
});
|
||||
if (!created) {
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundTexelBufferDescriptor: placeholder creation failed");
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
return {};
|
||||
}
|
||||
if (void* mapped = m_unboundTexelBuffer.GetMappedData()) {
|
||||
Memset(mapped, 0, static_cast<SizeT>(kUnboundTexelBufferDescriptorBytes));
|
||||
}
|
||||
}
|
||||
return m_unboundTexelBuffer.GetSlice();
|
||||
}
|
||||
|
||||
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
|
||||
@@ -131,6 +131,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// that indexes past it.
|
||||
BufferSlice AcquireUnboundStorageDescriptor();
|
||||
|
||||
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
|
||||
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
|
||||
// it has no GL buffer attached. Both are legal GL states that make a fetch return
|
||||
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
|
||||
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
|
||||
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
|
||||
// and the descriptor is a VkBufferView, not a buffer - is built by
|
||||
// UniformManager::AcquireUnboundTexelBufferView.
|
||||
BufferSlice AcquireUnboundTexelBufferDescriptor();
|
||||
|
||||
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||
// resource exists and is fully uploaded, marks it used this frame.
|
||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
@@ -194,6 +204,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||
// with the manager.
|
||||
VkBufferObject m_unboundStorageBuffer;
|
||||
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||
VkBufferObject m_unboundTexelBuffer;
|
||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
|
||||
@@ -3136,7 +3136,7 @@ void main() {
|
||||
m_uniformManager = MakeUnique<UniformManager>();
|
||||
MOBILEGL_ASSERT(m_uniformManager != nullptr, "UniformDescriptorBinder creation failed.");
|
||||
succeeded = m_uniformManager->Initialize(
|
||||
m_device, &m_bufferManager, m_programFactory.get(),
|
||||
m_device, m_physicalDevice.handle, &m_bufferManager, m_programFactory.get(),
|
||||
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
||||
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
||||
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
||||
|
||||
@@ -2406,7 +2406,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
|
||||
break;
|
||||
case GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = static_cast<GLint>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||
// The STORAGE alignment, which is its own limit - this used to answer with the
|
||||
// uniform one. They differ on real hardware (Adreno 830: 32 uniform, 64 storage), and
|
||||
// under-reporting it is silent: ValidateBindBufferRange accepts the offset, the ES
|
||||
// driver accepts it too without raising an error, and the shader's writes then land
|
||||
// at an address the application never bound.
|
||||
*params = static_cast<GLint>(dynamicParameters.ShaderStorageBufferOffsetAlignment);
|
||||
break;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
params[0] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMin);
|
||||
|
||||
@@ -107,6 +107,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/RelinkStageSetScenario.cpp
|
||||
Scenarios/GuiBatchScenario.cpp
|
||||
Scenarios/UnboundImageDescriptorScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -305,6 +306,40 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# The shader-compiler configurations AsyncCompileScenario needs, and the one
|
||||
# ViewportArrayScenario's negative control needs.
|
||||
#
|
||||
# These used to be poked into MG_Config::Features from inside the test bodies. They
|
||||
# cannot be any more - on Android this module links the SHIPPING libMobileGL.so, which
|
||||
# exports nothing internal - and they should not have been anyway: half of what each of
|
||||
# them decides is latched before the first GL call (the compile pool and its threads;
|
||||
# the advertised extension list, which a backend builds once from the configuration in
|
||||
# force at its first use), so an in-process write could only ever have moved the other
|
||||
# half. Every one of them is a whole-process property, and a whole-process property is
|
||||
# spelled with an environment variable and a ctest entry of its own.
|
||||
#
|
||||
# Note the shape of every list here: it APPENDS to MGL_ITEST_COMMON_ENV /
|
||||
# MGL_ITEST_VULKAN_ENV rather than standing alone. A ctest ENVIRONMENT property REPLACES
|
||||
# the job environment rather than adding to it, so an entry that lists only its mode
|
||||
# variable would silently lose the EGL vendor and Vulkan ICD pinning and run against
|
||||
# whatever the loader found first.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
|
||||
@@ -369,3 +404,100 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||
#
|
||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||
# MobileGL's built-in default happens to be, and the day that default flips they would
|
||||
# stop covering the asynchronous path without anything going red. These entries are the
|
||||
# ones that keep the asynchronous half tested no matter what ships. They are also the
|
||||
# only place ExtensionStringMatchesTheConfiguration can assert that the extension IS
|
||||
# advertised - the case derives its expectation from this variable and nothing else, and
|
||||
# skips where it is unset, precisely so that it is not asserting the implementation
|
||||
# against itself.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.AsyncOn."
|
||||
TEST_FILTER "AsyncCompileScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.AsyncOn."
|
||||
TEST_FILTER "AsyncCompileScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The other side of the same switch: asynchronous compilation OFF, so
|
||||
# GL_KHR_parallel_shader_compile must be WITHDRAWN from both spellings of the extension
|
||||
# list and GL_MAX_SHADER_COMPILER_THREADS_KHR must read 0. Only that one case is
|
||||
# registered here because it is the only one that has anything to say in this
|
||||
# configuration - the other four exist to observe worker-built artifacts, and there are
|
||||
# none - so registering the whole scenario would buy four guaranteed skips per backend.
|
||||
# Together with the AsyncOn. entries above, one ctest run still covers both flag states,
|
||||
# which is what the in-process forcing used to be for.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.AsyncOff."
|
||||
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.AsyncOff."
|
||||
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The optimistic-status quirk's end-to-end shape. Its own entries and not part of the
|
||||
# AsyncOn. ones because the quirk is not neutral for the rest of the scenario: with it in
|
||||
# force glGetShaderiv(GL_COMPILE_STATUS) deliberately answers without joining, which is
|
||||
# exactly what CompletionStatusPollingThenForcedJoin asserts must NOT happen. Off by
|
||||
# default and never advertised, so - unlike asynchronous compilation, which announces
|
||||
# itself through the extension string - the variable is the only thing that can tell the
|
||||
# case it is in force.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.OptimisticShaderStatus."
|
||||
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.OptimisticShaderStatus."
|
||||
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The negative control for the DirectGLES gl_ViewportIndex emulation, in a process that
|
||||
# has it switched off. One case, because it is the only one the switch may touch: with
|
||||
# the emulation off the three positive cases in the same fixture describe behaviour the
|
||||
# backend does not have, so a whole-scenario registration would be three guaranteed reds.
|
||||
# DirectGLES only - the flag steers nothing on DirectVulkan, which routes natively.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.NoViewportArrayEmulation."
|
||||
TEST_FILTER "ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
@@ -552,7 +552,15 @@ namespace MGITest {
|
||||
// before the pre-flight forks - the child must measure the same platform
|
||||
// the parent will use.
|
||||
EnsureHeadlessPlatform();
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||
// The backend that is actually about to come up, which is what every
|
||||
// `BackendName() == "DirectGLES"` gate in the scenarios means by the question.
|
||||
// MG_ConfigLoader::InitBackendType defaults an unset MOBILEGL_BACKEND_TYPE to
|
||||
// DirectGLES, so the same default belongs here; this used to report the literal
|
||||
// "<unset>" instead. Under ctest the variable is always set by the ENVIRONMENT
|
||||
// property, which is why that never showed - but run straight from a device
|
||||
// shell, where nothing sets it, DirectGLES came up and every case gated on the
|
||||
// NAME DirectGLES skipped as though it had not.
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "DirectGLES");
|
||||
m_usable = BringUp();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,49 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "HeadlessGL.h"
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// How a MOBILEGL_* quirk variable reads in THIS process's environment.
|
||||
//
|
||||
// A scenario that needs a non-default configuration takes it from here and skips
|
||||
// when the process it was launched into is not in that configuration, rather than
|
||||
// writing MG_Config::Features itself. Two reasons, and the second one decides it:
|
||||
//
|
||||
// - the feature table is an internal symbol. On Android this module links against
|
||||
// the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the
|
||||
// real artifact - and that library is built -fvisibility=hidden, so nothing
|
||||
// internal is reachable from here at all.
|
||||
// - a quirk poked in-process is already too late for everything latched at
|
||||
// initialization: the compile pool and its threads, and the backend's advertised
|
||||
// extension list, which is built once from the configuration in force at first
|
||||
// use. The process-wide variable is the only spelling that covers the whole
|
||||
// configuration instead of the half of it that is still mutable afterwards.
|
||||
//
|
||||
// The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp,
|
||||
// QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a
|
||||
// built-in default, i.e. a value only the implementation knows - a truthy value is
|
||||
// On, and anything else that IS set ("0", "false", "") is Off.
|
||||
enum class AmbientQuirk { Auto, On, Off };
|
||||
|
||||
inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr) return AmbientQuirk::Auto;
|
||||
std::string lowered(value);
|
||||
for (char& c : lowered) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off;
|
||||
return AmbientQuirk::On;
|
||||
}
|
||||
|
||||
class ScenarioTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
// be able to turn this into a red.
|
||||
// (b) Forcing the join afterwards produces the right answer for every one of them:
|
||||
// GL_COMPILE_STATUS true, an empty info log, and a program that links.
|
||||
// (c) The extension string matches the configuration. This is the half a recorded
|
||||
// trace can never cover - Iris and Sodium change their submission schedule the
|
||||
// moment they see the string - so it is asserted against a real backend's real
|
||||
// GL_EXTENSIONS, through both glGetString and glGetStringi.
|
||||
// (c) The extension string matches the configuration - where "the configuration" is
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE as this process inherited it, and NOT anything the
|
||||
// implementation says about itself. This is the half a recorded trace can never
|
||||
// cover - Iris and Sodium change their submission schedule the moment they see the
|
||||
// string - so it is asserted against a real backend's real GL_EXTENSIONS, through
|
||||
// both glGetString and glGetStringi.
|
||||
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
|
||||
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
||||
// is synchronous. That is what the extension requires of a zero count.
|
||||
@@ -40,6 +42,27 @@
|
||||
//
|
||||
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
|
||||
// so this file runs twice per ctest invocation.
|
||||
//
|
||||
// COMPILATION MODE IS PER PROCESS TOO. Every case here needs a particular configuration of
|
||||
// MobileGL's shader compiler, and takes it from the ENVIRONMENT
|
||||
// (MOBILEGL_ASYNC_SHADER_COMPILE, MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS) rather than by
|
||||
// writing MG_Config::Features on the way past. Half of what those variables decide is
|
||||
// latched before the first GL call - the compile pool and its threads, and the advertised
|
||||
// extension list a backend builds once from the configuration in force at its first use -
|
||||
// so an in-process poke could only ever have moved the other half; and on Android it could
|
||||
// move nothing at all, because this module links against the shipping libMobileGL.so, which
|
||||
// exports no such symbol. A case whose process is not in the configuration it needs SKIPS
|
||||
// with that as its reason. CMakeLists.txt registers the extra ctest entries that put a
|
||||
// process into each configuration (AsyncOn., AsyncOff., OptimisticShaderStatus.), so one
|
||||
// ctest run still covers both sides of every switch. Run straight from a shell with nothing
|
||||
// set - the on-device shape - the ambient configuration runs and the rest skip cleanly.
|
||||
//
|
||||
// WITHIN one process, "compiled on a worker" versus "compiled on this thread" is switched
|
||||
// through glMaxShaderCompilerThreadsKHR, the extension's own entry point: a zero count joins
|
||||
// everything outstanding and compiles inline from then on, any nonzero count lifts that
|
||||
// again, and 0xFFFFFFFF asks for the implementation maximum (GL_Program.cpp,
|
||||
// MaxShaderCompilerThreadsKHR_State). Doing it through the public call rather than the
|
||||
// feature table means the switching is itself part of what these cases exercise.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -47,9 +70,6 @@
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
@@ -76,8 +96,6 @@ extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
using MobileGL::MG_Config::QuirkOverride;
|
||||
|
||||
// Same shape as the other scenarios: a two-attribute pass-through, so the only
|
||||
// thing that can differ between the two compilation modes is the compilation.
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
@@ -139,50 +157,40 @@ void main() {
|
||||
return source;
|
||||
}
|
||||
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
|
||||
// the other one says so here and gets the ambient one back on scope exit. Forcing
|
||||
// it in-process is what lets ONE ctest run compare the two modes against each
|
||||
// other - the whole point of (e).
|
||||
class AsyncModeScope {
|
||||
public:
|
||||
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
|
||||
MobileGL::MG_Config::Features.AsyncShaderCompile =
|
||||
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
|
||||
// Whether this context advertises GL_KHR_parallel_shader_compile, which is exactly
|
||||
// "MobileGL is configured to compile asynchronously" as an application can see it:
|
||||
// the backends gate the string on AsyncShaderCompileEnabled() and on nothing else
|
||||
// (BackendObject_DirectGLES.cpp / BackendObject_DirectVulkan.cpp), and the string
|
||||
// is the only way MobileGL ever tells anyone. A case that needs asynchronous
|
||||
// compilation checks for it the way an application would, and skips without it.
|
||||
//
|
||||
// The INDEXED form, because that is the one a core-profile application reads.
|
||||
bool HasParallelShaderCompile() {
|
||||
GLint count = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||
for (GLint i = 0; i < count; ++i) {
|
||||
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, GLuint(i)));
|
||||
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") return true;
|
||||
}
|
||||
~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||
AsyncModeScope(const AsyncModeScope&) = delete;
|
||||
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
|
||||
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
|
||||
class OptimisticStatusScope {
|
||||
public:
|
||||
explicit OptimisticStatusScope(const QuirkOverride mode)
|
||||
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
||||
}
|
||||
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
||||
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls it
|
||||
// has to put the pool back or it changes how every scenario after it compiles.
|
||||
//
|
||||
// The restore is the extension's own "implementation maximum" spelling rather than a
|
||||
// hand-rolled poke at the pool. glMaxShaderCompilerThreadsKHR(0xFFFFFFFF) is defined
|
||||
// (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State) as precisely the two steps this
|
||||
// used to perform through internal entry points - concurrency := the pool's full
|
||||
// thread count, then lift any suspension a zero count had armed - in the safer order,
|
||||
// since it raises the budget before re-admitting work rather than after. Going through
|
||||
// the public call also puts the restore path itself under test, and it is the only
|
||||
// spelling available on Android, where this module links the shipping shared library
|
||||
// and can reach nothing but the GL entry points.
|
||||
class CompilerThreadScope {
|
||||
public:
|
||||
CompilerThreadScope() = default;
|
||||
~CompilerThreadScope() {
|
||||
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
||||
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
|
||||
pool.SetMaxConcurrency(pool.GetThreadCount());
|
||||
}
|
||||
~CompilerThreadScope() { glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); }
|
||||
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
||||
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
||||
};
|
||||
@@ -293,7 +301,12 @@ void main() {
|
||||
// interesting for shaders that (a) proved were genuinely still outstanding.
|
||||
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
||||
if (!Ready()) return;
|
||||
const AsyncModeScope async(true);
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so no compile can be "
|
||||
"outstanding; the AsyncOn. ctest entries run this case with "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
// One worker, so the queue behind it is what the poll observes.
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
@@ -341,14 +354,33 @@ void main() {
|
||||
}
|
||||
|
||||
// ---- (c) ------------------------------------------------------------------
|
||||
// The extension string, read from a real backend that really brought a driver
|
||||
// up. No mode forcing here: a backend builds its advertised list once, from the
|
||||
// configuration in force at its first use, so the meaningful assertion is
|
||||
// against the AMBIENT configuration - which is exactly what makes this case
|
||||
// worth running in both of the suite's flag states.
|
||||
// The extension string, read from a real backend that really brought a driver up.
|
||||
//
|
||||
// The expectation comes from the ENVIRONMENT, never from the implementation. This
|
||||
// case used to derive it by calling AsyncShaderCompileEnabled() - which is the same
|
||||
// function the backends gate the string on, so the two halves could only ever agree
|
||||
// and the case would have passed however wrong both of them were. Asserting an
|
||||
// implementation against itself pins nothing.
|
||||
//
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE is the whole input: the process inherited it before
|
||||
// any GL call, a backend builds its advertised list once from the configuration in
|
||||
// force at first use, and nothing in this process can move it afterwards. So reading
|
||||
// the variable IS reading the configuration, independently. With the variable unset
|
||||
// the configuration in force is MobileGL's built-in default, which only the
|
||||
// implementation knows - there is nothing independent left to compare against, and
|
||||
// this case says so rather than inventing an expectation. The AsyncOn. and AsyncOff.
|
||||
// ctest entries pin the variable to each of its two values, so one ctest run still
|
||||
// asserts both the advertised and the withdrawn side.
|
||||
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
|
||||
if (!Ready()) return;
|
||||
const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled();
|
||||
const AmbientQuirk configured = AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
if (configured == AmbientQuirk::Auto) {
|
||||
GTEST_SKIP() << "MOBILEGL_ASYNC_SHADER_COMPILE is unset, so the configuration in force is "
|
||||
"MobileGL's built-in default and the only way to learn it would be to ask "
|
||||
"the implementation this case exists to check; the AsyncOn. and AsyncOff. "
|
||||
"ctest entries run it with the variable pinned to each of its two values";
|
||||
}
|
||||
const bool expected = configured == AmbientQuirk::On;
|
||||
|
||||
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||
ASSERT_NE(extensions, nullptr);
|
||||
@@ -385,7 +417,12 @@ void main() {
|
||||
// A zero count must leave nothing in flight and keep it that way.
|
||||
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
||||
if (!Ready()) return;
|
||||
const AsyncModeScope async(true);
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so a zero count has "
|
||||
"nothing to settle; the AsyncOn. ctest entries run this case with "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
@@ -417,12 +454,28 @@ void main() {
|
||||
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
||||
// backend's orientation and present path live, so the comparison covers the
|
||||
// whole pipeline rather than the reflection tables alone.
|
||||
//
|
||||
// The two modes are selected through glMaxShaderCompilerThreadsKHR, the extension's
|
||||
// own entry point, rather than through the feature table: a zero count joins
|
||||
// everything outstanding and makes every later glCompileShader/glLinkProgram run its
|
||||
// body on the calling thread, and 0xFFFFFFFF lifts that again with the pool at its
|
||||
// full thread count (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State; the compile
|
||||
// and link paths both gate on AsyncShaderCompileActive(), which is what the zero
|
||||
// count switches). So this is still one process comparing worker-built artifacts
|
||||
// against inline-built ones - just asked for the way an application asks.
|
||||
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
|
||||
if (!Ready()) return;
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so both halves would "
|
||||
"be the same inline build and the comparison would be vacuous; the "
|
||||
"AsyncOn. ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
Image asyncImage;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u);
|
||||
asyncImage = DrawFrameWith(program);
|
||||
@@ -431,7 +484,7 @@ void main() {
|
||||
|
||||
Image syncImage;
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
glMaxShaderCompilerThreadsKHR(0);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u);
|
||||
syncImage = DrawFrameWith(program);
|
||||
@@ -456,11 +509,16 @@ void main() {
|
||||
// candidate) shows up here and not in the single-program case above.
|
||||
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
||||
if (!Ready()) return;
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so nothing would be "
|
||||
"built on a worker and there is no per-worker state to leak; the AsyncOn. "
|
||||
"ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
// Everything enqueued before anything is read: the only shape in which
|
||||
@@ -489,6 +547,21 @@ void main() {
|
||||
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||
if (!Ready()) return;
|
||||
// The quirk is off by default and never advertised, so unlike the cases above
|
||||
// there is no GL observable that says whether it is in force - only the variable
|
||||
// that put it there. It also has to be set BEFORE this process started for the
|
||||
// shape to be the real one: the optimistic answer is latched per compile, and a
|
||||
// quirk switched on mid-process would only cover the compiles after it.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case is the optimistic-status quirk's end-to-end shape and needs it on "
|
||||
"for the whole process; the OptimisticShaderStatus. ctest entries run it with "
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1";
|
||||
}
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "the optimistic status only ever applies to a compile that is still in flight "
|
||||
"(OptimisticShaderStatusActive() requires AsyncShaderCompileActive()), and "
|
||||
"this process is configured to compile inline";
|
||||
}
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||
@@ -508,8 +581,6 @@ void main() {
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnboundImageDescriptorScenario.cpp
|
||||
// Copyright (c) 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
|
||||
//
|
||||
// Scenario - A PROGRAM DECLARES AN IMAGE-BACKED RESOURCE AND THE APPLICATION BINDS NOTHING.
|
||||
//
|
||||
// The sibling of GuiBatchScenario's MeshesBlockLeftUnbound, one descriptor kind further out.
|
||||
// That one pinned an unbound shader storage BLOCK; the same "nothing is bound, so lose the
|
||||
// whole draw" shape survived in the three image-backed kinds:
|
||||
//
|
||||
// * `samplerBuffer` - a texture unit with no buffer texture on it, and a buffer texture with
|
||||
// no GL buffer attached to it. Both make the sampler INCOMPLETE (GL 4.6
|
||||
// core 8.9, 8.24), and sampling an incomplete texture returns undefined
|
||||
// VALUES. It is not an error and it is not a lost draw.
|
||||
// * `imageBuffer` - an image unit with nothing on it. GL 4.6 core 8.26 is explicit: loads
|
||||
// return zero and stores are discarded.
|
||||
// * `image2D` - the same rule, through a VkImageView rather than a VkBufferView.
|
||||
//
|
||||
// Vulkan has no such thing as an unwritten descriptor, so DirectVulkan's descriptor resolution
|
||||
// used to answer "no valid descriptor" and both SetupDraw and DispatchCompute skip everything on
|
||||
// that answer - the draw or dispatch simply never happened, silently. Every test below asserts
|
||||
// on the OTHER work in the same shader: the pixels the fragment stage painted, or the buffer the
|
||||
// dispatch filled. All of it is unrelated to the unbound resource and all of it disappeared.
|
||||
//
|
||||
// The unbound resource is STATICALLY USED in every case, because an unreferenced one is
|
||||
// optimised out before it ever reaches a descriptor and would prove nothing. Where the use is a
|
||||
// read it sits behind a uniform-controlled branch that is false at runtime - the descriptor is
|
||||
// declared and must be written, but no undefined value reaches an assertion. Where it is a write
|
||||
// (the `writeonly` cases, which is how the real workloads spell it) it is unconditional: GL says
|
||||
// the store is discarded, so there is nothing to guard against.
|
||||
//
|
||||
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound unit to the GLES driver,
|
||||
// which does what GL says, so it is the control - every test here must stay green on both.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
constexpr int kElements = 4;
|
||||
|
||||
// No vertex attributes: the quad's corners come from gl_VertexID, so nothing about the
|
||||
// vertex fetch can be confused with the descriptor question under test.
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The assertion in every draw case: opaque green everywhere. The unbound resource
|
||||
// contributes nothing to it - u_readUnbound is 0, so the fetch never runs - but the
|
||||
// descriptor for it still has to exist, which is the point.
|
||||
constexpr const char* kSamplerBufferFragmentSource = R"(#version 430 core
|
||||
uniform samplerBuffer u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kSamplerBufferComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
uniform samplerBuffer u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
uint value = index + 1u;
|
||||
if (u_readUnbound != 0) {
|
||||
value += uint(texelFetch(u_unbound, 0).r);
|
||||
}
|
||||
g_data[index] = value;
|
||||
}
|
||||
)";
|
||||
|
||||
// writeonly, and the store is unconditional: this is how AcceleratedRendering and the
|
||||
// conformance cases spell an image the shader only produces into. GL discards the store
|
||||
// when the unit is empty; nothing here reads it back.
|
||||
constexpr const char* kImageBufferFragmentSource = R"(#version 430 core
|
||||
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
imageStore(u_unbound, 0, uvec4(7u));
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImageBufferComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, int(index), uvec4(7u));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
||||
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
imageStore(u_unbound, ivec2(0, 0), vec4(1.0));
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// No layout format at all, which GLSL 4.20 allows for a write-only image. The reflection
|
||||
// then carries NO format for the binding, so the placeholder descriptor can only be
|
||||
// constrained by the declaration's numeric class - a different route through the fix than
|
||||
// every typed case above.
|
||||
constexpr const char* kFormatlessImage2DComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0) uniform writeonly image2D u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
class UnboundImageDescriptorScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenBuffers(1, &m_storage);
|
||||
// The harness shares one context across every scenario in the process, so an
|
||||
// earlier one may well have left a texture on unit 0 or an image on unit 0. The
|
||||
// whole subject here is that nothing is bound, so say so rather than assume it.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_storage != 0) glDeleteBuffers(1, &m_storage);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
// Each case needs exactly one kind of opaque uniform in one stage, and a host with
|
||||
// none of that kind there would report a failure that is about the host, not the fix.
|
||||
// Asked for by the limit that governs the kind under test and no other: a guard that
|
||||
// over-asks turns into a silent skip of the very thing the case exists for.
|
||||
static bool LimitIsAtLeastOne(GLenum limit) {
|
||||
GLint value = 0;
|
||||
glGetIntegerv(limit, &value);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return value >= 1;
|
||||
}
|
||||
|
||||
unsigned int MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Fills a four-element SSBO with 1..4 while the unbound resource is declared and
|
||||
// statically used. Zeros everywhere mean the dispatch never ran.
|
||||
void ExpectDispatchStillRuns(const char* source, const char* what) {
|
||||
m_program = MakeComputeProgram(source);
|
||||
ASSERT_NE(m_program, 0u);
|
||||
|
||||
const std::vector<unsigned int> zeros(static_cast<std::size_t>(kElements), 0u);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
||||
static_cast<GLsizeiptr>(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_storage);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting up the output buffer raised a GL error";
|
||||
|
||||
glUseProgram(m_program);
|
||||
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
||||
if (readUnbound != -1) {
|
||||
glUniform1i(readUnbound, 0);
|
||||
}
|
||||
glDispatchCompute(kElements, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error (" << what << ")";
|
||||
|
||||
std::vector<unsigned int> values(static_cast<std::size_t>(kElements), 0xDEADBEEFu);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
EXPECT_EQ(values[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
||||
<< "element " << i << " came back as " << values[static_cast<std::size_t>(i)]
|
||||
<< "; zero everywhere means the whole dispatch was dropped over the unbound " << what;
|
||||
}
|
||||
}
|
||||
|
||||
// Paints the whole render target green while the unbound resource is declared and
|
||||
// statically used. A black target means the draw never happened.
|
||||
void ExpectDrawStillRuns(const char* fragmentSource, const char* what) {
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, fragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
||||
if (readUnbound != -1) {
|
||||
glUniform1i(readUnbound, 0);
|
||||
}
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error (" << what << ")";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << "the readback came back empty";
|
||||
// Whole-region, not a centre pixel: the quad covers the target exactly, so
|
||||
// anything short of all of it is a failure worth naming.
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0,
|
||||
std::string("the quad drawn with an unbound ") + what))
|
||||
<< "an all-black target means the draw was dropped over the unbound " << what;
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_storage = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- uniform samplerBuffer (VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) --------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS)) {
|
||||
GTEST_SKIP() << "the compute stage has no texture image units";
|
||||
}
|
||||
ExpectDispatchStillRuns(kSamplerBufferComputeSource, "samplerBuffer");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "samplerBuffer");
|
||||
}
|
||||
|
||||
// The other way a texel-buffer descriptor comes out empty: the unit HAS a buffer texture, but
|
||||
// no glTexBuffer ever attached a buffer object to it. GL calls that texture incomplete, which
|
||||
// is undefined data and not a lost draw - a separate site in the resolve from the one above,
|
||||
// and it used to return false too.
|
||||
TEST_F(UnboundImageDescriptorScenario, ABufferTextureWithNoAttachedBufferDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
// Deliberately no glTexBuffer: the texture exists and is bound, and has no store.
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding an empty buffer texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "buffer texture with no attached buffer");
|
||||
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// ---- writeonly imageBuffer (VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER) --------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kImageBufferComputeSource, "imageBuffer");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
||||
}
|
||||
ExpectDrawStillRuns(kImageBufferFragmentSource, "imageBuffer");
|
||||
}
|
||||
|
||||
// ---- writeonly image2D (VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) -------------------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kImage2DComputeSource, "image2D");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
||||
}
|
||||
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kFormatlessImage2DComputeSource, "format-less image2D");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -55,10 +55,6 @@
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
|
||||
// to know which backend it is running on.
|
||||
#include <Config.h>
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
@@ -529,7 +525,8 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
//
|
||||
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
|
||||
// emulation that works from a backend that was going to be right anyway. This case builds
|
||||
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
|
||||
// the SAME program in a process started with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// (the NoViewportArrayEmulation. ctest entry) and requires case 1's
|
||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
||||
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
|
||||
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
|
||||
@@ -544,25 +541,31 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
"gl_ViewportIndex natively and ignores it";
|
||||
}
|
||||
|
||||
// The feature table is a process-global and this fixture shares its context with every
|
||||
// other scenario in the process, so the restore is not optional.
|
||||
struct ScopedEmulationOff {
|
||||
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
|
||||
MobileGL::MG_Config::Features.ViewportArrayEmulation =
|
||||
MobileGL::MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
|
||||
MobileGL::MG_Config::QuirkOverride saved;
|
||||
};
|
||||
// The switch comes from the ENVIRONMENT, and this case runs only in a process that
|
||||
// was started with it off. It used to write MG_Config::Features directly, which is
|
||||
// not available to it any more: on Android this module links the shipping
|
||||
// libMobileGL.so - so that the on-device run validates the real artifact - and that
|
||||
// library exports no such symbol. The process-wide variable is also the more honest
|
||||
// spelling of the control, since it is the one a developer chasing this failure
|
||||
// would actually set. CMakeLists.txt registers the NoViewportArrayEmulation. ctest
|
||||
// entry for it, so the control still runs in every ctest run; anywhere else - the
|
||||
// ambient ctest entries, or the binary run straight from a device shell - the
|
||||
// emulation is on and this case skips.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "this is the negative control for the emulation and needs it off for the "
|
||||
"whole process; the NoViewportArrayEmulation. ctest entry runs it with "
|
||||
"MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
}
|
||||
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
|
||||
GLuint unroutedProgram = 0;
|
||||
{
|
||||
const ScopedEmulationOff scopedEmulationOff;
|
||||
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
|
||||
// that carries this flag, so reusing m_program would just replay the routed build.
|
||||
// A program of its own rather than the fixture's, even though in this process
|
||||
// the fixture's was built unrouted too: the emitted ESSL is decided at link
|
||||
// time and memoized on a key that carries this flag, and building it here keeps
|
||||
// what this case measures independent of when SetUp happened to link.
|
||||
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
|
||||
glUseProgram(unroutedProgram);
|
||||
|
||||
@@ -988,6 +988,49 @@ TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries)
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT used to be answered with the UNIFORM buffer
|
||||
// alignment. The two are separate limits and the storage one is the larger on real hardware
|
||||
// (Adreno 830: 32 uniform, 64 storage), so the substitution under-reported it - and an
|
||||
// under-reported alignment is silent all the way down: the frontend validator accepts the
|
||||
// offset, the ES driver accepts the glBindBufferRange too without raising an error, and the
|
||||
// shader's stores land at an address the application never bound. The two values are
|
||||
// deliberately different here so a query that reads the wrong field cannot coincide with the
|
||||
// right answer.
|
||||
TEST(GetterSanity, StorageAndUniformBufferOffsetAlignmentsAreSeparateLimits) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.UniformBufferOffsetAlignment = 32;
|
||||
params.ShaderStorageBufferOffsetAlignment = 64;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
GLint uniformAlignment = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &uniformAlignment);
|
||||
EXPECT_EQ(uniformAlignment, 32);
|
||||
|
||||
GLint storageAlignment = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &storageAlignment);
|
||||
EXPECT_EQ(storageAlignment, 64);
|
||||
|
||||
// And the other way round, so the test fails on a getter that simply swapped the two fields.
|
||||
params.UniformBufferOffsetAlignment = 128;
|
||||
params.ShaderStorageBufferOffsetAlignment = 16;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &uniformAlignment);
|
||||
EXPECT_EQ(uniformAlignment, 128);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &storageAlignment);
|
||||
EXPECT_EQ(storageAlignment, 16);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -1550,6 +1593,30 @@ TEST(DirectVulkanSanity, SamplerUniformTypesPreserveTheirNumericDomain) {
|
||||
SamplerNumericDomain::Unknown);
|
||||
}
|
||||
|
||||
// The image half of the same question, which the sampler form above deliberately answers
|
||||
// Unknown. It decides the format of the placeholder descriptor an UNBOUND image unit gets, and a
|
||||
// `writeonly` declaration carries no format qualifier for it to fall back on - so an Unknown here
|
||||
// is a lost draw, not a cosmetic gap.
|
||||
TEST(DirectVulkanSanity, ImageUniformTypesPreserveTheirNumericDomain) {
|
||||
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_IMAGE_2D), SamplerNumericDomain::Float);
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_IMAGE_BUFFER), SamplerNumericDomain::Float);
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_IMAGE_CUBE_MAP_ARRAY),
|
||||
SamplerNumericDomain::Float);
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_INT_IMAGE_2D_ARRAY),
|
||||
SamplerNumericDomain::SignedInteger);
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_INT_IMAGE_BUFFER),
|
||||
SamplerNumericDomain::SignedInteger);
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_UNSIGNED_INT_IMAGE_3D),
|
||||
SamplerNumericDomain::UnsignedInteger);
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_UNSIGNED_INT_IMAGE_BUFFER),
|
||||
SamplerNumericDomain::UnsignedInteger);
|
||||
// Samplers are the other function's business, and answering for them here would let a
|
||||
// sampler binding silently take an image binding's placeholder rules.
|
||||
EXPECT_EQ(ProgramFactory::UniformTypeToImageNumericDomain(GL_SAMPLER_2D), SamplerNumericDomain::Unknown);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChangingComponentLayout) {
|
||||
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
@@ -24,6 +25,8 @@ using MobileGL::MG_Util::SelfTest::ProbeCrossStageImageQualifierMergeDropsWrites
|
||||
using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||
|
||||
namespace {
|
||||
@@ -46,6 +49,13 @@ namespace {
|
||||
// CollectGlesKnownDriverBugs(): the collector goes through the once-per-process memos, and a
|
||||
// memo latched by one test would decide the answer for every later one.
|
||||
|
||||
// The exact text an affected Adreno driver puts in the compile log when it refuses a
|
||||
// vertex input's layout(location = N). Quoted rather than paraphrased for the same reason the
|
||||
// link log below is: the report shows it to a human, so a probe that stopped capturing it
|
||||
// would stop being useful long before it stopped detecting.
|
||||
const char* const kAttributeRangeCompileLog =
|
||||
"ERROR: 0:2: '' : the location is not within attribute range [0, MAX_ATTRIBUTES-1] \nERROR: 1 compilation errors. No code generated.";
|
||||
|
||||
// The exact text an affected Adreno driver puts in the info log for this refusal.
|
||||
const char* const kImageLocationLinkLog =
|
||||
"Error: Image Image location or component exceeds max allowed.\nError: Linking failed.";
|
||||
@@ -82,6 +92,21 @@ namespace {
|
||||
int coherencyEmittedShapeFailedTexels = 0;
|
||||
int coherencyControlFailedTexels = 0;
|
||||
|
||||
// Probe 5: GL_MAX_VERTEX_ATTRIBS, and the two separate ceilings the probe has to tell
|
||||
// apart - how high `layout(location = N)` may go in the ESSL compiler, and how high
|
||||
// glBindAttribLocation may go at link. On an unaffected driver both are above the
|
||||
// advertised count.
|
||||
GLint maxVertexAttribs = 32;
|
||||
int explicitLocationCeiling = 1000;
|
||||
int bindAttribLocationCeiling = 1000;
|
||||
// Probe 5's inconclusive path: nothing compiles, including the location-0 control.
|
||||
bool everyCompileFails = false;
|
||||
// Probe 6: a blit writes the destination array layer the framebuffer names, or always
|
||||
// layer 0. The second knob is the inconclusive path - a driver that does not honour the
|
||||
// SOURCE layer either fails the probe's control.
|
||||
bool blitIgnoresDestinationLayer = false;
|
||||
bool blitIgnoresSourceLayer = false;
|
||||
|
||||
// ---- object bookkeeping ---------------------------------------------
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
GLuint nextShaderId = 1;
|
||||
@@ -102,6 +127,19 @@ namespace {
|
||||
// texture id -> GL_TEXTURE_SWIZZLE_A
|
||||
std::map<GLuint, GLenum> multisampleAlphaSwizzle;
|
||||
|
||||
std::map<GLuint, bool> shaderCompiled;
|
||||
std::map<GLuint, std::string> shaderInfoLogs;
|
||||
// program -> (attribute name -> location) as glBindAttribLocation left it.
|
||||
std::map<GLuint, std::map<std::string, GLint>> boundAttribLocations;
|
||||
// 2D array texture id -> the byte every texel of each layer holds. Two layers is all the
|
||||
// layered-blit probe uses, and one byte per layer is all it distinguishes.
|
||||
std::map<GLuint, std::array<GLubyte, 2>> arrayLayerFill;
|
||||
// framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached.
|
||||
std::map<GLuint, std::pair<GLuint, GLint>> framebufferLayerAttachment;
|
||||
GLuint boundArrayTexture = 0;
|
||||
GLuint boundDrawFramebuffer = 0;
|
||||
GLuint boundReadFramebuffer = 0;
|
||||
|
||||
GLuint boundMultisampleTexture = 0;
|
||||
GLuint currentProgram = 0;
|
||||
// How many programs that sample a multisample texture have been linked so far. The
|
||||
@@ -149,6 +187,22 @@ namespace {
|
||||
return names;
|
||||
}
|
||||
|
||||
// The N in `layout(location = N) in ...`, or -1 when the source declares no such input.
|
||||
// Read off the text the probe actually submitted, so a probe that stopped emitting the
|
||||
// qualifier would stop being detected here too.
|
||||
int ExplicitVertexInputLocationIn(const std::string& source) {
|
||||
const std::size_t at = source.find("layout(location = ");
|
||||
if (at == std::string::npos) return -1;
|
||||
const std::size_t start = at + std::strlen("layout(location = ");
|
||||
const std::size_t close = source.find(')', start);
|
||||
if (close == std::string::npos) return -1;
|
||||
// Only a VERTEX INPUT counts: `layout(location = 0) out vec4` is a different declaration
|
||||
// and no driver caps it against GL_MAX_VERTEX_ATTRIBS.
|
||||
const std::size_t declaration = source.find_first_not_of(" \t", close + 1);
|
||||
if (declaration == std::string::npos || source.compare(declaration, 3, "in ") != 0) return -1;
|
||||
return std::atoi(source.c_str() + start);
|
||||
}
|
||||
|
||||
std::string StageSourceContaining(GLuint program, const char* needle) {
|
||||
const auto attached = g_fake.programShaders.find(program);
|
||||
if (attached == g_fake.programShaders.end()) return {};
|
||||
@@ -217,6 +271,9 @@ namespace {
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
*data = g_fake.maxGeometrySsboBlocks;
|
||||
break;
|
||||
case GL_MAX_VERTEX_ATTRIBS:
|
||||
*data = g_fake.maxVertexAttribs;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -252,16 +309,41 @@ namespace {
|
||||
}
|
||||
g_fake.shaderSources[shader] = std::move(source);
|
||||
};
|
||||
funcs.glCompileShader = [](GLuint) {};
|
||||
funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) {
|
||||
if (pname == GL_COMPILE_STATUS) *params = GL_TRUE;
|
||||
funcs.glCompileShader = [](GLuint shader) {
|
||||
const std::string& source = SourceOf(shader);
|
||||
const int location = ExplicitVertexInputLocationIn(source);
|
||||
const bool refused =
|
||||
g_fake.everyCompileFails || (location >= 0 && location >= g_fake.explicitLocationCeiling);
|
||||
g_fake.shaderCompiled[shader] = !refused;
|
||||
g_fake.shaderInfoLogs[shader] = refused ? kAttributeRangeCompileLog : "";
|
||||
};
|
||||
funcs.glGetShaderInfoLog = [](GLuint, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
|
||||
if (bufSize > 0) infoLog[0] = '\0';
|
||||
funcs.glGetShaderiv = [](GLuint shader, GLenum pname, GLint* params) {
|
||||
if (pname != GL_COMPILE_STATUS) return;
|
||||
const auto it = g_fake.shaderCompiled.find(shader);
|
||||
*params = (it == g_fake.shaderCompiled.end() || it->second) ? GL_TRUE : GL_FALSE;
|
||||
};
|
||||
funcs.glGetShaderInfoLog = [](GLuint shader, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
|
||||
if (bufSize <= 0) return;
|
||||
const auto it = g_fake.shaderInfoLogs.find(shader);
|
||||
const std::string& log = it == g_fake.shaderInfoLogs.end() ? std::string() : it->second;
|
||||
const GLsizei copied = static_cast<GLsizei>(
|
||||
std::min<std::size_t>(log.size(), static_cast<std::size_t>(bufSize - 1)));
|
||||
std::memcpy(infoLog, log.data(), static_cast<std::size_t>(copied));
|
||||
infoLog[copied] = '\0';
|
||||
};
|
||||
funcs.glDeleteShader = [](GLuint shader) {
|
||||
if (shader != 0) --g_fake.aliveShaders;
|
||||
};
|
||||
funcs.glBindAttribLocation = [](GLuint program, GLuint index, const GLchar* name) {
|
||||
g_fake.boundAttribLocations[program][name] = static_cast<GLint>(index);
|
||||
};
|
||||
funcs.glGetAttribLocation = [](GLuint program, const GLchar* name) -> GLint {
|
||||
const auto programEntry = g_fake.boundAttribLocations.find(program);
|
||||
if (programEntry == g_fake.boundAttribLocations.end()) return -1;
|
||||
const auto nameEntry = programEntry->second.find(name);
|
||||
if (nameEntry == programEntry->second.end()) return -1;
|
||||
return nameEntry->second >= g_fake.bindAttribLocationCeiling ? -1 : nameEntry->second;
|
||||
};
|
||||
funcs.glCreateProgram = []() -> GLuint {
|
||||
++g_fake.alivePrograms;
|
||||
return g_fake.nextProgramId++;
|
||||
@@ -272,7 +354,17 @@ namespace {
|
||||
funcs.glLinkProgram = [](GLuint program) {
|
||||
const std::vector<std::string> names = DeclaredImageNames(program);
|
||||
const bool overBudget = static_cast<int>(names.size()) > g_fake.distinctImageNameBudget;
|
||||
g_fake.programLinked[program] = !overBudget;
|
||||
// A driver whose glBindAttribLocation ceiling is lower than the location asked for
|
||||
// refuses the LINK rather than the compile - which is the half of the vertex-input
|
||||
// probe that decides whether the attribute is reachable another way at all.
|
||||
bool attributeOutOfRange = false;
|
||||
if (const auto it = g_fake.boundAttribLocations.find(program);
|
||||
it != g_fake.boundAttribLocations.end()) {
|
||||
for (const auto& [attributeName, location] : it->second) {
|
||||
if (location >= g_fake.bindAttribLocationCeiling) attributeOutOfRange = true;
|
||||
}
|
||||
}
|
||||
g_fake.programLinked[program] = !overBudget && !attributeOutOfRange;
|
||||
g_fake.programInfoLogs[program] = overBudget ? kImageLocationLinkLog : "";
|
||||
if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) {
|
||||
++g_fake.sampledMultisampleProgramCount;
|
||||
@@ -308,6 +400,20 @@ namespace {
|
||||
};
|
||||
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture;
|
||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.boundArrayTexture = texture;
|
||||
};
|
||||
funcs.glTexStorage3D = [](GLenum target, GLsizei, GLenum, GLsizei, GLsizei, GLsizei) {
|
||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.arrayLayerFill[g_fake.boundArrayTexture] = {0, 0};
|
||||
};
|
||||
// One byte per layer: the layered-blit probe fills every texel of a layer with the same
|
||||
// value and only ever asks which layer a value ended up on.
|
||||
funcs.glTexSubImage3D = [](GLenum target, GLint, GLint, GLint, GLint zoffset, GLsizei, GLsizei,
|
||||
GLsizei, GLenum, GLenum, const void* pixels) {
|
||||
if (target != GL_TEXTURE_2D_ARRAY || pixels == nullptr) return;
|
||||
auto& fill = g_fake.arrayLayerFill[g_fake.boundArrayTexture];
|
||||
if (zoffset >= 0 && static_cast<std::size_t>(zoffset) < fill.size()) {
|
||||
fill[static_cast<std::size_t>(zoffset)] = static_cast<const GLubyte*>(pixels)[0];
|
||||
}
|
||||
};
|
||||
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
@@ -332,12 +438,50 @@ namespace {
|
||||
++g_fake.aliveFramebuffers;
|
||||
}
|
||||
};
|
||||
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
|
||||
funcs.glBindFramebuffer = [](GLenum target, GLuint framebuffer) {
|
||||
if (target == GL_FRAMEBUFFER || target == GL_DRAW_FRAMEBUFFER) {
|
||||
g_fake.boundDrawFramebuffer = framebuffer;
|
||||
}
|
||||
if (target == GL_FRAMEBUFFER || target == GL_READ_FRAMEBUFFER) {
|
||||
g_fake.boundReadFramebuffer = framebuffer;
|
||||
}
|
||||
};
|
||||
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
||||
funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint, GLint layer) {
|
||||
const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer
|
||||
: g_fake.boundDrawFramebuffer;
|
||||
g_fake.framebufferLayerAttachment[framebuffer] = {texture, layer};
|
||||
};
|
||||
funcs.glReadBuffer = [](GLenum) {};
|
||||
// The defect itself: the source layer is read from where the READ framebuffer says (unless
|
||||
// that knob is on too), and the result is written to the layer the DRAW framebuffer names -
|
||||
// or to layer 0 regardless, which is what an affected driver does.
|
||||
funcs.glBlitFramebuffer = [](GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield,
|
||||
GLenum) {
|
||||
const auto source = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer);
|
||||
const auto destination = g_fake.framebufferLayerAttachment.find(g_fake.boundDrawFramebuffer);
|
||||
if (source == g_fake.framebufferLayerAttachment.end() ||
|
||||
destination == g_fake.framebufferLayerAttachment.end()) {
|
||||
return;
|
||||
}
|
||||
const GLint sourceLayer = g_fake.blitIgnoresSourceLayer ? 0 : source->second.second;
|
||||
const GLint destinationLayer =
|
||||
g_fake.blitIgnoresDestinationLayer ? 0 : destination->second.second;
|
||||
auto& sourceFill = g_fake.arrayLayerFill[source->second.first];
|
||||
auto& destinationFill = g_fake.arrayLayerFill[destination->second.first];
|
||||
if (sourceLayer < 0 || static_cast<std::size_t>(sourceLayer) >= sourceFill.size()) return;
|
||||
if (destinationLayer < 0 ||
|
||||
static_cast<std::size_t>(destinationLayer) >= destinationFill.size()) {
|
||||
return;
|
||||
}
|
||||
destinationFill[static_cast<std::size_t>(destinationLayer)] =
|
||||
sourceFill[static_cast<std::size_t>(sourceLayer)];
|
||||
};
|
||||
funcs.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; };
|
||||
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||
g_fake.framebufferLayerAttachment.erase(framebuffers[i]);
|
||||
}
|
||||
};
|
||||
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||
@@ -417,6 +561,26 @@ namespace {
|
||||
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels) {
|
||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
// Answered before anything else: a read framebuffer that names an array LAYER is the
|
||||
// layered-blit probe asking what that layer holds, and its bytes have nothing to do
|
||||
// with the pass/fail texel encoding the image probes below share.
|
||||
if (const auto layered = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer);
|
||||
layered != g_fake.framebufferLayerAttachment.end()) {
|
||||
const auto& fill = g_fake.arrayLayerFill[layered->second.first];
|
||||
const GLint layer = layered->second.second;
|
||||
const GLubyte value =
|
||||
(layer >= 0 && static_cast<std::size_t>(layer) < fill.size())
|
||||
? fill[static_cast<std::size_t>(layer)]
|
||||
: 0;
|
||||
GLubyte* out = static_cast<GLubyte*>(pixels);
|
||||
for (std::size_t i = 0; i < texels; ++i) {
|
||||
out[i * 4 + 0] = value;
|
||||
out[i * 4 + 1] = value;
|
||||
out[i * 4 + 2] = value;
|
||||
out[i * 4 + 3] = 255;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (format == GL_RED && type == GL_FLOAT) {
|
||||
GLfloat* out = static_cast<GLfloat*>(pixels);
|
||||
for (std::size_t i = 0; i < texels; ++i) out[i] = g_fake.lastSampledValue;
|
||||
@@ -452,6 +616,10 @@ namespace {
|
||||
// section would stop meaning "this device has these bugs".
|
||||
TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
||||
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
|
||||
EXPECT_FALSE(ProbeBlitIgnoresDestinationArrayLayer(gl))
|
||||
<< "a probe with no entry points has measured nothing";
|
||||
EXPECT_FALSE(ProbeExplicitVertexInputLocationCeiling(gl).detected)
|
||||
<< "a probe with no entry points has measured nothing";
|
||||
EXPECT_FALSE(ProbeGeometryStageSsboWriteAfterEmitDropped(gl))
|
||||
<< "a probe with no entry points to call must not claim the driver is affected";
|
||||
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||
@@ -651,6 +819,125 @@ TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenTheFinishSeparatedControlI
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
}
|
||||
|
||||
// ===================== EXPLICIT VERTEX INPUT LOCATION CEILING =====================
|
||||
|
||||
// The clean case, and the one that has to stay cheap: a driver whose compiler accepts the
|
||||
// highest location it advertises is measured in a single compile and withdraws nothing.
|
||||
TEST(DriverBugProbes, VertexInputLocationCeilingIsCleanWhenTheAdvertisedMaximumCompiles) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexAttribs = 32;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeExplicitVertexInputLocationCeiling(gl);
|
||||
EXPECT_FALSE(measurement.detected);
|
||||
EXPECT_EQ(measurement.advertisedMaxVertexAttribs, 32);
|
||||
EXPECT_EQ(measurement.usableLocations, 32) << "an unaffected driver must be taken at its word";
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The defect: 32 advertised, the qualifier refused from 16 up. The bisection has to land on the
|
||||
// boundary exactly - one off in either direction advertises an attribute that cannot be declared,
|
||||
// or withdraws one that can.
|
||||
TEST(DriverBugProbes, VertexInputLocationCeilingIsMeasuredWhenTheQualifierIsCapped) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexAttribs = 32;
|
||||
g_fake.explicitLocationCeiling = 16;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeExplicitVertexInputLocationCeiling(gl);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_EQ(measurement.advertisedMaxVertexAttribs, 32);
|
||||
EXPECT_EQ(measurement.usableLocations, 16);
|
||||
EXPECT_TRUE(measurement.bindAttribLocationReachesAdvertisedMax)
|
||||
<< "this driver caps only the qualifier, so the report may say the attribute is still reachable";
|
||||
EXPECT_NE(measurement.driverMessage.find("attribute range"), std::string::npos)
|
||||
<< "the driver's own wording is what makes the row evidence rather than an assertion";
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// A ceiling that is not a power of two, so a bisection that happened to land on 16 by arithmetic
|
||||
// rather than by measurement fails here.
|
||||
TEST(DriverBugProbes, VertexInputLocationCeilingBisectsToAnAwkwardBoundary) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexAttribs = 32;
|
||||
g_fake.explicitLocationCeiling = 23;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeExplicitVertexInputLocationCeiling(gl);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_EQ(measurement.usableLocations, 23);
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// THE FIRST CONTROL. A compiler that refuses location 0 refuses everything, and a probe that
|
||||
// read that as "only one location is usable" would withdraw every vertex attribute the device has.
|
||||
TEST(DriverBugProbes, VertexInputLocationCeilingReportsNothingWhenTheLocationZeroControlFails) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexAttribs = 32;
|
||||
g_fake.everyCompileFails = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeExplicitVertexInputLocationCeiling(gl);
|
||||
EXPECT_FALSE(measurement.detected);
|
||||
EXPECT_EQ(measurement.usableLocations, 32)
|
||||
<< "an inconclusive probe has to leave the advertised count exactly where it found it";
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// THE SECOND CONTROL, which does not change the clamp but does change what the report may claim:
|
||||
// a driver that cannot reach the location through glBindAttribLocation either has fewer
|
||||
// attributes than it advertises, rather than merely an unspellable half.
|
||||
TEST(DriverBugProbes, VertexInputLocationCeilingSaysWhenTheAttributeIsUnreachableAnyWay) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexAttribs = 32;
|
||||
g_fake.explicitLocationCeiling = 16;
|
||||
g_fake.bindAttribLocationCeiling = 16;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeExplicitVertexInputLocationCeiling(gl);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_EQ(measurement.usableLocations, 16);
|
||||
EXPECT_FALSE(measurement.bindAttribLocationReachesAdvertisedMax);
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// ===================== LAYERED BLIT DESTINATION =====================
|
||||
|
||||
TEST(DriverBugProbes, LayeredBlitDestinationIsCleanWhenTheLayerIsHonoured) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeBlitIgnoresDestinationArrayLayer(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, LayeredBlitDestinationIsDetectedWhenTheCopyLandsOnLayerZero) {
|
||||
ResetFakeDriver();
|
||||
g_fake.blitIgnoresDestinationLayer = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_TRUE(ProbeBlitIgnoresDestinationArrayLayer(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// THE CONTROL. A driver that ignores the SOURCE layer too cannot address array layers through a
|
||||
// framebuffer at all - a bigger defect, and one this probe is not entitled to report as its own.
|
||||
// The control blit onto destination layer 0 is what catches it: the value it looks for lives only
|
||||
// on the source's layer 1, so a source read pinned to layer 0 never produces it.
|
||||
TEST(DriverBugProbes, LayeredBlitDestinationReportsNothingWhenTheSourceLayerIsIgnoredToo) {
|
||||
ResetFakeDriver();
|
||||
g_fake.blitIgnoresDestinationLayer = true;
|
||||
g_fake.blitIgnoresSourceLayer = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeBlitIgnoresDestinationArrayLayer(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// And the shape that is not this bug at all: a blit that moves nothing anywhere. The probe's
|
||||
// subject then finds its magic byte on no layer, which is "reached no verdict", not "landed on 0".
|
||||
TEST(DriverBugProbes, LayeredBlitDestinationReportsNothingWhenTheBlitMovesNothing) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
MG_External::GLESFunctionsTable inert = gl;
|
||||
inert.glBlitFramebuffer = [](GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield,
|
||||
GLenum) {};
|
||||
EXPECT_FALSE(ProbeBlitIgnoresDestinationArrayLayer(inert));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
|
||||
ResetFakeDriver();
|
||||
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Loader.h"
|
||||
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
@@ -1064,6 +1065,16 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I("OpenGL ES capabilities:");
|
||||
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
|
||||
MGLOG_I(" GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: %d", caps.UniformBufferOffsetAlignment);
|
||||
// ES 3.1 core, so no extension gate - but a driver that somehow leaves it at zero would
|
||||
// make every storage-range offset legal, so an unusable answer keeps the 256 default.
|
||||
GLint shaderStorageOffsetAlignment = 0;
|
||||
glesFuncs.glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &shaderStorageOffsetAlignment);
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (shaderStorageOffsetAlignment > 0) {
|
||||
caps.ShaderStorageBufferOffsetAlignment = shaderStorageOffsetAlignment;
|
||||
}
|
||||
MGLOG_I(" GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT: %d", caps.ShaderStorageBufferOffsetAlignment);
|
||||
GLfloat aliasedLineWidthRange[2] = {1.0f, 1.0f};
|
||||
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
||||
GLfloat smoothLineWidthGranularity = 1.0f;
|
||||
@@ -1495,7 +1506,26 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
|
||||
caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
|
||||
caps.MaxCombinedTextureImageUnits = maxCombinedTextureImageUnits;
|
||||
caps.MaxVertexAttribs = maxVertexAttribs;
|
||||
// Not the driver's answer alone: MobileGL emits every vertex input as a
|
||||
// layout(location = N) qualifier, so an attribute the driver counts but its ESSL
|
||||
// compiler will not let anything DECLARE is not an attribute MobileGL can hand to an
|
||||
// application. The probe measures where the qualifier actually stops (see
|
||||
// SelfTest::ProbeExplicitVertexInputLocationCeiling - Adreno 830 advertises 32 and
|
||||
// refuses the qualifier from 16 up) and answers with the advertised count on every
|
||||
// driver that has no such gap and on any run that reaches no verdict, so this only ever
|
||||
// lowers the number, and only on evidence.
|
||||
const SelfTest::VertexInputLocationCeilingMeasurement& locationCeiling =
|
||||
SelfTest::ExplicitVertexInputLocationCeiling(glesFuncs);
|
||||
// Guarded on `detected` rather than on the number alone: a probe that reached no verdict
|
||||
// has measured nothing, and the clamp must be driven by evidence or not applied at all.
|
||||
caps.MaxVertexAttribs = locationCeiling.detected
|
||||
? std::min(maxVertexAttribs, locationCeiling.usableLocations)
|
||||
: maxVertexAttribs;
|
||||
if (locationCeiling.detected) {
|
||||
MGLOG_I(" GL_MAX_VERTEX_ATTRIBS reduced from the driver's %d to %d: "
|
||||
"layout(location = N) on a vertex input is refused from N = %d upward",
|
||||
maxVertexAttribs, caps.MaxVertexAttribs, locationCeiling.usableLocations);
|
||||
}
|
||||
caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
|
||||
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
|
||||
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
|
||||
|
||||
@@ -1231,6 +1231,10 @@ namespace MobileGL {
|
||||
// InstanceIndex, which includes firstInstance.
|
||||
Bool IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
// Its storage-buffer counterpart, queried separately because it is a separate limit:
|
||||
// Adreno 830 answers 32 for GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and 64 for
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT.
|
||||
Int ShaderStorageBufferOffsetAlignment = 256;
|
||||
Float AliasedLineWidthRangeMin = 1.0f;
|
||||
Float AliasedLineWidthRangeMax = 1.0f;
|
||||
Float SmoothLineWidthRangeMin = 1.0f;
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.DriverVersionString = DecodeDriverVersion(p.driverVersion);
|
||||
caps.VendorId = p.vendorID;
|
||||
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
|
||||
caps.ShaderStorageBufferOffsetAlignment = static_cast<int>(p.limits.minStorageBufferOffsetAlignment);
|
||||
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
|
||||
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
|
||||
caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy;
|
||||
@@ -272,6 +273,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.DriverVersionString = DecodeDriverVersion(properties.driverVersion);
|
||||
caps.VendorId = properties.vendorID;
|
||||
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
|
||||
caps.ShaderStorageBufferOffsetAlignment =
|
||||
static_cast<int>(properties.limits.minStorageBufferOffsetAlignment);
|
||||
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
||||
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
||||
caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy;
|
||||
|
||||
@@ -18,6 +18,10 @@ namespace MobileGL {
|
||||
// VkPhysicalDeviceProperties::vendorID, for device-quirk vendor gating.
|
||||
Uint32 VendorId = 0;
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
// VkPhysicalDeviceLimits::minStorageBufferOffsetAlignment. A separate limit from
|
||||
// the uniform one on Vulkan too, and the one GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT
|
||||
// has to answer with.
|
||||
Int ShaderStorageBufferOffsetAlignment = 256;
|
||||
Float AliasedLineWidthRangeMin = 1.0f;
|
||||
Float AliasedLineWidthRangeMax = 1.0f;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. Whether it can be used at all depends on
|
||||
|
||||
@@ -122,6 +122,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
GLint activeTexture = GL_TEXTURE0;
|
||||
GLint texture2D = 0;
|
||||
GLint texture2DMultisample = 0;
|
||||
GLint texture2DArray = 0;
|
||||
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
GLint packAlignment = 4;
|
||||
GLint packRowLength = 0;
|
||||
@@ -153,8 +154,15 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
state.depthTest = gl.glIsEnabled(GL_DEPTH_TEST);
|
||||
state.blend = gl.glIsEnabled(GL_BLEND);
|
||||
gl.glGetIntegerv(GL_ACTIVE_TEXTURE, &state.activeTexture);
|
||||
// Unit 0 is selected BEFORE the per-unit bindings are read, because Restore puts them
|
||||
// back on unit 0 unconditionally. Reading them off whatever unit happened to be
|
||||
// active and writing them to unit 0 would corrupt unit 0's binding for whoever runs
|
||||
// next - harmless while every probe ran from the POST screen with nothing else using
|
||||
// the context, and not harmless now that one of them runs from a live draw path.
|
||||
if (gl.glActiveTexture != nullptr) gl.glActiveTexture(GL_TEXTURE0);
|
||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.texture2D);
|
||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &state.texture2DMultisample);
|
||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray);
|
||||
gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment);
|
||||
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
|
||||
if (gl.glGetFloatv != nullptr) {
|
||||
@@ -205,6 +213,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
gl.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(state.texture2D));
|
||||
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE,
|
||||
static_cast<GLuint>(state.texture2DMultisample));
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, static_cast<GLuint>(state.texture2DArray));
|
||||
}
|
||||
gl.glActiveTexture(static_cast<GLenum>(state.activeTexture));
|
||||
}
|
||||
@@ -1348,6 +1357,365 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== LAYERED BLIT DESTINATION =====================
|
||||
|
||||
constexpr const char* kLayeredBlitProbeName = "layered blit destination";
|
||||
// Four texels wide: a 1x1 blit is a shape drivers special-case, and a rectangle keeps
|
||||
// the probe on the ordinary path. Two layers is all the question needs.
|
||||
constexpr GLsizei kLayeredBlitSize = 4;
|
||||
constexpr GLsizei kLayeredBlitLayers = 2;
|
||||
|
||||
// One RGBA8 2D array whose every layer is filled with a distinguishable byte.
|
||||
GLuint MakeLayeredBlitTexture(const GLESFunctionsTable& gl, GLubyte layer0, GLubyte layer1) {
|
||||
GLuint texture = 0;
|
||||
gl.glGenTextures(1, &texture);
|
||||
if (texture == 0) return 0;
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
gl.glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kLayeredBlitSize, kLayeredBlitSize,
|
||||
kLayeredBlitLayers);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
const GLubyte fills[kLayeredBlitLayers] = {layer0, layer1};
|
||||
for (GLint layer = 0; layer < kLayeredBlitLayers; ++layer) {
|
||||
GLubyte texels[kLayeredBlitSize * kLayeredBlitSize * 4];
|
||||
for (SizeT i = 0; i < sizeof(texels); i += 4) {
|
||||
texels[i + 0] = fills[layer];
|
||||
texels[i + 1] = fills[layer];
|
||||
texels[i + 2] = fills[layer];
|
||||
texels[i + 3] = 255;
|
||||
}
|
||||
gl.glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kLayeredBlitSize, kLayeredBlitSize, 1,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, texels);
|
||||
}
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A framebuffer naming exactly one layer of one array texture.
|
||||
GLuint MakeLayeredBlitFramebuffer(const GLESFunctionsTable& gl, GLuint texture, GLint layer) {
|
||||
GLuint framebuffer = 0;
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
if (framebuffer == 0) return 0;
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, layer);
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
return 0;
|
||||
}
|
||||
return framebuffer;
|
||||
}
|
||||
|
||||
// The red byte of texel (0, 0) of one layer, read through a framebuffer that names it.
|
||||
// 256 is "could not read", which no fill value can be.
|
||||
Int ReadLayeredBlitTexel(const GLESFunctionsTable& gl, GLuint texture, GLint layer) {
|
||||
const GLuint framebuffer = MakeLayeredBlitFramebuffer(gl, texture, layer);
|
||||
if (framebuffer == 0) return 256;
|
||||
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
gl.glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
GLubyte pixel[4] = {0, 0, 0, 0};
|
||||
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
Drain(gl);
|
||||
return static_cast<Int>(pixel[0]);
|
||||
}
|
||||
|
||||
// Blits source layer 1 onto `destinationLayer` of a freshly filled destination and
|
||||
// reports which layer actually received it, or -1 when the blit could not be issued.
|
||||
Int LayeredBlitLandsOnLayer(const GLESFunctionsTable& gl, GLint destinationLayer, GLubyte magic) {
|
||||
const GLuint source = MakeLayeredBlitTexture(gl, 0x11, magic);
|
||||
const GLuint destination = MakeLayeredBlitTexture(gl, 0x33, 0x44);
|
||||
const GLuint sourceFramebuffer = MakeLayeredBlitFramebuffer(gl, source, 1);
|
||||
const GLuint destinationFramebuffer = MakeLayeredBlitFramebuffer(gl, destination, destinationLayer);
|
||||
Int landedOn = -1;
|
||||
if (source != 0 && destination != 0 && sourceFramebuffer != 0 && destinationFramebuffer != 0) {
|
||||
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFramebuffer);
|
||||
Drain(gl);
|
||||
gl.glBlitFramebuffer(0, 0, kLayeredBlitSize, kLayeredBlitSize, 0, 0, kLayeredBlitSize,
|
||||
kLayeredBlitSize, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
if (gl.glGetError() == GL_NO_ERROR) {
|
||||
landedOn = -2; // issued, but seen on no layer yet
|
||||
for (GLint layer = 0; layer < kLayeredBlitLayers; ++layer) {
|
||||
if (ReadLayeredBlitTexel(gl, destination, layer) == static_cast<Int>(magic)) {
|
||||
landedOn = layer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sourceFramebuffer != 0) gl.glDeleteFramebuffers(1, &sourceFramebuffer);
|
||||
if (destinationFramebuffer != 0) gl.glDeleteFramebuffers(1, &destinationFramebuffer);
|
||||
if (source != 0) gl.glDeleteTextures(1, &source);
|
||||
if (destination != 0) gl.glDeleteTextures(1, &destination);
|
||||
Drain(gl);
|
||||
return landedOn;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ProbeBlitIgnoresDestinationArrayLayer(const GLESFunctionsTable& gl) {
|
||||
if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexStorage3D || !gl.glTexSubImage3D ||
|
||||
!gl.glTexParameteri || !gl.glDeleteTextures || !gl.glGenFramebuffers || !gl.glBindFramebuffer ||
|
||||
!gl.glFramebufferTextureLayer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers ||
|
||||
!gl.glBlitFramebuffer || !gl.glReadBuffer || !gl.glReadPixels || !gl.glPixelStorei || !gl.glGetError ||
|
||||
!gl.glIsEnabled || !gl.glDisable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SavedState saved;
|
||||
Save(gl, saved);
|
||||
// A scissor left on by whoever ran before would clip the probe's own blit and make a
|
||||
// working driver look broken.
|
||||
gl.glDisable(GL_SCISSOR_TEST);
|
||||
Drain(gl);
|
||||
|
||||
// THE CONTROL: the same blit onto destination layer 0, which is the case no
|
||||
// implementation gets wrong. It also proves the SOURCE layer is honoured, since the
|
||||
// magic byte it looks for only exists on source layer 1 - so a driver that cannot blit
|
||||
// between array layers at all, or that has no working glFramebufferTextureLayer, fails
|
||||
// here and reaches no verdict rather than being reported as having this bug.
|
||||
const Int controlLanded = LayeredBlitLandsOnLayer(gl, 0, 0x5Au);
|
||||
Bool detected = false;
|
||||
if (controlLanded != 0) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the destination-layer-0 control "
|
||||
"landed on layer %d instead of 0)",
|
||||
kLayeredBlitProbeName, controlLanded);
|
||||
} else {
|
||||
// THE SUBJECT: the identical blit asking for layer 1. Only the destination layer moved.
|
||||
const Int subjectLanded = LayeredBlitLandsOnLayer(gl, 1, 0x5Au);
|
||||
detected = subjectLanded == 0;
|
||||
if (subjectLanded != 0 && subjectLanded != 1) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the subject blit landed on "
|
||||
"no layer at all: %d)",
|
||||
kLayeredBlitProbeName, subjectLanded);
|
||||
} else {
|
||||
MGLOG_I("[driver-bug] %s probe: a blit asking for destination layer 1 landed on "
|
||||
"layer %d%s",
|
||||
kLayeredBlitProbeName, subjectLanded,
|
||||
detected ? " - THE DESTINATION LAYER IS IGNORED" : "");
|
||||
}
|
||||
}
|
||||
|
||||
Restore(gl, saved);
|
||||
return detected;
|
||||
}
|
||||
|
||||
Bool BlitIgnoresDestinationArrayLayer(const GLESFunctionsTable& gl) {
|
||||
// One driver per process, and the answer is structural rather than sampled.
|
||||
static const Bool ignored = ProbeBlitIgnoresDestinationArrayLayer(gl);
|
||||
return ignored;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Optional<DriverBugFinding> ProbeLayeredBlitDestinationBug(const GLESFunctionsTable& gl) {
|
||||
if (!BlitIgnoresDestinationArrayLayer(gl)) return std::nullopt;
|
||||
return DriverBugFinding{
|
||||
"glBlitFramebuffer ignores the destination array layer",
|
||||
DriverBugVerdict::Fixed,
|
||||
"a glBlitFramebuffer whose DRAW framebuffer attaches a non-zero array layer with "
|
||||
"glFramebufferTextureLayer writes to layer 0 instead, and raises no error doing "
|
||||
"it. Measured here on the colour aspect; the depth aspect behaves the same way on "
|
||||
"the device this was characterised on. The layer is honoured everywhere else on "
|
||||
"the same driver - the blit's own SOURCE layer is read correctly, which is this "
|
||||
"probe's control - so neither layered attachments nor blitting is withdrawn. "
|
||||
"MobileGL performs such a blit with glCopyImageSubData instead, which takes the "
|
||||
"destination layer explicitly and honours it here, and applies that substitute to "
|
||||
"the depth and stencil aspects as well; a blit that scales, flips, changes format, "
|
||||
"resolves samples or is clipped by the scissor cannot be expressed as a copy and "
|
||||
"is still handed to the driver"};
|
||||
}
|
||||
|
||||
// ===================== EXPLICIT VERTEX INPUT LOCATION CEILING =====================
|
||||
|
||||
constexpr const char* kAttributeLocationProbeName = "explicit vertex input location";
|
||||
|
||||
// COMPILES ONE VERTEX STAGE and reports nothing else. A link would drag in every other
|
||||
// reason a program can be refused (varying budgets, the fragment stage, the linker's own
|
||||
// location rules), and the defect this measures is in the driver's ESSL COMPILER: it
|
||||
// rejects the declaration itself, before any of that can matter.
|
||||
Bool ExplicitVertexInputLocationCompiles(const GLESFunctionsTable& gl, Int location,
|
||||
String* firstRejectionMessage) {
|
||||
const String source = format("#version 320 es\n"
|
||||
"layout(location = {}) in vec4 a_probe;\n"
|
||||
"void main() {{ gl_Position = a_probe; }}\n",
|
||||
location);
|
||||
Drain(gl);
|
||||
const GLuint shader = gl.glCreateShader(GL_VERTEX_SHADER);
|
||||
if (shader == 0) return false;
|
||||
const char* text = source.c_str();
|
||||
gl.glShaderSource(shader, 1, &text, nullptr);
|
||||
gl.glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE && firstRejectionMessage != nullptr && firstRejectionMessage->empty()) {
|
||||
char log[256] = {0};
|
||||
gl.glGetShaderInfoLog(shader, static_cast<GLsizei>(sizeof(log) - 1), nullptr, log);
|
||||
// One line: the driver's own wording is the report's whole evidential value, and
|
||||
// the rest of the log is the same sentence repeated per declaration.
|
||||
String message = log;
|
||||
if (const SizeT newline = message.find('\n'); newline != String::npos) {
|
||||
message.resize(newline);
|
||||
}
|
||||
while (!message.empty() && (message.back() == ' ' || message.back() == '\r')) message.pop_back();
|
||||
*firstRejectionMessage = Move(message);
|
||||
}
|
||||
gl.glDeleteShader(shader);
|
||||
Drain(gl);
|
||||
return compiled != GL_FALSE;
|
||||
}
|
||||
|
||||
// THE SECOND CONTROL, and the one that decides whether the cap is about the LAYOUT
|
||||
// QUALIFIER or about the attribute itself. The same input, declared with no qualifier at
|
||||
// all and placed by glBindAttribLocation instead. If this links and glGetAttribLocation
|
||||
// answers with the location asked for, the driver can address that attribute perfectly
|
||||
// well and only the qualifier path is capped - which is what makes clamping the
|
||||
// advertised count the right response rather than a shrug. If it fails too, the driver
|
||||
// genuinely has fewer attributes than it advertises; the clamp is still correct, but the
|
||||
// report must not claim the attribute is reachable another way.
|
||||
Bool BindAttribLocationReaches(const GLESFunctionsTable& gl, Int location) {
|
||||
if (!gl.glCreateProgram || !gl.glAttachShader || !gl.glBindAttribLocation || !gl.glLinkProgram ||
|
||||
!gl.glGetProgramiv || !gl.glGetAttribLocation || !gl.glDeleteProgram) {
|
||||
return false;
|
||||
}
|
||||
constexpr const char* kVertexSource = "#version 320 es\n"
|
||||
"in vec4 a_probe;\n"
|
||||
"void main() { gl_Position = a_probe; }\n";
|
||||
constexpr const char* kFragmentSource = "#version 320 es\n"
|
||||
"precision highp float;\n"
|
||||
"out vec4 o_color;\n"
|
||||
"void main() { o_color = vec4(1.0); }\n";
|
||||
Drain(gl);
|
||||
const GLuint vertexShader =
|
||||
CompileStage(gl, GL_VERTEX_SHADER, kVertexSource, "vertex", kAttributeLocationProbeName);
|
||||
if (vertexShader == 0) return false;
|
||||
const GLuint fragmentShader =
|
||||
CompileStage(gl, GL_FRAGMENT_SHADER, kFragmentSource, "fragment", kAttributeLocationProbeName);
|
||||
if (fragmentShader == 0) {
|
||||
gl.glDeleteShader(vertexShader);
|
||||
return false;
|
||||
}
|
||||
const GLuint program = gl.glCreateProgram();
|
||||
gl.glAttachShader(program, vertexShader);
|
||||
gl.glAttachShader(program, fragmentShader);
|
||||
gl.glBindAttribLocation(program, static_cast<GLuint>(location), "a_probe");
|
||||
gl.glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
gl.glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
const Bool reached = linked != GL_FALSE && gl.glGetAttribLocation(program, "a_probe") == location;
|
||||
gl.glDeleteShader(vertexShader);
|
||||
gl.glDeleteShader(fragmentShader);
|
||||
gl.glDeleteProgram(program);
|
||||
Drain(gl);
|
||||
return reached;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
VertexInputLocationCeilingMeasurement ProbeExplicitVertexInputLocationCeiling(const GLESFunctionsTable& gl) {
|
||||
VertexInputLocationCeilingMeasurement measurement;
|
||||
// `usableLocations` is the number a caller clamps to, so it carries the driver's own
|
||||
// answer from the first line onward and every early return below leaves it there. A
|
||||
// probe that cannot run has to withdraw nothing at all, and a zero here would withdraw
|
||||
// every attribute the device has.
|
||||
if (gl.glGetIntegerv != nullptr) {
|
||||
GLint advertisedEarly = 0;
|
||||
gl.glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &advertisedEarly);
|
||||
if (gl.glGetError != nullptr) Drain(gl);
|
||||
measurement.advertisedMaxVertexAttribs = advertisedEarly;
|
||||
measurement.usableLocations = advertisedEarly;
|
||||
}
|
||||
if (!gl.glCreateShader || !gl.glShaderSource || !gl.glCompileShader || !gl.glGetShaderiv ||
|
||||
!gl.glGetShaderInfoLog || !gl.glDeleteShader || !gl.glGetIntegerv || !gl.glGetError) {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
const GLint advertised = measurement.advertisedMaxVertexAttribs;
|
||||
// Nothing to bisect, and nothing a clamp could usefully say.
|
||||
if (advertised < 2) return measurement;
|
||||
|
||||
// THE CONTROL, and the reason a compiler that is simply unavailable cannot be reported as
|
||||
// this bug: location 0 is the one every ES driver in existence accepts, so a probe that
|
||||
// cannot compile even that has measured its own failure, not the driver's.
|
||||
if (!ExplicitVertexInputLocationCompiles(gl, 0, nullptr)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the location-0 control did not "
|
||||
"compile, so nothing higher says anything)",
|
||||
kAttributeLocationProbeName);
|
||||
return measurement;
|
||||
}
|
||||
|
||||
// The common case is one compile: a conforming driver takes the highest location it
|
||||
// advertises and the probe stops there.
|
||||
if (ExplicitVertexInputLocationCompiles(gl, advertised - 1, nullptr)) return measurement;
|
||||
|
||||
// Bisect for the highest location that still compiles. `low` always compiles (the control
|
||||
// proved location 0 does) and `high` never does, so the loop closes on the boundary in
|
||||
// ceil(log2(advertised)) compiles - five for the 32 attributes Adreno advertises.
|
||||
String rejectionMessage;
|
||||
ExplicitVertexInputLocationCompiles(gl, advertised - 1, &rejectionMessage);
|
||||
Int low = 0;
|
||||
Int high = advertised - 1;
|
||||
while (high - low > 1) {
|
||||
const Int middle = low + (high - low) / 2;
|
||||
if (ExplicitVertexInputLocationCompiles(gl, middle, &rejectionMessage)) {
|
||||
low = middle;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
|
||||
measurement.detected = true;
|
||||
measurement.usableLocations = low + 1;
|
||||
measurement.driverMessage = Move(rejectionMessage);
|
||||
measurement.bindAttribLocationReachesAdvertisedMax = BindAttribLocationReaches(gl, advertised - 1);
|
||||
MGLOG_I("[driver-bug] %s probe: GL_MAX_VERTEX_ATTRIBS is %d but layout(location = N) on a "
|
||||
"vertex input is refused from N = %d upward - only %d location(s) are usable; "
|
||||
"glBindAttribLocation(%d) %s%s%s",
|
||||
kAttributeLocationProbeName, advertised, measurement.usableLocations,
|
||||
measurement.usableLocations, advertised - 1,
|
||||
measurement.bindAttribLocationReachesAdvertisedMax ? "still resolves correctly"
|
||||
: "does not resolve either",
|
||||
measurement.driverMessage.empty() ? "" : "; the driver says: ",
|
||||
measurement.driverMessage.c_str());
|
||||
return measurement;
|
||||
}
|
||||
|
||||
const VertexInputLocationCeilingMeasurement& ExplicitVertexInputLocationCeiling(const GLESFunctionsTable& gl) {
|
||||
static const VertexInputLocationCeilingMeasurement measurement =
|
||||
ProbeExplicitVertexInputLocationCeiling(gl);
|
||||
return measurement;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
|
||||
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
|
||||
if (!measurement.detected) return std::nullopt;
|
||||
String detail =
|
||||
format("GL_MAX_VERTEX_ATTRIBS is {} but the ESSL compiler refuses "
|
||||
"layout(location = N) on a vertex input for every N at or above {} - so {} of "
|
||||
"the {} attributes advertised cannot be declared at all",
|
||||
measurement.advertisedMaxVertexAttribs, measurement.usableLocations,
|
||||
measurement.advertisedMaxVertexAttribs - measurement.usableLocations,
|
||||
measurement.advertisedMaxVertexAttribs);
|
||||
if (!measurement.driverMessage.empty()) {
|
||||
detail += format(" - the driver says \"{}\"", measurement.driverMessage);
|
||||
}
|
||||
detail += measurement.bindAttribLocationReachesAdvertisedMax
|
||||
? format(". The same driver ACCEPTS glBindAttribLocation({}) on an unqualified "
|
||||
"input and resolves it correctly, so the attributes are there and only "
|
||||
"the layout qualifier is capped",
|
||||
measurement.advertisedMaxVertexAttribs - 1)
|
||||
: ". glBindAttribLocation does not reach those locations either, so the "
|
||||
"attributes appear genuinely absent rather than merely unspellable";
|
||||
detail += format(". MobileGL emits its vertex inputs as layout qualifiers, so it advertises the "
|
||||
"{} locations it can actually deliver rather than the {} the driver claims. An "
|
||||
"application asking for more used to be handed a count it could not build a "
|
||||
"shader against, which failed at the stage compile with no way back",
|
||||
measurement.usableLocations, measurement.advertisedMaxVertexAttribs);
|
||||
return DriverBugFinding{"Vertex input layout(location) capped below GL_MAX_VERTEX_ATTRIBS",
|
||||
DriverBugVerdict::Fixed, Move(detail)};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) {
|
||||
if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt;
|
||||
return DriverBugFinding{
|
||||
@@ -1448,6 +1816,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
&ProbeImageLocationPerNameBug,
|
||||
&ProbeCrossStageImageQualifierMergeBug,
|
||||
&ProbeImageCoherencyResidualBug,
|
||||
&ProbeExplicitVertexInputLocationCeilingBug,
|
||||
&ProbeLayeredBlitDestinationBug,
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -55,6 +55,74 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
String detail;
|
||||
};
|
||||
|
||||
// Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the
|
||||
// copy landed where it was asked to. Returns true only when the destination layer is ignored
|
||||
// while the control lands correctly.
|
||||
//
|
||||
// Adreno 830 writes to layer 0 whatever layer the DRAW framebuffer's
|
||||
// glFramebufferTextureLayer attachment names, for colour and depth alike, and raises no
|
||||
// error. Everything else about the layer works on the same driver, which is what makes this
|
||||
// a blit defect rather than a layered-attachment one.
|
||||
//
|
||||
// THE CONTROL is the same blit onto destination layer 0. It passes on every implementation
|
||||
// that can blit between array layers at all, and because the value it looks for exists only
|
||||
// on the SOURCE's layer 1 it also proves the source layer is honoured - so a driver with no
|
||||
// working glFramebufferTextureLayer reaches no verdict instead of being reported as this.
|
||||
//
|
||||
// Returns false when an entry point is missing, when the probe's own framebuffers come back
|
||||
// incomplete, or when the control fails. Restores every piece of GL state it touches.
|
||||
Bool ProbeBlitIgnoresDestinationArrayLayer(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeBlitIgnoresDestinationArrayLayer(), evaluated at most once per process.
|
||||
Bool BlitIgnoresDestinationArrayLayer(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// What the vertex-input location probe measured. The ceiling is reported rather than
|
||||
// hard-coded: it is a driver property, and a clamp derived from a number measured on some
|
||||
// other device is exactly the hard-coded vendor quirk this file exists to avoid.
|
||||
struct VertexInputLocationCeilingMeasurement {
|
||||
Bool detected = false;
|
||||
// GL_MAX_VERTEX_ATTRIBS as the driver answers it.
|
||||
Int advertisedMaxVertexAttribs = 0;
|
||||
// How many locations `layout(location = N)` on a vertex input actually accepts, i.e. the
|
||||
// highest N that compiles plus one. Equal to advertisedMaxVertexAttribs when the driver
|
||||
// is not affected, and when the probe reached no verdict - so a caller can clamp to it
|
||||
// unconditionally and an inconclusive probe changes nothing.
|
||||
Int usableLocations = 0;
|
||||
// Whether glBindAttribLocation(advertisedMaxVertexAttribs - 1) still links and resolves.
|
||||
// Only measured when `detected`; see the second control in the .cpp for why it decides
|
||||
// what the finding is allowed to claim.
|
||||
Bool bindAttribLocationReachesAdvertisedMax = false;
|
||||
// The first line of the driver's compile log for a refused declaration, so the report
|
||||
// quotes the driver rather than paraphrasing it.
|
||||
String driverMessage;
|
||||
};
|
||||
|
||||
// Compiles `layout(location = N) in vec4` on its own at a series of N and finds the highest
|
||||
// one the driver's ESSL compiler accepts.
|
||||
//
|
||||
// Adreno 830 advertises GL_MAX_VERTEX_ATTRIBS = 32 and then refuses the qualifier for every
|
||||
// N >= 16 ("the location is not within attribute range [0, MAX_ATTRIBUTES-1]"), for float and
|
||||
// integer inputs alike - so half the attributes it advertises cannot be declared. MobileGL
|
||||
// emits vertex inputs as layout qualifiers, which makes the advertised count a promise it
|
||||
// cannot keep; the measured ceiling is what it advertises instead.
|
||||
//
|
||||
// TWO CONTROLS. Location 0 must compile, or the probe has measured its own failure rather
|
||||
// than the driver's. And glBindAttribLocation at the advertised maximum is tried separately,
|
||||
// because that is what separates "only the layout qualifier is capped" (which is what this
|
||||
// driver does) from "the attributes are not there at all" - two findings that justify the
|
||||
// same clamp but very different report text.
|
||||
//
|
||||
// Compile-only, and bisected: one shader compile on a conforming driver, about seven on an
|
||||
// affected one. Returns a measurement with `detected` false and `usableLocations` equal to
|
||||
// the advertised count when an entry point is missing or a control fails, so an
|
||||
// inconclusive probe never withdraws anything.
|
||||
VertexInputLocationCeilingMeasurement ProbeExplicitVertexInputLocationCeiling(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeExplicitVertexInputLocationCeiling(), evaluated at most once per process.
|
||||
const VertexInputLocationCeilingMeasurement& ExplicitVertexInputLocationCeiling(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Draws one point through VS+GS+FS whose geometry stage writes two storage buffers: one
|
||||
// BEFORE its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the
|
||||
// before-emit write lands and the after-emit write does not.
|
||||
|
||||
@@ -44,9 +44,10 @@ namespace MobileGL {
|
||||
//
|
||||
// Why not simply rebase the offsets to zero and bind the buffer 8 bytes in: because
|
||||
// glBindBufferRange's offset must be a multiple of
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which the target device reports as 32.
|
||||
// A byte offset of 8 cannot be expressed as a binding at all, so the correction has
|
||||
// to live in the shader's indexing, where it costs nothing.
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is 64 on Adreno 830 and no smaller
|
||||
// than 32 on the other targets. A byte offset of 8 cannot be expressed as a binding
|
||||
// on any of them, so the correction has to live in the shader's indexing, where it
|
||||
// costs nothing.
|
||||
//
|
||||
// A block that is ALREADY laid out naturally - which is every shader that omits the
|
||||
// offset qualifier, and so very nearly all of them - is left byte-identical: the
|
||||
|
||||
Reference in New Issue
Block a user