mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +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 {
|
struct DynamicBackendParameters {
|
||||||
SizeT UniformBufferOffsetAlignment = 256;
|
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,
|
// 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.
|
// which is also why the extension is not advertised in that case.
|
||||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||||
|
|||||||
@@ -1323,6 +1323,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||||
|
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
|
||||||
|
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
||||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.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/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||||
@@ -5141,6 +5142,164 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
DrainBlitErrors();
|
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,
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||||
GLint dstY1, GLbitfield mask, GLenum filter) {
|
GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
#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,
|
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());
|
dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
|
||||||
|
// 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);
|
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||||
|
}
|
||||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
||||||
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
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,
|
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());
|
dstX0, dstY0, dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
|
||||||
|
// 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);
|
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||||
|
}
|
||||||
// Debug-only diagnostics: which GLES depth texture did this blit write?
|
// Debug-only diagnostics: which GLES depth texture did this blit write?
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
if (mask & GL_DEPTH_BUFFER_BIT) {
|
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
|
// 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
|
// 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
|
// 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
|
// has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 64 on Adreno 830 and 32 or
|
||||||
// 8-byte bind offset is not expressible). BEFORE SetAtomicCounterBlockBindings
|
// 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.
|
// 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
|
// 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;
|
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
||||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||||
|
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
|
||||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
// 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,
|
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||||
CompileOptionFlags flags) const {
|
CompileOptionFlags flags) const {
|
||||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||||
@@ -2956,6 +2999,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static_cast<Int>(numericDomain));
|
static_cast<Int>(numericDomain));
|
||||||
entry.samplerNumericDomainByBinding[binding] = 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 ||
|
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
|
||||||
entry.samplerUniformLocationByBinding[binding] == location,
|
entry.samplerUniformLocationByBinding[binding] == location,
|
||||||
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
|
"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 VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
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
|
// 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
|
// 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
|
// 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_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||||
#include "MG_State/GLState/Core.h"
|
#include "MG_State/GLState/Core.h"
|
||||||
#include "MG_State/GLState/ProgramState/ProgramObject.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/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/TextureObjectBuffer.h"
|
||||||
|
#include "MG_State/GLState/TextureState/TextureObjectStubs.h"
|
||||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
#include <vulkan/utility/vk_format_utils.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -28,6 +33,136 @@
|
|||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
namespace {
|
namespace {
|
||||||
constexpr Uint kFallbackTexture2DExternalIndex = 0xFFFFFF00u;
|
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,
|
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;
|
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,
|
ProgramFactory* programFactory,
|
||||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||||
Uint32 maxBindings, Uint32 setsPerFrame,
|
Uint32 maxBindings, Uint32 setsPerFrame,
|
||||||
@@ -123,6 +259,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Shutdown();
|
Shutdown();
|
||||||
|
|
||||||
MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "UniformDescriptorBinder::Initialize requires valid VkDevice");
|
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(bufferManager != nullptr, "UniformDescriptorBinder::Initialize requires valid buffer manager");
|
||||||
MOBILEGL_ASSERT(programFactory != nullptr,
|
MOBILEGL_ASSERT(programFactory != nullptr,
|
||||||
"UniformDescriptorBinder::Initialize requires valid program factory");
|
"UniformDescriptorBinder::Initialize requires valid program factory");
|
||||||
@@ -135,6 +273,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"UniformDescriptorBinder::Initialize requires valid sampler manager");
|
"UniformDescriptorBinder::Initialize requires valid sampler manager");
|
||||||
|
|
||||||
m_device = device;
|
m_device = device;
|
||||||
|
m_physicalDevice = physicalDevice;
|
||||||
m_bufferManager = bufferManager;
|
m_bufferManager = bufferManager;
|
||||||
m_programFactory = programFactory;
|
m_programFactory = programFactory;
|
||||||
m_minDynamicOffsetAlignment = std::max<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
|
m_minDynamicOffsetAlignment = std::max<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
|
||||||
@@ -173,6 +312,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UniformManager::Shutdown() {
|
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) {
|
for (auto& frame : m_frames) {
|
||||||
if (m_device != VK_NULL_HANDLE) {
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
for (auto& view : frame.texelBufferViews) {
|
for (auto& view : frame.texelBufferViews) {
|
||||||
@@ -199,6 +349,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_bufferManager = nullptr;
|
m_bufferManager = nullptr;
|
||||||
m_programFactory = nullptr;
|
m_programFactory = nullptr;
|
||||||
m_device = VK_NULL_HANDLE;
|
m_device = VK_NULL_HANDLE;
|
||||||
|
m_physicalDevice = VK_NULL_HANDLE;
|
||||||
m_minDynamicOffsetAlignment = 1;
|
m_minDynamicOffsetAlignment = 1;
|
||||||
m_frameCount = 0;
|
m_frameCount = 0;
|
||||||
m_maxBindings = 0;
|
m_maxBindings = 0;
|
||||||
@@ -693,12 +844,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null");
|
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null");
|
||||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "ResolveTexelBufferDescriptor: frame index out of range");
|
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;
|
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
||||||
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
||||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
// 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());
|
programObj.samplerNameByBinding[binding].c_str());
|
||||||
return false;
|
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 ||
|
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||||
@@ -712,10 +881,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||||
if (bufferObject == nullptr) {
|
if (bufferObject == nullptr) {
|
||||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
// A buffer texture with no buffer object attached is INCOMPLETE, not illegal (GL 4.6
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
// 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;
|
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{};
|
BufferSlice slice{};
|
||||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
||||||
@@ -804,13 +985,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
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);
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||||
const auto& texture = imageBinding.Texture;
|
const auto& texture = imageBinding.Texture;
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
// An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero
|
||||||
binding);
|
// 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;
|
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 ||
|
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
||||||
@@ -824,10 +1024,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||||
if (bufferObject == nullptr) {
|
if (bufferObject == nullptr) {
|
||||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
// Incomplete buffer texture, same as the sampled path: legal state, undefined data,
|
||||||
imageUnit);
|
// 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;
|
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
|
// Unlike the sampled texel buffer, the shader MAY write this one, and those writes land
|
||||||
// in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
|
// in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
|
||||||
@@ -852,8 +1063,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
|
// 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:
|
// 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.
|
// 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 auto internalFormat = textureBuffer->GetFormat();
|
||||||
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||||
@@ -1062,9 +1271,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||||
if (imageBinding.Texture == nullptr) {
|
if (imageBinding.Texture == nullptr) {
|
||||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
// 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;
|
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);
|
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
@@ -1155,6 +1396,138 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return m_fallbackTexture2D;
|
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,
|
Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Uint32 binding, Uint32 element,
|
Uint32 binding, Uint32 element,
|
||||||
@@ -1340,9 +1713,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
// ResolveStorageImageDescriptor will substitute the placeholder image for this
|
||||||
imageUnit, binding, element);
|
// binding; include it here for the same reason the sampled walk includes the
|
||||||
return false;
|
// 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()) {
|
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
|
||||||
outTextures.push_back(texture);
|
outTextures.push_back(texture);
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
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,
|
ProgramFactory* programFactory,
|
||||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||||
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
||||||
@@ -177,6 +180,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||||
|
// ---- 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
|
// `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
|
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||||
// override and the fallback separately from its neighbours.
|
// override and the fallback separately from its neighbours.
|
||||||
@@ -251,6 +280,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDescriptorSet& outDescriptorSet);
|
VkDescriptorSet& outDescriptorSet);
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
VkBufferManager* m_bufferManager = nullptr;
|
VkBufferManager* m_bufferManager = nullptr;
|
||||||
ProgramFactory* m_programFactory = nullptr;
|
ProgramFactory* m_programFactory = nullptr;
|
||||||
Vector<FrameResources> m_frames;
|
Vector<FrameResources> m_frames;
|
||||||
@@ -263,6 +293,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkTextureManager* m_textureManager = nullptr;
|
VkTextureManager* m_textureManager = nullptr;
|
||||||
VkSamplerManager* m_samplerManager = nullptr;
|
VkSamplerManager* m_samplerManager = nullptr;
|
||||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
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
|
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
// 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
|
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
|
||||||
// every minStorageBufferOffsetAlignment in the wild, and free.
|
// every minStorageBufferOffsetAlignment in the wild, and free.
|
||||||
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
|
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
|
// 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.
|
// 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_transientUploadArena.Shutdown();
|
||||||
m_unboundStorageBuffer.Destroy();
|
m_unboundStorageBuffer.Destroy();
|
||||||
|
m_unboundTexelBuffer.Destroy();
|
||||||
DestroyAllDeferredReleases();
|
DestroyAllDeferredReleases();
|
||||||
ReleaseAllLiveResources();
|
ReleaseAllLiveResources();
|
||||||
m_copyProvider = nullptr;
|
m_copyProvider = nullptr;
|
||||||
@@ -742,6 +748,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return m_unboundStorageBuffer.GetSlice();
|
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) {
|
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case BufferKind::Vertex:
|
case BufferKind::Vertex:
|
||||||
|
|||||||
@@ -131,6 +131,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// that indexes past it.
|
// that indexes past it.
|
||||||
BufferSlice AcquireUnboundStorageDescriptor();
|
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
|
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||||
// resource exists and is fully uploaded, marks it used this frame.
|
// resource exists and is fully uploaded, marks it used this frame.
|
||||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
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
|
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||||
// with the manager.
|
// with the manager.
|
||||||
VkBufferObject m_unboundStorageBuffer;
|
VkBufferObject m_unboundStorageBuffer;
|
||||||
|
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||||
|
VkBufferObject m_unboundTexelBuffer;
|
||||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||||
|
|||||||
@@ -3136,7 +3136,7 @@ void main() {
|
|||||||
m_uniformManager = MakeUnique<UniformManager>();
|
m_uniformManager = MakeUnique<UniformManager>();
|
||||||
MOBILEGL_ASSERT(m_uniformManager != nullptr, "UniformDescriptorBinder creation failed.");
|
MOBILEGL_ASSERT(m_uniformManager != nullptr, "UniformDescriptorBinder creation failed.");
|
||||||
succeeded = m_uniformManager->Initialize(
|
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,
|
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
||||||
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
||||||
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
||||||
|
|||||||
@@ -2406,7 +2406,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
|
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
|
||||||
break;
|
break;
|
||||||
case GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT:
|
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;
|
break;
|
||||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||||
params[0] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMin);
|
params[0] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMin);
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/StorageBufferRegrowScenario.cpp
|
Scenarios/StorageBufferRegrowScenario.cpp
|
||||||
Scenarios/RelinkStageSetScenario.cpp
|
Scenarios/RelinkStageSetScenario.cpp
|
||||||
Scenarios/GuiBatchScenario.cpp
|
Scenarios/GuiBatchScenario.cpp
|
||||||
|
Scenarios/UnboundImageDescriptorScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
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
|
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})
|
"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.
|
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||||
set(MGL_ITEST_TIMEOUT 120)
|
set(MGL_ITEST_TIMEOUT 120)
|
||||||
|
|
||||||
@@ -369,3 +404,100 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
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
|
// before the pre-flight forks - the child must measure the same platform
|
||||||
// the parent will use.
|
// the parent will use.
|
||||||
EnsureHeadlessPlatform();
|
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();
|
m_usable = BringUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,49 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include "HeadlessGL.h"
|
#include "HeadlessGL.h"
|
||||||
|
|
||||||
namespace MGITest {
|
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 {
|
class ScenarioTest : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
void SetUp() override {
|
void SetUp() override {
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
// be able to turn this into a red.
|
// be able to turn this into a red.
|
||||||
// (b) Forcing the join afterwards produces the right answer for every one of them:
|
// (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.
|
// 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
|
// (c) The extension string matches the configuration - where "the configuration" is
|
||||||
// trace can never cover - Iris and Sodium change their submission schedule the
|
// MOBILEGL_ASYNC_SHADER_COMPILE as this process inherited it, and NOT anything the
|
||||||
// moment they see the string - so it is asserted against a real backend's real
|
// implementation says about itself. This is the half a recorded trace can never
|
||||||
// GL_EXTENSIONS, through both glGetString and glGetStringi.
|
// 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
|
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
|
||||||
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
||||||
// is synchronous. That is what the extension requires of a zero count.
|
// 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),
|
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
|
||||||
// so this file runs twice per ctest invocation.
|
// 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 <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -47,9 +70,6 @@
|
|||||||
#include "../Harness/HeadlessGL.h"
|
#include "../Harness/HeadlessGL.h"
|
||||||
#include "../Harness/ScenarioFixture.h"
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
#include "Config.h"
|
|
||||||
#include "MG_Util/Async/ShaderCompilePool.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
#ifdef GLAPI
|
||||||
#undef GLAPI
|
#undef GLAPI
|
||||||
#endif
|
#endif
|
||||||
@@ -76,8 +96,6 @@ extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
|
|||||||
namespace MGITest {
|
namespace MGITest {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using MobileGL::MG_Config::QuirkOverride;
|
|
||||||
|
|
||||||
// Same shape as the other scenarios: a two-attribute pass-through, so the only
|
// 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.
|
// thing that can differ between the two compilation modes is the compilation.
|
||||||
constexpr const char* kVertexSource = R"(#version 330 core
|
constexpr const char* kVertexSource = R"(#version 330 core
|
||||||
@@ -139,50 +157,40 @@ void main() {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
|
// Whether this context advertises GL_KHR_parallel_shader_compile, which is exactly
|
||||||
// the other one says so here and gets the ambient one back on scope exit. Forcing
|
// "MobileGL is configured to compile asynchronously" as an application can see it:
|
||||||
// it in-process is what lets ONE ctest run compare the two modes against each
|
// the backends gate the string on AsyncShaderCompileEnabled() and on nothing else
|
||||||
// other - the whole point of (e).
|
// (BackendObject_DirectGLES.cpp / BackendObject_DirectVulkan.cpp), and the string
|
||||||
class AsyncModeScope {
|
// is the only way MobileGL ever tells anyone. A case that needs asynchronous
|
||||||
public:
|
// compilation checks for it the way an application would, and skips without it.
|
||||||
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
|
//
|
||||||
MobileGL::MG_Config::Features.AsyncShaderCompile =
|
// The INDEXED form, because that is the one a core-profile application reads.
|
||||||
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
|
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; }
|
return false;
|
||||||
AsyncModeScope(const AsyncModeScope&) = delete;
|
|
||||||
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
|
||||||
|
|
||||||
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:
|
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls it
|
||||||
const QuirkOverride m_saved;
|
// 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
|
||||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
// hand-rolled poke at the pool. glMaxShaderCompilerThreadsKHR(0xFFFFFFFF) is defined
|
||||||
// it has to put the pool back or it changes how every scenario after it compiles.
|
// (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 {
|
class CompilerThreadScope {
|
||||||
public:
|
public:
|
||||||
CompilerThreadScope() = default;
|
CompilerThreadScope() = default;
|
||||||
~CompilerThreadScope() {
|
~CompilerThreadScope() { glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); }
|
||||||
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
|
||||||
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
|
|
||||||
pool.SetMaxConcurrency(pool.GetThreadCount());
|
|
||||||
}
|
|
||||||
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
||||||
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
||||||
};
|
};
|
||||||
@@ -293,7 +301,12 @@ void main() {
|
|||||||
// interesting for shaders that (a) proved were genuinely still outstanding.
|
// interesting for shaders that (a) proved were genuinely still outstanding.
|
||||||
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
||||||
if (!Ready()) return;
|
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;
|
const CompilerThreadScope threads;
|
||||||
// One worker, so the queue behind it is what the poll observes.
|
// One worker, so the queue behind it is what the poll observes.
|
||||||
glMaxShaderCompilerThreadsKHR(1);
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
@@ -341,14 +354,33 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- (c) ------------------------------------------------------------------
|
// ---- (c) ------------------------------------------------------------------
|
||||||
// The extension string, read from a real backend that really brought a driver
|
// The extension string, read from a real backend that really brought a driver up.
|
||||||
// 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
|
// The expectation comes from the ENVIRONMENT, never from the implementation. This
|
||||||
// against the AMBIENT configuration - which is exactly what makes this case
|
// case used to derive it by calling AsyncShaderCompileEnabled() - which is the same
|
||||||
// worth running in both of the suite's flag states.
|
// 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) {
|
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
|
||||||
if (!Ready()) return;
|
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));
|
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||||
ASSERT_NE(extensions, nullptr);
|
ASSERT_NE(extensions, nullptr);
|
||||||
@@ -385,7 +417,12 @@ void main() {
|
|||||||
// A zero count must leave nothing in flight and keep it that way.
|
// A zero count must leave nothing in flight and keep it that way.
|
||||||
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
||||||
if (!Ready()) return;
|
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;
|
const CompilerThreadScope threads;
|
||||||
glMaxShaderCompilerThreadsKHR(1);
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
|
||||||
@@ -417,12 +454,28 @@ void main() {
|
|||||||
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
||||||
// backend's orientation and present path live, so the comparison covers the
|
// backend's orientation and present path live, so the comparison covers the
|
||||||
// whole pipeline rather than the reflection tables alone.
|
// 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) {
|
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
|
||||||
if (!Ready()) return;
|
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;
|
Image asyncImage;
|
||||||
{
|
{
|
||||||
const AsyncModeScope async(true);
|
glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu);
|
||||||
const GLuint program = BuildProgram();
|
const GLuint program = BuildProgram();
|
||||||
ASSERT_NE(program, 0u);
|
ASSERT_NE(program, 0u);
|
||||||
asyncImage = DrawFrameWith(program);
|
asyncImage = DrawFrameWith(program);
|
||||||
@@ -431,7 +484,7 @@ void main() {
|
|||||||
|
|
||||||
Image syncImage;
|
Image syncImage;
|
||||||
{
|
{
|
||||||
const AsyncModeScope async(false);
|
glMaxShaderCompilerThreadsKHR(0);
|
||||||
const GLuint program = BuildProgram();
|
const GLuint program = BuildProgram();
|
||||||
ASSERT_NE(program, 0u);
|
ASSERT_NE(program, 0u);
|
||||||
syncImage = DrawFrameWith(program);
|
syncImage = DrawFrameWith(program);
|
||||||
@@ -456,11 +509,16 @@ void main() {
|
|||||||
// candidate) shows up here and not in the single-program case above.
|
// candidate) shows up here and not in the single-program case above.
|
||||||
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
||||||
if (!Ready()) return;
|
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;
|
constexpr int kPrograms = 12;
|
||||||
|
|
||||||
std::vector<GLuint> programs;
|
std::vector<GLuint> programs;
|
||||||
{
|
{
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const CompilerThreadScope threads;
|
const CompilerThreadScope threads;
|
||||||
glMaxShaderCompilerThreadsKHR(1);
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
// Everything enqueued before anything is read: the only shape in which
|
// 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.
|
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||||
if (!Ready()) return;
|
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;
|
constexpr int kPrograms = 12;
|
||||||
|
|
||||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||||
@@ -508,8 +581,6 @@ void main() {
|
|||||||
|
|
||||||
std::vector<GLuint> programs;
|
std::vector<GLuint> programs;
|
||||||
{
|
{
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
|
||||||
const CompilerThreadScope threads;
|
const CompilerThreadScope threads;
|
||||||
glMaxShaderCompilerThreadsKHR(1);
|
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/HeadlessGL.h"
|
||||||
#include "../Harness/ScenarioFixture.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
|
#ifdef GLAPI
|
||||||
#undef GLAPI
|
#undef GLAPI
|
||||||
#endif
|
#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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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";
|
"gl_ViewportIndex natively and ignores it";
|
||||||
}
|
}
|
||||||
|
|
||||||
// The feature table is a process-global and this fixture shares its context with every
|
// The switch comes from the ENVIRONMENT, and this case runs only in a process that
|
||||||
// other scenario in the process, so the restore is not optional.
|
// was started with it off. It used to write MG_Config::Features directly, which is
|
||||||
struct ScopedEmulationOff {
|
// not available to it any more: on Android this module links the shipping
|
||||||
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
|
// libMobileGL.so - so that the on-device run validates the real artifact - and that
|
||||||
MobileGL::MG_Config::Features.ViewportArrayEmulation =
|
// library exports no such symbol. The process-wide variable is also the more honest
|
||||||
MobileGL::MG_Config::QuirkOverride::ForceOff;
|
// 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";
|
||||||
}
|
}
|
||||||
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
|
|
||||||
MobileGL::MG_Config::QuirkOverride saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||||
SetupGridViewports(kCellSize, kCellSize);
|
SetupGridViewports(kCellSize, kCellSize);
|
||||||
|
|
||||||
GLuint unroutedProgram = 0;
|
GLuint unroutedProgram = 0;
|
||||||
{
|
{
|
||||||
const ScopedEmulationOff scopedEmulationOff;
|
// A program of its own rather than the fixture's, even though in this process
|
||||||
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
|
// the fixture's was built unrouted too: the emitted ESSL is decided at link
|
||||||
// that carries this flag, so reusing m_program would just replay the routed build.
|
// 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);
|
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||||
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
|
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
|
||||||
glUseProgram(unroutedProgram);
|
glUseProgram(unroutedProgram);
|
||||||
|
|||||||
@@ -988,6 +988,49 @@ TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries)
|
|||||||
MG_State::pGLContext = Move(previousContext);
|
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) {
|
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -1550,6 +1593,30 @@ TEST(DirectVulkanSanity, SamplerUniformTypesPreserveTheirNumericDomain) {
|
|||||||
SamplerNumericDomain::Unknown);
|
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) {
|
TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChangingComponentLayout) {
|
||||||
using namespace MobileGL::MG_Backend::DirectVulkan;
|
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -24,6 +25,8 @@ using MobileGL::MG_Util::SelfTest::ProbeCrossStageImageQualifierMergeDropsWrites
|
|||||||
using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -46,6 +49,13 @@ namespace {
|
|||||||
// CollectGlesKnownDriverBugs(): the collector goes through the once-per-process memos, and a
|
// 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.
|
// 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.
|
// The exact text an affected Adreno driver puts in the info log for this refusal.
|
||||||
const char* const kImageLocationLinkLog =
|
const char* const kImageLocationLinkLog =
|
||||||
"Error: Image Image location or component exceeds max allowed.\nError: Linking failed.";
|
"Error: Image Image location or component exceeds max allowed.\nError: Linking failed.";
|
||||||
@@ -82,6 +92,21 @@ namespace {
|
|||||||
int coherencyEmittedShapeFailedTexels = 0;
|
int coherencyEmittedShapeFailedTexels = 0;
|
||||||
int coherencyControlFailedTexels = 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 ---------------------------------------------
|
// ---- object bookkeeping ---------------------------------------------
|
||||||
GLenum pendingError = GL_NO_ERROR;
|
GLenum pendingError = GL_NO_ERROR;
|
||||||
GLuint nextShaderId = 1;
|
GLuint nextShaderId = 1;
|
||||||
@@ -102,6 +127,19 @@ namespace {
|
|||||||
// texture id -> GL_TEXTURE_SWIZZLE_A
|
// texture id -> GL_TEXTURE_SWIZZLE_A
|
||||||
std::map<GLuint, GLenum> multisampleAlphaSwizzle;
|
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 boundMultisampleTexture = 0;
|
||||||
GLuint currentProgram = 0;
|
GLuint currentProgram = 0;
|
||||||
// How many programs that sample a multisample texture have been linked so far. The
|
// How many programs that sample a multisample texture have been linked so far. The
|
||||||
@@ -149,6 +187,22 @@ namespace {
|
|||||||
return names;
|
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) {
|
std::string StageSourceContaining(GLuint program, const char* needle) {
|
||||||
const auto attached = g_fake.programShaders.find(program);
|
const auto attached = g_fake.programShaders.find(program);
|
||||||
if (attached == g_fake.programShaders.end()) return {};
|
if (attached == g_fake.programShaders.end()) return {};
|
||||||
@@ -217,6 +271,9 @@ namespace {
|
|||||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||||
*data = g_fake.maxGeometrySsboBlocks;
|
*data = g_fake.maxGeometrySsboBlocks;
|
||||||
break;
|
break;
|
||||||
|
case GL_MAX_VERTEX_ATTRIBS:
|
||||||
|
*data = g_fake.maxVertexAttribs;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -252,16 +309,41 @@ namespace {
|
|||||||
}
|
}
|
||||||
g_fake.shaderSources[shader] = std::move(source);
|
g_fake.shaderSources[shader] = std::move(source);
|
||||||
};
|
};
|
||||||
funcs.glCompileShader = [](GLuint) {};
|
funcs.glCompileShader = [](GLuint shader) {
|
||||||
funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) {
|
const std::string& source = SourceOf(shader);
|
||||||
if (pname == GL_COMPILE_STATUS) *params = GL_TRUE;
|
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) {
|
funcs.glGetShaderiv = [](GLuint shader, GLenum pname, GLint* params) {
|
||||||
if (bufSize > 0) infoLog[0] = '\0';
|
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) {
|
funcs.glDeleteShader = [](GLuint shader) {
|
||||||
if (shader != 0) --g_fake.aliveShaders;
|
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 {
|
funcs.glCreateProgram = []() -> GLuint {
|
||||||
++g_fake.alivePrograms;
|
++g_fake.alivePrograms;
|
||||||
return g_fake.nextProgramId++;
|
return g_fake.nextProgramId++;
|
||||||
@@ -272,7 +354,17 @@ namespace {
|
|||||||
funcs.glLinkProgram = [](GLuint program) {
|
funcs.glLinkProgram = [](GLuint program) {
|
||||||
const std::vector<std::string> names = DeclaredImageNames(program);
|
const std::vector<std::string> names = DeclaredImageNames(program);
|
||||||
const bool overBudget = static_cast<int>(names.size()) > g_fake.distinctImageNameBudget;
|
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 : "";
|
g_fake.programInfoLogs[program] = overBudget ? kImageLocationLinkLog : "";
|
||||||
if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) {
|
if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) {
|
||||||
++g_fake.sampledMultisampleProgramCount;
|
++g_fake.sampledMultisampleProgramCount;
|
||||||
@@ -308,6 +400,20 @@ namespace {
|
|||||||
};
|
};
|
||||||
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||||
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = 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) {
|
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||||
for (GLsizei i = 0; i < n; ++i) {
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
@@ -332,12 +438,50 @@ namespace {
|
|||||||
++g_fake.aliveFramebuffers;
|
++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.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.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; };
|
||||||
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
||||||
for (GLsizei i = 0; i < n; ++i) {
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||||
|
g_fake.framebufferLayerAttachment.erase(framebuffers[i]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||||
@@ -417,6 +561,26 @@ namespace {
|
|||||||
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels) {
|
void* pixels) {
|
||||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
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) {
|
if (format == GL_RED && type == GL_FLOAT) {
|
||||||
GLfloat* out = static_cast<GLfloat*>(pixels);
|
GLfloat* out = static_cast<GLfloat*>(pixels);
|
||||||
for (std::size_t i = 0; i < texels; ++i) out[i] = g_fake.lastSampledValue;
|
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".
|
// section would stop meaning "this device has these bugs".
|
||||||
TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
||||||
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
|
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))
|
EXPECT_FALSE(ProbeGeometryStageSsboWriteAfterEmitDropped(gl))
|
||||||
<< "a probe with no entry points to call must not claim the driver is affected";
|
<< "a probe with no entry points to call must not claim the driver is affected";
|
||||||
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||||
@@ -651,6 +819,125 @@ TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenTheFinishSeparatedControlI
|
|||||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
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) {
|
TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
|
||||||
ResetFakeDriver();
|
ResetFakeDriver();
|
||||||
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "Loader.h"
|
#include "Loader.h"
|
||||||
|
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -1064,6 +1065,16 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I("OpenGL ES capabilities:");
|
MGLOG_I("OpenGL ES capabilities:");
|
||||||
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
|
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
|
||||||
MGLOG_I(" GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: %d", 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 aliasedLineWidthRange[2] = {1.0f, 1.0f};
|
||||||
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
||||||
GLfloat smoothLineWidthGranularity = 1.0f;
|
GLfloat smoothLineWidthGranularity = 1.0f;
|
||||||
@@ -1495,7 +1506,26 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
|
caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
|
||||||
caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
|
caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
|
||||||
caps.MaxCombinedTextureImageUnits = maxCombinedTextureImageUnits;
|
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.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
|
||||||
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
|
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
|
||||||
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
|
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
|
||||||
|
|||||||
@@ -1231,6 +1231,10 @@ namespace MobileGL {
|
|||||||
// InstanceIndex, which includes firstInstance.
|
// InstanceIndex, which includes firstInstance.
|
||||||
Bool IndirectDrawInstanceIdIncludesBaseInstance = false;
|
Bool IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||||
Int UniformBufferOffsetAlignment = 256;
|
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 AliasedLineWidthRangeMin = 1.0f;
|
||||||
Float AliasedLineWidthRangeMax = 1.0f;
|
Float AliasedLineWidthRangeMax = 1.0f;
|
||||||
Float SmoothLineWidthRangeMin = 1.0f;
|
Float SmoothLineWidthRangeMin = 1.0f;
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.DriverVersionString = DecodeDriverVersion(p.driverVersion);
|
caps.DriverVersionString = DecodeDriverVersion(p.driverVersion);
|
||||||
caps.VendorId = p.vendorID;
|
caps.VendorId = p.vendorID;
|
||||||
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
|
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
|
||||||
|
caps.ShaderStorageBufferOffsetAlignment = static_cast<int>(p.limits.minStorageBufferOffsetAlignment);
|
||||||
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
|
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
|
||||||
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
|
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
|
||||||
caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy;
|
caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy;
|
||||||
@@ -272,6 +273,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.DriverVersionString = DecodeDriverVersion(properties.driverVersion);
|
caps.DriverVersionString = DecodeDriverVersion(properties.driverVersion);
|
||||||
caps.VendorId = properties.vendorID;
|
caps.VendorId = properties.vendorID;
|
||||||
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
|
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
|
||||||
|
caps.ShaderStorageBufferOffsetAlignment =
|
||||||
|
static_cast<int>(properties.limits.minStorageBufferOffsetAlignment);
|
||||||
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
||||||
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
||||||
caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy;
|
caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ namespace MobileGL {
|
|||||||
// VkPhysicalDeviceProperties::vendorID, for device-quirk vendor gating.
|
// VkPhysicalDeviceProperties::vendorID, for device-quirk vendor gating.
|
||||||
Uint32 VendorId = 0;
|
Uint32 VendorId = 0;
|
||||||
Int UniformBufferOffsetAlignment = 256;
|
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 AliasedLineWidthRangeMin = 1.0f;
|
||||||
Float AliasedLineWidthRangeMax = 1.0f;
|
Float AliasedLineWidthRangeMax = 1.0f;
|
||||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. Whether it can be used at all depends on
|
// 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 activeTexture = GL_TEXTURE0;
|
||||||
GLint texture2D = 0;
|
GLint texture2D = 0;
|
||||||
GLint texture2DMultisample = 0;
|
GLint texture2DMultisample = 0;
|
||||||
|
GLint texture2DArray = 0;
|
||||||
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
GLint packAlignment = 4;
|
GLint packAlignment = 4;
|
||||||
GLint packRowLength = 0;
|
GLint packRowLength = 0;
|
||||||
@@ -153,8 +154,15 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
state.depthTest = gl.glIsEnabled(GL_DEPTH_TEST);
|
state.depthTest = gl.glIsEnabled(GL_DEPTH_TEST);
|
||||||
state.blend = gl.glIsEnabled(GL_BLEND);
|
state.blend = gl.glIsEnabled(GL_BLEND);
|
||||||
gl.glGetIntegerv(GL_ACTIVE_TEXTURE, &state.activeTexture);
|
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, &state.texture2D);
|
||||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &state.texture2DMultisample);
|
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_ALIGNMENT, &state.packAlignment);
|
||||||
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
|
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
|
||||||
if (gl.glGetFloatv != nullptr) {
|
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, static_cast<GLuint>(state.texture2D));
|
||||||
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE,
|
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE,
|
||||||
static_cast<GLuint>(state.texture2DMultisample));
|
static_cast<GLuint>(state.texture2DMultisample));
|
||||||
|
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, static_cast<GLuint>(state.texture2DArray));
|
||||||
}
|
}
|
||||||
gl.glActiveTexture(static_cast<GLenum>(state.activeTexture));
|
gl.glActiveTexture(static_cast<GLenum>(state.activeTexture));
|
||||||
}
|
}
|
||||||
@@ -1348,6 +1357,365 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
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) {
|
Optional<DriverBugFinding> ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) {
|
||||||
if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt;
|
if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt;
|
||||||
return DriverBugFinding{
|
return DriverBugFinding{
|
||||||
@@ -1448,6 +1816,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
&ProbeImageLocationPerNameBug,
|
&ProbeImageLocationPerNameBug,
|
||||||
&ProbeCrossStageImageQualifierMergeBug,
|
&ProbeCrossStageImageQualifierMergeBug,
|
||||||
&ProbeImageCoherencyResidualBug,
|
&ProbeImageCoherencyResidualBug,
|
||||||
|
&ProbeExplicitVertexInputLocationCeilingBug,
|
||||||
|
&ProbeLayeredBlitDestinationBug,
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,74 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
String detail;
|
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
|
// 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 its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the
|
||||||
// before-emit write lands and the after-emit write does not.
|
// 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
|
// Why not simply rebase the offsets to zero and bind the buffer 8 bytes in: because
|
||||||
// glBindBufferRange's offset must be a multiple of
|
// glBindBufferRange's offset must be a multiple of
|
||||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which the target device reports as 32.
|
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is 64 on Adreno 830 and no smaller
|
||||||
// A byte offset of 8 cannot be expressed as a binding at all, so the correction has
|
// than 32 on the other targets. A byte offset of 8 cannot be expressed as a binding
|
||||||
// to live in the shader's indexing, where it costs nothing.
|
// 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
|
// 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
|
// offset qualifier, and so very nearly all of them - is left byte-identical: the
|
||||||
|
|||||||
Reference in New Issue
Block a user