mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
17
Commits
efa0345c36
...
16c010985f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16c010985f | ||
|
|
6f64ec0f51 | ||
|
|
5d47698349 | ||
|
|
7b593e39ef | ||
|
|
d83b4dbbb5 | ||
|
|
964a7fcc92 | ||
|
|
5a7bd9942d | ||
|
|
21a43bf6a4 | ||
|
|
5b6dec2d81 | ||
|
|
543c29bf86 | ||
|
|
ef562ee9b5 | ||
|
|
fa0f6693d0 | ||
|
|
a6c362c6ce | ||
|
|
921504eccf | ||
|
|
7c5fc03b26 | ||
|
|
ed29e63543 | ||
|
|
5c8a9c41d6 |
@@ -1214,7 +1214,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_unitTextureSyncListValid &&
|
if (g_unitTextureSyncListValid &&
|
||||||
g_unitTextureSyncListContextId == keys.contextId &&
|
g_unitTextureSyncListContextId == keys.contextId &&
|
||||||
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
||||||
g_unitTextureSyncListContextGeneration == g_textureContextGeneration &&
|
g_unitTextureSyncListContextGeneration == g_backendContextGeneration &&
|
||||||
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
||||||
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
|
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
|
||||||
PairingsIntact(g_unitTextureSyncList)) {
|
PairingsIntact(g_unitTextureSyncList)) {
|
||||||
@@ -1246,7 +1246,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
g_unitTextureSyncListContextId = keys.contextId;
|
g_unitTextureSyncListContextId = keys.contextId;
|
||||||
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
|
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
|
||||||
g_unitTextureSyncListContextGeneration = g_textureContextGeneration;
|
g_unitTextureSyncListContextGeneration = g_backendContextGeneration;
|
||||||
g_unitTextureSyncListEpoch = unitBindingsEpoch;
|
g_unitTextureSyncListEpoch = unitBindingsEpoch;
|
||||||
g_unitTextureSyncListSamplingGeneration = samplingGeneration;
|
g_unitTextureSyncListSamplingGeneration = samplingGeneration;
|
||||||
g_unitTextureSyncListValid = true;
|
g_unitTextureSyncListValid = true;
|
||||||
@@ -1274,7 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
|
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
|
||||||
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
|
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
|
||||||
g_fboTextureSyncListContextId == keys.contextId &&
|
g_fboTextureSyncListContextId == keys.contextId &&
|
||||||
g_fboTextureSyncListContextGeneration == g_textureContextGeneration &&
|
g_fboTextureSyncListContextGeneration == g_backendContextGeneration &&
|
||||||
PairingsIntact(g_fboTextureSyncList);
|
PairingsIntact(g_fboTextureSyncList);
|
||||||
if (fboListValid) {
|
if (fboListValid) {
|
||||||
for (const auto& entry : g_fboTextureSyncList) {
|
for (const auto& entry : g_fboTextureSyncList) {
|
||||||
@@ -1302,7 +1302,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_fboTextureSyncListSlotVersion = fboSlotVersion;
|
g_fboTextureSyncListSlotVersion = fboSlotVersion;
|
||||||
g_fboTextureSyncListObjectVersion = fboObjectVersion;
|
g_fboTextureSyncListObjectVersion = fboObjectVersion;
|
||||||
g_fboTextureSyncListContextId = keys.contextId;
|
g_fboTextureSyncListContextId = keys.contextId;
|
||||||
g_fboTextureSyncListContextGeneration = g_textureContextGeneration;
|
g_fboTextureSyncListContextGeneration = g_backendContextGeneration;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g_fboTextureSyncListFbo = nullptr;
|
g_fboTextureSyncListFbo = nullptr;
|
||||||
@@ -2423,7 +2423,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
|
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
|
||||||
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
|
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
|
||||||
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
|
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
|
||||||
g_unitSamplerWalkContextGeneration == TextureImpl::g_textureContextGeneration &&
|
g_unitSamplerWalkContextGeneration == g_backendContextGeneration &&
|
||||||
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
|
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2444,7 +2444,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_unitSamplerWalkContextId = keys.contextId;
|
g_unitSamplerWalkContextId = keys.contextId;
|
||||||
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
|
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
|
||||||
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
|
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
|
||||||
g_unitSamplerWalkContextGeneration = TextureImpl::g_textureContextGeneration;
|
g_unitSamplerWalkContextGeneration = g_backendContextGeneration;
|
||||||
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
|
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
|
||||||
g_unitSamplerWalkValid = true;
|
g_unitSamplerWalkValid = true;
|
||||||
}
|
}
|
||||||
@@ -2554,7 +2554,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
memo.programBackendStateVersion ==
|
memo.programBackendStateVersion ==
|
||||||
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
||||||
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
|
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
|
||||||
memo.contextGeneration == TextureImpl::g_textureContextGeneration;
|
memo.contextGeneration == g_backendContextGeneration;
|
||||||
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
|
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
|
||||||
// snapshotted row count) matches.
|
// snapshotted row count) matches.
|
||||||
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
|
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
|
||||||
@@ -2569,7 +2569,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
||||||
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
||||||
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
||||||
memo.contextGeneration = TextureImpl::g_textureContextGeneration;
|
memo.contextGeneration = g_backendContextGeneration;
|
||||||
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
|
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
|
||||||
memo.valid = true;
|
memo.valid = true;
|
||||||
}
|
}
|
||||||
@@ -2744,7 +2744,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
||||||
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
|
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
|
||||||
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
|
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
|
||||||
samplerPassMemo.textureContextGeneration == TextureImpl::g_textureContextGeneration;
|
samplerPassMemo.textureContextGeneration == g_backendContextGeneration;
|
||||||
if (samplerPassClean) {
|
if (samplerPassClean) {
|
||||||
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
||||||
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
|
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
|
||||||
@@ -2843,7 +2843,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
|
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
|
||||||
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
|
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
|
||||||
samplerPassMemo.backendStateVersion = programBackendStateVersion;
|
samplerPassMemo.backendStateVersion = programBackendStateVersion;
|
||||||
samplerPassMemo.textureContextGeneration = TextureImpl::g_textureContextGeneration;
|
samplerPassMemo.textureContextGeneration = g_backendContextGeneration;
|
||||||
samplerPassMemo.valid = true;
|
samplerPassMemo.valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3605,12 +3605,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_resolveContextGeneration != TextureImpl::g_textureContextGeneration) {
|
if (s_resolveContextGeneration != g_backendContextGeneration) {
|
||||||
// The ids belonged to a dead context; the context reclaimed them with it.
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
||||||
s_resolveFramebuffer = 0;
|
s_resolveFramebuffer = 0;
|
||||||
s_resolveRenderbuffer = 0;
|
s_resolveRenderbuffer = 0;
|
||||||
s_resolveFormat = 0;
|
s_resolveFormat = 0;
|
||||||
s_resolveContextGeneration = TextureImpl::g_textureContextGeneration;
|
s_resolveContextGeneration = g_backendContextGeneration;
|
||||||
}
|
}
|
||||||
if (s_resolveFramebuffer == 0) {
|
if (s_resolveFramebuffer == 0) {
|
||||||
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
|
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
|
||||||
@@ -3758,7 +3758,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool EnsureResources() {
|
static Bool EnsureResources() {
|
||||||
if (s_contextGeneration != TextureImpl::g_textureContextGeneration) {
|
if (s_contextGeneration != g_backendContextGeneration) {
|
||||||
// The ids belonged to a dead context; the context reclaimed them with it.
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
||||||
s_framebuffer = 0;
|
s_framebuffer = 0;
|
||||||
s_texture = 0;
|
s_texture = 0;
|
||||||
@@ -3769,7 +3769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
s_depthProgram = 0;
|
s_depthProgram = 0;
|
||||||
s_stencilProgram = 0;
|
s_stencilProgram = 0;
|
||||||
s_programsFailed = false;
|
s_programsFailed = false;
|
||||||
s_contextGeneration = TextureImpl::g_textureContextGeneration;
|
s_contextGeneration = g_backendContextGeneration;
|
||||||
}
|
}
|
||||||
if (s_programsFailed) {
|
if (s_programsFailed) {
|
||||||
return false;
|
return false;
|
||||||
@@ -7482,7 +7482,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PixelStoreImpl::InvalidatePackStateCache();
|
PixelStoreImpl::InvalidatePackStateCache();
|
||||||
// Texture ids belong to the dying context; wrappers destroyed later must
|
// Texture ids belong to the dying context; wrappers destroyed later must
|
||||||
// not glDeleteTextures a recycled name in a successor context.
|
// not glDeleteTextures a recycled name in a successor context.
|
||||||
++TextureImpl::g_textureContextGeneration;
|
++g_backendContextGeneration;
|
||||||
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
||||||
// Outstanding fence handles now refer to a dead context; treat them as
|
// Outstanding fence handles now refer to a dead context; treat them as
|
||||||
// signaled from here on.
|
// signaled from here on.
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
|
Uint g_backendContextGeneration = 1;
|
||||||
|
|
||||||
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
|
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
|
||||||
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
|
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
|
||||||
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
|
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
|
||||||
@@ -1646,7 +1648,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||||
m_contextGeneration = g_textureContextGeneration;
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
if (m_backendTextureId == 0) {
|
if (m_backendTextureId == 0) {
|
||||||
MGLOG_E("Failed to generate texture object.");
|
MGLOG_E("Failed to generate texture object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -1673,7 +1675,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
}
|
}
|
||||||
m_backendTextureId = 0;
|
m_backendTextureId = 0;
|
||||||
@@ -1712,7 +1714,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BackendTextureObject::RecreateBackendTexture() {
|
void BackendTextureObject::RecreateBackendTexture() {
|
||||||
if (m_backendTextureId != 0) {
|
if (m_backendTextureId != 0) {
|
||||||
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||||
if (m_contextGeneration == g_textureContextGeneration) {
|
if (m_contextGeneration == g_backendContextGeneration) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
}
|
}
|
||||||
for (auto& unitCache : g_boundTexturesCache) {
|
for (auto& unitCache : g_boundTexturesCache) {
|
||||||
@@ -1725,7 +1727,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||||
m_contextGeneration = g_textureContextGeneration;
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
if (m_backendTextureId == 0) {
|
if (m_backendTextureId == 0) {
|
||||||
MGLOG_E("Failed to regenerate texture object.");
|
MGLOG_E("Failed to regenerate texture object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -2809,7 +2811,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
// TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a
|
||||||
|
// backstop for a state object that grew a new storage kind. Skipping the upload
|
||||||
|
// renders wrong; throwing unwinds through the C GL ABI and kills the process.
|
||||||
|
MGLOG_I("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
|
||||||
|
"skipping this sync",
|
||||||
|
static_cast<int>(stateTextureObject->GetStorageType()),
|
||||||
|
stateTextureObject->GetExternalIndex());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||||
@@ -3074,7 +3083,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uint g_activeTextureUnit = 0;
|
Uint g_activeTextureUnit = 0;
|
||||||
Uint g_textureContextGeneration = 1;
|
|
||||||
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
@@ -3093,6 +3101,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
||||||
}
|
}
|
||||||
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
if (m_backendFBOId == 0) {
|
if (m_backendFBOId == 0) {
|
||||||
MGLOG_E("Failed to generate framebuffer object.");
|
MGLOG_E("Failed to generate framebuffer object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -3101,6 +3110,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackendFramebufferObject::~BackendFramebufferObject() {
|
||||||
|
if (InProcessTeardown()) {
|
||||||
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
||||||
|
}
|
||||||
|
if (m_backendFBOId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Scrub the binding shadow whether or not the id can still be deleted: a
|
||||||
|
// recycled name must never satisfy the shadow's dedup.
|
||||||
|
NoteFramebufferIdDeleted(m_backendFBOId);
|
||||||
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) {
|
||||||
|
g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId);
|
||||||
|
}
|
||||||
|
m_backendFBOId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void BackendFramebufferObject::Bind(FramebufferTarget target) const {
|
void BackendFramebufferObject::Bind(FramebufferTarget target) const {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -3156,6 +3181,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return g_driverFBOBindings[idx];
|
return g_driverFBOBindings[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NoteFramebufferIdDeleted(Uint id) {
|
||||||
|
if (id == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) {
|
||||||
|
if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) {
|
||||||
|
g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void InvalidateFramebufferBindingCache() {
|
void InvalidateFramebufferBindingCache() {
|
||||||
g_driverFBOBindings = {0, 0};
|
g_driverFBOBindings = {0, 0};
|
||||||
g_driverFBOBindingKnown = {false, false};
|
g_driverFBOBindingKnown = {false, false};
|
||||||
@@ -4563,6 +4599,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
|
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
if (m_backendSamplerId == 0) {
|
if (m_backendSamplerId == 0) {
|
||||||
MGLOG_E("Failed to generate sampler object.");
|
MGLOG_E("Failed to generate sampler object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -4571,6 +4608,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackendSamplerObject::~BackendSamplerObject() {
|
||||||
|
if (InProcessTeardown()) {
|
||||||
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
||||||
|
}
|
||||||
|
if (m_backendSamplerId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Scrub the unit shadow whether or not the id can still be deleted - the next
|
||||||
|
// twin can land on this heap address and would otherwise false-skip its Bind.
|
||||||
|
for (auto& boundSampler : g_boundSamplersCache) {
|
||||||
|
if (boundSampler == this) {
|
||||||
|
boundSampler = nullptr; // glDeleteSamplers unbinds from every unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) {
|
||||||
|
g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId);
|
||||||
|
}
|
||||||
|
m_backendSamplerId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void BackendSamplerObject::SyncToBackend(
|
void BackendSamplerObject::SyncToBackend(
|
||||||
const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {
|
const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -4686,12 +4743,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
||||||
|
m_contextGeneration = g_backendContextGeneration;
|
||||||
if (m_backendRBOId == 0) {
|
if (m_backendRBOId == 0) {
|
||||||
MGLOG_E("Failed to generate renderbuffer object.");
|
MGLOG_E("Failed to generate renderbuffer object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackendRenderbufferObject::~BackendRenderbufferObject() {
|
||||||
|
if (InProcessTeardown()) {
|
||||||
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
||||||
|
}
|
||||||
|
if (m_backendRBOId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No driver-level renderbuffer-binding shadow exists (Bind() always issues the
|
||||||
|
// call), so there is nothing to scrub here - only the id to release.
|
||||||
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) {
|
||||||
|
g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId);
|
||||||
|
}
|
||||||
|
m_backendRBOId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void BackendRenderbufferObject::Bind() const {
|
void BackendRenderbufferObject::Bind() const {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool InProcessTeardown();
|
Bool InProcessTeardown();
|
||||||
void EnsureProcessTeardownSentinel();
|
void EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
|
// Generation of the backend ES context that owns the driver ids currently handed
|
||||||
|
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
|
||||||
|
// driver name (texture, framebuffer, renderbuffer, sampler) stamps this at
|
||||||
|
// construction and compares it in its destructor: a twin outliving its context
|
||||||
|
// must NOT glDelete* its id, because a successor context may already have recycled
|
||||||
|
// that name and the delete would take out a live object of the new context.
|
||||||
|
extern Uint g_backendContextGeneration;
|
||||||
|
|
||||||
// Which optional pieces of state a draw needs synchronized before it is issued.
|
// Which optional pieces of state a draw needs synchronized before it is issued.
|
||||||
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||||
// draws that provably cannot read them.
|
// draws that provably cannot read them.
|
||||||
@@ -657,15 +665,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
extern Uint g_activeTextureUnit;
|
extern Uint g_activeTextureUnit;
|
||||||
// Bumped when the backend ES context is destroyed; texture ids stamped with
|
|
||||||
// an older generation belong to a dead context and must not be deleted.
|
|
||||||
extern Uint g_textureContextGeneration;
|
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
class BackendFramebufferObject {
|
class BackendFramebufferObject {
|
||||||
public:
|
public:
|
||||||
BackendFramebufferObject();
|
BackendFramebufferObject();
|
||||||
|
// Deletes the driver framebuffer and scrubs the binding shadow. Without it every
|
||||||
|
// frontend glDeleteFramebuffers leaked one ES framebuffer for the process lifetime;
|
||||||
|
// an app that creates a framebuffer per readback (GL CTS packed_pixels does ~3300
|
||||||
|
// per case) walked the driver into hundreds of megabytes of dead framebuffers and
|
||||||
|
// out of the resources a later attachment needs.
|
||||||
|
~BackendFramebufferObject();
|
||||||
|
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
|
||||||
|
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
||||||
FramebufferTarget asTarget);
|
FramebufferTarget asTarget);
|
||||||
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
||||||
@@ -680,6 +693,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendFBOId = 0;
|
Uint m_backendFBOId = 0;
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
|
|
||||||
/* this will save buffers in its original form,
|
/* this will save buffers in its original form,
|
||||||
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
||||||
@@ -821,6 +835,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||||
void InvalidateFramebufferBindingCache();
|
void InvalidateFramebufferBindingCache();
|
||||||
|
// A driver framebuffer id is about to be deleted: ES reverts every target that
|
||||||
|
// currently binds it to 0, so the binding shadow has to follow or the next
|
||||||
|
// BindFramebufferId(0) would be deduped away and leave the deleted name bound.
|
||||||
|
void NoteFramebufferIdDeleted(Uint id);
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
|
|
||||||
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||||
@@ -1087,12 +1105,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendSamplerObject {
|
class BackendSamplerObject {
|
||||||
public:
|
public:
|
||||||
BackendSamplerObject();
|
BackendSamplerObject();
|
||||||
|
// Deletes the driver sampler and clears the units whose binding shadow still names
|
||||||
|
// this twin (a recycled heap address would otherwise false-skip a later Bind).
|
||||||
|
// Frontend glDeleteSamplers used to leak the backend id for the process lifetime.
|
||||||
|
~BackendSamplerObject();
|
||||||
|
BackendSamplerObject(const BackendSamplerObject&) = delete;
|
||||||
|
BackendSamplerObject& operator=(const BackendSamplerObject&) = delete;
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
||||||
void Bind(Uint unit);
|
void Bind(Uint unit);
|
||||||
Uint GetBackendSamplerId() const;
|
Uint GetBackendSamplerId() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendSamplerId = 0;
|
Uint m_backendSamplerId = 0;
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
SamplerParameters m_cacheSamplerParameters;
|
SamplerParameters m_cacheSamplerParameters;
|
||||||
Uint16 m_syncedSamplerVersion = 0;
|
Uint16 m_syncedSamplerVersion = 0;
|
||||||
@@ -1110,12 +1135,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendRenderbufferObject {
|
class BackendRenderbufferObject {
|
||||||
public:
|
public:
|
||||||
BackendRenderbufferObject();
|
BackendRenderbufferObject();
|
||||||
|
// Deletes the driver renderbuffer; frontend glDeleteRenderbuffers used to leak it
|
||||||
|
// (with its whole image allocation) for the process lifetime.
|
||||||
|
~BackendRenderbufferObject();
|
||||||
|
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
|
||||||
|
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
||||||
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendRBOId = 0;
|
Uint m_backendRBOId = 0;
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
||||||
Int m_cacheWidth = 0;
|
Int m_cacheWidth = 0;
|
||||||
|
|||||||
@@ -768,14 +768,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
||||||
m_dynamicParameters.MaxVertexAttribs = std::min(
|
m_dynamicParameters.MaxVertexAttribs = std::min(
|
||||||
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
// Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
|
||||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
// limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
|
||||||
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
// queries at descriptor-indexing scale - the same driver whose
|
||||||
|
// GL_MAX_SHADER_STORAGE_BLOCK_SIZE is clamped from 2147483647 further down - so
|
||||||
|
// KHR-GL44.multi_bind.dispatch_bind_buffers_base read GL_MAX_COMPUTE_UNIFORM_BLOCKS,
|
||||||
|
// created that many buffers and spliced that many UBO declarations into a single compute
|
||||||
|
// shader: ~14 s of allocation, then death on std::bad_alloc. Its sibling
|
||||||
|
// dispatch_bind_buffers_range hard-codes 4 buffers and passes, which is the clean
|
||||||
|
// discriminator. Every ceiling below is far above what any desktop driver advertises for
|
||||||
|
// these (84-96 for the binding families) and far below a descriptor-indexing count, so it
|
||||||
|
// can only lower a limit that was never usable in the first place. The zero floor is not
|
||||||
|
// decoration: a driver reporting UINT32_MAX used to arrive here as -1.
|
||||||
|
const auto clampLimit = [](const char* name, Int reported, Int ceiling) {
|
||||||
|
const Int clamped = std::min(std::max(reported, 0), ceiling);
|
||||||
|
if (clamped != reported) {
|
||||||
|
MGLOG_I("DirectVulkan: clamped %s from %d to %d", name, reported, clamped);
|
||||||
|
}
|
||||||
|
return clamped;
|
||||||
|
};
|
||||||
|
// GL 4.6 required minimums, for the record: MAX_COMPUTE_UNIFORM_BLOCKS 12,
|
||||||
|
// MAX_COMPUTE/COMBINED_SHADER_STORAGE_BLOCKS 8, MAX_SHADER_STORAGE_BUFFER_BINDINGS 8,
|
||||||
|
// MAX_UNIFORM_BUFFER_BINDINGS 84, MAX_TEXTURE_BUFFER_SIZE 65536.
|
||||||
|
constexpr Int kMaxAdvertisedBufferBlocks = 256;
|
||||||
|
constexpr Int kMaxAdvertisedTextureBufferSize = 1 << 27; // texels; what desktop GL reports
|
||||||
|
m_dynamicParameters.MaxComputeShaderStorageBlocks =
|
||||||
|
clampLimit("GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxComputeShaderStorageBlocks,
|
||||||
|
kMaxAdvertisedBufferBlocks);
|
||||||
|
m_dynamicParameters.MaxCombinedShaderStorageBlocks =
|
||||||
|
clampLimit("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxCombinedShaderStorageBlocks,
|
||||||
|
kMaxAdvertisedBufferBlocks);
|
||||||
|
m_dynamicParameters.MaxComputeUniformBlocks =
|
||||||
|
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
||||||
|
kMaxAdvertisedBufferBlocks);
|
||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||||
|
kMaxAdvertisedBufferBlocks);
|
||||||
|
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
||||||
|
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
||||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
|
||||||
|
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
|
||||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||||
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||||
|
|||||||
@@ -252,6 +252,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline pipeline = CreatePipeline(payload);
|
VkPipeline pipeline = CreatePipeline(payload);
|
||||||
|
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
|
||||||
|
// the rest of the process, so one transient driver rejection turned every later draw with
|
||||||
|
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
|
||||||
|
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
|
||||||
|
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
||||||
|
// skipped.
|
||||||
|
if (pipeline == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_I("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
||||||
|
"programHash=0x%llx; not caching the failure",
|
||||||
|
static_cast<unsigned long long>(hash),
|
||||||
|
static_cast<unsigned long long>(payload.programHash));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||||
m_frameCounter});
|
m_frameCounter});
|
||||||
return pipeline;
|
return pipeline;
|
||||||
@@ -507,6 +520,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
||||||
payload.vertexInputState->vertexBindingDescriptionCount,
|
payload.vertexInputState->vertexBindingDescriptionCount,
|
||||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
payload.vertexInputState->vertexAttributeDescriptionCount);
|
||||||
|
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
||||||
|
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
||||||
|
// investigation) is to name the modules. MGLOG_I, not _D/_E: this must survive in the
|
||||||
|
// INFO-level builds that CTS actually runs against.
|
||||||
|
if (payload.stageSpirvDigests) {
|
||||||
|
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
||||||
|
const auto& digest = (*payload.stageSpirvDigests)[i];
|
||||||
|
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
|
||||||
|
"hash=0x%llx",
|
||||||
|
i, digest.stage, digest.wordCount,
|
||||||
|
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
|
||||||
|
static_cast<unsigned long long>(digest.hash));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
|
||||||
|
}
|
||||||
|
if (payload.stages) {
|
||||||
|
for (SizeT i = 0; i < payload.stages->size(); ++i) {
|
||||||
|
const auto& stage = (*payload.stages)[i];
|
||||||
|
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
|
||||||
|
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
|
||||||
|
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
|
||||||
|
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
|
||||||
|
"specialization=%d",
|
||||||
|
i, static_cast<Uint32>(stage.stage),
|
||||||
|
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
|
||||||
|
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
const auto& attachment = payload.colorBlendAttachments[i];
|
const auto& attachment = payload.colorBlendAttachments[i];
|
||||||
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
||||||
|
|||||||
@@ -14,6 +14,16 @@
|
|||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
|
||||||
|
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
|
||||||
|
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
|
||||||
|
// of any pipeline or program hash.
|
||||||
|
struct ShaderStageSpirvDigest {
|
||||||
|
Uint32 stage = 0; // VkShaderStageFlagBits
|
||||||
|
Uint32 wordCount = 0;
|
||||||
|
Uint64 hash = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class PipelineFactory {
|
class PipelineFactory {
|
||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -62,6 +72,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||||
|
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||||
|
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <utility>
|
||||||
#include <spirv-tools/libspirv.h>
|
#include <spirv-tools/libspirv.h>
|
||||||
#include <spirv-tools/optimizer.hpp>
|
#include <spirv-tools/optimizer.hpp>
|
||||||
#include <source/opt/build_module.h>
|
#include <source/opt/build_module.h>
|
||||||
@@ -937,6 +940,189 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_transformFlags;
|
ProgramFactory::CompileOptionFlags m_transformFlags;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// gl_FragCoord back into GL's window space, for default-framebuffer draws only.
|
||||||
|
//
|
||||||
|
// Vulkan's gl_FragCoord.y is the framebuffer ROW being written - not a value the
|
||||||
|
// viewport rect can move independently of placement. The default framebuffer's image is
|
||||||
|
// stored display-side-up and the vertex stage compensates by negating gl_Position.y, so
|
||||||
|
// for every default-FBO draw the framebuffer row of a fragment is exactly
|
||||||
|
// `height - y_GL` (the viewport terms cancel: yf_VK = H - yf_GL for any viewport rect).
|
||||||
|
// A shader that reads gl_FragCoord therefore sees a flipped Y, and once the viewport
|
||||||
|
// rect started being converted to the stored orientation it also sees a Y that is
|
||||||
|
// OUTSIDE the range GL promises - a 32-pixel-tall viewport at GL y=0 reports 224..255 on
|
||||||
|
// a 256-tall surface. GL CTS shader_image_load_store writes imageStore(image,
|
||||||
|
// ivec2(gl_FragCoord.xy)) into an image exactly the size of that viewport, so every
|
||||||
|
// store fell outside the image and the test read back zeroes.
|
||||||
|
//
|
||||||
|
// The rewrite redirects every read of the builtin to a Private copy initialised once at
|
||||||
|
// entry, which is exact for all access forms (whole-vector loads, `.y` access chains,
|
||||||
|
// OpCopyMemory) and leaves the builtin itself - and its decorations - untouched.
|
||||||
|
class GlFragCoordYFlipPass final : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "mobilegl-fragcoord-y-flip"; }
|
||||||
|
explicit GlFragCoordYFlipPass(Uint32 framebufferHeight) : m_framebufferHeight(framebufferHeight) {}
|
||||||
|
|
||||||
|
Status Process() override {
|
||||||
|
using namespace spvtools::opt;
|
||||||
|
if (m_framebufferHeight == 0) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
Instruction* entryPoint = nullptr;
|
||||||
|
for (auto& candidate : get_module()->entry_points()) {
|
||||||
|
if (candidate.NumInOperands() >= 2 &&
|
||||||
|
static_cast<spv::ExecutionModel>(candidate.GetSingleWordInOperand(0)) ==
|
||||||
|
spv::ExecutionModel::Fragment) {
|
||||||
|
entryPoint = &candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!entryPoint) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
const Uint32 builtinVarId = FindFragCoordVariable();
|
||||||
|
if (builtinVarId == 0) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
Instruction* builtinVar = context()->get_def_use_mgr()->GetDef(builtinVarId);
|
||||||
|
if (!builtinVar || builtinVar->opcode() != spv::Op::OpVariable) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
// The builtin is `Input vec4`; take the vector and component types from its own
|
||||||
|
// pointer type rather than assuming float32x4, so a module that spells it
|
||||||
|
// differently declines instead of miscompiling.
|
||||||
|
Instruction* inputPtrType = context()->get_def_use_mgr()->GetDef(builtinVar->type_id());
|
||||||
|
if (!inputPtrType || inputPtrType->opcode() != spv::Op::OpTypePointer) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
const Uint32 vectorTypeId = inputPtrType->GetSingleWordInOperand(1);
|
||||||
|
Instruction* vectorType = context()->get_def_use_mgr()->GetDef(vectorTypeId);
|
||||||
|
if (!vectorType || vectorType->opcode() != spv::Op::OpTypeVector ||
|
||||||
|
vectorType->GetSingleWordInOperand(1) != 4) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
const Uint32 floatTypeId = vectorType->GetSingleWordInOperand(0);
|
||||||
|
auto* floatType = context()->get_type_mgr()->GetType(floatTypeId);
|
||||||
|
if (!floatType || !floatType->AsFloat() || floatType->AsFloat()->width() != 32) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto heightBits = std::bit_cast<Uint32>(static_cast<float>(m_framebufferHeight));
|
||||||
|
const auto* heightConst = context()->get_constant_mgr()->GetConstant(floatType, {heightBits});
|
||||||
|
auto* heightInst = context()->get_constant_mgr()->GetDefiningInstruction(heightConst);
|
||||||
|
if (!heightInst) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
auto* function = context()->GetFunction(entryPoint->GetSingleWordInOperand(1));
|
||||||
|
if (!function || function->begin() == function->end()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
const Uint32 privatePtrTypeId =
|
||||||
|
context()->get_type_mgr()->FindPointerToType(vectorTypeId, spv::StorageClass::Private);
|
||||||
|
if (privatePtrTypeId == 0) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
const Uint32 copyVarId = context()->TakeNextId();
|
||||||
|
if (copyVarId == 0) return Status::SuccessWithoutChange;
|
||||||
|
auto copyVar = std::make_unique<Instruction>(
|
||||||
|
context(), spv::Op::OpVariable, privatePtrTypeId, copyVarId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Private)}}});
|
||||||
|
context()->AddGlobalValue(std::move(copyVar));
|
||||||
|
|
||||||
|
// Redirect the reads BEFORE emitting the initialiser, so the initialiser's own
|
||||||
|
// load of the builtin is not rewritten into a load of the (still empty) copy.
|
||||||
|
if (!RedirectReads(builtinVarId, copyVarId)) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
auto& entryBlock = *function->begin();
|
||||||
|
auto insertPoint = entryBlock.begin();
|
||||||
|
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
|
||||||
|
++insertPoint;
|
||||||
|
}
|
||||||
|
if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
InstructionBuilder builder(context(), &*insertPoint,
|
||||||
|
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||||
|
auto* raw = builder.AddLoad(vectorTypeId, builtinVarId);
|
||||||
|
if (!raw) return Status::SuccessWithoutChange;
|
||||||
|
auto* x = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {0});
|
||||||
|
auto* y = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {1});
|
||||||
|
auto* z = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {2});
|
||||||
|
auto* w = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {3});
|
||||||
|
if (!x || !y || !z || !w) return Status::SuccessWithoutChange;
|
||||||
|
auto* flippedY =
|
||||||
|
builder.AddBinaryOp(floatTypeId, spv::Op::OpFSub, heightInst->result_id(), y->result_id());
|
||||||
|
if (!flippedY) return Status::SuccessWithoutChange;
|
||||||
|
auto* corrected = builder.AddCompositeConstruct(
|
||||||
|
vectorTypeId, {x->result_id(), flippedY->result_id(), z->result_id(), w->result_id()});
|
||||||
|
if (!corrected) return Status::SuccessWithoutChange;
|
||||||
|
if (!builder.AddStore(copyVarId, corrected->result_id())) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
// SPIR-V 1.4 widened the entry-point interface to every global the entry point
|
||||||
|
// statically uses, Private included; earlier versions accept Input/Output only,
|
||||||
|
// so listing it there would be invalid.
|
||||||
|
if (get_module()->version() >= 0x00010400u) {
|
||||||
|
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {copyVarId}});
|
||||||
|
context()->AnalyzeUses(entryPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisDefUse |
|
||||||
|
spvtools::opt::IRContext::kAnalysisInstrToBlockMapping);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Uint32 FindFragCoordVariable() const {
|
||||||
|
for (const auto& annotation : get_module()->annotations()) {
|
||||||
|
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||||
|
if (annotation.NumInOperands() < 3) continue;
|
||||||
|
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||||
|
spv::Decoration::BuiltIn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != spv::BuiltIn::FragCoord) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return annotation.GetSingleWordInOperand(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every instruction that reads through the builtin's POINTER gets the copy instead.
|
||||||
|
// Decorations, names and the entry-point interface keep naming the builtin.
|
||||||
|
Bool RedirectReads(Uint32 builtinVarId, Uint32 copyVarId) {
|
||||||
|
using namespace spvtools::opt;
|
||||||
|
Bool ok = true;
|
||||||
|
Vector<Instruction*> users;
|
||||||
|
context()->get_def_use_mgr()->ForEachUser(builtinVarId, [&](Instruction* user) {
|
||||||
|
switch (user->opcode()) {
|
||||||
|
case spv::Op::OpLoad:
|
||||||
|
case spv::Op::OpAccessChain:
|
||||||
|
case spv::Op::OpInBoundsAccessChain:
|
||||||
|
case spv::Op::OpPtrAccessChain:
|
||||||
|
case spv::Op::OpInBoundsPtrAccessChain:
|
||||||
|
case spv::Op::OpCopyMemory:
|
||||||
|
case spv::Op::OpCopyMemorySized:
|
||||||
|
users.push_back(user);
|
||||||
|
break;
|
||||||
|
case spv::Op::OpStore:
|
||||||
|
// gl_FragCoord is read-only; a store through it means this is not the
|
||||||
|
// module we think it is.
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!ok) return false;
|
||||||
|
for (Instruction* user : users) {
|
||||||
|
for (Uint32 i = 0; i < user->NumInOperands(); ++i) {
|
||||||
|
auto& operand = user->GetInOperand(i);
|
||||||
|
if (operand.type == SPV_OPERAND_TYPE_ID && !operand.words.empty() &&
|
||||||
|
operand.words[0] == builtinVarId) {
|
||||||
|
operand.words[0] = copyVarId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context()->AnalyzeUses(user);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 m_framebufferHeight = 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
||||||
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
||||||
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
||||||
@@ -948,6 +1134,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
std::string name;
|
std::string name;
|
||||||
Uint32 bufferIndex = 0;
|
Uint32 bufferIndex = 0;
|
||||||
Uint32 offsetBytes = 0;
|
Uint32 offsetBytes = 0;
|
||||||
|
// Set when the capture names a member of an output interface block
|
||||||
|
// ("Block.member"): the decoration target is then the block's struct TYPE,
|
||||||
|
// decorated per member, not the variable. `name` keeps the GL spelling and
|
||||||
|
// is useless for the id lookup, so the instance name is carried separately.
|
||||||
|
std::string blockInstanceName;
|
||||||
|
std::string blockName;
|
||||||
|
Int blockMemberIndex = -1;
|
||||||
|
Int blockMemberElement = -1; // array element of that member, -1 = the whole member
|
||||||
|
Uint32 byteSize = 0;
|
||||||
};
|
};
|
||||||
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
|
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
|
||||||
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
|
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
|
||||||
@@ -979,6 +1174,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
|
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
|
||||||
offsetBytes);
|
offsetBytes);
|
||||||
};
|
};
|
||||||
|
// SPIR-V puts XfbBuffer/XfbStride/Offset on the struct MEMBER when the
|
||||||
|
// captured varying lives in an interface block (SPIR-V 1.6 §3.20 lists all
|
||||||
|
// three as member-decoratable); Offset in particular is illegal on the block
|
||||||
|
// variable once the type is decorated Block.
|
||||||
|
const auto decorateMemberForXfb = [&](Uint32 structTypeId, Uint32 memberIndex, Uint32 bufferIndex,
|
||||||
|
Uint32 offsetBytes) {
|
||||||
|
const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0;
|
||||||
|
decorationManager->AddMemberDecoration(structTypeId, memberIndex,
|
||||||
|
static_cast<Uint32>(spv::Decoration::XfbBuffer),
|
||||||
|
bufferIndex);
|
||||||
|
decorationManager->AddMemberDecoration(structTypeId, memberIndex,
|
||||||
|
static_cast<Uint32>(spv::Decoration::XfbStride), stride);
|
||||||
|
decorationManager->AddMemberDecoration(structTypeId, memberIndex,
|
||||||
|
static_cast<Uint32>(spv::Decoration::Offset), offsetBytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A member array captured element by element ("Block.attrib[0]" .. "[15]")
|
||||||
|
// is one SPIR-V member, so its captures collapse into a single decoration
|
||||||
|
// placed at the first element's offset - the rest follow from the member's
|
||||||
|
// own layout. Collected first so the group is complete before it decorates.
|
||||||
|
struct MemberGroup {
|
||||||
|
Uint32 bufferIndex = 0;
|
||||||
|
Uint32 minOffset = 0;
|
||||||
|
Uint32 elementBytes = 0;
|
||||||
|
Vector<Uint32> offsets;
|
||||||
|
};
|
||||||
|
std::map<std::pair<Uint32, Uint32>, MemberGroup> memberGroups;
|
||||||
|
|
||||||
Bool modified = false;
|
Bool modified = false;
|
||||||
Bool needsPositionMirror = false;
|
Bool needsPositionMirror = false;
|
||||||
@@ -991,6 +1213,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
positionOffset = varying.offsetBytes;
|
positionOffset = varying.offsetBytes;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (varying.blockMemberIndex >= 0) {
|
||||||
|
// glslang names the block's instance variable and its struct type
|
||||||
|
// separately; an anonymous instance leaves only the type named, so
|
||||||
|
// both spellings are tried before giving up.
|
||||||
|
Uint32 structTypeId = 0;
|
||||||
|
if (const auto it = idsByName.find(varying.blockInstanceName); it != idsByName.end()) {
|
||||||
|
structTypeId = BlockStructTypeOf(it->second);
|
||||||
|
}
|
||||||
|
if (structTypeId == 0) {
|
||||||
|
if (const auto it = idsByName.find(varying.blockName); it != idsByName.end()) {
|
||||||
|
const spvtools::opt::Instruction* def = context()->get_def_use_mgr()->GetDef(it->second);
|
||||||
|
if (def != nullptr && def->opcode() == spv::Op::OpTypeStruct) {
|
||||||
|
structTypeId = it->second;
|
||||||
|
} else if (def != nullptr && def->opcode() == spv::Op::OpVariable) {
|
||||||
|
structTypeId = BlockStructTypeOf(it->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (structTypeId == 0) {
|
||||||
|
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V interface block '%s' (instance '%s') for "
|
||||||
|
"capture '%s'",
|
||||||
|
varying.blockName.c_str(), varying.blockInstanceName.c_str(),
|
||||||
|
varying.name.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto& group =
|
||||||
|
memberGroups[{structTypeId, static_cast<Uint32>(varying.blockMemberIndex)}];
|
||||||
|
if (group.offsets.empty() || varying.offsetBytes < group.minOffset) {
|
||||||
|
group.minOffset = varying.offsetBytes;
|
||||||
|
}
|
||||||
|
group.bufferIndex = varying.bufferIndex;
|
||||||
|
group.elementBytes = varying.byteSize;
|
||||||
|
group.offsets.push_back(varying.offsetBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const auto idIt = idsByName.find(varying.name);
|
const auto idIt = idsByName.find(varying.name);
|
||||||
if (idIt == idsByName.end()) {
|
if (idIt == idsByName.end()) {
|
||||||
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
|
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
|
||||||
@@ -1000,6 +1257,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto& [key, group] : memberGroups) {
|
||||||
|
// The single Offset can only stand for the whole group when the group's
|
||||||
|
// captures are a gap-free ascending run - that is what SPIR-V lays the
|
||||||
|
// member's elements out as. Anything else still gets a best-effort
|
||||||
|
// decoration, but say so, because the capture layout will not match GL.
|
||||||
|
std::sort(group.offsets.begin(), group.offsets.end());
|
||||||
|
for (SizeT i = 1; i < group.offsets.size(); ++i) {
|
||||||
|
if (group.elementBytes == 0 ||
|
||||||
|
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
|
||||||
|
MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
|
||||||
|
"non-contiguous element set; the capture layout will differ from GL's",
|
||||||
|
key.second, key.first);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decorateMemberForXfb(key.first, key.second, group.bufferIndex, group.minOffset);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (needsPositionMirror) {
|
if (needsPositionMirror) {
|
||||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||||
positionOffset, decorateForXfb);
|
positionOffset, decorateForXfb);
|
||||||
@@ -1021,6 +1297,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// The struct type an interface-block variable points at, peeling an array of
|
||||||
|
// block instances on the way. 0 when the id is not a block variable at all.
|
||||||
|
Uint32 BlockStructTypeOf(Uint32 variableId) {
|
||||||
|
auto* defUse = context()->get_def_use_mgr();
|
||||||
|
const spvtools::opt::Instruction* variable = defUse->GetDef(variableId);
|
||||||
|
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable) return 0;
|
||||||
|
const spvtools::opt::Instruction* pointer = defUse->GetDef(variable->type_id());
|
||||||
|
if (pointer == nullptr || pointer->opcode() != spv::Op::OpTypePointer) return 0;
|
||||||
|
Uint32 pointeeId = pointer->GetSingleWordInOperand(1);
|
||||||
|
for (const spvtools::opt::Instruction* pointee = defUse->GetDef(pointeeId); pointee != nullptr;
|
||||||
|
pointee = defUse->GetDef(pointeeId)) {
|
||||||
|
if (pointee->opcode() == spv::Op::OpTypeStruct) return pointeeId;
|
||||||
|
if (pointee->opcode() != spv::Op::OpTypeArray &&
|
||||||
|
pointee->opcode() != spv::Op::OpTypeRuntimeArray) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
pointeeId = pointee->GetSingleWordInOperand(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
template <typename DecorateFn>
|
template <typename DecorateFn>
|
||||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||||
@@ -1301,6 +1598,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
|
||||||
|
Uint32 framebufferHeight) {
|
||||||
|
if (input.empty()) {
|
||||||
|
output.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (framebufferHeight == 0) {
|
||||||
|
output = input;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||||
|
spvtools::OptimizerOptions options;
|
||||||
|
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||||
|
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||||
|
const char* message) {
|
||||||
|
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
|
||||||
|
});
|
||||||
|
optimizer.RegisterPass(
|
||||||
|
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
|
||||||
|
|
||||||
|
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||||
|
if (!success) {
|
||||||
|
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
|
||||||
|
output = input;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||||
const MG_State::GLState::ProgramObject& program) {
|
const MG_State::GLState::ProgramObject& program) {
|
||||||
if (input.empty()) {
|
if (input.empty()) {
|
||||||
@@ -1310,7 +1636,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
|
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
|
||||||
varyings.reserve(program.GetTransformFeedbackVaryingCount());
|
varyings.reserve(program.GetTransformFeedbackVaryingCount());
|
||||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||||
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes});
|
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes,
|
||||||
|
varying.blockInstanceName, varying.blockName, varying.blockMemberIndex,
|
||||||
|
varying.blockMemberElement, varying.byteSize});
|
||||||
}
|
}
|
||||||
Vector<Uint32> strides;
|
Vector<Uint32> strides;
|
||||||
strides.reserve(program.GetTransformFeedbackBufferCount());
|
strides.reserve(program.GetTransformFeedbackBufferCount());
|
||||||
@@ -1772,6 +2100,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||||
}
|
}
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
||||||
|
// Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would
|
||||||
|
// re-key every program in the cache on a resize for no reason.
|
||||||
|
if (flags & CompileOptionBit::FragCoordYFlip) {
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
|
||||||
|
sizeof(m_defaultFramebufferHeight)));
|
||||||
|
}
|
||||||
|
|
||||||
// Include UBO block bindings in hash so different binding configurations produce different entries
|
// Include UBO block bindings in hash so different binding configurations produce different entries
|
||||||
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
||||||
@@ -2380,14 +2714,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgramFactory::SetDefaultFramebufferHeight(Uint32 height) {
|
||||||
|
if (m_defaultFramebufferHeight == height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_defaultFramebufferHeight = height;
|
||||||
|
// Both memos key on (program, flags) alone, so neither can tell the two heights apart:
|
||||||
|
// drop the lookup memo, and bump the structure epoch so every caller holding a
|
||||||
|
// VkProgramObject* re-runs GetOrCreateProgram and lands on the new hash. The cached
|
||||||
|
// entries themselves stay - they are keyed by a hash that now includes the old height,
|
||||||
|
// so they can only be reached again if that height comes back, and the frame-boundary
|
||||||
|
// sweep retires them otherwise.
|
||||||
|
m_lastLookup = {};
|
||||||
|
++m_cacheStructureEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
|
||||||
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
|
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
|
||||||
// reuse the program's memoized hash while its backend state version is unchanged.
|
// reuse the program's memoized hash while its backend state version is unchanged.
|
||||||
|
// The memo keys on the flags word, which ComputeHash is no longer a pure function of:
|
||||||
|
// a FragCoordYFlip variant also depends on the baked default-framebuffer height, so
|
||||||
|
// that height rides in the free high half of the key. Flags occupy the low bits, and a
|
||||||
|
// height cannot exceed the 16 bits a swapchain extent fits in.
|
||||||
|
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
|
||||||
|
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
||||||
|
: flags.GetRaw();
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
if (!program.GetBackendHashMemo(flags.GetRaw(), hash)) {
|
if (!program.GetBackendHashMemo(memoKey, hash)) {
|
||||||
hash = ComputeHash(program, flags);
|
hash = ComputeHash(program, flags);
|
||||||
program.SetBackendHashMemo(flags.GetRaw(), hash);
|
program.SetBackendHashMemo(memoKey, hash);
|
||||||
}
|
}
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
@@ -2440,6 +2796,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] &&
|
||||||
|
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
||||||
|
Vector<Uint> fragCoordSpirv;
|
||||||
|
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
||||||
|
moduleSpirvs[i] = Move(fragCoordSpirv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||||
// stored as - which addresses [0,1] where the application addressed texels.
|
// stored as - which addresses [0,1] where the application addressed texels.
|
||||||
@@ -2568,6 +2932,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
entry.modules.push_back(module);
|
entry.modules.push_back(module);
|
||||||
entry.stages.push_back(stage);
|
entry.stages.push_back(stage);
|
||||||
|
entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{
|
||||||
|
static_cast<Uint32>(stage.stage), static_cast<Uint32>(moduleSpv.size()),
|
||||||
|
XXH64(moduleSpv.data(), moduleSpv.size() * sizeof(Uint), 0)});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reflect and create layout as part of the program object
|
// Reflect and create layout as part of the program object
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
|
#include "PipelineFactory.h"
|
||||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||||
@@ -52,6 +53,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recorded while GL transform feedback is active, so plain draws keep the
|
// recorded while GL transform feedback is active, so plain draws keep the
|
||||||
// undecorated variant.
|
// undecorated variant.
|
||||||
XfbCapture = 1 << 6,
|
XfbCapture = 1 << 6,
|
||||||
|
// Rewrites the fragment stage's gl_FragCoord reads to GL's bottom-left window
|
||||||
|
// origin. Vulkan's gl_FragCoord.y IS the framebuffer row being written, and the
|
||||||
|
// default framebuffer's image is stored in display (top-left) order, so a shader
|
||||||
|
// that reads gl_FragCoord there sees `height - y_GL`. Set together with
|
||||||
|
// PositionYFlip (the two are the same fact about the same draws) except under a
|
||||||
|
// quarter turn, which this renderer does not convert rectangles for either.
|
||||||
|
FragCoordYFlip = 1 << 7,
|
||||||
};
|
};
|
||||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -62,6 +70,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
Vector<VkPipelineShaderStageCreateInfo> stages;
|
Vector<VkPipelineShaderStageCreateInfo> stages;
|
||||||
Vector<VkShaderModule> modules;
|
Vector<VkShaderModule> modules;
|
||||||
|
// Parallel to stages; identifies the exact module bytes handed to the driver when a
|
||||||
|
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
|
||||||
|
Vector<ShaderStageSpirvDigest> stageSpirvDigests;
|
||||||
|
|
||||||
// Layout data (previously in separate VkProgramLayout)
|
// Layout data (previously in separate VkProgramLayout)
|
||||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
@@ -263,6 +274,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkProgramObject& GetOrCreateProgram(
|
const VkProgramObject& GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||||
|
|
||||||
|
// The default framebuffer's current image height, baked as a literal into every
|
||||||
|
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
|
||||||
|
// adding one for a value that changes only on swapchain recreation would cost the draw
|
||||||
|
// path more than a recompile costs a resize). It is therefore part of those variants'
|
||||||
|
// identity: ComputeHash mixes it in when the bit is set, so a height change re-keys them
|
||||||
|
// and leaves every other program's hash untouched. Setting a NEW height also bumps the
|
||||||
|
// cache-structure epoch, because a caller holding a memoised VkProgramObject* would
|
||||||
|
// otherwise keep using a module compiled against the old height.
|
||||||
|
void SetDefaultFramebufferHeight(Uint32 height);
|
||||||
|
Uint32 GetDefaultFramebufferHeight() const { return m_defaultFramebufferHeight; }
|
||||||
|
|
||||||
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
|
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
|
||||||
// an open-addressing map holding entries by value, so both moves existing entries.
|
// an open-addressing map holding entries by value, so both moves existing entries.
|
||||||
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
||||||
@@ -320,6 +342,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True only when the logical device enabled both
|
// True only when the logical device enabled both
|
||||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
|
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||||
|
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||||
|
Uint32 m_defaultFramebufferHeight = 0;
|
||||||
mutable ProgramLookupCache m_lastLookup;
|
mutable ProgramLookupCache m_lastLookup;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameCounter = 0;
|
Uint64 m_frameCounter = 0;
|
||||||
|
|||||||
@@ -220,6 +220,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return static_cast<Int>((static_cast<Int64>(value) * toExtent + fromExtent / 2) / fromExtent);
|
return static_cast<Int>((static_cast<Int64>(value) * toExtent + fromExtent / 2) / fromExtent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// Default-framebuffer rectangles.
|
||||||
|
//
|
||||||
|
// GL's window origin is the BOTTOM-left. The default framebuffer's Vulkan image is stored in
|
||||||
|
// DISPLAY (top-left) orientation, and the difference is reconciled for VERTICES by negating
|
||||||
|
// gl_Position.y - but only for default-FBO draws (GetShaderTransformFlags ->
|
||||||
|
// CompileOptionBit::PositionYFlip, applied in ProgramFactory::InsertPositionFixup).
|
||||||
|
//
|
||||||
|
// Rectangles were never converted. The viewport, the scissor and the ReadPixels copy offset
|
||||||
|
// all used the GL bottom-origin Y verbatim as a Vulkan top-origin Y, which is correct only
|
||||||
|
// when y == H - y - h (full height, or vertically centred) - and full height is the only case
|
||||||
|
// any test ever exercised. In the conformance suite the errors CANCEL in placement (the draw
|
||||||
|
// lands in Vulkan rows [y, y+h) and the readback copies the same rows back) and compose into
|
||||||
|
// an exact vertical flip: 1,759 of Magma's 1,793 non-passing cases, 861 vertical flips and
|
||||||
|
// nothing else across all of gl33.
|
||||||
|
//
|
||||||
|
// The mapping below is derived from - and at full extent exactly reproduces - the pixel
|
||||||
|
// mapping RemapDefaultFboReadbackToGLOrientation has always used:
|
||||||
|
// identity : image(x, H-1-y) -> flip Y
|
||||||
|
// 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows)
|
||||||
|
// Quarter turns swap the axes; nothing in this renderer models that (the readback declines to
|
||||||
|
// remap them and the viewport path only rescales), so they are left exactly as they were.
|
||||||
|
struct DefaultFramebufferRectMapping {
|
||||||
|
Bool flipY = false;
|
||||||
|
Bool mirrorX = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static DefaultFramebufferRectMapping GetDefaultFramebufferRectMapping(
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform) {
|
||||||
|
if (preTransform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR) return {false, true};
|
||||||
|
if (IsQuarterTurnPreTransform(preTransform)) return {false, false};
|
||||||
|
return {true, false};
|
||||||
|
}
|
||||||
|
|
||||||
|
// [origin, origin+size) counted from one end is [extent-origin-size, extent-origin) counted
|
||||||
|
// from the other. A full-extent rect is a fixed point, which is why this can be introduced
|
||||||
|
// without moving anything that works today.
|
||||||
|
static Int MapDefaultFramebufferRectAxis(Int origin, Int size, Int extent, Bool invert) {
|
||||||
|
return invert ? extent - origin - size : origin;
|
||||||
|
}
|
||||||
|
|
||||||
// Redundant dynamic-state elimination for the per-draw hot path: within one
|
// Redundant dynamic-state elimination for the per-draw hot path: within one
|
||||||
// command-buffer recording, a vkCmdSet* whose values already match what the
|
// command-buffer recording, a vkCmdSet* whose values already match what the
|
||||||
// command buffer holds is skipped. Valid because every PipelineFactory
|
// command buffer holds is skipped. Valid because every PipelineFactory
|
||||||
@@ -417,6 +458,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewportHeight = ScaleFramebufferCoordinate(viewportHeight, logicalExtent.y(), framebufferExtent.y());
|
viewportHeight = ScaleFramebufferCoordinate(viewportHeight, logicalExtent.y(), framebufferExtent.y());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The GL viewport rect, expressed against the default framebuffer's stored orientation.
|
||||||
|
// A full-height viewport is unchanged by this, which is why every existing scenario keeps
|
||||||
|
// its exact behaviour.
|
||||||
|
if (isDefaultFramebuffer) {
|
||||||
|
const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform);
|
||||||
|
viewportX = MapDefaultFramebufferRectAxis(viewportX, viewportWidth, framebufferExtent.x(),
|
||||||
|
mapping.mirrorX);
|
||||||
|
viewportY = MapDefaultFramebufferRectAxis(viewportY, viewportHeight, framebufferExtent.y(),
|
||||||
|
mapping.flipY);
|
||||||
|
}
|
||||||
|
|
||||||
VkViewport viewport{};
|
VkViewport viewport{};
|
||||||
viewport.x = static_cast<float>(viewportX);
|
viewport.x = static_cast<float>(viewportX);
|
||||||
viewport.y = static_cast<float>(viewportY);
|
viewport.y = static_cast<float>(viewportY);
|
||||||
@@ -518,11 +570,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return scissor;
|
return scissor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The clamped rect, re-expressed against the default framebuffer's stored orientation. Same
|
||||||
|
// conversion as the viewport - and it must be the same one, or the scissor would cut a band
|
||||||
|
// the draw never touched.
|
||||||
|
static VkRect2D MapScissorRectToDefaultFramebuffer(VkRect2D scissor, const IntVec2& framebufferExtent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform) {
|
||||||
|
const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform);
|
||||||
|
scissor.offset.x = MapDefaultFramebufferRectAxis(scissor.offset.x, static_cast<Int>(scissor.extent.width),
|
||||||
|
framebufferExtent.x(), mapping.mirrorX);
|
||||||
|
scissor.offset.y = MapDefaultFramebufferRectAxis(scissor.offset.y, static_cast<Int>(scissor.extent.height),
|
||||||
|
framebufferExtent.y(), mapping.flipY);
|
||||||
|
return scissor;
|
||||||
|
}
|
||||||
|
|
||||||
static VkRect2D MakeDefaultFramebufferScissorRect(const IntVec4& scissorBox,
|
static VkRect2D MakeDefaultFramebufferScissorRect(const IntVec4& scissorBox,
|
||||||
const IntVec2& framebufferExtent,
|
const IntVec2& framebufferExtent,
|
||||||
VkSurfaceTransformFlagBitsKHR preTransform) {
|
VkSurfaceTransformFlagBitsKHR preTransform) {
|
||||||
if (!IsQuarterTurnPreTransform(preTransform)) {
|
if (!IsQuarterTurnPreTransform(preTransform)) {
|
||||||
return MakeClampedScissorRect(scissorBox, framebufferExtent);
|
return MapScissorRectToDefaultFramebuffer(MakeClampedScissorRect(scissorBox, framebufferExtent),
|
||||||
|
framebufferExtent, preTransform);
|
||||||
}
|
}
|
||||||
|
|
||||||
const IntVec2 logicalExtent = ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent);
|
const IntVec2 logicalExtent = ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent);
|
||||||
@@ -542,7 +608,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static_cast<Uint32>(std::max<Int>(0, rawX1 - rawX0)),
|
static_cast<Uint32>(std::max<Int>(0, rawX1 - rawX0)),
|
||||||
static_cast<Uint32>(std::max<Int>(0, rawY1 - rawY0)),
|
static_cast<Uint32>(std::max<Int>(0, rawY1 - rawY0)),
|
||||||
};
|
};
|
||||||
return scissor;
|
// A quarter turn maps to {false, false}, so this is a no-op today; it is here so the
|
||||||
|
// branch cannot drift away from the identity/180 one when quarter turns are modelled.
|
||||||
|
return MapScissorRectToDefaultFramebuffer(scissor, framebufferExtent, preTransform);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ApplyStencilState(VkCommandBuffer commandBuffer) {
|
static void ApplyStencilState(VkCommandBuffer commandBuffer) {
|
||||||
@@ -1928,6 +1996,29 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same conversion on the READ side, which never had one: a blit whose source is the
|
||||||
|
// default framebuffer used raw GL offsets against a display-oriented image, so it sampled
|
||||||
|
// the mirrored band and wrote it upside down. Mapping BOTH endpoints inverts the offset
|
||||||
|
// pair, and an inverted pair is exactly how VkImageBlit spells "flip this axis" - so the
|
||||||
|
// band and the row order are corrected in one step. A full-extent blit is unchanged in
|
||||||
|
// band and gains the row flip it always needed.
|
||||||
|
static void ApplyNativeBlitDefaultFramebufferSourceTransform(VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
const BlitImageBinding& srcBinding,
|
||||||
|
VkImageBlit& blitRegion) {
|
||||||
|
switch (preTransform) {
|
||||||
|
case VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR:
|
||||||
|
blitRegion.srcOffsets[0].y = srcBinding.extent.y() - blitRegion.srcOffsets[0].y;
|
||||||
|
blitRegion.srcOffsets[1].y = srcBinding.extent.y() - blitRegion.srcOffsets[1].y;
|
||||||
|
break;
|
||||||
|
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
|
||||||
|
blitRegion.srcOffsets[0].x = srcBinding.extent.x() - blitRegion.srcOffsets[0].x;
|
||||||
|
blitRegion.srcOffsets[1].x = srcBinding.extent.x() - blitRegion.srcOffsets[1].x;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Bool DecodeReadbackPixel(const Uint8* source, VkFormat sourceFormat, Float* rgba) {
|
static Bool DecodeReadbackPixel(const Uint8* source, VkFormat sourceFormat, Float* rgba) {
|
||||||
switch (sourceFormat) {
|
switch (sourceFormat) {
|
||||||
case VK_FORMAT_R8G8B8A8_UNORM:
|
case VK_FORMAT_R8G8B8A8_UNORM:
|
||||||
@@ -2033,42 +2124,42 @@ void main() {
|
|||||||
return static_cast<Uint8>(value * 255.0f + 0.5f);
|
return static_cast<Uint8>(value * 255.0f + 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remap raw swapchain pixels (top-left origin, preTransform-rotated) into
|
// Re-order the copied BLOCK - not the whole image - from the default framebuffer's stored
|
||||||
// GL-oriented pixels (bottom-left origin) for the retrace snapshot path.
|
// orientation into GL's. The caller has already aimed the copy at the right place with
|
||||||
// Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip
|
// MapDefaultFramebufferRectAxis, so what arrives here is exactly the requested
|
||||||
// apitrace's flipped=true Image expects. Only identity/180 share the
|
// rectWidth x rectHeight rect, and all that is left is the order of rows (identity) or of
|
||||||
// swapchain extent with the default framebuffer; 90/270 swap extents and
|
// columns (180) WITHIN it.
|
||||||
// are not handled here.
|
//
|
||||||
|
// This used to iterate the full swapchain extent and index both sides with that stride,
|
||||||
|
// which is why its caller could only use it on an exact full-extent read - and why every
|
||||||
|
// partial glReadPixels of the default framebuffer came back in Vulkan row order. Only
|
||||||
|
// identity/180 share the swapchain extent with the default framebuffer; 90/270 swap
|
||||||
|
// extents and are still declined.
|
||||||
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
|
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
|
||||||
VkExtent2D rawExtent,
|
Uint32 rectWidth,
|
||||||
|
Uint32 rectHeight,
|
||||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
SizeT texelSize,
|
SizeT texelSize,
|
||||||
Uint8* outPixels) {
|
Uint8* outPixels) {
|
||||||
if (IsQuarterTurnPreTransform(preTransform)) {
|
if (IsQuarterTurnPreTransform(preTransform)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Uint32 w = rawExtent.width;
|
if (rectWidth == 0 || rectHeight == 0 || texelSize == 0) {
|
||||||
const Uint32 h = rawExtent.height;
|
|
||||||
if (w == 0 || h == 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (Uint32 outY = 0; outY < h; ++outY) {
|
const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform);
|
||||||
const Uint32 displayY = h - 1 - outY; // GL bottom-origin -> display top-origin
|
const SizeT rowBytes = static_cast<SizeT>(rectWidth) * texelSize;
|
||||||
for (Uint32 outX = 0; outX < w; ++outX) {
|
for (Uint32 outY = 0; outY < rectHeight; ++outY) {
|
||||||
const Uint32 displayX = outX;
|
const Uint32 srcY = mapping.flipY ? (rectHeight - 1 - outY) : outY;
|
||||||
Uint32 rawX = displayX;
|
const Uint8* srcRow = rawPixels + static_cast<SizeT>(srcY) * rowBytes;
|
||||||
Uint32 rawY = displayY;
|
Uint8* dstRow = outPixels + static_cast<SizeT>(outY) * rowBytes;
|
||||||
switch (preTransform) {
|
if (!mapping.mirrorX) {
|
||||||
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
|
Memcpy(dstRow, srcRow, rowBytes);
|
||||||
rawX = w - 1 - displayX;
|
continue;
|
||||||
rawY = h - 1 - displayY;
|
}
|
||||||
break;
|
for (Uint32 outX = 0; outX < rectWidth; ++outX) {
|
||||||
default:
|
Memcpy(dstRow + static_cast<SizeT>(outX) * texelSize,
|
||||||
break;
|
srcRow + static_cast<SizeT>(rectWidth - 1 - outX) * texelSize, texelSize);
|
||||||
}
|
|
||||||
const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * texelSize;
|
|
||||||
Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * texelSize;
|
|
||||||
Memcpy(dst, src, texelSize);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -2688,6 +2779,14 @@ void main() {
|
|||||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
if (currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer()) {
|
if (currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer()) {
|
||||||
flags |= ProgramFactory::CompileOptionBit::PositionYFlip;
|
flags |= ProgramFactory::CompileOptionBit::PositionYFlip;
|
||||||
|
// gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow
|
||||||
|
// (GetDefaultFramebufferRectMapping): flipped for identity/180, left alone under a
|
||||||
|
// quarter turn, which this renderer converts nothing for. Keeping the two in step
|
||||||
|
// is the whole point - a fragment's window Y and the viewport that placed it must
|
||||||
|
// agree on which end of the image they count from.
|
||||||
|
if (!IsQuarterTurnPreTransform(preTransform)) {
|
||||||
|
flags |= ProgramFactory::CompileOptionBit::FragCoordYFlip;
|
||||||
|
}
|
||||||
switch (preTransform) {
|
switch (preTransform) {
|
||||||
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
|
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
|
||||||
flags |= ProgramFactory::CompileOptionBit::SurfaceRotate90;
|
flags |= ProgramFactory::CompileOptionBit::SurfaceRotate90;
|
||||||
@@ -2842,6 +2941,9 @@ void main() {
|
|||||||
m_shaderDrawParametersFeatureEnabled,
|
m_shaderDrawParametersFeatureEnabled,
|
||||||
m_unformattedFloatStorageImagesEnabled);
|
m_unformattedFloatStorageImagesEnabled);
|
||||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||||
|
// The swapchain already exists at this point (Initialize creates it first), so seed the
|
||||||
|
// height the factory could not be told about from CreateSwapchain.
|
||||||
|
m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height);
|
||||||
// Aging evictions (render passes and program entries) must purge the dependent
|
// Aging evictions (render passes and program entries) must purge the dependent
|
||||||
// pipeline / compute-pipeline / descriptor-set caches in the same step; both
|
// pipeline / compute-pipeline / descriptor-set caches in the same step; both
|
||||||
// sweeps only run from the frame-boundary seams, long after initialization.
|
// sweeps only run from the frame-boundary seams, long after initialization.
|
||||||
@@ -4049,7 +4151,8 @@ void main() {
|
|||||||
.depthWriteEnable = false,
|
.depthWriteEnable = false,
|
||||||
.depthCompareOp = VK_COMPARE_OP_ALWAYS,
|
.depthCompareOp = VK_COMPARE_OP_ALWAYS,
|
||||||
.stages = &programObj.stages,
|
.stages = &programObj.stages,
|
||||||
.vertexInputState = &kEmptyVertexInputState
|
.vertexInputState = &kEmptyVertexInputState,
|
||||||
|
.stageSpirvDigests = &programObj.stageSpirvDigests
|
||||||
};
|
};
|
||||||
static constexpr VkColorComponentFlags kColorWriteMask =
|
static constexpr VkColorComponentFlags kColorWriteMask =
|
||||||
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
||||||
@@ -4727,7 +4830,8 @@ void main() {
|
|||||||
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
|
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
|
||||||
.fragmentReplacesDepth = programObj.fragmentReplacesDepth,
|
.fragmentReplacesDepth = programObj.fragmentReplacesDepth,
|
||||||
.stages = &programObj.stages,
|
.stages = &programObj.stages,
|
||||||
.vertexInputState = pipelineVertexInputState
|
.vertexInputState = pipelineVertexInputState,
|
||||||
|
.stageSpirvDigests = &programObj.stageSpirvDigests
|
||||||
};
|
};
|
||||||
if (!payload.stencilTestEnable) {
|
if (!payload.stencilTestEnable) {
|
||||||
payload.frontStencilFailOp = VK_STENCIL_OP_KEEP;
|
payload.frontStencilFailOp = VK_STENCIL_OP_KEEP;
|
||||||
@@ -5911,6 +6015,17 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry);
|
auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry);
|
||||||
|
// GetOrCreatePipeline documents a VK_NULL_HANDLE return (empty stages, or a driver that
|
||||||
|
// rejected vkCreateGraphicsPipelines). Binding it dereferences null inside the driver -
|
||||||
|
// 9 of the 15 CTS process deaths were exactly this vkCmdBindPipeline. A draw that has no
|
||||||
|
// pipeline is a skipped draw, which is what every other failure below already does.
|
||||||
|
// MGLOG_I so the skip is visible in the INFO builds CTS runs against.
|
||||||
|
if (pipeline == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_I("SetupDraw skipped: no graphics pipeline for program=%u (creation failed or the "
|
||||||
|
"program has no shader stages)",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
|
|
||||||
// Begin render pass, and handle clear
|
// Begin render pass, and handle clear
|
||||||
@@ -7079,6 +7194,93 @@ void main() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A glClear on the DEFAULT framebuffer is parked as a pending clear and folded into the next
|
||||||
|
// render pass's loadOp. With no draw in between there is no render pass, so a readback that
|
||||||
|
// followed such a clear blitted the untouched swapchain image and returned the PREVIOUS
|
||||||
|
// frame's colour - which is exactly what the whole KHR-GL40.draw_indirect.negative-* family
|
||||||
|
// sees (clear, an erroring draw that never executes, glReadPixels expecting zeroes).
|
||||||
|
//
|
||||||
|
// Materializing it means clearing the acquired swapchain image itself, which is why this
|
||||||
|
// cannot reuse MaterializePendingClearForTexture: the default FBO's colour attachment is a
|
||||||
|
// placeholder ITextureObject, and syncing it would allocate and clear an unrelated image.
|
||||||
|
Bool VulkanRenderer::MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
||||||
|
MG_State::GLState::FramebufferObject& fbo,
|
||||||
|
FramebufferAttachmentType attachmentType) {
|
||||||
|
if (!fbo.IsDefaultFramebuffer() || attachmentType == FramebufferAttachmentType::None) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto& attachment = fbo.GetAttachment(attachmentType);
|
||||||
|
if (!attachment.IsTexture() || attachment.IsRenderbuffer()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ClearAttachmentPayload payload{};
|
||||||
|
if (!m_clearManager->GetPendingClear(attachment, payload)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((payload.mask & GL_COLOR_BUFFER_BIT) == 0) {
|
||||||
|
// Depth/stencil on the default framebuffer keeps the loadOp route; the readback
|
||||||
|
// path for it declines default framebuffers outright (ReadDepthStencilPixels).
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr ||
|
||||||
|
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
|
||||||
|
"MaterializePendingClearForDefaultFramebuffer requires no active render pass");
|
||||||
|
|
||||||
|
const VkImage swapchainImage = m_swapchainObject.GetImage(m_imageIndexAcquired);
|
||||||
|
if (swapchainImage == VK_NULL_HANDLE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
VkImageLayout currentLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
|
||||||
|
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
VkAccessFlags srcAccessMask = 0;
|
||||||
|
GetImageTransitionSourceState(currentLayout, srcStageMask, srcAccessMask);
|
||||||
|
VkImageLayout clearLayout = currentLayout;
|
||||||
|
if (!VkTextureManager::TransitionImageLayout(commandBuffer, swapchainImage, clearLayout,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask,
|
||||||
|
VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask,
|
||||||
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_ASPECT_COLOR_BIT)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The clear colour goes in verbatim, alpha included. Forcing opaque alpha here is what
|
||||||
|
// makes a glClear(0,0,0,0) read back as (0,0,0,1) - the default framebuffer's placeholder
|
||||||
|
// attachment can describe an alpha-less format while the swapchain image it stands for
|
||||||
|
// has a real alpha channel.
|
||||||
|
VkClearColorValue clearColor{};
|
||||||
|
clearColor.float32[0] = payload.color.x();
|
||||||
|
clearColor.float32[1] = payload.color.y();
|
||||||
|
clearColor.float32[2] = payload.color.z();
|
||||||
|
clearColor.float32[3] = payload.color.w();
|
||||||
|
VkImageSubresourceRange range{};
|
||||||
|
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
range.baseMipLevel = 0;
|
||||||
|
range.levelCount = 1;
|
||||||
|
range.baseArrayLayer = 0;
|
||||||
|
range.layerCount = 1;
|
||||||
|
vkCmdClearColorImage(commandBuffer, swapchainImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1,
|
||||||
|
&range);
|
||||||
|
|
||||||
|
VkImageLayout settledLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||||
|
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
VkAccessFlags dstAccessMask = 0;
|
||||||
|
GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, dstStageMask, dstAccessMask);
|
||||||
|
if (!VkTextureManager::TransitionImageLayout(commandBuffer, swapchainImage, settledLayout,
|
||||||
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||||
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||||
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask,
|
||||||
|
VK_IMAGE_ASPECT_COLOR_BIT)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||||
|
|
||||||
|
// Popped, not left behind: the clear has executed, so letting the next render pass load
|
||||||
|
// it again as a loadOp would erase whatever is drawn between here and there.
|
||||||
|
m_clearManager->PopPendingClear(attachment);
|
||||||
|
MGLOG_D("MaterializePendingClearForDefaultFramebuffer: swapchain image %u pending clear materialized",
|
||||||
|
m_imageIndexAcquired);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame,
|
Bool VulkanRenderer::TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame,
|
||||||
MG_State::GLState::FramebufferObject& readFbo,
|
MG_State::GLState::FramebufferObject& readFbo,
|
||||||
MG_State::GLState::FramebufferObject& drawFbo,
|
MG_State::GLState::FramebufferObject& drawFbo,
|
||||||
@@ -7667,11 +7869,19 @@ void main() {
|
|||||||
blitRegion.dstSubresource.layerCount = dstBinding.layerCount;
|
blitRegion.dstSubresource.layerCount = dstBinding.layerCount;
|
||||||
blitRegion.dstOffsets[0] = {dstX0, dstY0, 0};
|
blitRegion.dstOffsets[0] = {dstX0, dstY0, 0};
|
||||||
blitRegion.dstOffsets[1] = {dstX1, dstY1, 1};
|
blitRegion.dstOffsets[1] = {dstX1, dstY1, 1};
|
||||||
|
if (readIsDefaultFbo) {
|
||||||
|
ApplyNativeBlitDefaultFramebufferSourceTransform(m_swapchainObject.GetPreTransform(), srcBinding,
|
||||||
|
blitRegion);
|
||||||
|
}
|
||||||
if (drawIsDefaultFbo) {
|
if (drawIsDefaultFbo) {
|
||||||
ApplyNativeBlitDefaultFramebufferTransform(m_swapchainObject.GetPreTransform(), dstBinding, blitRegion);
|
ApplyNativeBlitDefaultFramebufferTransform(m_swapchainObject.GetPreTransform(), dstBinding, blitRegion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (srcBinding.sampleCount != VK_SAMPLE_COUNT_1_BIT && dstBinding.sampleCount == VK_SAMPLE_COUNT_1_BIT) {
|
if (srcBinding.sampleCount != VK_SAMPLE_COUNT_1_BIT && dstBinding.sampleCount == VK_SAMPLE_COUNT_1_BIT) {
|
||||||
|
// NOTE: vkCmdResolveImage cannot flip, and this region is still built from the raw GL
|
||||||
|
// offsets. A multisample-resolve blit whose source or destination is the default
|
||||||
|
// framebuffer therefore keeps the pre-fix behaviour; it needs a resolve-then-blit
|
||||||
|
// (or blit-then-resolve) split, which is its own change.
|
||||||
// GL multisample resolve blits are 1:1 by spec; vkCmdBlitImage cannot read a
|
// GL multisample resolve blits are 1:1 by spec; vkCmdBlitImage cannot read a
|
||||||
// multisampled source.
|
// multisampled source.
|
||||||
VkImageResolve resolveRegion{};
|
VkImageResolve resolveRegion{};
|
||||||
@@ -7875,6 +8085,19 @@ void main() {
|
|||||||
copyRegion.srcSubresource.mipLevel = srcBinding.mipLevel;
|
copyRegion.srcSubresource.mipLevel = srcBinding.mipLevel;
|
||||||
copyRegion.srcSubresource.baseArrayLayer = srcBinding.baseArrayLayer;
|
copyRegion.srcSubresource.baseArrayLayer = srcBinding.baseArrayLayer;
|
||||||
copyRegion.srcSubresource.layerCount = srcBinding.layerCount;
|
copyRegion.srcSubresource.layerCount = srcBinding.layerCount;
|
||||||
|
// KNOWN GAP, deliberately not half-fixed here: when the read framebuffer is the default
|
||||||
|
// one this samples GL rows [y, y+h) counted from the TOP of a display-oriented image, so
|
||||||
|
// it takes the mirrored band AND writes it into the (GL-oriented) destination texture
|
||||||
|
// upside down. Correcting only the offset would swap one wrong answer for another,
|
||||||
|
// because vkCmdCopyImage cannot reverse rows: this path has to become a vkCmdBlitImage
|
||||||
|
// with an inverted source Y pair, the way BlitFramebuffer above now does it. Tracked
|
||||||
|
// separately; the four sites behind the 1,759-case orientation defect are the viewport,
|
||||||
|
// the scissor, the ReadPixels copy offset and the readback remap.
|
||||||
|
if (readIsDefaultFbo) {
|
||||||
|
MGLOG_I("DirectVulkan::CopyTexSubImage2D: copying from the DEFAULT framebuffer still uses the raw GL "
|
||||||
|
"Y origin (x=%d y=%d w=%d h=%d); the result is the mirrored band, stored flipped",
|
||||||
|
x, y, width, height);
|
||||||
|
}
|
||||||
copyRegion.srcOffset = {x, y, 0};
|
copyRegion.srcOffset = {x, y, 0};
|
||||||
copyRegion.dstSubresource.aspectMask = dstBinding.aspectMask;
|
copyRegion.dstSubresource.aspectMask = dstBinding.aspectMask;
|
||||||
copyRegion.dstSubresource.mipLevel = dstBinding.mipLevel;
|
copyRegion.dstSubresource.mipLevel = dstBinding.mipLevel;
|
||||||
@@ -8158,7 +8381,18 @@ void main() {
|
|||||||
// rehash on that insertion, invalidating any RenderbufferResource*/TextureResource*
|
// rehash on that insertion, invalidating any RenderbufferResource*/TextureResource*
|
||||||
// obtained beforehand - so ResolveColorBlitBinding's cached `trackedLayout` pointer
|
// obtained beforehand - so ResolveColorBlitBinding's cached `trackedLayout` pointer
|
||||||
// must be taken AFTER this, never before it.
|
// must be taken AFTER this, never before it.
|
||||||
if (!readIsDefaultFbo) {
|
//
|
||||||
|
// The default framebuffer needs this just as much, and used to be excluded: its clear is
|
||||||
|
// parked the same way, and with no draw between the clear and the readback no render
|
||||||
|
// pass ever runs to fold it in, so the readback returned the previous frame's image
|
||||||
|
// (KHR-GL40.draw_indirect.negative-*). It only takes a different materializer because the
|
||||||
|
// image to clear is the acquired swapchain image, not the attachment's placeholder
|
||||||
|
// texture.
|
||||||
|
if (readIsDefaultFbo) {
|
||||||
|
const Bool clearReady = MaterializePendingClearForDefaultFramebuffer(frame.commandBuffer, *readFbo,
|
||||||
|
readFbo->GetReadBuffer());
|
||||||
|
MOBILEGL_ASSERT(clearReady, "ReadPixels: failed to materialize the default framebuffer's pending clear");
|
||||||
|
} else {
|
||||||
const auto& sourceAttachment = readFbo->GetAttachment(readFbo->GetReadBuffer());
|
const auto& sourceAttachment = readFbo->GetAttachment(readFbo->GetReadBuffer());
|
||||||
auto sourceTexture = sourceAttachment.GetTexture();
|
auto sourceTexture = sourceAttachment.GetTexture();
|
||||||
if (sourceTexture != nullptr) {
|
if (sourceTexture != nullptr) {
|
||||||
@@ -8235,7 +8469,21 @@ void main() {
|
|||||||
copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel;
|
copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel;
|
||||||
copyRegion.imageSubresource.baseArrayLayer = srcBinding.baseArrayLayer;
|
copyRegion.imageSubresource.baseArrayLayer = srcBinding.baseArrayLayer;
|
||||||
copyRegion.imageSubresource.layerCount = 1;
|
copyRegion.imageSubresource.layerCount = 1;
|
||||||
copyRegion.imageOffset = {x, y, static_cast<Int32>(srcBinding.depthOffset)};
|
// The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y
|
||||||
|
// verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for
|
||||||
|
// every read that was not full-height.
|
||||||
|
Int32 copyOffsetX = x;
|
||||||
|
Int32 copyOffsetY = y;
|
||||||
|
if (readIsDefaultFbo) {
|
||||||
|
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
||||||
|
const DefaultFramebufferRectMapping mapping =
|
||||||
|
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
|
||||||
|
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
|
||||||
|
mapping.mirrorX);
|
||||||
|
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
|
||||||
|
mapping.flipY);
|
||||||
|
}
|
||||||
|
copyRegion.imageOffset = {copyOffsetX, copyOffsetY, static_cast<Int32>(srcBinding.depthOffset)};
|
||||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
readback.GetHandle(), 1, ©Region);
|
readback.GetHandle(), 1, ©Region);
|
||||||
@@ -8273,23 +8521,23 @@ void main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (readIsDefaultFbo) {
|
if (readIsDefaultFbo) {
|
||||||
const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
|
|
||||||
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
|
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
|
||||||
if (static_cast<Uint32>(width) == swapchainExtent.width &&
|
// No full-extent gate any more: the remap works on the copied rect, and the copy was
|
||||||
static_cast<Uint32>(height) == swapchainExtent.height) {
|
// already aimed with the same mapping. The gate is exactly what made every partial
|
||||||
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
|
// read of the default framebuffer come back in Vulkan row order.
|
||||||
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
|
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
|
||||||
sourceTexelSize,
|
if (RemapDefaultFboReadbackToGLOrientation(mapped, static_cast<Uint32>(width),
|
||||||
remapped.data())) {
|
static_cast<Uint32>(height), preTransform, sourceTexelSize,
|
||||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
remapped.data())) {
|
||||||
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
||||||
return;
|
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
MGLOG_W("DirectVulkan::ReadPixels: default-FBO remap skipped (w=%d h=%d swapchain=%ux%u preTransform=%d); "
|
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer models
|
||||||
"falling back to raw readback",
|
// one. MGLOG_I because the INFO builds are the ones that run conformance.
|
||||||
width, height, swapchainExtent.width, swapchainExtent.height,
|
MGLOG_I("DirectVulkan::ReadPixels: default-FBO remap declined (w=%d h=%d preTransform=%d); falling back "
|
||||||
static_cast<Int>(preTransform));
|
"to raw readback",
|
||||||
|
width, height, static_cast<Int>(preTransform));
|
||||||
}
|
}
|
||||||
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, 1, format, type, pixels,
|
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, 1, format, type, pixels,
|
||||||
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
||||||
@@ -11882,6 +12130,12 @@ void main() {
|
|||||||
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily),
|
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily),
|
||||||
static_cast<Uint32>(m_physicalDevice.queueFamilies.presentFamily),
|
static_cast<Uint32>(m_physicalDevice.queueFamilies.presentFamily),
|
||||||
m_config.MaxFramesInFlight, desiredExtent);
|
m_config.MaxFramesInFlight, desiredExtent);
|
||||||
|
// The FragCoordYFlip variants bake this height in; it is the only input to a shader
|
||||||
|
// module that lives outside the GL program, so the factory has to learn it here (and on
|
||||||
|
// every recreation, which is the only way it can change).
|
||||||
|
if (m_programFactory) {
|
||||||
|
m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::CreateCommandPool() {
|
void VulkanRenderer::CreateCommandPool() {
|
||||||
|
|||||||
@@ -1118,6 +1118,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool MaterializePendingClearForRenderbuffer(
|
Bool MaterializePendingClearForRenderbuffer(
|
||||||
VkCommandBuffer commandBuffer,
|
VkCommandBuffer commandBuffer,
|
||||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||||
|
// The default framebuffer's twin of the two above. It cannot go through
|
||||||
|
// MaterializePendingClearForTexture: the default FBO's colour attachment is a
|
||||||
|
// placeholder texture object, and syncing THAT would clear a texture image nobody
|
||||||
|
// presents instead of the acquired swapchain image.
|
||||||
|
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
||||||
|
MG_State::GLState::FramebufferObject& fbo,
|
||||||
|
FramebufferAttachmentType attachmentType);
|
||||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||||
MG_State::GLState::ITextureObject& texture,
|
MG_State::GLState::ITextureObject& texture,
|
||||||
|
|||||||
@@ -1491,8 +1491,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||||
// (a NULL buffers array, or a zero entry inside one).
|
// (a NULL buffers array, or a zero entry inside one).
|
||||||
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||||
const char* funcName) {
|
const char* funcName, Bool hasBuffer = true) {
|
||||||
if (size <= 0) {
|
if (hasBuffer && size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
@@ -1527,16 +1527,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the
|
// GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
|
||||||
// offset and the size must be multiples of 4.
|
// TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
|
||||||
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) {
|
// queryable alignment pname, which is why it was missing here), and the SIZE only for
|
||||||
|
// transform feedback, whose capture is written in whole 32-bit components. Extending the
|
||||||
|
// size rule to atomic counters as well breaks a legal bind: the conformance suite splits
|
||||||
|
// MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
|
||||||
|
// not required to land on four.
|
||||||
|
if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) must be a multiple of 4 for {}.", offset,
|
||||||
|
MG_Util::ConvertGLEnumToString(target))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", funcName,
|
"MG_Impl/GLImpl", funcName,
|
||||||
std::format("offset ({}) and size ({}) must both be multiples of 4 for "
|
std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
|
||||||
"GL_TRANSFORM_FEEDBACK_BUFFER.",
|
|
||||||
offset, size)));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1548,7 +1559,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||||
if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return;
|
// The target's alignment rules are a property of the BINDING POINT, not of the buffer,
|
||||||
|
// so they apply even when buffer is zero - which is exactly how
|
||||||
|
// KHR-GL43.shader_storage_buffer_object.negative-api-bind probes the SSBO alignment
|
||||||
|
// (glBindBufferRange(SHADER_STORAGE_BUFFER, 0, 0, alignment - 1, 0)). Only the size
|
||||||
|
// rules need a buffer, since buffer 0 detaches the binding point and ignores size.
|
||||||
|
if (!ValidateBufferRangeOffsetAndSize(target, offset, size, __func__, /*hasBuffer: */ buffer != 0)) return;
|
||||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1732,10 +1748,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARB_multi_bind states the equivalence to a loop of single binds "except that ... buffers
|
||||||
|
// will not be created if they do not exist": glBindBuffer instantiates a name glGenBuffers
|
||||||
|
// merely reserved, glBindBuffers* must refuse it and raise INVALID_OPERATION instead
|
||||||
|
// (KHR-GL44.multi_bind.errors_bind_buffers).
|
||||||
|
//
|
||||||
|
// Deliberately PER ELEMENT, not all-or-nothing: the equivalence the extension defines is a
|
||||||
|
// loop, so a bad entry costs its own binding point and nothing else. Rejecting the whole
|
||||||
|
// call instead cost multi_bind.functional_bind_buffers_base its bindings.
|
||||||
|
static Bool IsExistingBufferForMultiBind(GLuint buffer, GLsizei index, const char* funcName) {
|
||||||
|
if (buffer == 0 || MG_State::pGLContext->ValidateBufferObject(buffer)) return true;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("buffers[{}] ({}) is not the name of an existing buffer object.", index, buffer)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
||||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
|
const GLuint buffer = buffers ? buffers[i] : 0;
|
||||||
|
if (!IsExistingBufferForMultiBind(buffer, i, __func__)) continue;
|
||||||
|
BindBufferBase_State(target, first + i, buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1749,6 +1785,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLsizeiptr* sizes) {
|
const GLsizeiptr* sizes) {
|
||||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
|
||||||
if (!buffers || buffers[i] == 0) {
|
if (!buffers || buffers[i] == 0) {
|
||||||
BindBufferBase_State(target, first + i, 0);
|
BindBufferBase_State(target, first + i, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -493,15 +493,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DispatchComputeIndirect(GLintptr indirect) {
|
void DispatchComputeIndirect(GLintptr indirect) {
|
||||||
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
// Argument and binding validation runs FIRST. Both are properties of the call and of GL
|
||||||
if (!dispatchComputeIndirect) {
|
// state, so a context whose backend cannot dispatch at all must still report the
|
||||||
MG_State::pGLContext->RecordError(
|
// argument error the spec names rather than masking every one of them with
|
||||||
ErrorCode::InvalidOperation,
|
// "unsupported" - which is what put GL_INVALID_OPERATION where
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
// KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE.
|
||||||
"Backend does not support indirect compute dispatch."));
|
//
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
|
||||||
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
||||||
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||||
if (indirect < 0 || (indirect % 4) != 0) {
|
if (indirect < 0 || (indirect % 4) != 0) {
|
||||||
@@ -520,6 +517,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ...and the same INVALID_OPERATION covers "the command would source data beyond the end
|
||||||
|
// of the bound buffer object" (GL 4.6 core 19): the dispatch reads three uints starting
|
||||||
|
// at `indirect`.
|
||||||
|
constexpr SizeT kDispatchIndirectCommandSize = 3 * sizeof(Uint32);
|
||||||
|
if (static_cast<SizeT>(indirect) + kDispatchIndirectCommandSize > indirectBuffer->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", __func__,
|
||||||
|
std::format("indirect ({}) + 12 bytes runs past the end of the {}-byte buffer bound to "
|
||||||
|
"GL_DISPATCH_INDIRECT_BUFFER.",
|
||||||
|
indirect, indirectBuffer->GetSize())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
||||||
|
if (!dispatchComputeIndirect) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Backend does not support indirect compute dispatch."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,8 +600,69 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARB_indirect_parameters / GL 4.6 core 10.4: `drawcount` is a byte offset into the buffer
|
||||||
|
// bound to PARAMETER_BUFFER and holds one uint draw count. Three errors have to be raised
|
||||||
|
// before the call reaches a backend, and none of them was
|
||||||
|
// (KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount):
|
||||||
|
// * drawcount not a multiple of four INVALID_VALUE
|
||||||
|
// * nothing bound to PARAMETER_BUFFER, or the uint at `drawcount`
|
||||||
|
// lies past its end INVALID_OPERATION
|
||||||
|
// * maxdrawcount commands from `indirect` run past the end of the
|
||||||
|
// buffer bound to DRAW_INDIRECT_BUFFER INVALID_OPERATION
|
||||||
|
static Bool ValidateIndirectCountDraw(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||||
|
GLsizei stride, SizeT commandSize, const char* funcName) {
|
||||||
|
if (drawcount < 0 || (drawcount % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
"drawcount must be non-negative and a multiple of four."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& parameterBuffer =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||||
|
if (!parameterBuffer ||
|
||||||
|
static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
"No buffer is bound to GL_PARAMETER_BUFFER, or drawcount runs past "
|
||||||
|
"the end of the one that is."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (maxdrawcount < 0 || stride < 0 || indirect < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
"indirect, maxdrawcount and stride must all be non-negative."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SizeT effectiveStride = stride != 0 ? static_cast<SizeT>(stride) : commandSize;
|
||||||
|
const auto& indirectBuffer =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
// A zero maxdrawcount sources nothing, so it cannot run past anything.
|
||||||
|
const SizeT requiredBytes =
|
||||||
|
maxdrawcount == 0 ? 0
|
||||||
|
: static_cast<SizeT>(indirect) +
|
||||||
|
static_cast<SizeT>(maxdrawcount - 1) * effectiveStride + commandSize;
|
||||||
|
if (!indirectBuffer || requiredBytes > indirectBuffer->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
"maxdrawcount commands would be sourced from beyond the end of the "
|
||||||
|
"buffer bound to GL_DRAW_INDIRECT_BUFFER."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride) {
|
GLsizei maxdrawcount, GLsizei stride) {
|
||||||
|
// Argument validation before the backend-availability check: see DispatchComputeIndirect.
|
||||||
|
// DrawElementsIndirectCommand: count, instanceCount, firstIndex, baseVertex, baseInstance.
|
||||||
|
if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
|
||||||
|
5 * sizeof(Uint32), __func__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||||
if (!multiDrawElementsIndirectCount) {
|
if (!multiDrawElementsIndirectCount) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -595,6 +676,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride) {
|
GLsizei maxdrawcount, GLsizei stride) {
|
||||||
|
// Argument validation before the backend-availability check: see DispatchComputeIndirect.
|
||||||
|
// DrawArraysIndirectCommand: count, instanceCount, first, baseInstance.
|
||||||
|
if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
|
||||||
|
4 * sizeof(Uint32), __func__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||||
if (!multiDrawArraysIndirectCount) {
|
if (!multiDrawArraysIndirectCount) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
|
|||||||
@@ -961,15 +961,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto getInteger64i = MG_Backend::gBackendFunctionsTable.GL.GetInteger64i_v;
|
// The one indexed pname whose value genuinely needs 64 bits: a vertex buffer binding
|
||||||
if (!getInteger64i) {
|
// offset is an intptr, so taking the 32-bit route below would truncate it.
|
||||||
*data = 0;
|
if (target == GL_VERTEX_BINDING_OFFSET) {
|
||||||
MG_State::pGLContext->RecordError(
|
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||||
ErrorCode::InvalidOperation,
|
MG_State::pGLContext->RecordError(
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support indexed integer queries."));
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Vertex buffer binding index is out of range."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
*data = vao ? static_cast<GLint64>(vao->GetBindingPoint(index).Offset) : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getInteger64i(target, index, data);
|
|
||||||
|
// Everything else is 32-bit indexed state that the glGetIntegeri_v pname table already
|
||||||
|
// owns, and GL 4.6 core 22.1 says every indexed query answers every indexed pname.
|
||||||
|
// Handing the leftovers straight to the backend instead made glGetInteger64i_v disagree
|
||||||
|
// with glGetIntegeri_v on the very same pname - GL_MAX_COMPUTE_WORK_GROUP_COUNT read
|
||||||
|
// back 0 while the 32-bit view said 65535 (KHR-GL43.compute_shader.max), because a
|
||||||
|
// frontend-only value simply is not in the driver's table.
|
||||||
|
GLint values[4] = {};
|
||||||
|
GetIntegeri_v(target, index, values);
|
||||||
|
*data = static_cast<GLint64>(values[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||||
|
|||||||
@@ -2704,6 +2704,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
||||||
|
// Every early-out below reports "nothing was written", and it has to say so before it can
|
||||||
|
// take one: callers legitimately leave *length uninitialised and then loop to it. The CTS
|
||||||
|
// does exactly that (gl4cProgramInterfaceQueryTests.cpp:2172 declares `GLsizei length;` and
|
||||||
|
// walks `for (i = 0; i < length; ++i)` over a 1000-entry stack array), so an untouched
|
||||||
|
// *length turned every error path here into a stack overrun inside the caller -
|
||||||
|
// KHR-GL43.program_interface_query.subroutines-vertex read 0x20202020 entries and died on
|
||||||
|
// both backends. The success path overwrites this with the real count.
|
||||||
|
if (length) *length = 0;
|
||||||
|
|
||||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "GL_RenderState.h"
|
#include "GL_RenderState.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||||
@@ -380,7 +381,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
*data = IsEnabledi_State(target, index);
|
// GL 4.6 core 22.1: glGetBooleani_v answers EVERY indexed state, not just the indexed
|
||||||
|
// capabilities - a non-boolean value simply reads back as "is it non-zero". Routing the
|
||||||
|
// non-capability enums to the pname table glGetIntegeri_v already owns is what makes
|
||||||
|
// that true; without it a query like glGetBooleani_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0)
|
||||||
|
// came back GL_INVALID_ENUM (KHR-GL43.compute_shader.max).
|
||||||
|
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||||
|
*data = IsEnabledi_State(target, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint values[4] = {};
|
||||||
|
GetIntegeri_v(target, index, values);
|
||||||
|
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLboolean IsEnabled_State(GLenum cap) {
|
GLboolean IsEnabled_State(GLenum cap) {
|
||||||
|
|||||||
@@ -336,8 +336,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARB_multi_bind adds one rule the single-bind path does not have: "samplers will not be
|
||||||
|
// created if they do not exist", so a name that is not an existing sampler OBJECT is
|
||||||
|
// INVALID_OPERATION here (KHR-GL44.multi_bind.errors_bind_samplers). Per element, not
|
||||||
|
// all-or-nothing - the extension defines glBindSamplers as a loop, so a bad entry costs
|
||||||
|
// its own texture unit and leaves the rest of the range bound.
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
BindSampler_State(first + i, samplers ? samplers[i] : 0);
|
const GLuint sampler = samplers ? samplers[i] : 0;
|
||||||
|
if (sampler != 0 && !MG_State::pGLContext->ValidateSamplerObject(sampler)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "BindSamplers",
|
||||||
|
std::format("samplers[{}] ({}) is not the name of an existing sampler object.", i, sampler)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BindSampler_State(first + i, sampler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -613,6 +613,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
"Compressed texture formats are not supported."));
|
"Compressed texture formats are not supported."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only
|
||||||
|
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is
|
||||||
|
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead
|
||||||
|
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes
|
||||||
|
// the process down, which is never an acceptable answer to a query - see the same reasoning
|
||||||
|
// above for the compressed-format path.
|
||||||
|
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
||||||
|
MGLOG_I("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
||||||
|
"storage; recording GL_INVALID_OPERATION instead of terminating",
|
||||||
|
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", caller,
|
||||||
|
"Level queries are not supported for texture-buffer storage."));
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
||||||
@@ -2910,7 +2927,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2924,7 +2942,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2938,7 +2957,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3045,7 +3065,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3059,7 +3080,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3073,7 +3095,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3403,7 +3426,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GET_SRC_INTERNAL_FORMAT(readBufferType);
|
GET_SRC_INTERNAL_FORMAT(readBufferType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(internalFormat, srcInternalFormat)) THROW_UNIMPL_EXCEPTION;
|
// The validator has already recorded GL_INVALID_OPERATION; just decline. Throwing
|
||||||
|
// here unwound a C++ exception through the C GL ABI and killed the process (see the
|
||||||
|
// same reasoning at :604-609).
|
||||||
|
if (!TextureImpl::ValidateCopyTexImageBaseFormatSubset(internalFormat, srcInternalFormat)) return false;
|
||||||
|
|
||||||
GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
|
GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
|
||||||
GLenum realInternalFormat = GL_RGBA8;
|
GLenum realInternalFormat = GL_RGBA8;
|
||||||
@@ -3426,8 +3452,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||||
GLint border) {
|
GLint border) {
|
||||||
// TODO: implement
|
// 1D textures are not implemented by this backend set. Record the error the way every
|
||||||
THROW_UNIMPL_EXCEPTION;
|
// other unsupported entry point does - throwing unwinds through the C GL ABI and kills
|
||||||
|
// the process, which is never an acceptable answer to an unsupported call.
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyTexImage1D",
|
||||||
|
"1D textures are not supported by this implementation"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
@@ -4079,10 +4110,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
|
||||||
|
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
|
||||||
|
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
|
||||||
|
// .compressed_data). Written against the enum ranges rather than a name list because the
|
||||||
|
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
|
||||||
|
// carry, which would make this check silently narrower than the API surface.
|
||||||
|
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
|
||||||
|
switch (internalformat) {
|
||||||
|
case 0x8225: // GL_COMPRESSED_RED
|
||||||
|
case 0x8226: // GL_COMPRESSED_RG
|
||||||
|
case 0x84ED: // GL_COMPRESSED_RGB
|
||||||
|
case 0x84EE: // GL_COMPRESSED_RGBA
|
||||||
|
case 0x8C48: // GL_COMPRESSED_SRGB
|
||||||
|
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
|
||||||
|
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
|
||||||
|
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
|
||||||
|
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
|
||||||
|
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
|
||||||
|
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
|
||||||
|
}
|
||||||
|
|
||||||
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||||
GLsizei depth) {
|
GLsizei depth) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
if (!textureObject) return;
|
if (!textureObject) return;
|
||||||
|
if (textureObject->GetTarget() == TextureTarget::Texture3D &&
|
||||||
|
IsCompressedGLInternalFormat(internalformat)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", __func__,
|
||||||
|
std::format("{} is a compressed internal format and cannot back GL_TEXTURE_3D storage.",
|
||||||
|
MG_Util::ConvertGLEnumToString(internalformat))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
||||||
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
||||||
|
|||||||
@@ -424,19 +424,81 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6
|
||||||
|
// reasons about. Colour components are independent bits so "subset" is a plain
|
||||||
|
// mask test; depth and stencil are their own components and never satisfy a
|
||||||
|
// colour request (or each other).
|
||||||
|
enum : Uint32 {
|
||||||
|
kComponentR = 1u << 0,
|
||||||
|
kComponentG = 1u << 1,
|
||||||
|
kComponentB = 1u << 2,
|
||||||
|
kComponentA = 1u << 3,
|
||||||
|
kComponentDepth = 1u << 4,
|
||||||
|
kComponentStencil = 1u << 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
Uint32 BaseFormatComponents(TextureInternalFormat unsizedFormat) {
|
||||||
|
switch (unsizedFormat) {
|
||||||
|
case TextureInternalFormat::Red:
|
||||||
|
return kComponentR;
|
||||||
|
case TextureInternalFormat::RG:
|
||||||
|
return kComponentR | kComponentG;
|
||||||
|
case TextureInternalFormat::RGB:
|
||||||
|
return kComponentR | kComponentG | kComponentB;
|
||||||
|
case TextureInternalFormat::RGBA:
|
||||||
|
return kComponentR | kComponentG | kComponentB | kComponentA;
|
||||||
|
case TextureInternalFormat::DepthComponent:
|
||||||
|
return kComponentDepth;
|
||||||
|
case TextureInternalFormat::DepthStencil:
|
||||||
|
return kComponentDepth | kComponentStencil;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||||
auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||||
auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||||
if (unsizedFormat1 != unsizedFormat2) {
|
if (unsizedFormat1 != unsizedFormat2) {
|
||||||
|
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
|
||||||
|
// std::format() call whose format string was the component name, so every
|
||||||
|
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
|
||||||
|
// hand over component/function/message separately.
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
std::format("MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||||
"The base internal format of the two formats do not match ({} vs. {})",
|
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1).c_str(),
|
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2).c_str())));
|
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} // namespace TextureImpl
|
}
|
||||||
|
|
||||||
|
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||||
|
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||||
|
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||||
|
// GL 4.6 SS 8.6: glCopyTexImage* may request a SUBSET of the read buffer's components,
|
||||||
|
// not an exact match - GL_RGB from an RGBA8 framebuffer is textbook legal and is what
|
||||||
|
// Minecraft and its mods do. glCopyTexImage2D used to run the exact-match predicate
|
||||||
|
// above and turn its rejection into an uncaught exception through the C GL ABI, so the
|
||||||
|
// app died rather than seeing a GL error.
|
||||||
|
const Uint32 destComponents = BaseFormatComponents(unsizedDest);
|
||||||
|
const Uint32 srcComponents = BaseFormatComponents(unsizedSrc);
|
||||||
|
if (destComponents == 0 || srcComponents == 0 || (destComponents & ~srcComponents) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "ValidateCopyTexImageBaseFormatSubset",
|
||||||
|
std::format("the read buffer's base internal format {} does not provide every component of "
|
||||||
|
"the requested internal format {}",
|
||||||
|
MG_Util::ConvertTextureInternalFormatToString(unsizedSrc),
|
||||||
|
MG_Util::ConvertTextureInternalFormatToString(unsizedDest))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -40,5 +40,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
TextureTarget target);
|
TextureTarget target);
|
||||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||||
|
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||||
|
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||||
|
// the requested internalformat asks for, but may supply more.
|
||||||
|
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -179,6 +179,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return vao;
|
return vao;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ARB_vertex_attrib_binding entry points that take no vertex array name modify the
|
||||||
|
// *bound* vertex array, and in a core profile the default vertex array (name 0) is not
|
||||||
|
// one: every one of them is INVALID_OPERATION there (GL 4.6 core 10.3.1, and the tail of
|
||||||
|
// each KHR-GL4x.vertex_attrib_binding.negative-* case checks exactly this). MobileGL
|
||||||
|
// keeps a real object at name 0 for the compatibility paths, so GetBoundVertexArray
|
||||||
|
// never returns null and the rule has to be spelled out - behind the same gate the VAO-0
|
||||||
|
// draw rule already uses (MOBILEGL_RELAXED_SEMANTICS, plus "the context never asked for
|
||||||
|
// a core profile"), so applications that legitimately run relaxed keep working.
|
||||||
|
static SharedPtr<MG_State::GLState::VertexArrayObject> GetBoundVertexArrayForBindingApi(const char* funcName) {
|
||||||
|
auto vao = GetBoundVertexArrayOrError(funcName);
|
||||||
|
if (!vao) return nullptr;
|
||||||
|
if (vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", funcName,
|
||||||
|
"The default vertex array object cannot be modified in a core profile."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return vao;
|
||||||
|
}
|
||||||
|
|
||||||
static bool ValidateVertexAttribPname(GLenum pname) {
|
static bool ValidateVertexAttribPname(GLenum pname) {
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||||
@@ -944,7 +966,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[0] = static_cast<GLfloat>(attr->Size);
|
params[0] = static_cast<GLfloat>(attr->Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = static_cast<GLfloat>(attr->Stride);
|
params[0] = static_cast<GLfloat>(attr->LegacyStride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||||
@@ -1014,7 +1036,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[0] = static_cast<GLdouble>(attr->Size);
|
params[0] = static_cast<GLdouble>(attr->Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = static_cast<GLdouble>(attr->Stride);
|
params[0] = static_cast<GLdouble>(attr->LegacyStride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||||
@@ -1079,8 +1101,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||||
params[0] = attr->Size;
|
params[0] = attr->Size;
|
||||||
return;
|
return;
|
||||||
|
// The legacy shadow, not the resolved draw stride: GL 4.6 core table 23.3 defines this
|
||||||
|
// as the last glVertexAttrib*Pointer argument, which glBindVertexBuffer must not
|
||||||
|
// overwrite even though it does overwrite what the backend actually reads.
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = attr->Stride;
|
params[0] = attr->LegacyStride;
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||||
@@ -1138,7 +1163,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto& attr = vao->GetAttribute(index);
|
const auto& attr = vao->GetAttribute(index);
|
||||||
*pointer = reinterpret_cast<void*>(attr.Offset);
|
*pointer = reinterpret_cast<void*>(attr.LegacyPointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
||||||
@@ -1222,7 +1247,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*param = static_cast<GLint>(attr.Size);
|
*param = static_cast<GLint>(attr.Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
*param = static_cast<GLint>(attr.Stride);
|
*param = static_cast<GLint>(attr.LegacyStride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
||||||
@@ -1294,14 +1319,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
||||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffer");
|
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffer");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||||
const GLsizei* strides) {
|
const GLsizei* strides) {
|
||||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffers");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
@@ -1315,21 +1340,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribFormat");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
||||||
"VertexAttribFormat");
|
"VertexAttribFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribIFormat");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribIFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
||||||
"VertexAttribIFormat");
|
"VertexAttribIFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribLFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
}
|
}
|
||||||
@@ -1341,7 +1366,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribBinding");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
||||||
@@ -1349,7 +1374,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexBindingDivisor");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexBindingDivisor");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
||||||
vao->SetBindingDivisor(bindingindex, divisor);
|
vao->SetBindingDivisor(bindingindex, divisor);
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/AsyncCompileScenario.cpp
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
|
Scenarios/PipelineFailureScenario.cpp
|
||||||
|
Scenarios/AdvertisedLimitsScenario.cpp
|
||||||
|
Scenarios/PixelStoreSweepScenario.cpp
|
||||||
|
Scenarios/FragCoordOriginScenario.cpp
|
||||||
|
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
|||||||
@@ -601,9 +601,13 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Image ReadPixels(int width, int height) {
|
Image ReadPixels(int width, int height) {
|
||||||
|
return ReadPixelsRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image ReadPixelsRect(int x, int y, int width, int height) {
|
||||||
Image image(width, height);
|
Image image(width, height);
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,11 +184,18 @@ namespace MGITest {
|
|||||||
|
|
||||||
void ClearTo(float r, float g, float b, float a);
|
void ClearTo(float r, float g, float b, float a);
|
||||||
|
|
||||||
// Reads back the whole currently bound READ framebuffer. width/height must
|
// Reads back the whole currently bound READ framebuffer.
|
||||||
// be the target's full size - DirectVulkan's default-framebuffer readback
|
|
||||||
// only re-orients a full-extent read.
|
|
||||||
Image ReadPixels(int width, int height);
|
Image ReadPixels(int width, int height);
|
||||||
|
|
||||||
|
// A PARTIAL glReadPixels. Row 0 of the returned image is GL row `y` of the
|
||||||
|
// framebuffer, i.e. the bottom row of the requested rect - the same
|
||||||
|
// convention ReadPixels uses, just with an origin. This is the shape the
|
||||||
|
// conformance suite reads in (a random sub-rect of the default
|
||||||
|
// framebuffer), and the shape DirectVulkan's default-FBO readback used to
|
||||||
|
// hand back in Vulkan row order because its re-orientation only ran on an
|
||||||
|
// exact full-extent read.
|
||||||
|
Image ReadPixelsRect(int x, int y, int width, int height);
|
||||||
|
|
||||||
// Drains any GL error queue and returns the first error, or 0.
|
// Drains any GL error queue and returns the first error, or 0.
|
||||||
unsigned int FirstGLError();
|
unsigned int FirstGLError();
|
||||||
const char* GLErrorName(unsigned int error);
|
const char* GLErrorName(unsigned int error);
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// "The limit we advertise is a promise, and an application will hold us to it."
|
||||||
|
//
|
||||||
|
// DirectVulkan copied Vulkan descriptor limits straight into the GL limit table. Those are not
|
||||||
|
// the same quantity: Adreno answers maxPerStageDescriptorUniformBuffers at descriptor-indexing
|
||||||
|
// scale, and GL_MAX_COMPUTE_UNIFORM_BLOCKS is a count an app will allocate. KHR-GL44.multi_bind
|
||||||
|
// .dispatch_bind_buffers_base does exactly that - createsO(limit) buffers and splices O(limit)
|
||||||
|
// UBO declarations into one compute shader - and spent ~14 s allocating before dying on
|
||||||
|
// std::bad_alloc. Its sibling dispatch_bind_buffers_range hard-codes 4 buffers and passes.
|
||||||
|
//
|
||||||
|
// Two failure modes, one table:
|
||||||
|
// - too LARGE: an unusable promise (the OOM above).
|
||||||
|
// - too SMALL or negative: a uint32 limit that lost its top bit on the way to a signed Int -
|
||||||
|
// UINT32_MAX arrived as -1, which every downstream std::min then accepted as "small enough".
|
||||||
|
// A conformant GL 4.x implementation may never advertise below the spec minimum either.
|
||||||
|
//
|
||||||
|
// Every bound below is checked on BOTH backends, because the loader casts are shared and the
|
||||||
|
// DirectGLES lane is the control: it takes its limits from a driver that already reports GL
|
||||||
|
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
|
||||||
|
// fails on both is a table bug.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
struct LimitBound {
|
||||||
|
GLenum pname;
|
||||||
|
const char* name;
|
||||||
|
// The GL 4.x required minimum. A value below this is a conformance failure in its own
|
||||||
|
// right, and is what a sign-flipped uint32 looks like.
|
||||||
|
int minimum;
|
||||||
|
// The largest value this implementation is willing to promise. Chosen well above every
|
||||||
|
// desktop driver's answer, so it can only catch a descriptor-scale number.
|
||||||
|
int ceiling;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<LimitBound>& BufferLimitTable() {
|
||||||
|
static const std::vector<LimitBound> table = {
|
||||||
|
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
|
||||||
|
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
|
||||||
|
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||||
|
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||||
|
{GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 8, 256},
|
||||||
|
{GL_MAX_TEXTURE_BUFFER_SIZE, "GL_MAX_TEXTURE_BUFFER_SIZE", 65536, 1 << 27},
|
||||||
|
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", 16384, 1 << 30},
|
||||||
|
// Already clamped before this campaign; in the table so a regression there is
|
||||||
|
// caught by the same case.
|
||||||
|
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE", 1 << 24, 512 * 1024 * 1024},
|
||||||
|
{GL_MAX_TEXTURE_IMAGE_UNITS, "GL_MAX_TEXTURE_IMAGE_UNITS", 16, 32},
|
||||||
|
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS", 48, 192},
|
||||||
|
};
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdvertisedLimitsScenario : public ScenarioTest {};
|
||||||
|
|
||||||
|
TEST_F(AdvertisedLimitsScenario, EveryBufferLimitIsWithinItsAdvertisedRange) {
|
||||||
|
for (const LimitBound& bound : BufferLimitTable()) {
|
||||||
|
GLint value = -424242;
|
||||||
|
glGetIntegerv(bound.pname, &value);
|
||||||
|
const unsigned int error = FirstGLError();
|
||||||
|
EXPECT_EQ(error, GLenum(GL_NO_ERROR))
|
||||||
|
<< bound.name << " is not answerable: " << GLErrorName(error);
|
||||||
|
if (error != GL_NO_ERROR) continue;
|
||||||
|
|
||||||
|
EXPECT_GE(value, bound.minimum)
|
||||||
|
<< bound.name << " = " << value << " is below the GL required minimum "
|
||||||
|
<< bound.minimum << " (a negative or tiny value here is a uint32 limit that lost "
|
||||||
|
"its top bit on the way to a signed Int)";
|
||||||
|
EXPECT_LE(value, bound.ceiling)
|
||||||
|
<< bound.name << " = " << value << " exceeds the ceiling " << bound.ceiling
|
||||||
|
<< " this implementation is willing to promise - an application that allocates "
|
||||||
|
"what we advertise will run out of memory";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The OOM case in isolation, because it is the one with a known CTS victim and the one a
|
||||||
|
// future refactor is most likely to reintroduce by copying the Vulkan limit back.
|
||||||
|
TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {
|
||||||
|
GLint blocks = -1;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &blocks);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_GE(blocks, 12);
|
||||||
|
EXPECT_LE(blocks, 256) << "KHR-GL44.multi_bind.dispatch_bind_buffers_base creates one GL buffer "
|
||||||
|
"and one UBO declaration per advertised block";
|
||||||
|
|
||||||
|
GLint blockSize = -1;
|
||||||
|
glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &blockSize);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_GT(blockSize, 0);
|
||||||
|
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS is derived from the product of these two,
|
||||||
|
// so their product has to stay representable.
|
||||||
|
EXPECT_LE(static_cast<long long>(blocks) * blockSize,
|
||||||
|
static_cast<long long>(2147483647))
|
||||||
|
<< "blocks(" << blocks << ") * blockSize(" << blockSize << ") overflows the GLint the "
|
||||||
|
"derived component limits are computed in";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A CLEAR OF THE DEFAULT FRAMEBUFFER IS VISIBLE TO glReadPixels WITH NO DRAW BETWEEN.
|
||||||
|
//
|
||||||
|
// DirectVulkan parks a glClear as a pending clear and folds it into the next render pass's
|
||||||
|
// loadOp. When nothing is drawn after the clear there is no render pass, and the readback path
|
||||||
|
// used to materialize pending clears only for USER framebuffers - so a readback right after a
|
||||||
|
// clear of the DEFAULT framebuffer blitted the untouched swapchain image and handed back the
|
||||||
|
// previous frame's colour.
|
||||||
|
//
|
||||||
|
// That is the whole of KHR-GL40.draw_indirect.negative-* (12 Magma failures): each case clears,
|
||||||
|
// issues a draw that correctly raises INVALID_OPERATION and therefore never executes, then reads
|
||||||
|
// the frame back expecting (0,0,0,0) and gets the previous case's (0.1,0.2,0.3,1). The staleness
|
||||||
|
// cannot appear in one frame, so the scenario paints a frame first and clears in the next.
|
||||||
|
//
|
||||||
|
// The alpha assertion is the second half of the same census finding: a cleared default
|
||||||
|
// framebuffer read back (0,0,0,1) where (0,0,0,0) was written, because the clear was routed
|
||||||
|
// through the default FBO's placeholder attachment, whose format can lack alpha, rather than
|
||||||
|
// through the swapchain image that actually has one.
|
||||||
|
//
|
||||||
|
// DirectGLES is the built-in control: a native GL driver has no deferred-clear model at all, so
|
||||||
|
// a failure there would mean the scenario, not the backend.
|
||||||
|
|
||||||
|
#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 const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The colour KHR-GL40.draw_indirect's fshSimple paints, so a stale readback shows up as
|
||||||
|
// the same value the conformance log reports.
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.1, 0.2, 0.3, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ClearThenReadPixelsScenario : public ScenarioTest {};
|
||||||
|
|
||||||
|
void DrawFullViewportQuad(unsigned int program) {
|
||||||
|
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
GLuint vao = 0, vbo = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glGenBuffers(1, &vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||||
|
glUseProgram(program);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(ClearThenReadPixelsScenario, ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
ASSERT_GE(width, 8);
|
||||||
|
ASSERT_GE(height, 8);
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
// Frame 1: paint the whole default framebuffer, so there IS something stale to return.
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
DrawFullViewportQuad(program);
|
||||||
|
{
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||||
|
ASSERT_NEAR(centre.r, 26, 2) << "the setup frame did not paint; the staleness test would be vacuous";
|
||||||
|
ASSERT_NEAR(centre.g, 51, 2);
|
||||||
|
ASSERT_NEAR(centre.b, 77, 2);
|
||||||
|
}
|
||||||
|
gl.EndFrame();
|
||||||
|
|
||||||
|
// Frame 2: clear to transparent black and read back with NO draw at all.
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
const Image cleared = ReadPixels(width, height);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
int nonZero = 0;
|
||||||
|
int firstX = -1;
|
||||||
|
int firstY = -1;
|
||||||
|
Rgba8 firstOffender{};
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const Rgba8 pixel = cleared.At(x, y);
|
||||||
|
if (pixel.r == 0 && pixel.g == 0 && pixel.b == 0 && pixel.a == 0) continue;
|
||||||
|
if (nonZero == 0) {
|
||||||
|
firstX = x;
|
||||||
|
firstY = y;
|
||||||
|
firstOffender = pixel;
|
||||||
|
}
|
||||||
|
++nonZero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(nonZero, 0) << "glClear(0,0,0,0) followed by glReadPixels with no draw returned " << nonZero
|
||||||
|
<< " of " << (width * height) << " non-zero pixels; first at (" << firstX << ", "
|
||||||
|
<< firstY << ") = (" << static_cast<int>(firstOffender.r) << ", "
|
||||||
|
<< static_cast<int>(firstOffender.g) << ", " << static_cast<int>(firstOffender.b)
|
||||||
|
<< ", " << static_cast<int>(firstOffender.a) << ")";
|
||||||
|
|
||||||
|
gl.EndFrame();
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same claim for a sub-rect read, which is the shape the conformance suite uses most and
|
||||||
|
// the one whose orientation handling is separate (see OrientationScenario).
|
||||||
|
TEST_F(ClearThenReadPixelsScenario, ClearWithNoDrawIsVisibleToASubRectReadback) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
ASSERT_GE(width, 8);
|
||||||
|
ASSERT_GE(height, 8);
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
DrawFullViewportQuad(program);
|
||||||
|
gl.EndFrame();
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
const int rectWidth = width / 2;
|
||||||
|
const int rectHeight = height / 2;
|
||||||
|
const Image cleared = ReadPixelsRect(width / 4, height / 4, rectWidth, rectHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
int nonZero = 0;
|
||||||
|
for (int y = 0; y < rectHeight; ++y) {
|
||||||
|
for (int x = 0; x < rectWidth; ++x) {
|
||||||
|
const Rgba8 pixel = cleared.At(x, y);
|
||||||
|
if (pixel.r != 0 || pixel.g != 0 || pixel.b != 0 || pixel.a != 0) ++nonZero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(nonZero, 0) << nonZero << " of " << (rectWidth * rectHeight)
|
||||||
|
<< " pixels in a sub-rect read after a draw-free clear were not zero";
|
||||||
|
|
||||||
|
gl.EndFrame();
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FragCoordOriginScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - gl_FragCoord ON THE DEFAULT FRAMEBUFFER CARRIES GL'S WINDOW ORIGIN.
|
||||||
|
//
|
||||||
|
// GL measures gl_FragCoord.y from the BOTTOM of the window. Vulkan's gl_FragCoord.y is the
|
||||||
|
// framebuffer ROW being written, and DirectVulkan stores the default framebuffer display-side-up
|
||||||
|
// (compensating for vertices by negating gl_Position.y), so a fragment's reported Y there was
|
||||||
|
// `height - y_GL` - flipped, and for a viewport that does not span the full height, outside the
|
||||||
|
// range GL promises entirely. GL CTS
|
||||||
|
// `KHR-GL42.shader_image_load_store.basic-{allTargets-atomic,glsl-earlyFragTests,glsl-misc}`
|
||||||
|
// caught it: each sets a small viewport at GL y=0 and does
|
||||||
|
// `imageStore(image, ivec2(gl_FragCoord.xy), ...)` into an image exactly that size, so on a
|
||||||
|
// 256-tall surface every store addressed rows 224..255 of a 32-row image and was dropped.
|
||||||
|
//
|
||||||
|
// The shader here paints each row with its own GL window Y, which is the whole claim in one
|
||||||
|
// value: row j of the readback must be j, for a full-height viewport and for a half-height one
|
||||||
|
// (the case where a flip and an offset can no longer hide each other). DirectGLES is the
|
||||||
|
// built-in control - a native GL driver gets this right by construction, so a failure there
|
||||||
|
// would mean the test, not the backend.
|
||||||
|
|
||||||
|
#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 const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// floor(gl_FragCoord.y) is the fragment's window row; 1/255 steps survive an RGBA8
|
||||||
|
// round trip exactly, so the readback byte IS the row the shader believes it is on.
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(floor(gl_FragCoord.y) / 255.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
class FragCoordOriginScenario : public ScenarioTest {};
|
||||||
|
|
||||||
|
// A quad covering the whole viewport, drawn with attribute 0 = aPos.
|
||||||
|
void DrawFullViewportQuad(unsigned int program) {
|
||||||
|
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
GLuint vao = 0, vbo = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glGenBuffers(1, &vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||||
|
glUseProgram(program);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paints `viewportHeight` rows starting at GL y=0 and returns the red byte of each row.
|
||||||
|
std::vector<int> RowsPaintedWithTheirOwnWindowY(unsigned int program, int width, int viewportHeight) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, viewportHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
DrawFullViewportQuad(program);
|
||||||
|
|
||||||
|
const Image image = ReadPixelsRect(0, 0, width, viewportHeight);
|
||||||
|
std::vector<int> rows;
|
||||||
|
rows.reserve(static_cast<std::size_t>(viewportHeight));
|
||||||
|
for (int y = 0; y < viewportHeight; ++y) {
|
||||||
|
rows.push_back(image.At(width / 2, y).r);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
::testing::AssertionResult RowsAreTheirOwnIndex(const std::vector<int>& rows, const char* when) {
|
||||||
|
for (std::size_t y = 0; y < rows.size(); ++y) {
|
||||||
|
if (rows[y] != static_cast<int>(y)) {
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< when << ": GL window row " << y << " reported gl_FragCoord.y = " << rows[y]
|
||||||
|
<< " (expected " << y << "). Rows 0.." << (rows.size() - 1) << " read back as ["
|
||||||
|
<< rows.front() << " .. " << rows.back() << "].";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ::testing::AssertionSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(FragCoordOriginScenario, DefaultFramebufferFragCoordCountsFromTheBottom) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
// 1/255 steps only stay distinguishable while the row index fits in a byte.
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int fullHeight = std::min(gl.Height(), 256);
|
||||||
|
ASSERT_GE(fullHeight, 8) << "the harness surface is too small to tell rows apart";
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
// Full height first: this one passed even before the fix (a flip alone maps the row set
|
||||||
|
// onto itself), so it is the control that the shader and the readback agree at all.
|
||||||
|
EXPECT_TRUE(RowsAreTheirOwnIndex(RowsPaintedWithTheirOwnWindowY(program, width, fullHeight),
|
||||||
|
"full-height viewport"));
|
||||||
|
|
||||||
|
// Half height at GL y=0: the case the CTS failures were made of. A backend that reports
|
||||||
|
// the stored row here answers `height - y` for every row - off the bottom of the range,
|
||||||
|
// not merely reversed within it.
|
||||||
|
const int halfHeight = fullHeight / 2;
|
||||||
|
EXPECT_TRUE(RowsAreTheirOwnIndex(RowsPaintedWithTheirOwnWindowY(program, width, halfHeight),
|
||||||
|
"half-height viewport at GL y=0"));
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glViewport(0, 0, gl.Width(), gl.Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -101,6 +102,39 @@ void main() {
|
|||||||
// "every single pixel" an achievable (and therefore useful) demand.
|
// "every single pixel" an achievable (and therefore useful) demand.
|
||||||
constexpr int kQuadrantInset = 2;
|
constexpr int kQuadrantInset = 2;
|
||||||
|
|
||||||
|
// A deliberately asymmetric sub-rect of the 128x96 surface: neither centred nor
|
||||||
|
// full-extent in either axis, mirroring the conformance suite's randomised
|
||||||
|
// sub-viewport geometry (glcShaderRenderCase.cpp:735-741). Asymmetry is the whole
|
||||||
|
// point - y == H - y - h is exactly the case an unconverted Y origin gets right by
|
||||||
|
// accident, and it is the only case the shipped code ever exercised.
|
||||||
|
// correct band = GL rows [13, 55)
|
||||||
|
// mirrored band = GL rows [41, 83) (what H-y-h produces)
|
||||||
|
constexpr int kSubX = 17;
|
||||||
|
constexpr int kSubY = 13;
|
||||||
|
constexpr int kSubW = 60;
|
||||||
|
constexpr int kSubH = 42;
|
||||||
|
|
||||||
|
Image CropRect(const Image& source, int x0, int y0, int width, int height) {
|
||||||
|
Image out(width, height);
|
||||||
|
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
const std::uint8_t* sourceRow =
|
||||||
|
source.Data() + (static_cast<std::size_t>(y0 + y) * source.Width() + x0) * 4;
|
||||||
|
std::memcpy(out.Data() + static_cast<std::size_t>(y) * rowBytes, sourceRow, rowBytes);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image VFlip(const Image& source) {
|
||||||
|
Image out(source.Width(), source.Height());
|
||||||
|
const std::size_t rowBytes = static_cast<std::size_t>(source.Width()) * 4;
|
||||||
|
for (int y = 0; y < source.Height(); ++y) {
|
||||||
|
std::memcpy(out.Data() + static_cast<std::size_t>(y) * rowBytes,
|
||||||
|
source.Data() + static_cast<std::size_t>(source.Height() - 1 - y) * rowBytes, rowBytes);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
struct Vertex {
|
struct Vertex {
|
||||||
float x, y;
|
float x, y;
|
||||||
float r, g, b;
|
float r, g, b;
|
||||||
@@ -377,5 +411,174 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ sub-rect / M-1 ----
|
||||||
|
//
|
||||||
|
// Everything above reads the FULL extent of its target, which is the one case
|
||||||
|
// DirectVulkan's default-framebuffer readback ever re-oriented: the remap at
|
||||||
|
// VulkanRenderer.cpp:2042 had no rect parameters at all, so :8278 gated it on
|
||||||
|
// `width == swapchainExtent.width && height == swapchainExtent.height` and fell back to a
|
||||||
|
// raw copy otherwise. Meanwhile the viewport (:422), the scissor (:506-546) and the
|
||||||
|
// ReadPixels copy offset (:8238) all used the GL bottom-origin Y verbatim as a Vulkan
|
||||||
|
// top-origin Y.
|
||||||
|
//
|
||||||
|
// In the conformance suite those defects CANCEL in placement - the draw lands in Vulkan
|
||||||
|
// rows [y, y+h) and the readback copies the same rows back - and compose into an exact
|
||||||
|
// vertical flip of a correct image. That is 1,759 of Magma's 1,793 non-pass cases, and
|
||||||
|
// image forensics over all 861 gl33 failures found 861 vertical flips and nothing else.
|
||||||
|
// Taken apart, they are two independent user-visible bugs, so they are tested apart:
|
||||||
|
// SubViewportDraw pins placement with a full-extent read, SubRectReadback pins the
|
||||||
|
// readback rect after a full-viewport draw, and SubViewportSubRectRoundTrip is the CTS
|
||||||
|
// shape where the two cancel.
|
||||||
|
|
||||||
|
// Placement: a sub-viewport draw must land in GL rows [y0, y0+h), not mirrored about the
|
||||||
|
// surface centre. Read back full-extent, which is the path that already worked, so a
|
||||||
|
// failure here can only be the viewport's Y origin.
|
||||||
|
TEST_F(OrientationScenario, SubViewportDrawLandsWhereGLPutsIt) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glViewport(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
DrawQuadrants();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
|
||||||
|
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
|
||||||
|
const Image placed = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_EQ(placed.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the sub-viewport draw is not upright inside its own rect";
|
||||||
|
ExpectUprightQuadrants(placed, "sub-viewport draw, cropped out of a full-extent read");
|
||||||
|
|
||||||
|
// Nothing may have been painted outside the viewport. This is what catches the
|
||||||
|
// mirrored placement: the drawn band would sit at GL rows [41, 83) instead.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(whole, 0, Gl().Width() - 1, 0, kSubY - 2, "black", 0.0,
|
||||||
|
"below the sub-viewport"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(whole, 0, Gl().Width() - 1, kSubY + kSubH + 1, Gl().Height() - 1, "black",
|
||||||
|
0.0, "above the sub-viewport"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readback: a full-viewport draw read back through a sub-rect must return the requested
|
||||||
|
// band, in GL row order. Band and orientation are asserted separately so that fixing only
|
||||||
|
// one of the two cannot pass this case.
|
||||||
|
TEST_F(OrientationScenario, SubRectReadbackReturnsTheRequestedBandUpright) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
|
||||||
|
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_EQ(whole.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the full-extent read is already wrong, so nothing below can be trusted";
|
||||||
|
|
||||||
|
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ASSERT_EQ(sub.Width(), kSubW);
|
||||||
|
ASSERT_EQ(sub.Height(), kSubH);
|
||||||
|
|
||||||
|
const Image requestedBand = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
const Image mirroredBand = CropRect(whole, kSubX, Gl().Height() - kSubY - kSubH, kSubW, kSubH);
|
||||||
|
|
||||||
|
// The geometry has to be able to see both mistakes; if a future surface size made the
|
||||||
|
// band symmetric these assertions would be vacuous, so say so loudly instead.
|
||||||
|
ASSERT_FALSE(requestedBand == VFlip(requestedBand))
|
||||||
|
<< "the chosen sub-rect is vertically symmetric - it cannot detect a row flip";
|
||||||
|
ASSERT_FALSE(requestedBand == mirroredBand)
|
||||||
|
<< "the chosen sub-rect equals its mirror band - it cannot detect a wrong band";
|
||||||
|
|
||||||
|
EXPECT_FALSE(sub == VFlip(requestedBand))
|
||||||
|
<< "ORIENTATION: the requested band came back with its rows in Vulkan (top-first) order";
|
||||||
|
EXPECT_FALSE(sub == mirroredBand || sub == VFlip(mirroredBand))
|
||||||
|
<< "BAND: the read returned GL rows [H-y-h, H-y) instead of [y, y+h)";
|
||||||
|
EXPECT_TRUE(sub == requestedBand)
|
||||||
|
<< "the sub-rect readback differs from the same rect of the full-extent read in "
|
||||||
|
<< sub.ByteDiffCount(requestedBand) << " bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The exact conformance-suite shape: an asymmetric sub-viewport draw read back through the
|
||||||
|
// very same sub-rect. The placement and readback errors cancel, leaving an image that is
|
||||||
|
// correct in every pixel VALUE and vertically flipped - which is precisely the 861-case
|
||||||
|
// signature. One assertion, and it pins all of them.
|
||||||
|
TEST_F(OrientationScenario, SubViewportSubRectRoundTripIsUpright) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glViewport(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
DrawQuadrants();
|
||||||
|
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(sub.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "sub-viewport draw + same-rect readback came back flipped - this is the shape "
|
||||||
|
"behind KHR-GL33/GL40.shaders.* (861 cases each)";
|
||||||
|
ExpectUprightQuadrants(sub, "sub-viewport draw read back through the same sub-rect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same conversion, on the other rect consumer that reads the default framebuffer.
|
||||||
|
// glBlitFramebuffer already converted its DESTINATION rect when the draw framebuffer was
|
||||||
|
// the default one (ApplyNativeBlitDefaultFramebufferTransform), but never its SOURCE rect,
|
||||||
|
// so a blit OUT of the default framebuffer took the mirrored band and wrote it upside
|
||||||
|
// down. Blitting a sub-rect and comparing against the same sub-rect of a direct read pins
|
||||||
|
// both halves at once.
|
||||||
|
TEST_F(OrientationScenario, BlitOutOfTheDefaultFramebufferKeepsBandAndOrientation) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_EQ(whole.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the full-extent read is already wrong, so nothing below can be trusted";
|
||||||
|
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_offscreen.fbo);
|
||||||
|
glBlitFramebuffer(kSubX, kSubY, kSubX + kSubW, kSubY + kSubH, kSubX, kSubY, kSubX + kSubW,
|
||||||
|
kSubY + kSubH, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||||
|
const unsigned int blitError = FirstGLError();
|
||||||
|
if (blitError != GL_NO_ERROR) {
|
||||||
|
GTEST_SKIP() << "this backend refused the default-framebuffer blit: "
|
||||||
|
<< GLErrorName(blitError);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_offscreen.fbo);
|
||||||
|
const Image blitted = ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
|
||||||
|
const Image landed = CropRect(blitted, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
const Image expected = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_FALSE(landed == VFlip(expected))
|
||||||
|
<< "ORIENTATION: the blitted band arrived upside down";
|
||||||
|
EXPECT_TRUE(landed == expected)
|
||||||
|
<< "the blitted sub-rect differs from the same sub-rect of a direct read in "
|
||||||
|
<< landed.ByteDiffCount(expected) << " bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative control. A non-default framebuffer is already self-consistent - no
|
||||||
|
// gl_Position.y negation, GL row 0 IS Vulkan row 0 - so none of the fixes above may touch
|
||||||
|
// it. If this ever starts failing, the default-FBO remap has leaked into the FBO path.
|
||||||
|
TEST_F(OrientationScenario, FboSubRectReadbackAndSubViewportAreUnaffected) {
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
|
||||||
|
const Image whole = ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_TRUE(sub == CropRect(whole, kSubX, kSubY, kSubW, kSubH))
|
||||||
|
<< "an FBO sub-rect readback differs from the same rect of its full-extent read in "
|
||||||
|
<< sub.ByteDiffCount(CropRect(whole, kSubX, kSubY, kSubW, kSubH)) << " bytes";
|
||||||
|
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glViewport(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
DrawQuadrants();
|
||||||
|
glViewport(0, 0, m_offscreen.width, m_offscreen.height);
|
||||||
|
const Image placedWhole = ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(CropRect(placedWhole, kSubX, kSubY, kSubW, kSubH).QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "an FBO sub-viewport draw must land in GL rows [y0, y0+h) upright";
|
||||||
|
EXPECT_TRUE(RegionIsMostly(placedWhole, 0, m_offscreen.width - 1, 0, kSubY - 2, "black", 0.0,
|
||||||
|
"below an FBO sub-viewport"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(placedWhole, 0, m_offscreen.width - 1, kSubY + kSubH + 1,
|
||||||
|
m_offscreen.height - 1, "black", 0.0, "above an FBO sub-viewport"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipelineFailureScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// "The draw had no pipeline, so we bound null."
|
||||||
|
//
|
||||||
|
// DirectVulkan's SetupDraw called GetOrCreatePipeline - a function that DOCUMENTS a
|
||||||
|
// VK_NULL_HANDLE return - and passed the result straight to vkCmdBindPipeline. When the
|
||||||
|
// Adreno driver answered vkCreateGraphicsPipelines with VK_ERROR_UNKNOWN, the next
|
||||||
|
// instruction dereferenced null inside the driver: SIGSEGV at fault addr 0x8, and that one
|
||||||
|
// shape accounted for 9 of the 15 process deaths in the 2026-08-10 GL-CTS run
|
||||||
|
// (KHR-GL33/GL40.shaders.struct.uniform.sampler_array_vertex, six
|
||||||
|
// KHR-GL42.shader_image_load_store cases, one shader_storage_buffer_object case).
|
||||||
|
//
|
||||||
|
// It was made permanent by a second defect: PipelineFactory memoized the failure, so the
|
||||||
|
// null was served for the rest of the process. Every later draw with the same state died
|
||||||
|
// too, which is why a single bad program took whole CTS groups down with it.
|
||||||
|
//
|
||||||
|
// What this scenario pins, on both backends:
|
||||||
|
// 1. The GL program shape the CTS crashed on (an array of structs each containing a
|
||||||
|
// sampler, sampled from the VERTEX stage) draws without killing the process.
|
||||||
|
// 2. It draws AGAIN and produces the identical image. A second draw is the only thing
|
||||||
|
// that can tell a working pipeline apart from a poisoned cache entry: if the first
|
||||||
|
// creation had failed and been memoized, the second draw is where the null would be
|
||||||
|
// served back.
|
||||||
|
//
|
||||||
|
// A deterministic driver-side pipeline-creation FAILURE is not reachable from the GL API on
|
||||||
|
// the llvmpipe/lavapipe lanes - both accept every pipeline these scenarios can describe - so
|
||||||
|
// the guard itself is proven structurally (PipelineFactory returns before it can emplace a
|
||||||
|
// VK_NULL_HANDLE, SetupDraw returns false before it can bind one) and this scenario holds
|
||||||
|
// the surrounding path honest.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Lifted from KHR-GL33.shaders.struct.uniform.sampler_array_vertex (the QPA records the
|
||||||
|
// source verbatim): an array of structs, each carrying an opaque sampler, sampled in the
|
||||||
|
// vertex stage. The fragment sibling of this case only FAILS on Magma; only the vertex one
|
||||||
|
// takes the process down, so the stage matters and is kept.
|
||||||
|
constexpr const char* kSamplerArrayVertexSource = R"(#version 330 core
|
||||||
|
struct S {
|
||||||
|
float a;
|
||||||
|
vec3 b;
|
||||||
|
sampler2D c;
|
||||||
|
};
|
||||||
|
uniform S s[2];
|
||||||
|
in vec2 aPos;
|
||||||
|
out vec4 vColor;
|
||||||
|
void main() {
|
||||||
|
vec2 coords = aPos * 0.5 + 0.5;
|
||||||
|
vColor = vec4(texture(s[1].c, coords * s[0].b.xy + s[1].b.z).rgb, s[0].a);
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kPassthroughFragmentSource = R"(#version 330 core
|
||||||
|
in vec4 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vColor;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vertex> FullscreenTriangleStrip() {
|
||||||
|
return {{-1.0f, -1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, 1.0f}};
|
||||||
|
}
|
||||||
|
|
||||||
|
class PipelineFailureScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kSamplerArrayVertexSource, kPassthroughFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
const std::vector<Vertex> vertices = FullscreenTriangleStrip();
|
||||||
|
m_vertexCount = static_cast<int>(vertices.size());
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(0));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
// A solid red 2x2 texture, so the sampled colour is the same wherever the
|
||||||
|
// (deliberately degenerate) coordinates land.
|
||||||
|
const unsigned char red[] = {255, 0, 0, 255, 255, 0, 0, 255,
|
||||||
|
255, 0, 0, 255, 255, 0, 0, 255};
|
||||||
|
glGenTextures(1, &m_texture);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, red);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glUseProgram(m_program);
|
||||||
|
const int samplerLocation = glGetUniformLocation(m_program, "s[1].c");
|
||||||
|
if (samplerLocation >= 0) glUniform1i(samplerLocation, 0);
|
||||||
|
const int alphaLocation = glGetUniformLocation(m_program, "s[0].a");
|
||||||
|
if (alphaLocation >= 0) glUniform1f(alphaLocation, 1.0f);
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
m_target = MakeColorFbo(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_NE(m_target.fbo, 0u) << "offscreen FBO is not framebuffer-complete";
|
||||||
|
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DestroyColorFbo(m_target);
|
||||||
|
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image DrawOnce() {
|
||||||
|
BindFbo(m_target);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, m_vertexCount);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
return ReadPixels(m_target.width, m_target.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
unsigned int m_texture = 0;
|
||||||
|
int m_vertexCount = 0;
|
||||||
|
ColorFbo m_target;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reaching the assertion at all is most of the point: the shipped code SIGSEGV'd inside
|
||||||
|
// the driver on this draw.
|
||||||
|
TEST_F(PipelineFailureScenario, SamplerArrayInAStructDrawsWithoutKillingTheProcess) {
|
||||||
|
const Image drawn = DrawOnce();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(drawn, 2, drawn.Width() - 3, 2, drawn.Height() - 3, "red", 0.0,
|
||||||
|
"sampler-array-in-struct draw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second draw is what a poisoned cache entry cannot survive: a memoized
|
||||||
|
// VK_NULL_HANDLE is served on every subsequent lookup, so a run that dies (or silently
|
||||||
|
// stops drawing) on the second draw and not the first is exactly the "failed pipeline was
|
||||||
|
// cached" defect.
|
||||||
|
TEST_F(PipelineFailureScenario, TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache) {
|
||||||
|
const Image first = DrawOnce();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the first draw already errored";
|
||||||
|
Gl().EndFrame();
|
||||||
|
const Image second = DrawOnce();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the second draw errored";
|
||||||
|
|
||||||
|
EXPECT_TRUE(RegionIsMostly(second, 2, second.Width() - 3, 2, second.Height() - 3, "red", 0.0,
|
||||||
|
"second draw"));
|
||||||
|
EXPECT_TRUE(second == first) << "the second draw differs from the first in "
|
||||||
|
<< second.ByteDiffCount(first) << " bytes - the pipeline the second "
|
||||||
|
"draw resolved is not the one the first draw used";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PixelStoreSweepScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - PIXEL-STORE MODES RESTORE, and FRAMEBUFFER CHURN STAYS EXACT.
|
||||||
|
//
|
||||||
|
// Both cases here replay the shape of KHR-GL3x.packed_pixels.varied_rectangle, the single
|
||||||
|
// heaviest polluter in the GL CTS: for each of 46 (pixel-store mode, value) pairs it uploads a
|
||||||
|
// gradient into a fresh texture, attaches that texture to a FRESH framebuffer, reads it back and
|
||||||
|
// deletes both - ~3300 texture+framebuffer pairs per test case.
|
||||||
|
//
|
||||||
|
// What that found: DirectGLES had no destructor for BackendFramebufferObject (nor for the
|
||||||
|
// renderbuffer and sampler twins), so every frontend glDeleteFramebuffers leaked one driver
|
||||||
|
// framebuffer for the process lifetime. On an Adreno 830 the CTS run walked the driver to 1.2 GB
|
||||||
|
// of dead objects, and from that point on EVERY readback through a freshly attached framebuffer
|
||||||
|
// came back with someone else's pixels - which is what made ~1,500 otherwise-correct cases fail
|
||||||
|
// depending only on how much ran before them. The unit-level pin for the missing destructors is
|
||||||
|
// MG_Test/SanityTest.cpp (DirectGLESBackendFramebuffer/Renderbuffer/Sampler); this file pins the
|
||||||
|
// end-to-end behaviour they protect.
|
||||||
|
//
|
||||||
|
// The mode sweep is the second half of the same story: 46 modes are set and reset per case, so a
|
||||||
|
// mode that fails to restore is indistinguishable from the leak in a full-batch CTS run. The
|
||||||
|
// assertion here is RESTORATION - after every single mode is set and put back, a readback at
|
||||||
|
// default state must be byte-identical to one taken before the sweep ever started.
|
||||||
|
//
|
||||||
|
// Backend-agnostic on purpose: both bugs this guards against are frontend/backend bookkeeping,
|
||||||
|
// and DirectVulkan is the built-in control.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Small enough that the table's row lengths (10, 15) and image heights are all >= the
|
||||||
|
// image, which is the shape the CTS uses (its gradient is 7x3).
|
||||||
|
constexpr int kTexSize = 8;
|
||||||
|
// Every buffer handed to GL is this big regardless of the image size: with row length 15,
|
||||||
|
// two skipped rows/pixels and alignment 8 the driver strides well past the natural image
|
||||||
|
// extent, and a tight buffer would be an out-of-bounds access rather than a test. (It was:
|
||||||
|
// the first version of this scenario passed its assertions and then segfaulted at
|
||||||
|
// teardown, because glReadPixels had written past a 1 KiB destination.)
|
||||||
|
constexpr std::size_t kScratchBytes = 64 * 1024;
|
||||||
|
|
||||||
|
// Every pixel-store mode GL 4.0 has, so a reset provably covers the whole state and not
|
||||||
|
// just the subset a particular test happened to touch.
|
||||||
|
struct PixelStoreMode {
|
||||||
|
GLenum name;
|
||||||
|
GLint defaultValue;
|
||||||
|
};
|
||||||
|
const PixelStoreMode kAllModes[] = {
|
||||||
|
{GL_UNPACK_SWAP_BYTES, 0}, {GL_UNPACK_LSB_FIRST, 0}, {GL_UNPACK_ROW_LENGTH, 0},
|
||||||
|
{GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_PIXELS, 0},
|
||||||
|
{GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_ALIGNMENT, 4}, {GL_PACK_SWAP_BYTES, 0},
|
||||||
|
{GL_PACK_LSB_FIRST, 0}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_IMAGE_HEIGHT, 0},
|
||||||
|
{GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_IMAGES, 0},
|
||||||
|
{GL_PACK_ALIGNMENT, 4},
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CTS table verbatim (glcPackedPixelsTests.cpp VariedRectangleTest::iterate): 32
|
||||||
|
// common cases plus the 14 core-only ones ES has no equivalent for and MobileGL therefore
|
||||||
|
// honours on the CPU. IMAGE_WIDTH_1/2 and IMAGE_HEIGHT_1/2 are the CTS's 10 and 15.
|
||||||
|
struct SweepCase {
|
||||||
|
GLenum mode;
|
||||||
|
GLint value;
|
||||||
|
};
|
||||||
|
const SweepCase kSweep[] = {
|
||||||
|
{GL_UNPACK_ROW_LENGTH, 0}, {GL_UNPACK_ROW_LENGTH, 10}, {GL_UNPACK_ROW_LENGTH, 15},
|
||||||
|
{GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_ROWS, 1}, {GL_UNPACK_SKIP_ROWS, 2},
|
||||||
|
{GL_UNPACK_SKIP_PIXELS, 0}, {GL_UNPACK_SKIP_PIXELS, 1}, {GL_UNPACK_SKIP_PIXELS, 2},
|
||||||
|
{GL_UNPACK_ALIGNMENT, 1}, {GL_UNPACK_ALIGNMENT, 2}, {GL_UNPACK_ALIGNMENT, 4},
|
||||||
|
{GL_UNPACK_ALIGNMENT, 8}, {GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_IMAGE_HEIGHT, 10},
|
||||||
|
{GL_UNPACK_IMAGE_HEIGHT, 15}, {GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_SKIP_IMAGES, 1},
|
||||||
|
{GL_UNPACK_SKIP_IMAGES, 2}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_ROW_LENGTH, 10},
|
||||||
|
{GL_PACK_ROW_LENGTH, 15}, {GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_ROWS, 1},
|
||||||
|
{GL_PACK_SKIP_ROWS, 2}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_PIXELS, 1},
|
||||||
|
{GL_PACK_SKIP_PIXELS, 2}, {GL_PACK_ALIGNMENT, 1}, {GL_PACK_ALIGNMENT, 2},
|
||||||
|
{GL_PACK_ALIGNMENT, 4}, {GL_PACK_ALIGNMENT, 8},
|
||||||
|
// core-only, no ES equivalent
|
||||||
|
{GL_UNPACK_SWAP_BYTES, GL_FALSE}, {GL_UNPACK_SWAP_BYTES, GL_TRUE},
|
||||||
|
{GL_UNPACK_LSB_FIRST, GL_FALSE}, {GL_UNPACK_LSB_FIRST, GL_TRUE},
|
||||||
|
{GL_PACK_SWAP_BYTES, GL_FALSE}, {GL_PACK_SWAP_BYTES, GL_TRUE},
|
||||||
|
{GL_PACK_LSB_FIRST, GL_FALSE}, {GL_PACK_LSB_FIRST, GL_TRUE},
|
||||||
|
{GL_PACK_IMAGE_HEIGHT, 0}, {GL_PACK_IMAGE_HEIGHT, 10},
|
||||||
|
{GL_PACK_IMAGE_HEIGHT, 15}, {GL_PACK_SKIP_IMAGES, 0},
|
||||||
|
{GL_PACK_SKIP_IMAGES, 1}, {GL_PACK_SKIP_IMAGES, 2},
|
||||||
|
};
|
||||||
|
|
||||||
|
std::size_t ImageBytes(int size) { return static_cast<std::size_t>(size) * size * 4; }
|
||||||
|
|
||||||
|
// Padded to kScratchBytes so it is safe to hand to an upload running under any of the
|
||||||
|
// sweep's stride/skip settings.
|
||||||
|
std::vector<std::uint8_t> MakeGradient(int size, unsigned seed) {
|
||||||
|
std::vector<std::uint8_t> pixels(kScratchBytes, 0);
|
||||||
|
for (int y = 0; y < size; ++y) {
|
||||||
|
for (int x = 0; x < size; ++x) {
|
||||||
|
const std::size_t base = (static_cast<std::size_t>(y) * size + x) * 4;
|
||||||
|
pixels[base + 0] = static_cast<std::uint8_t>((x * 11 + seed) & 0xFF);
|
||||||
|
pixels[base + 1] = static_cast<std::uint8_t>((y * 13 + seed) & 0xFF);
|
||||||
|
pixels[base + 2] = static_cast<std::uint8_t>((x * y + seed) & 0xFF);
|
||||||
|
pixels[base + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetAllPixelStoreModes() {
|
||||||
|
for (const PixelStoreMode& mode : kAllModes) {
|
||||||
|
glPixelStorei(mode.name, mode.defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one operation the CTS repeats: a fresh texture, a fresh framebuffer, one readback,
|
||||||
|
// both deleted. Returns the readback; `outStatus` carries the completeness answer so a
|
||||||
|
// caller can tell an incomplete framebuffer apart from wrong pixels.
|
||||||
|
std::vector<std::uint8_t> UploadAndReadBack(const std::vector<std::uint8_t>& source, int size,
|
||||||
|
GLenum* outStatus) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, source.data());
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
*outStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> read(kScratchBytes, 0);
|
||||||
|
if (*outStatus == GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index of the first differing byte within the image, or `bytes` when they agree.
|
||||||
|
std::size_t FirstDifference(const std::vector<std::uint8_t>& a, const std::vector<std::uint8_t>& b,
|
||||||
|
std::size_t bytes) {
|
||||||
|
for (std::size_t i = 0; i < bytes; ++i) {
|
||||||
|
if (a[i] != b[i]) return i;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PixelStoreSweepScenario : public ScenarioTest {};
|
||||||
|
class FramebufferChurnScenario : public ScenarioTest {};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Every mode in the CTS table is set, exercised and put back; the readback at default state
|
||||||
|
// afterwards must be bit-identical to the one taken before the sweep. A mode that silently
|
||||||
|
// fails to restore corrupts every later case in the batch, which is exactly how the CTS
|
||||||
|
// failures presented (the FIRST sub-case, at default state, is what failed).
|
||||||
|
TEST_F(PixelStoreSweepScenario, DefaultStateSurvivesTheFullModeSweep) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
ResetAllPixelStoreModes();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "resetting the pixel-store modes must be legal on a GL 4.0 context";
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> gradient = MakeGradient(kTexSize, 0);
|
||||||
|
GLenum status = 0;
|
||||||
|
const std::vector<std::uint8_t> baseline = UploadAndReadBack(gradient, kTexSize, &status);
|
||||||
|
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> scratchSource(kScratchBytes, 0x5A);
|
||||||
|
|
||||||
|
for (const SweepCase& sweep : kSweep) {
|
||||||
|
glPixelStorei(sweep.mode, sweep.value);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glPixelStorei(0x" << std::hex << sweep.mode << std::dec << ", "
|
||||||
|
<< sweep.value << ") must be accepted";
|
||||||
|
|
||||||
|
// Exercise the mode: an upload and a readback that both run with it in force.
|
||||||
|
GLenum sweepStatus = 0;
|
||||||
|
(void)UploadAndReadBack(scratchSource, kTexSize, &sweepStatus);
|
||||||
|
|
||||||
|
ResetAllPixelStoreModes();
|
||||||
|
|
||||||
|
GLenum afterStatus = 0;
|
||||||
|
const std::vector<std::uint8_t> after = UploadAndReadBack(gradient, kTexSize, &afterStatus);
|
||||||
|
ASSERT_EQ(afterStatus, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
const std::size_t diff = FirstDifference(baseline, after, ImageBytes(kTexSize));
|
||||||
|
ASSERT_EQ(diff, ImageBytes(kTexSize))
|
||||||
|
<< "default-state readback changed after setting and resetting 0x" << std::hex << sweep.mode
|
||||||
|
<< std::dec << " = " << sweep.value << "; first differing byte " << diff << " (baseline "
|
||||||
|
<< static_cast<int>(baseline[diff]) << ", now " << static_cast<int>(after[diff]) << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the modes themselves must read back as the defaults the reset asked for.
|
||||||
|
for (const PixelStoreMode& mode : kAllModes) {
|
||||||
|
GLint value = -1;
|
||||||
|
glGetIntegerv(mode.name, &value);
|
||||||
|
EXPECT_EQ(value, mode.defaultValue)
|
||||||
|
<< "pixel-store mode 0x" << std::hex << mode.name << std::dec << " did not return to its default";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The leak regression. Each iteration is one complete CTS inner step, and every readback has
|
||||||
|
// to be exactly the gradient THIS iteration uploaded - never the previous one's. Before the
|
||||||
|
// missing destructors were added, the driver-side framebuffer count grew without bound here.
|
||||||
|
TEST_F(FramebufferChurnScenario, RepeatedFramebufferReadbackStaysExact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
ResetAllPixelStoreModes();
|
||||||
|
constexpr int kSize = 8;
|
||||||
|
constexpr int kIterations = 1024;
|
||||||
|
|
||||||
|
for (int i = 0; i < kIterations; ++i) {
|
||||||
|
// A distinct gradient per iteration: a stale attachment or a recycled driver name
|
||||||
|
// reads back the PREVIOUS iteration's image, which a constant fill could not tell
|
||||||
|
// apart from a correct read.
|
||||||
|
const std::vector<std::uint8_t> gradient = MakeGradient(kSize, static_cast<unsigned>(i * 7 + 1));
|
||||||
|
GLenum status = 0;
|
||||||
|
const std::vector<std::uint8_t> read = UploadAndReadBack(gradient, kSize, &status);
|
||||||
|
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE)) << "iteration " << i;
|
||||||
|
const std::size_t diff = FirstDifference(gradient, read, ImageBytes(kSize));
|
||||||
|
ASSERT_EQ(diff, ImageBytes(kSize))
|
||||||
|
<< "iteration " << i << " read back a different image than it uploaded; first differing byte "
|
||||||
|
<< diff << " (uploaded " << static_cast<int>(gradient[diff]) << ", read "
|
||||||
|
<< static_cast<int>(read[diff]) << ")";
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "iteration " << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -446,6 +446,23 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
||||||
outShaders.assign(in.shaders.size(), nullptr);
|
outShaders.assign(in.shaders.size(), nullptr);
|
||||||
|
|
||||||
|
// GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders -
|
||||||
|
// the compute pipeline has no other stages to link against, so a program that mixes
|
||||||
|
// them must fail to link (KHR-GL43.compute_shader.api-program).
|
||||||
|
{
|
||||||
|
Bool hasCompute = false;
|
||||||
|
Bool hasNonCompute = false;
|
||||||
|
for (const LinkShaderInput& input : in.shaders) {
|
||||||
|
(input.stage == ShaderStage::Compute ? hasCompute : hasNonCompute) = true;
|
||||||
|
}
|
||||||
|
if (hasCompute && hasNonCompute) {
|
||||||
|
artifacts.infoLog =
|
||||||
|
"A compute shader cannot be linked with shaders of any other stage.";
|
||||||
|
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
const LinkShaderInput& input = in.shaders[i];
|
const LinkShaderInput& input = in.shaders[i];
|
||||||
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
||||||
@@ -1030,21 +1047,80 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// GL 4.6 core 11.1.2.1 (and the resource-name rule of 7.3.1.1): a member of
|
||||||
|
// an output interface block is named "<BLOCK name>.<member>" - the block's
|
||||||
|
// TYPE name, never the instance name, and that holds for an anonymous
|
||||||
|
// instance too. glslang's linker object for such a block is the *instance*
|
||||||
|
// symbol ("vs_out", or "anon@N" when there is none), so the head of the
|
||||||
|
// dotted path has to be matched against getType().getTypeName() instead of
|
||||||
|
// getName(). Without this every capture of a block member resolved to
|
||||||
|
// nothing and the link failed with "is not an output of the vertex stage".
|
||||||
|
String blockName;
|
||||||
|
String memberName;
|
||||||
|
if (const SizeT dot = declaredName.find('.'); dot != String::npos) {
|
||||||
|
blockName = declaredName.substr(0, dot);
|
||||||
|
memberName = declaredName.substr(dot + 1);
|
||||||
|
// An array of block instances is spelled "<block>[i].<member>"; every
|
||||||
|
// instance shares one member list, so the subscript only has to go.
|
||||||
|
if (!blockName.empty() && blockName.back() == ']') {
|
||||||
|
const SizeT bracket = blockName.rfind('[');
|
||||||
|
if (bracket != String::npos) blockName.resize(bracket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto* node : linkerObjects->getSequence()) {
|
for (const auto* node : linkerObjects->getSequence()) {
|
||||||
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
||||||
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (symbol->getName() != declaredName.c_str()) {
|
const glslang::TType& symbolType = symbol->getType();
|
||||||
continue;
|
const glslang::TType* capturedType = nullptr;
|
||||||
|
if (memberName.empty()) {
|
||||||
|
if (symbol->getName() != declaredName.c_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
capturedType = &symbolType;
|
||||||
|
} else {
|
||||||
|
if (symbolType.getBasicType() != glslang::EbtBlock) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The spec spelling is the block name; the instance name is accepted
|
||||||
|
// as a fallback so a request written the (common, non-conformant)
|
||||||
|
// instance-qualified way resolves instead of failing the whole link.
|
||||||
|
if (symbolType.getTypeName() != blockName.c_str() &&
|
||||||
|
symbol->getName() != blockName.c_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const glslang::TTypeList* members = symbolType.getStruct();
|
||||||
|
if (members == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (SizeT m = 0; m < members->size(); ++m) {
|
||||||
|
const glslang::TType* memberType = (*members)[m].type;
|
||||||
|
if (memberType == nullptr || memberType->getFieldName() != memberName.c_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
capturedType = memberType;
|
||||||
|
varying.blockMemberIndex = static_cast<Int>(m);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (capturedType == nullptr) {
|
||||||
|
// Right block, wrong member: no other linker object can match.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
varying.blockName = symbolType.getTypeName().c_str();
|
||||||
|
varying.blockInstanceName = symbol->getName().c_str();
|
||||||
}
|
}
|
||||||
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
|
resolved = ResolveXfbSymbolType(*capturedType, varying.type, varying.size, bytesPerElement);
|
||||||
if (resolved && singleElement) {
|
if (resolved && singleElement) {
|
||||||
if (static_cast<Int>(element) >= varying.size) {
|
if (static_cast<Int>(element) >= varying.size) {
|
||||||
resolved = false;
|
resolved = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
varying.size = 1;
|
varying.size = 1;
|
||||||
|
if (varying.blockMemberIndex >= 0) {
|
||||||
|
varying.blockMemberElement = static_cast<Int>(element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -516,12 +516,27 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Artifacts().infoLog = "No program binary format is supported.";
|
Artifacts().infoLog = "No program binary format is supported.";
|
||||||
}
|
}
|
||||||
Bool GetValidateStatus() const { return m_validateStatus; }
|
Bool GetValidateStatus() const { return m_validateStatus; }
|
||||||
Int GetActiveAtomicCounterCount() const { return Artifacts().program->getNumAtomicCounters(); }
|
// Artifacts().program is null until a link produces reflection, and glGetProgramiv is
|
||||||
Int GetActiveAttributesCount() const { return Artifacts().program->getNumPipeInputs(); }
|
// perfectly legal on a program that never linked (GL 4.6 sec. 7.3: the queried state is
|
||||||
|
// simply its initial value, zero). Dereferencing it there took the process down with a
|
||||||
|
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
|
||||||
|
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
|
||||||
|
// throw ahead of it stopped killing the run first.
|
||||||
|
Int GetActiveAtomicCounterCount() const {
|
||||||
|
const auto& program = Artifacts().program;
|
||||||
|
return program ? program->getNumAtomicCounters() : 0;
|
||||||
|
}
|
||||||
|
Int GetActiveAttributesCount() const {
|
||||||
|
const auto& program = Artifacts().program;
|
||||||
|
return program ? program->getNumPipeInputs() : 0;
|
||||||
|
}
|
||||||
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
||||||
// materializes for default-block uniforms is filtered out by DoReflection.
|
// materializes for default-block uniforms is filtered out by DoReflection.
|
||||||
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
||||||
GLuint GetComputeLocalSize(Uint dim) const { return Artifacts().program->getLocalSize(static_cast<Int>(dim)); }
|
GLuint GetComputeLocalSize(Uint dim) const {
|
||||||
|
const auto& program = Artifacts().program;
|
||||||
|
return program ? program->getLocalSize(static_cast<Int>(dim)) : 0;
|
||||||
|
}
|
||||||
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
||||||
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
||||||
Uint GetUniformBlockIndex(const char* name) const {
|
Uint GetUniformBlockIndex(const char* name) const {
|
||||||
@@ -657,6 +672,21 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Offset within the gap-free record a backend that cannot express the GL
|
// Offset within the gap-free record a backend that cannot express the GL
|
||||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||||
Uint32 packedOffsetBytes = 0;
|
Uint32 packedOffsetBytes = 0;
|
||||||
|
|
||||||
|
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
|
||||||
|
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
|
||||||
|
// what the interface queries and the ESSL backend's driver-side capture list
|
||||||
|
// need, since SPIRV-Cross re-emits the block under its own type name), while
|
||||||
|
// the three fields below carry what a SPIR-V backend needs instead: the
|
||||||
|
// decoration target is the block's *instance* variable and the member index
|
||||||
|
// inside it. blockMemberIndex < 0 means "not a block member".
|
||||||
|
String blockInstanceName;
|
||||||
|
String blockName;
|
||||||
|
Int blockMemberIndex = -1;
|
||||||
|
// Which element of an arrayed block member this capture names, -1 for "the
|
||||||
|
// member as a whole". SPIR-V cannot decorate a single array element, so a
|
||||||
|
// backend needs the element index to tell a full run from a partial one.
|
||||||
|
Int blockMemberElement = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- P1: everything a link PRODUCES, in one movable block ----
|
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "RenderState.h"
|
#include "RenderState.h"
|
||||||
|
#include "MG_Util/Debug/Log.h"
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
@@ -268,9 +269,14 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||||
// Only for BlendState currently
|
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
||||||
|
// reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this
|
||||||
|
// is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++
|
||||||
|
// exception through the C GL ABI and terminates the process.
|
||||||
if (cap != CapabilityInput::Blend) {
|
if (cap != CapabilityInput::Blend) {
|
||||||
THROW_UNIMPL_EXCEPTION;
|
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
||||||
|
"GL_BLEND (cap=%d, index=%u); ignoring",
|
||||||
|
static_cast<int>(cap), index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||||
@@ -284,9 +290,13 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||||
// Only for BlendState currently
|
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
||||||
|
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
||||||
|
// query must never be able to terminate the process.
|
||||||
if (cap != CapabilityInput::Blend) {
|
if (cap != CapabilityInput::Blend) {
|
||||||
THROW_UNIMPL_EXCEPTION;
|
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
||||||
|
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
||||||
|
static_cast<int>(cap), index);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
attr.Normalized = false;
|
attr.Normalized = false;
|
||||||
attr.Stride = 0;
|
attr.Stride = 0;
|
||||||
attr.Offset = 0;
|
attr.Offset = 0;
|
||||||
|
attr.LegacyStride = 0;
|
||||||
|
attr.LegacyPointer = 0;
|
||||||
attr.Buffer = nullptr;
|
attr.Buffer = nullptr;
|
||||||
|
|
||||||
BumpAttributeFormatVersion(index);
|
BumpAttributeFormatVersion(index);
|
||||||
@@ -61,10 +63,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
|
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
|
||||||
SizeT offset, Bool isInteger, Bool isBgra) {
|
SizeT offset, Bool isInteger, Bool isBgra) {
|
||||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||||
|
if (size < 1 || size > 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The classic pointer-style API takes back full ownership of the resolved fields.
|
// The classic pointer-style API takes back full ownership of the resolved fields.
|
||||||
m_attributeUsesBindingModel[index] = false;
|
m_attributeUsesBindingModel[index] = false;
|
||||||
|
|
||||||
|
// The legacy query shadows: written here and nowhere else, so a later binding-model
|
||||||
|
// mutation cannot leak into VERTEX_ATTRIB_ARRAY_STRIDE / _POINTER. They are pure
|
||||||
|
// query state, so they carry no version bump of their own.
|
||||||
|
m_attributes[index].LegacyStride = stride;
|
||||||
|
m_attributes[index].LegacyPointer = offset;
|
||||||
|
|
||||||
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
||||||
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
||||||
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
||||||
@@ -72,10 +83,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size < 1 || size > 4) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& attr = m_attributes[index];
|
auto& attr = m_attributes[index];
|
||||||
attr.Size = size;
|
attr.Size = size;
|
||||||
attr.Type = type;
|
attr.Type = type;
|
||||||
@@ -111,6 +118,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
binding.Offset = offset;
|
binding.Offset = offset;
|
||||||
binding.Stride = effectiveStride;
|
binding.Stride = effectiveStride;
|
||||||
binding.Divisor = m_attributes[index].Divisor;
|
binding.Divisor = m_attributes[index].Divisor;
|
||||||
|
|
||||||
|
// Other attributes may already be pointed at this binding point through
|
||||||
|
// glVertexAttribBinding; they see the new buffer/offset/stride too (basic-state3
|
||||||
|
// checks exactly that after a glVertexAttribPointer). They are not adopted into the
|
||||||
|
// binding model here - only the ones already in it re-resolve.
|
||||||
|
ResolveAttributesForBinding(index, /*adopt: */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
|
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
|
||||||
@@ -147,10 +160,24 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
|
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
|
||||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||||
// glVertexAttribDivisor is VertexBindingDivisor on the attribute's own binding point
|
// GL 4.6 core 10.3.2 defines VertexAttribDivisor(i, d) as
|
||||||
// (GL 4.6 core 10.3.2), so the binding-point view has to follow the resolved attribute.
|
// VertexAttribBinding(i, i); VertexBindingDivisor(i, d)
|
||||||
if (index < MAX_VERTEX_ATTRIB_BINDINGS && m_attributeBindingIndex[index] == index) {
|
// - the binding is RE-POINTED at i, it is not merely written through when it already
|
||||||
|
// happens to be i. Guarding the write on "binding == index" (which is what this did)
|
||||||
|
// left an attribute that glVertexAttribBinding had moved elsewhere pointing at the old
|
||||||
|
// binding, so the next resolve restored that binding's divisor and the new one was
|
||||||
|
// lost (KHR-GL4x.vertex_attrib_binding.basic-state4).
|
||||||
|
//
|
||||||
|
// What is deliberately NOT copied from VertexAttribBinding is the adoption into the
|
||||||
|
// binding model: an attribute configured the classic way keeps its pointer-resolved
|
||||||
|
// stride/offset, exactly as before. The binding point mirrors that state already
|
||||||
|
// (MirrorPointerIntoBinding), so nothing observable differs - and adopting it here
|
||||||
|
// would silently swap the raw pointer stride for the effective one under every
|
||||||
|
// application that calls glVertexAttribDivisor after glVertexAttribPointer.
|
||||||
|
if (index < MAX_VERTEX_ATTRIB_BINDINGS) {
|
||||||
|
m_attributeBindingIndex[index] = index;
|
||||||
m_bindingPoints[index].Divisor = divisor;
|
m_bindingPoints[index].Divisor = divisor;
|
||||||
|
ResolveAttributesForBinding(index, /*adopt: */ false);
|
||||||
}
|
}
|
||||||
if (m_attributes[index].Divisor == divisor) return;
|
if (m_attributes[index].Divisor == divisor) return;
|
||||||
m_attributes[index].Divisor = divisor;
|
m_attributes[index].Divisor = divisor;
|
||||||
@@ -164,7 +191,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void VertexArrayObject::ResolveAttributeFromBinding(Uint attribIndex) {
|
void VertexArrayObject::ResolveAttributeFromBinding(Uint attribIndex) {
|
||||||
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
||||||
if (!m_attributeUsesBindingModel[attribIndex]) return;
|
|
||||||
|
|
||||||
const Uint bindingIndex = m_attributeBindingIndex[attribIndex];
|
const Uint bindingIndex = m_attributeBindingIndex[attribIndex];
|
||||||
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||||
@@ -172,11 +198,24 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
auto& attr = m_attributes[attribIndex];
|
auto& attr = m_attributes[attribIndex];
|
||||||
|
|
||||||
|
// VERTEX_ATTRIB_ARRAY_DIVISOR is not independent per-attribute state: it IS the divisor
|
||||||
|
// of the binding point the attribute is attached to (GL 4.6 core 10.3.2), whichever API
|
||||||
|
// configured the attribute. glVertexBindingDivisor therefore has to reach a classic
|
||||||
|
// pointer-configured attribute as well - basic-state4 alternates the two spellings on
|
||||||
|
// the same attribute and expects each to win in turn.
|
||||||
|
if (attr.Divisor != binding.Divisor) {
|
||||||
|
attr.Divisor = binding.Divisor;
|
||||||
|
BumpAttributeFormatVersion(attribIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else stays owned by whichever API configured the attribute: a classic
|
||||||
|
// glVertexAttrib*Pointer attribute keeps its pointer-resolved stride and offset.
|
||||||
|
if (!m_attributeUsesBindingModel[attribIndex]) return;
|
||||||
|
|
||||||
const SizeT resolvedOffset = binding.Offset + m_attributeRelativeOffset[attribIndex];
|
const SizeT resolvedOffset = binding.Offset + m_attributeRelativeOffset[attribIndex];
|
||||||
if (attr.Stride != binding.Stride || attr.Offset != resolvedOffset || attr.Divisor != binding.Divisor) {
|
if (attr.Stride != binding.Stride || attr.Offset != resolvedOffset) {
|
||||||
attr.Stride = binding.Stride;
|
attr.Stride = binding.Stride;
|
||||||
attr.Offset = resolvedOffset;
|
attr.Offset = resolvedOffset;
|
||||||
attr.Divisor = binding.Divisor;
|
|
||||||
BumpAttributeFormatVersion(attribIndex);
|
BumpAttributeFormatVersion(attribIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +225,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VertexArrayObject::ResolveAttributesForBinding(Uint bindingIndex, Bool adopt) {
|
||||||
|
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
||||||
|
if (m_attributeBindingIndex[attribIndex] != bindingIndex) continue;
|
||||||
|
if (adopt) m_attributeUsesBindingModel[attribIndex] = true;
|
||||||
|
ResolveAttributeFromBinding(attribIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void VertexArrayObject::SetBindingBuffer(Uint bindingIndex, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
void VertexArrayObject::SetBindingBuffer(Uint bindingIndex, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
||||||
int stride) {
|
int stride) {
|
||||||
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||||
@@ -195,15 +242,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
binding.Offset = offset;
|
binding.Offset = offset;
|
||||||
binding.Stride = stride;
|
binding.Stride = stride;
|
||||||
|
|
||||||
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
// Binding a vertex buffer to a binding point adopts every attribute currently mapped to
|
||||||
if (m_attributeBindingIndex[attribIndex] == bindingIndex) {
|
// that binding point into the binding model (the default mapping is attribute i ->
|
||||||
// Binding a vertex buffer to a binding point adopts every attribute currently
|
// binding i, which matches the GL 4.3 rules for state mixing).
|
||||||
// mapped to that binding point into the binding model (the default mapping is
|
ResolveAttributesForBinding(bindingIndex, /*adopt: */ true);
|
||||||
// attribute i -> binding i, which matches the GL 4.3 rules for state mixing).
|
|
||||||
m_attributeUsesBindingModel[attribIndex] = true;
|
|
||||||
ResolveAttributeFromBinding(attribIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::SetBindingDivisor(Uint bindingIndex, Uint divisor) {
|
void VertexArrayObject::SetBindingDivisor(Uint bindingIndex, Uint divisor) {
|
||||||
@@ -211,11 +253,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
m_bindingPoints[bindingIndex].Divisor = divisor;
|
m_bindingPoints[bindingIndex].Divisor = divisor;
|
||||||
|
|
||||||
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
ResolveAttributesForBinding(bindingIndex, /*adopt: */ false);
|
||||||
if (m_attributeBindingIndex[attribIndex] == bindingIndex && m_attributeUsesBindingModel[attribIndex]) {
|
|
||||||
ResolveAttributeFromBinding(attribIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::SetAttributeBinding(Uint attribIndex, Uint bindingIndex) {
|
void VertexArrayObject::SetAttributeBinding(Uint attribIndex, Uint bindingIndex) {
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ namespace MobileGL {
|
|||||||
Bool IsBgra = false;
|
Bool IsBgra = false;
|
||||||
Uint Divisor = 0;
|
Uint Divisor = 0;
|
||||||
SharedPtr<BufferObject> Buffer;
|
SharedPtr<BufferObject> Buffer;
|
||||||
|
|
||||||
|
// GL 4.6 core table 23.3: VERTEX_ATTRIB_ARRAY_STRIDE and _POINTER are the
|
||||||
|
// arguments of the last glVertexAttrib*Pointer call on this attribute,
|
||||||
|
// reported verbatim, and NOTHING else writes them - not glVertexAttribFormat,
|
||||||
|
// not glBindVertexBuffer. Stride/Offset above are the *resolved* draw inputs
|
||||||
|
// and the binding model does overwrite those, so the two views have to be
|
||||||
|
// stored apart or the binding-model sequence reports a legacy state it never
|
||||||
|
// set (KHR-GL4x.vertex_attrib_binding.basic-state3).
|
||||||
|
int LegacyStride = 0;
|
||||||
|
SizeT LegacyPointer = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
||||||
@@ -40,7 +50,8 @@ namespace MobileGL {
|
|||||||
struct VertexBufferBindingPoint {
|
struct VertexBufferBindingPoint {
|
||||||
SharedPtr<BufferObject> Buffer;
|
SharedPtr<BufferObject> Buffer;
|
||||||
SizeT Offset = 0;
|
SizeT Offset = 0;
|
||||||
int Stride = 0;
|
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
|
||||||
|
int Stride = 16;
|
||||||
Uint Divisor = 0;
|
Uint Divisor = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,6 +196,10 @@ namespace MobileGL {
|
|||||||
void BumpAttributeBufferVersion(Uint index);
|
void BumpAttributeBufferVersion(Uint index);
|
||||||
void BumpAttributeSwitchVersion(Uint index);
|
void BumpAttributeSwitchVersion(Uint index);
|
||||||
void ResolveAttributeFromBinding(Uint attribIndex);
|
void ResolveAttributeFromBinding(Uint attribIndex);
|
||||||
|
// Re-resolve every attribute currently pointed at `bindingIndex`. `adopt` turns
|
||||||
|
// the ones that are not in the binding model yet into binding-model attributes
|
||||||
|
// first (what glBindVertexBuffer does, GL 4.3 rules for state mixing).
|
||||||
|
void ResolveAttributesForBinding(Uint bindingIndex, Bool adopt);
|
||||||
|
|
||||||
// The default mapping is attribute i -> binding point i. Keep it an iota over
|
// The default mapping is attribute i -> binding point i. Keep it an iota over
|
||||||
// MAX_VERTEX_ATTRIBS rather than a literal list: a literal list silently leaves the
|
// MAX_VERTEX_ATTRIBS rather than a literal list: a literal list silently leaves the
|
||||||
|
|||||||
@@ -164,6 +164,22 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
XfbBlockVaryingTest
|
||||||
|
XfbBlockVaryingTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(XfbBlockVaryingTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
XfbBlockVaryingTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
ProgramInterfaceTest
|
ProgramInterfaceTest
|
||||||
ProgramInterfaceTest.cpp
|
ProgramInterfaceTest.cpp
|
||||||
@@ -195,6 +211,7 @@ include(GoogleTest)
|
|||||||
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
gtest_discover_tests(XfbBlockVaryingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
|
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
|
||||||
# compile pool so there is something in flight to race against.
|
# compile pool so there is something in flight to race against.
|
||||||
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
|
|||||||
@@ -1100,4 +1100,119 @@ void main() { color = u + v; }
|
|||||||
EXPECT_EQ(viaActiveUniformBlockiv, 5);
|
EXPECT_EQ(viaActiveUniformBlockiv, 5);
|
||||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------- queries on an unlinked program ----
|
||||||
|
// glGetProgramiv is legal on a program that has never linked - GL 4.6 sec. 7.3 says the
|
||||||
|
// queried state simply has its initial value - but the reflection-backed pnames read
|
||||||
|
// Artifacts().program, which is null until a link produces one. That dereference was a
|
||||||
|
// SIGSEGV inside glslang::TProgram::getNumPipeInputs, and KHR-GL30.api.coverage walks into it
|
||||||
|
// (it queries GL_ACTIVE_ATTRIBUTES right after a glGetAttribLocation that failed). It only
|
||||||
|
// became reachable once the glCopyTexImage2D throw ahead of it in the same case stopped
|
||||||
|
// killing the run first.
|
||||||
|
TEST_F(ProgramInterfaceTest, ReflectionQueriesOnAnUnlinkedProgramAnswerZero) {
|
||||||
|
const GLuint neverLinked = CreateProgram();
|
||||||
|
ASSERT_NE(neverLinked, 0u);
|
||||||
|
ClearErrors();
|
||||||
|
|
||||||
|
for (const GLenum pname : {GL_ACTIVE_ATTRIBUTES, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, GL_ACTIVE_UNIFORMS,
|
||||||
|
GL_ACTIVE_UNIFORM_MAX_LENGTH, GL_ACTIVE_UNIFORM_BLOCKS,
|
||||||
|
GL_ACTIVE_ATOMIC_COUNTER_BUFFERS}) {
|
||||||
|
GLint value = -1;
|
||||||
|
GetProgramiv(neverLinked, pname, &value);
|
||||||
|
ClearErrors();
|
||||||
|
EXPECT_GE(value, 0) << "pname 0x" << std::hex << pname << " left its output untouched";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A program that was linked and FAILED is the shape api.coverage actually hits.
|
||||||
|
const GLuint brokenSource = MakeProgram("#version 430\nvoid main() { this is not glsl }\n", kSimpleFs);
|
||||||
|
LinkProgram(brokenSource);
|
||||||
|
ClearErrors();
|
||||||
|
GLint linked = GL_TRUE;
|
||||||
|
GetProgramiv(brokenSource, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_FALSE) << "the shader was supposed to fail to compile";
|
||||||
|
ClearErrors();
|
||||||
|
|
||||||
|
GLint attributes = -1;
|
||||||
|
GetProgramiv(brokenSource, GL_ACTIVE_ATTRIBUTES, &attributes);
|
||||||
|
ClearErrors();
|
||||||
|
EXPECT_EQ(attributes, 0);
|
||||||
|
|
||||||
|
// GL_COMPUTE_WORK_GROUP_SIZE is GL_INVALID_OPERATION on a program that has not linked (GL
|
||||||
|
// 4.6 sec. 7.13), so it is allowed to leave the output alone - but it still reaches
|
||||||
|
// GetComputeLocalSize(), and it may not do so through a null reflection.
|
||||||
|
GLint localSize[3] = {-1, -1, -1};
|
||||||
|
GetProgramiv(brokenSource, GL_COMPUTE_WORK_GROUP_SIZE, localSize);
|
||||||
|
const GLenum computeError = TakeError();
|
||||||
|
ClearErrors();
|
||||||
|
EXPECT_TRUE(computeError == GL_INVALID_OPERATION || (localSize[0] == 0 && localSize[1] == 0 &&
|
||||||
|
localSize[2] == 0))
|
||||||
|
<< "either the query is refused, or it answers the initial value - never both untouched "
|
||||||
|
"and unreported";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- length on every path ----
|
||||||
|
// glGetProgramResourceiv's *length is the caller's only signal for how many entries params
|
||||||
|
// holds, and callers are entitled to leave it uninitialised: the CTS declares `GLsizei
|
||||||
|
// length;` next to a 1000-entry stack array and then loops `for (i = 0; i < length; ++i)`
|
||||||
|
// (gl4cProgramInterfaceQueryTests.cpp:2172). Leaving it untouched on an error path therefore
|
||||||
|
// does not "return nothing" - it hands the caller whatever was on its stack and makes it walk
|
||||||
|
// that far. KHR-GL43.program_interface_query.subroutines-vertex read 0x20202020 (" ")
|
||||||
|
// entries and took the process down on BOTH backends. So: zero on every exit, real count on
|
||||||
|
// success. Poisoning with the exact CTS-observed value keeps the assertion honest.
|
||||||
|
TEST_F(ProgramInterfaceTest, GetProgramResourceivReportsLengthOnEveryExitPath) {
|
||||||
|
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
|
||||||
|
BindAttribLocation(p, 0, "position");
|
||||||
|
BindFragDataLocation(p, 0, "color");
|
||||||
|
LinkProgram(p);
|
||||||
|
ExpectLinked(p);
|
||||||
|
ClearErrors();
|
||||||
|
|
||||||
|
constexpr GLsizei kPoison = 0x20202020;
|
||||||
|
constexpr GLsizei kBufSize = 16;
|
||||||
|
GLint params[kBufSize] = {};
|
||||||
|
|
||||||
|
const GLenum nameLengthProp = GL_NAME_LENGTH;
|
||||||
|
const GLenum compatibleSubroutinesProp = GL_COMPATIBLE_SUBROUTINES;
|
||||||
|
const GLenum notAProp = GL_TEXTURE_2D;
|
||||||
|
|
||||||
|
const auto lengthAfter = [&](GLuint program, GLenum iface, GLuint index, GLsizei propCount,
|
||||||
|
const GLenum* props, GLsizei bufSize, GLint* out) {
|
||||||
|
GLsizei length = kPoison;
|
||||||
|
GetProgramResourceiv(program, iface, index, propCount, props, bufSize, &length, out);
|
||||||
|
ClearErrors();
|
||||||
|
return length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The case that actually crashed: no subroutine reflection exists, so the query errors
|
||||||
|
// out - and the caller then trusts *length.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_VERTEX_SUBROUTINE_UNIFORM, 0, 1, &compatibleSubroutinesProp, kBufSize, params), 0)
|
||||||
|
<< "GL_VERTEX_SUBROUTINE_UNIFORM";
|
||||||
|
// Not a program name.
|
||||||
|
EXPECT_EQ(lengthAfter(p + 4242, GL_UNIFORM, 0, 1, &nameLengthProp, kBufSize, params), 0) << "bad program";
|
||||||
|
// Not an interface enum.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_TEXTURE_2D, 0, 1, &nameLengthProp, kBufSize, params), 0) << "bad interface";
|
||||||
|
// propCount <= 0, bufSize < 0.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 0, &nameLengthProp, kBufSize, params), 0) << "propCount 0";
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &nameLengthProp, -1, params), 0) << "negative bufSize";
|
||||||
|
// props == nullptr.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, nullptr, kBufSize, params), 0) << "null props";
|
||||||
|
// A prop this command does not know at all.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, ¬AProp, kBufSize, params), 0) << "unknown prop";
|
||||||
|
// A prop it knows but this interface does not carry.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &compatibleSubroutinesProp, kBufSize, params), 0)
|
||||||
|
<< "prop/interface mismatch";
|
||||||
|
// Index past the end of a real interface.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 9999, 1, &nameLengthProp, kBufSize, params), 0) << "bad index";
|
||||||
|
// Nowhere to put the values.
|
||||||
|
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &nameLengthProp, kBufSize, nullptr), 0) << "null params";
|
||||||
|
|
||||||
|
// ...and the success path still reports the count it actually wrote.
|
||||||
|
const GLuint outputIndex = GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "color");
|
||||||
|
ASSERT_NE(outputIndex, GL_INVALID_INDEX);
|
||||||
|
GLsizei length = kPoison;
|
||||||
|
GetProgramResourceiv(p, GL_PROGRAM_OUTPUT, outputIndex, 1, &nameLengthProp, kBufSize, &length, params);
|
||||||
|
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||||
|
EXPECT_EQ(length, 1);
|
||||||
|
EXPECT_EQ(params[0], 6) << "GL_NAME_LENGTH counts the terminator";
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Program/XfbBlockVaryingTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// Transform-feedback capture of a member of an output interface block.
|
||||||
|
//
|
||||||
|
// GL 4.6 core 11.1.2.1 names such a varying "<BLOCK name>.<member>" - the block's TYPE
|
||||||
|
// name, never the instance name - which is exactly what KHR-GL4x.vertex_attrib_binding
|
||||||
|
// (gl4cVertexAttribBindingTests.cpp:419-437, `out StageData { vec4 attrib[16]; } vs_out;`
|
||||||
|
// captured as "StageData.attrib[0]".."[15]") relies on. The resolver used to match the
|
||||||
|
// requested name against glslang's linker-object symbol name, which for a block is the
|
||||||
|
// INSTANCE ("vs_out"), so every one of those captures came back unresolved and the link
|
||||||
|
// failed with "is not an output of the vertex stage" + GL_INVALID_VALUE.
|
||||||
|
//
|
||||||
|
// GPU-free: everything asserted here is a property of the link, not of any driver.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
|
||||||
|
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
||||||
|
#include "MG_State/GLState/Core.h"
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Impl::GLImpl;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class XfbBlockVaryingTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override { MobileGL::Initialize(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
GLuint MakeVsOnlyProgram(const char* vs) {
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
ShaderSource(shader, 1, &vs, nullptr);
|
||||||
|
CompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
EXPECT_EQ(compiled, GL_TRUE) << [&] {
|
||||||
|
char log[4096] = "";
|
||||||
|
GetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||||
|
return std::string(log);
|
||||||
|
}();
|
||||||
|
AttachShader(program, shader);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LinkLog(GLuint program) {
|
||||||
|
char log[4096] = "";
|
||||||
|
GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||||
|
return std::string(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint Programiv(GLuint program, GLenum pname) {
|
||||||
|
GLint value = -1;
|
||||||
|
GetProgramiv(program, pname, &value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VaryingRecord {
|
||||||
|
std::string name;
|
||||||
|
GLsizei size = 0;
|
||||||
|
GLenum type = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
VaryingRecord Varying(GLuint program, GLuint index) {
|
||||||
|
VaryingRecord record;
|
||||||
|
GLchar buffer[256] = {'\0'};
|
||||||
|
GLsizei length = 0;
|
||||||
|
GetTransformFeedbackVarying(program, index, sizeof(buffer), &length, &record.size, &record.type, buffer);
|
||||||
|
record.name.assign(buffer, buffer + (length < 0 ? 0 : length));
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearErrors() {
|
||||||
|
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CTS shader, narrowed to two elements so the expectations stay readable.
|
||||||
|
const char* kNamedBlockVs = R"(#version 430 core
|
||||||
|
layout(location = 0) in vec4 vs_in_attrib[2];
|
||||||
|
out StageData {
|
||||||
|
vec4 attrib[2];
|
||||||
|
} vs_out;
|
||||||
|
void main() {
|
||||||
|
for (int i = 0; i < vs_in_attrib.length(); ++i) {
|
||||||
|
vs_out.attrib[i] = vs_in_attrib[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
TEST_F(XfbBlockVaryingTest, CapturesBlockMemberElementsByBlockTypeName) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
||||||
|
const GLchar* const varyings[2] = {"StageData.attrib[0]", "StageData.attrib[1]"};
|
||||||
|
TransformFeedbackVaryings(program, 2, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 2);
|
||||||
|
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE), GL_INTERLEAVED_ATTRIBS);
|
||||||
|
|
||||||
|
for (GLuint i = 0; i < 2; ++i) {
|
||||||
|
const VaryingRecord record = Varying(program, i);
|
||||||
|
EXPECT_EQ(record.name, std::string("StageData.attrib[") + std::to_string(i) + "]");
|
||||||
|
// One element of the member array, not the whole array.
|
||||||
|
EXPECT_EQ(record.size, 1) << "index " << i;
|
||||||
|
EXPECT_EQ(record.type, static_cast<GLenum>(GL_FLOAT_VEC4)) << "index " << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole member, no subscript: the array size has to survive.
|
||||||
|
TEST_F(XfbBlockVaryingTest, CapturesAWholeBlockMemberArray) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
||||||
|
const GLchar* const varyings[1] = {"StageData.attrib"};
|
||||||
|
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||||
|
const VaryingRecord record = Varying(program, 0);
|
||||||
|
EXPECT_EQ(record.name, "StageData.attrib");
|
||||||
|
EXPECT_EQ(record.size, 2);
|
||||||
|
EXPECT_EQ(record.type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Members of an anonymous instance are named the same way - the block name is still
|
||||||
|
// what identifies them, and there is no instance name to fall back on.
|
||||||
|
TEST_F(XfbBlockVaryingTest, CapturesAnonymousInstanceBlockMember) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(R"(#version 430 core
|
||||||
|
layout(location = 0) in vec4 vs_in_attrib;
|
||||||
|
out StageData {
|
||||||
|
vec4 color;
|
||||||
|
vec2 uv;
|
||||||
|
};
|
||||||
|
void main() {
|
||||||
|
color = vs_in_attrib;
|
||||||
|
uv = vs_in_attrib.xy;
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
const GLchar* const varyings[2] = {"StageData.color", "StageData.uv"};
|
||||||
|
TransformFeedbackVaryings(program, 2, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||||
|
EXPECT_EQ(Varying(program, 0).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||||
|
EXPECT_EQ(Varying(program, 1).type, static_cast<GLenum>(GL_FLOAT_VEC2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The instance-qualified spelling is not what the spec asks for, but it is what a lot of
|
||||||
|
// application code writes; resolving it too costs nothing and keeps those links alive.
|
||||||
|
TEST_F(XfbBlockVaryingTest, AlsoAcceptsTheInstanceQualifiedSpelling) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
||||||
|
const GLchar* const varyings[1] = {"vs_out.attrib[1]"};
|
||||||
|
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||||
|
EXPECT_EQ(Varying(program, 0).size, 1);
|
||||||
|
EXPECT_EQ(Varying(program, 0).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dotted path that resolves to nothing must still fail the link, and say so - the
|
||||||
|
// fix must not turn "unknown member" into a silently dropped capture.
|
||||||
|
TEST_F(XfbBlockVaryingTest, RejectsAnUnknownBlockMember) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
||||||
|
const GLchar* const varyings[1] = {"StageData.missing"};
|
||||||
|
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE);
|
||||||
|
EXPECT_NE(LinkLog(program).find("StageData.missing"), std::string::npos) << LinkLog(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(XfbBlockVaryingTest, RejectsAnUnknownBlock) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
||||||
|
const GLchar* const varyings[1] = {"NoSuchBlock.attrib[0]"};
|
||||||
|
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain (non-block) outputs must keep resolving exactly as before.
|
||||||
|
TEST_F(XfbBlockVaryingTest, StillResolvesPlainOutputs) {
|
||||||
|
ClearErrors();
|
||||||
|
const GLuint program = MakeVsOnlyProgram(R"(#version 430 core
|
||||||
|
layout(location = 0) in vec4 vs_in_attrib;
|
||||||
|
out vec4 plain[2];
|
||||||
|
out vec3 single;
|
||||||
|
void main() {
|
||||||
|
plain[0] = vs_in_attrib;
|
||||||
|
plain[1] = vs_in_attrib;
|
||||||
|
single = vs_in_attrib.xyz;
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
const GLchar* const varyings[3] = {"plain[1]", "single", "gl_Position"};
|
||||||
|
TransformFeedbackVaryings(program, 3, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||||
|
EXPECT_EQ(Varying(program, 0).size, 1);
|
||||||
|
EXPECT_EQ(Varying(program, 0).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||||
|
EXPECT_EQ(Varying(program, 1).type, static_cast<GLenum>(GL_FLOAT_VEC3));
|
||||||
|
EXPECT_EQ(Varying(program, 2).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -1822,13 +1822,170 @@ TEST(DirectGLESBackendTexture, DestructorDeletesIdAndScrubsBindingCache) {
|
|||||||
// A wrapper whose context died must NOT delete a foreign (recycled) name.
|
// A wrapper whose context died must NOT delete a foreign (recycled) name.
|
||||||
{
|
{
|
||||||
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
||||||
++TextureImpl::g_textureContextGeneration;
|
++g_backendContextGeneration;
|
||||||
backendTexture.reset();
|
backendTexture.reset();
|
||||||
--TextureImpl::g_textureContextGeneration; // restore for later tests
|
--g_backendContextGeneration; // restore for later tests
|
||||||
EXPECT_EQ(deleted.size(), 1u);
|
EXPECT_EQ(deleted.size(), 1u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- DirectGLES backend twins release their driver ids --------------------------------------
|
||||||
|
// Framebuffers, renderbuffers and samplers had no destructor at all: every frontend object the
|
||||||
|
// application deleted leaked its ES twin for the whole process lifetime. An application that
|
||||||
|
// creates a framebuffer per readback (GL CTS packed_pixels.varied_rectangle makes ~3300 of them
|
||||||
|
// per case) walked the driver into a gigabyte of dead framebuffers, and past that point every
|
||||||
|
// readback through a freshly attached framebuffer came back with stale pixels.
|
||||||
|
namespace {
|
||||||
|
struct TwinDeletionSinks {
|
||||||
|
MobileGL::Vector<GLuint> framebuffers;
|
||||||
|
MobileGL::Vector<GLuint> renderbuffers;
|
||||||
|
MobileGL::Vector<GLuint> samplers;
|
||||||
|
};
|
||||||
|
|
||||||
|
TwinDeletionSinks* g_twinDeletionSinks = nullptr;
|
||||||
|
GLuint g_nextTwinDriverId = 900;
|
||||||
|
|
||||||
|
void TW_GenFramebuffers(GLsizei count, GLuint* ids) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
||||||
|
}
|
||||||
|
void TW_DeleteFramebuffers(GLsizei count, const GLuint* ids) {
|
||||||
|
if (!g_twinDeletionSinks) return;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->framebuffers.push_back(ids[i]);
|
||||||
|
}
|
||||||
|
void TW_GenRenderbuffers(GLsizei count, GLuint* ids) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
||||||
|
}
|
||||||
|
void TW_DeleteRenderbuffers(GLsizei count, const GLuint* ids) {
|
||||||
|
if (!g_twinDeletionSinks) return;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->renderbuffers.push_back(ids[i]);
|
||||||
|
}
|
||||||
|
void TW_GenSamplers(GLsizei count, GLuint* ids) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
||||||
|
}
|
||||||
|
void TW_DeleteSamplers(GLsizei count, const GLuint* ids) {
|
||||||
|
if (!g_twinDeletionSinks) return;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->samplers.push_back(ids[i]);
|
||||||
|
}
|
||||||
|
void TW_BindFramebuffer(GLenum target, GLuint framebuffer) {
|
||||||
|
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
|
||||||
|
}
|
||||||
|
void TW_BindSampler(GLuint, GLuint) {}
|
||||||
|
void TW_BindRenderbuffer(GLenum, GLuint) {}
|
||||||
|
|
||||||
|
// Installs a table that can create and destroy all three twin kinds, and unwinds it (plus the
|
||||||
|
// recording pointer) even when an assertion aborts the test body.
|
||||||
|
struct ScopedBackendTwinMocks {
|
||||||
|
ScopedBackendTwinMocks(): previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs) {
|
||||||
|
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||||
|
MobileGL::MG_External::GLESFunctionsTable functions{};
|
||||||
|
functions.glGenFramebuffers = TW_GenFramebuffers;
|
||||||
|
functions.glDeleteFramebuffers = TW_DeleteFramebuffers;
|
||||||
|
functions.glBindFramebuffer = TW_BindFramebuffer;
|
||||||
|
functions.glGenRenderbuffers = TW_GenRenderbuffers;
|
||||||
|
functions.glDeleteRenderbuffers = TW_DeleteRenderbuffers;
|
||||||
|
functions.glBindRenderbuffer = TW_BindRenderbuffer;
|
||||||
|
functions.glGenSamplers = TW_GenSamplers;
|
||||||
|
functions.glDeleteSamplers = TW_DeleteSamplers;
|
||||||
|
functions.glBindSampler = TW_BindSampler;
|
||||||
|
functions.glGetError = SG_NoError;
|
||||||
|
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||||
|
g_twinDeletionSinks = &sinks;
|
||||||
|
g_stateGuardLog = &log;
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedBackendTwinMocks() {
|
||||||
|
g_stateGuardLog = nullptr;
|
||||||
|
g_twinDeletionSinks = nullptr;
|
||||||
|
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
|
||||||
|
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedBackendTwinMocks(const ScopedBackendTwinMocks&) = delete;
|
||||||
|
ScopedBackendTwinMocks& operator=(const ScopedBackendTwinMocks&) = delete;
|
||||||
|
|
||||||
|
TwinDeletionSinks sinks;
|
||||||
|
StateGuardCallLog log;
|
||||||
|
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendFramebuffer, DestructorDeletesIdAndScrubsBindingShadow) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedBackendTwinMocks mocks;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
{
|
||||||
|
auto backendFBO = MobileGL::MakeShared<FramebufferImpl::BackendFramebufferObject>();
|
||||||
|
id = backendFBO->GetBackendFramebufferId();
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
backendFBO->Bind(MobileGL::FramebufferTarget::Draw);
|
||||||
|
ASSERT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), id);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(mocks.sinks.framebuffers.size(), 1u);
|
||||||
|
EXPECT_EQ(mocks.sinks.framebuffers[0], id);
|
||||||
|
// ES reverts every target bound to a deleted framebuffer to 0. The shadow has to follow, or
|
||||||
|
// the next BindFramebufferId(0) is deduped away and the driver keeps the dead name bound.
|
||||||
|
EXPECT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), 0u);
|
||||||
|
|
||||||
|
// A twin whose context died must NOT delete a name a successor context may have recycled.
|
||||||
|
{
|
||||||
|
auto backendFBO = MobileGL::MakeShared<FramebufferImpl::BackendFramebufferObject>();
|
||||||
|
++g_backendContextGeneration;
|
||||||
|
backendFBO.reset();
|
||||||
|
--g_backendContextGeneration; // restore for later tests
|
||||||
|
EXPECT_EQ(mocks.sinks.framebuffers.size(), 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendRenderbuffer, DestructorDeletesId) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedBackendTwinMocks mocks;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
{
|
||||||
|
auto backendRBO = MobileGL::MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
||||||
|
id = backendRBO->GetBackendRenderbufferId();
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(mocks.sinks.renderbuffers.size(), 1u);
|
||||||
|
EXPECT_EQ(mocks.sinks.renderbuffers[0], id);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto backendRBO = MobileGL::MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
||||||
|
++g_backendContextGeneration;
|
||||||
|
backendRBO.reset();
|
||||||
|
--g_backendContextGeneration;
|
||||||
|
EXPECT_EQ(mocks.sinks.renderbuffers.size(), 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendSampler, DestructorDeletesIdAndScrubsUnitCache) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedBackendTwinMocks mocks;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
{
|
||||||
|
auto backendSampler = MobileGL::MakeShared<SamplerImpl::BackendSamplerObject>();
|
||||||
|
id = backendSampler->GetBackendSamplerId();
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
backendSampler->Bind(3);
|
||||||
|
ASSERT_EQ(SamplerImpl::g_boundSamplersCache[3], backendSampler.get());
|
||||||
|
}
|
||||||
|
ASSERT_EQ(mocks.sinks.samplers.size(), 1u);
|
||||||
|
EXPECT_EQ(mocks.sinks.samplers[0], id);
|
||||||
|
// glDeleteSamplers unbinds from every unit, and the next twin can land on this heap
|
||||||
|
// address - a stale row would false-skip its Bind.
|
||||||
|
EXPECT_EQ(SamplerImpl::g_boundSamplersCache[3], nullptr);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto backendSampler = MobileGL::MakeShared<SamplerImpl::BackendSamplerObject>();
|
||||||
|
++g_backendContextGeneration;
|
||||||
|
backendSampler.reset();
|
||||||
|
--g_backendContextGeneration;
|
||||||
|
EXPECT_EQ(mocks.sinks.samplers.size(), 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
||||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
ScopedStateGuardMocks mocks;
|
ScopedStateGuardMocks mocks;
|
||||||
|
|||||||
@@ -25,3 +25,53 @@ endif()
|
|||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
RenderStateTest
|
||||||
|
RenderStateTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderStateTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/xxHash
|
||||||
|
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
RenderStateTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(RenderStateTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
gtest_discover_tests(RenderStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
NegativeApiErrorsTest
|
||||||
|
NegativeApiErrorsTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(NegativeApiErrorsTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/xxHash
|
||||||
|
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
NegativeApiErrorsTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(NegativeApiErrorsTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
gtest_discover_tests(NegativeApiErrorsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The negative-path GL errors the conformance suite checks and MobileGL used to answer
|
||||||
|
// GL_NO_ERROR to. Every row here is a call the spec requires to fail, lifted from the CTS case
|
||||||
|
// that found it:
|
||||||
|
// * KHR-GL44.multi_bind.errors_bind_buffers / .errors_bind_samplers - ARB_multi_bind's
|
||||||
|
// "buffers/samplers will not be created if they do not exist" rule, plus the atomic-counter
|
||||||
|
// offset alignment the single-bind path never had.
|
||||||
|
// * KHR-GL43.shader_storage_buffer_object.negative-api-bind - the SSBO offset alignment is a
|
||||||
|
// property of the binding point and applies with buffer 0 too.
|
||||||
|
// * KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount - the three
|
||||||
|
// errors that guard a parameter-buffer draw.
|
||||||
|
// * KHR-GL43.compute_shader.api-indirect / .api-program.
|
||||||
|
// * KHR-GLxx.texture_storage.compressed_data - compressed formats on TEXTURE_3D.
|
||||||
|
// Plus the indexed-getter parity RC-7b is about: glGetBooleani_v / glGetInteger64i_v /
|
||||||
|
// glGetFloati_v / glGetDoublei_v must answer every pname glGetIntegeri_v answers.
|
||||||
|
//
|
||||||
|
// GPU-free: all of it is frontend validation.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||||
|
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
|
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||||
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
|
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
||||||
|
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||||
|
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Impl::GLImpl;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class NegativeApiErrorsTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (int i = 0; i < 16 && GetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint MakeBuffer(GLenum target, GLsizeiptr size) {
|
||||||
|
GLuint buffer = 0;
|
||||||
|
GenBuffers(1, &buffer);
|
||||||
|
BindBuffer(target, buffer);
|
||||||
|
BufferData(target, size, nullptr, GL_STATIC_DRAW);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One table row: run the call, assert exactly the expected error, leave nothing pending.
|
||||||
|
struct Row {
|
||||||
|
const char* what;
|
||||||
|
std::function<void()> call;
|
||||||
|
GLenum expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void RunRows(const std::vector<Row>& rows) {
|
||||||
|
for (const Row& row : rows) {
|
||||||
|
DrainErrors();
|
||||||
|
row.call();
|
||||||
|
EXPECT_EQ(GetError(), row.expected) << row.what;
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(NegativeApiErrorsTest, MultiBindRejectsNamesThatAreNotObjectsYet) {
|
||||||
|
const GLuint buffer = MakeBuffer(GL_UNIFORM_BUFFER, 1024);
|
||||||
|
// Reserved by glGenBuffers but never turned into an object: legal for glBindBuffer,
|
||||||
|
// which creates it, and illegal for glBindBuffersBase, which must not.
|
||||||
|
GLuint reservedOnly = 0;
|
||||||
|
GenBuffers(1, &reservedOnly);
|
||||||
|
ASSERT_NE(reservedOnly, 0u);
|
||||||
|
ASSERT_EQ(IsBuffer(reservedOnly), GL_FALSE);
|
||||||
|
|
||||||
|
// glGenSamplers, unlike glGenBuffers, creates the objects outright, so a sampler name is
|
||||||
|
// only "not an existing object" once it has been deleted.
|
||||||
|
GLuint deadSampler = 0;
|
||||||
|
GenSamplers(1, &deadSampler);
|
||||||
|
ASSERT_NE(deadSampler, 0u);
|
||||||
|
DeleteSamplers(1, &deadSampler);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
const GLuint mixedBuffers[2] = {buffer, reservedOnly};
|
||||||
|
const GLuint samplers[1] = {deadSampler};
|
||||||
|
const GLintptr offsets[2] = {0, 0};
|
||||||
|
const GLsizeiptr sizes[2] = {256, 256};
|
||||||
|
|
||||||
|
RunRows({
|
||||||
|
{"glBindBuffersBase with a reserved-but-uncreated name",
|
||||||
|
[&] { BindBuffersBase(GL_UNIFORM_BUFFER, 0, 2, mixedBuffers); }, GL_INVALID_OPERATION},
|
||||||
|
{"glBindBuffersRange with a reserved-but-uncreated name",
|
||||||
|
[&] { BindBuffersRange(GL_UNIFORM_BUFFER, 0, 2, mixedBuffers, offsets, sizes); },
|
||||||
|
GL_INVALID_OPERATION},
|
||||||
|
{"glBindSamplers with a deleted sampler name", [&] { BindSamplers(0, 1, samplers); },
|
||||||
|
GL_INVALID_OPERATION},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ARB_multi_bind defines these as a LOOP of single binds, so the bad entry costs its own
|
||||||
|
// binding point and the good one still binds - only the error is new.
|
||||||
|
GLint bound = -1;
|
||||||
|
GetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, 0, &bound);
|
||||||
|
EXPECT_EQ(static_cast<GLuint>(bound), buffer) << "a rejected element must not take the valid ones with it";
|
||||||
|
GetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, 1, &bound);
|
||||||
|
EXPECT_EQ(bound, 0) << "the rejected element must not have bound anything";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NegativeApiErrorsTest, BufferRangeOffsetAlignmentAppliesToTheBindingPoint) {
|
||||||
|
GLint ssboAlignment = 0;
|
||||||
|
GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||||
|
ASSERT_GT(ssboAlignment, 1) << "the alignment rule is untestable at alignment 1";
|
||||||
|
const GLuint atomicBuffer = MakeBuffer(GL_ATOMIC_COUNTER_BUFFER, 1024);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
RunRows({
|
||||||
|
// buffer 0 detaches the binding point, but the target's alignment rule still holds.
|
||||||
|
{"glBindBufferRange(SHADER_STORAGE_BUFFER, buffer 0, misaligned offset)",
|
||||||
|
[&] { BindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, 0, ssboAlignment - 1, 0); }, GL_INVALID_VALUE},
|
||||||
|
// An atomic counter binding is addressed in 32-bit counters; it has no queryable
|
||||||
|
// alignment pname, which is how its rule went missing.
|
||||||
|
{"glBindBufferRange(ATOMIC_COUNTER_BUFFER, offset 3)",
|
||||||
|
[&] { BindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, atomicBuffer, 3, 16); }, GL_INVALID_VALUE},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...and the aligned form still works.
|
||||||
|
DrainErrors();
|
||||||
|
BindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, atomicBuffer, 4, 16);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NegativeApiErrorsTest, DispatchComputeIndirectChecksTheBoundBufferExtent) {
|
||||||
|
// Six uints: an indirect dispatch reads three, so offset 16 runs off the end.
|
||||||
|
const GLuint dispatchBuffer = MakeBuffer(GL_DISPATCH_INDIRECT_BUFFER, 6 * sizeof(GLuint));
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
RunRows({
|
||||||
|
{"glDispatchComputeIndirect(-2)", [] { DispatchComputeIndirect(-2); }, GL_INVALID_VALUE},
|
||||||
|
{"glDispatchComputeIndirect(3)", [] { DispatchComputeIndirect(3); }, GL_INVALID_VALUE},
|
||||||
|
{"glDispatchComputeIndirect(16) past the end of a 24-byte buffer",
|
||||||
|
[] { DispatchComputeIndirect(16); }, GL_INVALID_OPERATION},
|
||||||
|
{"glDispatchComputeIndirect(0) with nothing bound",
|
||||||
|
[&] {
|
||||||
|
BindBuffer(GL_DISPATCH_INDIRECT_BUFFER, 0);
|
||||||
|
DispatchComputeIndirect(0);
|
||||||
|
},
|
||||||
|
GL_INVALID_OPERATION},
|
||||||
|
});
|
||||||
|
static_cast<void>(dispatchBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NegativeApiErrorsTest, IndirectParameterDrawsCheckBothBuffers) {
|
||||||
|
// Two DrawArraysIndirectCommands (16 bytes each) and a roomy parameter buffer.
|
||||||
|
MakeBuffer(GL_DRAW_INDIRECT_BUFFER, 2 * 4 * sizeof(GLuint));
|
||||||
|
const GLuint parameterBuffer = MakeBuffer(GL_PARAMETER_BUFFER, 200);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
RunRows({
|
||||||
|
{"glMultiDrawArraysIndirectCount with drawcount 2 (not a multiple of four)",
|
||||||
|
[] { MultiDrawArraysIndirectCount(GL_TRIANGLE_STRIP, nullptr, 2, 1, 0); }, GL_INVALID_VALUE},
|
||||||
|
{"glMultiDrawArraysIndirectCount with maxdrawcount past the indirect buffer",
|
||||||
|
[] { MultiDrawArraysIndirectCount(GL_TRIANGLE_STRIP, nullptr, 0, 4, 0); }, GL_INVALID_OPERATION},
|
||||||
|
{"glMultiDrawElementsIndirectCount with drawcount 2",
|
||||||
|
[] { MultiDrawElementsIndirectCount(GL_TRIANGLE_STRIP, GL_UNSIGNED_BYTE, nullptr, 2, 1, 0); },
|
||||||
|
GL_INVALID_VALUE},
|
||||||
|
{"glMultiDrawArraysIndirectCount with no parameter buffer bound",
|
||||||
|
[&] {
|
||||||
|
BindBuffer(GL_PARAMETER_BUFFER, 0);
|
||||||
|
MultiDrawArraysIndirectCount(GL_TRIANGLE_STRIP, nullptr, 0, 2, 0);
|
||||||
|
},
|
||||||
|
GL_INVALID_OPERATION},
|
||||||
|
});
|
||||||
|
static_cast<void>(parameterBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NegativeApiErrorsTest, TexStorage3DRejectsCompressedFormatsOnTexture3D) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
GenTextures(1, &texture);
|
||||||
|
BindTexture(GL_TEXTURE_3D, texture);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
RunRows({
|
||||||
|
{"glTexStorage3D(TEXTURE_3D, GL_COMPRESSED_RED_RGTC1)",
|
||||||
|
[] { TexStorage3D(GL_TEXTURE_3D, 1, 0x8DBB /* GL_COMPRESSED_RED_RGTC1 */, 8, 8, 8); },
|
||||||
|
GL_INVALID_OPERATION},
|
||||||
|
{"glTexStorage3D(TEXTURE_3D, GL_COMPRESSED_RG_RGTC2)",
|
||||||
|
[] { TexStorage3D(GL_TEXTURE_3D, 1, 0x8DBD /* GL_COMPRESSED_RG_RGTC2 */, 8, 8, 8); },
|
||||||
|
GL_INVALID_OPERATION},
|
||||||
|
});
|
||||||
|
|
||||||
|
// An uncompressed sized format on the same target still allocates.
|
||||||
|
DrainErrors();
|
||||||
|
TexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 8, 8, 8);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NegativeApiErrorsTest, LinkRejectsAComputeAndNonComputeMix) {
|
||||||
|
const auto attach = [](GLuint program, GLenum stage, const char* source) {
|
||||||
|
const GLuint shader = CreateShader(stage);
|
||||||
|
ShaderSource(shader, 1, &source, nullptr);
|
||||||
|
CompileShader(shader);
|
||||||
|
AttachShader(program, shader);
|
||||||
|
};
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
attach(program, GL_COMPUTE_SHADER, R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std430) buffer Output { uint g_output[]; };
|
||||||
|
void main() { g_output[gl_GlobalInvocationID.x] = 0; }
|
||||||
|
)");
|
||||||
|
attach(program, GL_VERTEX_SHADER, R"(#version 430 core
|
||||||
|
layout(location = 0) in vec4 g_position;
|
||||||
|
void main() { gl_Position = g_position; }
|
||||||
|
)");
|
||||||
|
attach(program, GL_FRAGMENT_SHADER, R"(#version 430 core
|
||||||
|
layout(location = 0) out vec4 g_color;
|
||||||
|
void main() { g_color = vec4(1); }
|
||||||
|
)");
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
GLint status = GL_TRUE;
|
||||||
|
GetProgramiv(program, GL_LINK_STATUS, &status);
|
||||||
|
EXPECT_EQ(status, GL_FALSE) << "a compute shader must not link with any other stage";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// RC-7b: the four non-int indexed getters have to answer the same pname table glGetIntegeri_v
|
||||||
|
// does. glGetBooleani_v used to route everything through the indexed-capability path
|
||||||
|
// (GL_INVALID_ENUM for anything else) and glGetInteger64i_v straight to the driver, which
|
||||||
|
// does not have MobileGL's frontend-only values at all.
|
||||||
|
TEST_F(NegativeApiErrorsTest, IndexedGettersAgreeWithGetIntegeriv) {
|
||||||
|
DrainErrors();
|
||||||
|
const GLenum pnames[] = {GL_MAX_COMPUTE_WORK_GROUP_COUNT, GL_MAX_COMPUTE_WORK_GROUP_SIZE};
|
||||||
|
for (GLenum pname : pnames) {
|
||||||
|
for (GLuint index = 0; index < 3; ++index) {
|
||||||
|
GLint reference = -1;
|
||||||
|
GetIntegeri_v(pname, index, &reference);
|
||||||
|
ASSERT_EQ(GetError(), GL_NO_ERROR) << "glGetIntegeri_v(" << pname << ", " << index << ")";
|
||||||
|
ASSERT_GT(reference, 0) << "the reference value has to be non-trivial to compare against";
|
||||||
|
|
||||||
|
GLint64 as64 = -1;
|
||||||
|
GetInteger64i_v(pname, index, &as64);
|
||||||
|
EXPECT_EQ(as64, static_cast<GLint64>(reference)) << "glGetInteger64i_v(" << pname << ")";
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLfloat asFloat = -1.0f;
|
||||||
|
GetFloati_v(pname, index, &asFloat);
|
||||||
|
EXPECT_FLOAT_EQ(asFloat, static_cast<GLfloat>(reference)) << "glGetFloati_v(" << pname << ")";
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLdouble asDouble = -1.0;
|
||||||
|
GetDoublei_v(pname, index, &asDouble);
|
||||||
|
EXPECT_DOUBLE_EQ(asDouble, static_cast<GLdouble>(reference)) << "glGetDoublei_v(" << pname << ")";
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLboolean asBool = GL_FALSE;
|
||||||
|
GetBooleani_v(pname, index, &asBool);
|
||||||
|
EXPECT_EQ(asBool, GL_TRUE) << "glGetBooleani_v(" << pname << ")";
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and the vertex-binding offset keeps its 64-bit width through glGetInteger64i_v, which is
|
||||||
|
// how KHR-GL4x.vertex_attrib_binding reads it.
|
||||||
|
TEST_F(NegativeApiErrorsTest, VertexBindingOffsetIsReadableThroughTheSixtyFourBitGetter) {
|
||||||
|
GLuint vao = 0;
|
||||||
|
GenVertexArrays(1, &vao);
|
||||||
|
BindVertexArray(vao);
|
||||||
|
const GLuint vbo = MakeBuffer(GL_ARRAY_BUFFER, 4096);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
GLint64 offset = -1;
|
||||||
|
GetInteger64i_v(GL_VERTEX_BINDING_OFFSET, 0, &offset);
|
||||||
|
EXPECT_EQ(offset, 0);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
BindVertexBuffer(0, vbo, 2048, 128);
|
||||||
|
GetInteger64i_v(GL_VERTEX_BINDING_OFFSET, 0, &offset);
|
||||||
|
EXPECT_EQ(offset, 2048);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/State/RenderStateTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this
|
||||||
|
// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and,
|
||||||
|
// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and
|
||||||
|
// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
||||||
|
// which unwinds a C++ exception through the C GL ABI and terminates the process.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class RenderStateTest: public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
// GL error flags are sticky per code and the context outlives an individual test in this
|
||||||
|
// binary, so a pending error from an earlier case would be handed to the next GetError().
|
||||||
|
static void DrainPendingGlErrors() {
|
||||||
|
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ExpectSingleGlError(GLenum expected) {
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
DrainPendingGlErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) {
|
||||||
|
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
|
||||||
|
// a CTS negative test would hit.
|
||||||
|
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) {
|
||||||
|
MG_Impl::GLImpl::Enablei(cap, 0);
|
||||||
|
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Disablei(cap, 0);
|
||||||
|
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(cap, 0), GL_FALSE);
|
||||||
|
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectAnOutOfRangeBufferIndex) {
|
||||||
|
const GLuint outOfRange = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_BLEND, outOfRange);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Disablei(GL_BLEND, outOfRange);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, outOfRange), GL_FALSE);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) {
|
||||||
|
// The rejection path must not have cost the one capability that is genuinely indexed.
|
||||||
|
MG_Impl::GLImpl::Enablei(GL_BLEND, 1);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_TRUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::Disablei(GL_BLEND, 1);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
@@ -3176,3 +3176,115 @@ TEST_F(TextureTest, WidenedRenderTargetUploadExpandsThreeChannelDataWithOpaqueAl
|
|||||||
EXPECT_EQ(PrepareChannelWidenedUpload(3, texelSize, nullptr, 0, GL_FLOAT, widened), nullptr);
|
EXPECT_EQ(PrepareChannelWidenedUpload(3, texelSize, nullptr, 0, GL_FLOAT, widened), nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// A GL entry point may return an error, but it may never throw through the C GL ABI: unwinding a
|
||||||
|
// C++ exception across it terminates the process. These cover the sites that used to do exactly
|
||||||
|
// that (KHR-GL30.api.coverage died on the first of them on both backends).
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct CopyTexImage2DCall {
|
||||||
|
Bool Called = false;
|
||||||
|
GLenum Target = 0;
|
||||||
|
GLint Level = 0;
|
||||||
|
GLenum InternalFormat = 0;
|
||||||
|
GLsizei Width = 0;
|
||||||
|
GLsizei Height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
CopyTexImage2DCall g_copyTexImage2DCall;
|
||||||
|
|
||||||
|
void RecordCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint, GLint, GLsizei width,
|
||||||
|
GLsizei height, GLint) {
|
||||||
|
g_copyTexImage2DCall = {true, target, level, internalformat, width, height};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A colour read framebuffer of the requested sized format, bound to GL_READ_FRAMEBUFFER, which
|
||||||
|
// is what glCopyTexImage2D takes its source base format from.
|
||||||
|
void BindReadFramebufferWithColorFormat(GLenum sizedInternalFormat) {
|
||||||
|
GLuint framebuffer = 0;
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||||
|
MG_Impl::GLImpl::TextureStorage2D(texture, 1, sizedInternalFormat, 16, 16);
|
||||||
|
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||||
|
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BindFreshMutableTexture2D() {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(TextureTest, CopyTexImage2DAcceptsEveryComponentSubsetOfTheReadBuffer) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D = RecordCopyTexImage2D;
|
||||||
|
|
||||||
|
BindReadFramebufferWithColorFormat(GL_RGBA8);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "read framebuffer setup itself failed";
|
||||||
|
|
||||||
|
// GL 4.6 sec. 8.6: internalformat may name a SUBSET of the read buffer's components. This is
|
||||||
|
// exactly the list KHR-GL30.api.coverage walks against an rgba8888 colour buffer, and it is
|
||||||
|
// also what an ordinary GL app does with glCopyTexImage2D(GL_RGB) from an RGBA8 framebuffer.
|
||||||
|
for (const GLenum internalFormat : {GL_RED, GL_RG, GL_RGB, GL_RGBA}) {
|
||||||
|
BindFreshMutableTexture2D();
|
||||||
|
g_copyTexImage2DCall = {};
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::CopyTexImage2D(GL_TEXTURE_2D, 0, internalFormat, 0, 0, 1, 1, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "internalformat " << internalFormat;
|
||||||
|
EXPECT_TRUE(g_copyTexImage2DCall.Called) << "internalformat " << internalFormat;
|
||||||
|
EXPECT_EQ(g_copyTexImage2DCall.InternalFormat, internalFormat);
|
||||||
|
EXPECT_EQ(g_copyTexImage2DCall.Width, 1);
|
||||||
|
EXPECT_EQ(g_copyTexImage2DCall.Height, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, CopyTexImage2DRejectsAFormatTheReadBufferCannotSupply) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D = RecordCopyTexImage2D;
|
||||||
|
|
||||||
|
BindReadFramebufferWithColorFormat(GL_R8);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "read framebuffer setup itself failed";
|
||||||
|
|
||||||
|
BindFreshMutableTexture2D();
|
||||||
|
g_copyTexImage2DCall = {};
|
||||||
|
|
||||||
|
// The subset rule still has a wrong side: GL_RGBA asks for components a GL_R8 read buffer does
|
||||||
|
// not have. That must be GL_INVALID_OPERATION and nothing else - not a throw, not silence.
|
||||||
|
MG_Impl::GLImpl::CopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 1, 1, 0);
|
||||||
|
|
||||||
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
|
EXPECT_FALSE(g_copyTexImage2DCall.Called) << "a rejected copy must not reach the backend";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) {
|
||||||
|
// 1D textures have no upload path in this stack; the entry point used to throw unconditionally.
|
||||||
|
MG_Impl::GLImpl::CopyTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 0, 0, 1, 0);
|
||||||
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) {
|
||||||
|
// TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap
|
||||||
|
// chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
|
||||||
|
// THROW_UNIMPL_EXCEPTION default: label and killed the process.
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
|
||||||
|
DrainPendingGlErrors();
|
||||||
|
|
||||||
|
for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) {
|
||||||
|
GLint intParam = 0x20202020;
|
||||||
|
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam);
|
||||||
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
|
|
||||||
|
GLfloat floatParam = 12345.0f;
|
||||||
|
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam);
|
||||||
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,5 +16,22 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
VertexAttribBindingStateTest
|
||||||
|
VertexAttribBindingStateTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(VertexAttribBindingStateTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
VertexAttribBindingStateTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
gtest_discover_tests(VertexAttribBindingStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,430 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/VertexArray/VertexAttribBindingStateTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The ARB_vertex_attrib_binding state model, replayed exactly as
|
||||||
|
// KHR-GL4x.vertex_attrib_binding.basic-state1/3/4 and .negative-* walk it
|
||||||
|
// (external/openglcts/modules/gl/gl4cVertexAttribBindingTests.cpp): after each mutation the
|
||||||
|
// ten per-attribute pnames and the four per-binding-point pnames are read back in full, which
|
||||||
|
// is what makes a single wrong field visible as itself instead of as a downstream render
|
||||||
|
// difference.
|
||||||
|
//
|
||||||
|
// Four defects are pinned here, all of them frontend-only (both backends reported them
|
||||||
|
// byte-identically):
|
||||||
|
// * VERTEX_BINDING_STRIDE defaulted to 0; the spec's initial value is 16.
|
||||||
|
// * The eager binding -> attribute resolve overwrote VERTEX_ATTRIB_ARRAY_STRIDE / _POINTER,
|
||||||
|
// which are legacy state only glVertexAttrib*Pointer may write.
|
||||||
|
// * glVertexAttribDivisor did not re-point the attribute at its own binding point, so a
|
||||||
|
// later resolve restored the old binding's divisor.
|
||||||
|
// * The binding entry points accepted the default vertex array (name 0) in a core profile.
|
||||||
|
//
|
||||||
|
// GPU-free: this is all GL object state, no backend is consulted.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
|
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
||||||
|
#include <MG_State/EGLState/Core.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Impl::GLImpl;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Mirrors the CTS's VertexAttribState: the initial per-attribute state, mutated field by
|
||||||
|
// field as the sequence proceeds, and verified in full after every call.
|
||||||
|
struct AttribState {
|
||||||
|
explicit AttribState(GLuint attribIndex) : index(attribIndex), binding(attribIndex) {}
|
||||||
|
|
||||||
|
GLuint index = 0;
|
||||||
|
GLint enabled = 0;
|
||||||
|
GLint size = 4;
|
||||||
|
GLint stride = 0;
|
||||||
|
GLenum type = GL_FLOAT;
|
||||||
|
GLint normalized = 0;
|
||||||
|
GLint integer = 0;
|
||||||
|
GLint isLong = 0;
|
||||||
|
GLint divisor = 0;
|
||||||
|
GLuint pointer = 0;
|
||||||
|
GLuint bufferBinding = 0;
|
||||||
|
GLuint binding = 0;
|
||||||
|
GLint relativeOffset = 0;
|
||||||
|
|
||||||
|
void Verify(const char* where) const {
|
||||||
|
GLint p = -1;
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &p);
|
||||||
|
EXPECT_EQ(p, enabled) << where << ": ENABLED(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &p);
|
||||||
|
EXPECT_EQ(p, size) << where << ": SIZE(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &p);
|
||||||
|
EXPECT_EQ(p, stride) << where << ": STRIDE(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &p);
|
||||||
|
EXPECT_EQ(static_cast<GLenum>(p), type) << where << ": TYPE(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &p);
|
||||||
|
EXPECT_EQ(p, normalized) << where << ": NORMALIZED(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_INTEGER, &p);
|
||||||
|
EXPECT_EQ(p, integer) << where << ": INTEGER(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_LONG, &p);
|
||||||
|
EXPECT_EQ(p, isLong) << where << ": LONG(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &p);
|
||||||
|
EXPECT_EQ(p, divisor) << where << ": DIVISOR(" << index << ")";
|
||||||
|
void* pp = nullptr;
|
||||||
|
GetVertexAttribPointerv(index, GL_VERTEX_ATTRIB_ARRAY_POINTER, &pp);
|
||||||
|
EXPECT_EQ(reinterpret_cast<uintptr_t>(pp), static_cast<uintptr_t>(pointer))
|
||||||
|
<< where << ": POINTER(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, &p);
|
||||||
|
EXPECT_EQ(static_cast<GLuint>(p), bufferBinding) << where << ": BUFFER_BINDING(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_BINDING, &p);
|
||||||
|
EXPECT_EQ(static_cast<GLuint>(p), binding) << where << ": BINDING(" << index << ")";
|
||||||
|
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &p);
|
||||||
|
EXPECT_EQ(p, relativeOffset) << where << ": RELATIVE_OFFSET(" << index << ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mirrors the CTS's VertexBindingState, initial stride 16 included.
|
||||||
|
struct BindingState {
|
||||||
|
explicit BindingState(GLuint bindingIndex) : index(bindingIndex) {}
|
||||||
|
|
||||||
|
GLuint index = 0;
|
||||||
|
GLuint buffer = 0;
|
||||||
|
GLint offset = 0;
|
||||||
|
GLint stride = 16;
|
||||||
|
GLint divisor = 0;
|
||||||
|
|
||||||
|
void Verify(const char* where) const {
|
||||||
|
GLint p = -1;
|
||||||
|
GetIntegeri_v(GL_VERTEX_BINDING_BUFFER, index, &p);
|
||||||
|
EXPECT_EQ(static_cast<GLuint>(p), buffer) << where << ": VERTEX_BINDING_BUFFER(" << index << ")";
|
||||||
|
// The CTS reads the offset through glGetInteger64i_v; that entry point's pname
|
||||||
|
// routing is a separate defect with its own regression (see the indexed-getter
|
||||||
|
// parity test), so the state model is pinned through the 32-bit view here.
|
||||||
|
GetIntegeri_v(GL_VERTEX_BINDING_OFFSET, index, &p);
|
||||||
|
EXPECT_EQ(p, offset) << where << ": VERTEX_BINDING_OFFSET(" << index << ")";
|
||||||
|
GetIntegeri_v(GL_VERTEX_BINDING_STRIDE, index, &p);
|
||||||
|
EXPECT_EQ(p, stride) << where << ": VERTEX_BINDING_STRIDE(" << index << ")";
|
||||||
|
GetIntegeri_v(GL_VERTEX_BINDING_DIVISOR, index, &p);
|
||||||
|
EXPECT_EQ(p, divisor) << where << ": VERTEX_BINDING_DIVISOR(" << index << ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Strict core rules only apply when the current EGL context explicitly asked for a core
|
||||||
|
// profile; the suite's default (no current context) is relaxed. RAII so a failed
|
||||||
|
// expectation cannot leave the context current for the rest of the binary.
|
||||||
|
struct ScopedCoreProfileContext {
|
||||||
|
ScopedCoreProfileContext() {
|
||||||
|
auto& egl = *MG_State::pEGLContext;
|
||||||
|
m_display = egl.GetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
EXPECT_NE(m_display, EGL_NO_DISPLAY);
|
||||||
|
EXPECT_TRUE(egl.InitializeDisplay(m_display, nullptr, nullptr));
|
||||||
|
EGLint configCount = 0;
|
||||||
|
EXPECT_TRUE(egl.ChooseConfig(m_display, nullptr, &m_config, 1, &configCount));
|
||||||
|
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
|
||||||
|
m_surface = egl.CreatePbufferSurface(m_display, m_config, surfaceAttribs);
|
||||||
|
EXPECT_NE(m_surface, EGL_NO_SURFACE);
|
||||||
|
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
|
||||||
|
3,
|
||||||
|
EGL_CONTEXT_MINOR_VERSION,
|
||||||
|
3,
|
||||||
|
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||||
|
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||||
|
EGL_NONE};
|
||||||
|
m_context = egl.CreateContext(m_display, m_config, EGL_NO_CONTEXT, contextAttribs);
|
||||||
|
EXPECT_NE(m_context, EGL_NO_CONTEXT);
|
||||||
|
EXPECT_TRUE(egl.MakeCurrent(m_display, m_surface, m_surface, m_context));
|
||||||
|
}
|
||||||
|
~ScopedCoreProfileContext() {
|
||||||
|
auto& egl = *MG_State::pEGLContext;
|
||||||
|
egl.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
if (m_context != EGL_NO_CONTEXT) egl.DestroyContext(m_display, m_context);
|
||||||
|
if (m_surface != EGL_NO_SURFACE) egl.DestroySurface(m_display, m_surface);
|
||||||
|
}
|
||||||
|
ScopedCoreProfileContext(const ScopedCoreProfileContext&) = delete;
|
||||||
|
ScopedCoreProfileContext& operator=(const ScopedCoreProfileContext&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
EGLDisplay m_display = EGL_NO_DISPLAY;
|
||||||
|
EGLConfig m_config = nullptr;
|
||||||
|
EGLSurface m_surface = EGL_NO_SURFACE;
|
||||||
|
MG_State::EGLState::EGLContext::EGLContextHandle m_context = EGL_NO_CONTEXT;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VertexAttribBindingStateTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
// A fresh context per case: the state model under test is cumulative, so a leftover
|
||||||
|
// VAO binding from a neighbour would silently change what "default state" means.
|
||||||
|
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||||
|
GenVertexArrays(1, &m_vao);
|
||||||
|
BindVertexArray(m_vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint CreateVbo(GLsizeiptr size) {
|
||||||
|
GLuint vbo = 0;
|
||||||
|
GenBuffers(1, &vbo);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
BufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_COPY);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
return vbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (int i = 0; i < 16 && GetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// basic-state1's opening block: the initial per-attribute mapping and the per-binding-point
|
||||||
|
// defaults, VERTEX_BINDING_STRIDE = 16 included. That check is the FIRST thing the CTS case
|
||||||
|
// does, so a wrong default masked everything the case would have found after it.
|
||||||
|
TEST_F(VertexAttribBindingStateTest, DefaultsMatchTheSpecInitialState) {
|
||||||
|
for (GLuint i = 0; i < 16; ++i) {
|
||||||
|
AttribState(i).Verify("defaults");
|
||||||
|
BindingState(i).Verify("defaults");
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic-state3, verbatim: a full separate-format sequence, then a pointer call, then a
|
||||||
|
// binding update on top of it. The legacy STRIDE/POINTER pair must stay untouched by every
|
||||||
|
// step except the glVertexAttribPointer one, and must survive the binding update after it.
|
||||||
|
TEST_F(VertexAttribBindingStateTest, SeparateFormatSequenceKeepsLegacyStrideAndPointerAtZero) {
|
||||||
|
const GLuint vbo0 = CreateVbo(10000);
|
||||||
|
const GLuint vbo1 = CreateVbo(10000);
|
||||||
|
const GLuint vbo2 = CreateVbo(10000);
|
||||||
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
AttribState va0(0), va2(2), va15(15);
|
||||||
|
BindingState vb0(0), vb2(2), vb15(15);
|
||||||
|
|
||||||
|
VertexAttribFormat(0, 2, GL_BYTE, GL_TRUE, 16);
|
||||||
|
va0.size = 2;
|
||||||
|
va0.type = GL_BYTE;
|
||||||
|
va0.normalized = 1;
|
||||||
|
va0.relativeOffset = 16;
|
||||||
|
va0.Verify("after glVertexAttribFormat");
|
||||||
|
// The format call says nothing about a buffer, so binding point 0 keeps its defaults -
|
||||||
|
// stride 16 among them.
|
||||||
|
vb0.Verify("after glVertexAttribFormat");
|
||||||
|
|
||||||
|
VertexAttribIFormat(2, 3, GL_INT, 512);
|
||||||
|
va2.size = 3;
|
||||||
|
va2.type = GL_INT;
|
||||||
|
va2.integer = 1;
|
||||||
|
va2.relativeOffset = 512;
|
||||||
|
va2.Verify("after glVertexAttribIFormat");
|
||||||
|
vb2.Verify("after glVertexAttribIFormat");
|
||||||
|
|
||||||
|
BindVertexBuffer(0, vbo0, 2048, 128);
|
||||||
|
va0.bufferBinding = vbo0;
|
||||||
|
vb0.buffer = vbo0;
|
||||||
|
vb0.offset = 2048;
|
||||||
|
vb0.stride = 128;
|
||||||
|
va0.Verify("after glBindVertexBuffer(0)");
|
||||||
|
vb0.Verify("after glBindVertexBuffer(0)");
|
||||||
|
|
||||||
|
BindVertexBuffer(2, vbo2, 64, 256);
|
||||||
|
va2.bufferBinding = vbo2;
|
||||||
|
vb2.buffer = vbo2;
|
||||||
|
vb2.offset = 64;
|
||||||
|
vb2.stride = 256;
|
||||||
|
va2.Verify("after glBindVertexBuffer(2)");
|
||||||
|
vb2.Verify("after glBindVertexBuffer(2)");
|
||||||
|
|
||||||
|
// Attribute 2 moves onto binding 0 and takes that binding point's buffer with it.
|
||||||
|
VertexAttribBinding(2, 0);
|
||||||
|
va2.binding = 0;
|
||||||
|
va2.bufferBinding = vbo0;
|
||||||
|
va0.Verify("after glVertexAttribBinding(2,0)");
|
||||||
|
vb0.Verify("after glVertexAttribBinding(2,0)");
|
||||||
|
va2.Verify("after glVertexAttribBinding(2,0)");
|
||||||
|
vb2.Verify("after glVertexAttribBinding(2,0)");
|
||||||
|
|
||||||
|
VertexAttribBinding(0, 15);
|
||||||
|
va0.binding = 15;
|
||||||
|
va0.bufferBinding = 0;
|
||||||
|
va0.Verify("after glVertexAttribBinding(0,15)");
|
||||||
|
vb0.Verify("after glVertexAttribBinding(0,15)");
|
||||||
|
va15.Verify("after glVertexAttribBinding(0,15)");
|
||||||
|
vb15.Verify("after glVertexAttribBinding(0,15)");
|
||||||
|
|
||||||
|
BindVertexBuffer(15, vbo1, 16, 32);
|
||||||
|
va0.bufferBinding = vbo1;
|
||||||
|
va15.bufferBinding = vbo1;
|
||||||
|
vb15.buffer = vbo1;
|
||||||
|
vb15.offset = 16;
|
||||||
|
vb15.stride = 32;
|
||||||
|
va0.Verify("after glBindVertexBuffer(15)");
|
||||||
|
va15.Verify("after glBindVertexBuffer(15)");
|
||||||
|
vb15.Verify("after glBindVertexBuffer(15)");
|
||||||
|
|
||||||
|
// The one call that IS allowed to write the legacy pair - and it also re-points the
|
||||||
|
// attribute at its own binding point and rewrites that binding point.
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, vbo2);
|
||||||
|
VertexAttribPointer(0, 4, GL_UNSIGNED_BYTE, GL_FALSE, 8, reinterpret_cast<const void*>(640));
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
va0.size = 4;
|
||||||
|
va0.type = GL_UNSIGNED_BYTE;
|
||||||
|
va0.stride = 8;
|
||||||
|
va0.pointer = 640;
|
||||||
|
va0.relativeOffset = 0;
|
||||||
|
va0.normalized = 0;
|
||||||
|
va0.binding = 0;
|
||||||
|
va0.bufferBinding = vbo2;
|
||||||
|
vb0.buffer = vbo2;
|
||||||
|
vb0.offset = 640;
|
||||||
|
vb0.stride = 8;
|
||||||
|
va2.bufferBinding = vbo2;
|
||||||
|
va0.Verify("after glVertexAttribPointer");
|
||||||
|
vb0.Verify("after glVertexAttribPointer");
|
||||||
|
va2.Verify("after glVertexAttribPointer");
|
||||||
|
va15.Verify("after glVertexAttribPointer");
|
||||||
|
vb15.Verify("after glVertexAttribPointer");
|
||||||
|
|
||||||
|
// ...and a binding update on top of it leaves the legacy pair exactly where the pointer
|
||||||
|
// call left it. This is the assertion the eager resolve used to fail.
|
||||||
|
BindVertexBuffer(0, vbo1, 80, 24);
|
||||||
|
vb0.buffer = vbo1;
|
||||||
|
vb0.offset = 80;
|
||||||
|
vb0.stride = 24;
|
||||||
|
va0.bufferBinding = vbo1;
|
||||||
|
va2.bufferBinding = vbo1;
|
||||||
|
va0.Verify("after the trailing glBindVertexBuffer(0)");
|
||||||
|
vb0.Verify("after the trailing glBindVertexBuffer(0)");
|
||||||
|
va2.Verify("after the trailing glBindVertexBuffer(0)");
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic-state4: glVertexAttribDivisor is VertexAttribBinding(i,i) + VertexBindingDivisor(i,d),
|
||||||
|
// and glVertexBindingDivisor reaches the attribute's own DIVISOR query either way.
|
||||||
|
TEST_F(VertexAttribBindingStateTest, DivisorGoesThroughTheBindingPoint) {
|
||||||
|
for (GLuint i = 0; i < 16; ++i) {
|
||||||
|
AttribState va(i);
|
||||||
|
BindingState vb(i);
|
||||||
|
VertexAttribDivisor(i, i + 7);
|
||||||
|
va.divisor = static_cast<GLint>(i + 7);
|
||||||
|
vb.divisor = static_cast<GLint>(i + 7);
|
||||||
|
va.Verify("after glVertexAttribDivisor");
|
||||||
|
vb.Verify("after glVertexAttribDivisor");
|
||||||
|
}
|
||||||
|
for (GLuint i = 0; i < 16; ++i) {
|
||||||
|
AttribState va(i);
|
||||||
|
BindingState vb(i);
|
||||||
|
VertexBindingDivisor(i, i);
|
||||||
|
va.divisor = static_cast<GLint>(i);
|
||||||
|
vb.divisor = static_cast<GLint>(i);
|
||||||
|
va.Verify("after glVertexBindingDivisor");
|
||||||
|
vb.Verify("after glVertexBindingDivisor");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute 2 moves onto binding 5 and inherits binding 5's divisor; binding 2 keeps its
|
||||||
|
// own.
|
||||||
|
VertexAttribBinding(2, 5);
|
||||||
|
AttribState va5(5);
|
||||||
|
va5.divisor = 5;
|
||||||
|
BindingState vb5(5);
|
||||||
|
vb5.divisor = 5;
|
||||||
|
AttribState va2(2);
|
||||||
|
va2.divisor = 5;
|
||||||
|
va2.binding = 5;
|
||||||
|
BindingState vb2(2);
|
||||||
|
vb2.divisor = 2;
|
||||||
|
va5.Verify("after glVertexAttribBinding(2,5)");
|
||||||
|
vb5.Verify("after glVertexAttribBinding(2,5)");
|
||||||
|
va2.Verify("after glVertexAttribBinding(2,5)");
|
||||||
|
vb2.Verify("after glVertexAttribBinding(2,5)");
|
||||||
|
|
||||||
|
// ...and glVertexAttribDivisor pulls it back onto binding 2. Guarding the write on
|
||||||
|
// "binding already == index" left the attribute on binding 5 and threw the divisor away.
|
||||||
|
VertexAttribDivisor(2, 23);
|
||||||
|
va2.binding = 2;
|
||||||
|
va2.divisor = 23;
|
||||||
|
vb2.divisor = 23;
|
||||||
|
va5.Verify("after glVertexAttribDivisor(2,23)");
|
||||||
|
vb5.Verify("after glVertexAttribDivisor(2,23)");
|
||||||
|
va2.Verify("after glVertexAttribDivisor(2,23)");
|
||||||
|
vb2.Verify("after glVertexAttribDivisor(2,23)");
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tail of every negative-* case: with the default vertex array bound, a core profile
|
||||||
|
// rejects all four binding entry points.
|
||||||
|
TEST_F(VertexAttribBindingStateTest, BindingApiRejectsTheDefaultVertexArrayInCoreProfile) {
|
||||||
|
ScopedCoreProfileContext coreContext;
|
||||||
|
ASSERT_FALSE(MG_State::IsRelaxedSemanticsActive());
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
BindVertexArray(0);
|
||||||
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
BindVertexBuffer(0, 7, 0, 12);
|
||||||
|
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glBindVertexBuffer";
|
||||||
|
VertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
||||||
|
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribFormat";
|
||||||
|
VertexAttribIFormat(0, 4, GL_INT, 0);
|
||||||
|
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribIFormat";
|
||||||
|
VertexAttribBinding(0, 0);
|
||||||
|
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribBinding";
|
||||||
|
VertexBindingDivisor(0, 1);
|
||||||
|
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexBindingDivisor";
|
||||||
|
|
||||||
|
BindVertexArray(m_vao);
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and the relaxed default - which is what every context that never asked for a core
|
||||||
|
// profile gets - keeps accepting them, because applications depend on it.
|
||||||
|
TEST_F(VertexAttribBindingStateTest, BindingApiStillAcceptsTheDefaultVertexArrayWhenRelaxed) {
|
||||||
|
ASSERT_TRUE(MG_State::IsRelaxedSemanticsActive());
|
||||||
|
const GLuint vbo = CreateVbo(1024);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
BindVertexArray(0);
|
||||||
|
BindVertexBuffer(0, vbo, 0, 12);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glBindVertexBuffer under relaxed semantics";
|
||||||
|
VertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexAttribFormat under relaxed semantics";
|
||||||
|
VertexAttribBinding(0, 0);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexAttribBinding under relaxed semantics";
|
||||||
|
VertexBindingDivisor(0, 1);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexBindingDivisor under relaxed semantics";
|
||||||
|
|
||||||
|
BindVertexArray(m_vao);
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MOBILEGL_RELAXED_SEMANTICS wins even on an explicit core-profile context.
|
||||||
|
TEST_F(VertexAttribBindingStateTest, RelaxedSemanticsOverrideReopensTheDefaultVertexArray) {
|
||||||
|
ScopedCoreProfileContext coreContext;
|
||||||
|
const Bool saved = MG_Config::Features.RelaxedSemantics;
|
||||||
|
MG_Config::Features.RelaxedSemantics = true;
|
||||||
|
const GLuint vbo = CreateVbo(1024);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
BindVertexArray(0);
|
||||||
|
BindVertexBuffer(0, vbo, 0, 12);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
BindVertexArray(m_vao);
|
||||||
|
MG_Config::Features.RelaxedSemantics = saved;
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -10,9 +10,20 @@
|
|||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::BackendLoader {
|
namespace MobileGL::MG_Util::BackendLoader {
|
||||||
namespace {
|
namespace {
|
||||||
|
// A Vulkan limit is an unsigned 32-bit count; a GL limit is a signed Int. Drivers do report
|
||||||
|
// values with the top bit set (UINT32_MAX is the idiomatic "effectively unlimited"), and a
|
||||||
|
// plain static_cast turned those into small negatives - which every downstream std::min or
|
||||||
|
// ceiling comparison then accepted as "already small enough". Saturate instead, so a clamp
|
||||||
|
// above this can be trusted to be the only thing that lowers a limit.
|
||||||
|
Int SaturateToInt(Uint32 value) {
|
||||||
|
constexpr Uint32 kMaxInt = static_cast<Uint32>(std::numeric_limits<Int>::max());
|
||||||
|
return static_cast<Int>(std::min<Uint32>(value, kMaxInt));
|
||||||
|
}
|
||||||
|
|
||||||
struct VulkanDynamicFunctions {
|
struct VulkanDynamicFunctions {
|
||||||
PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties = nullptr;
|
PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties = nullptr;
|
||||||
PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2 = nullptr;
|
PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2 = nullptr;
|
||||||
@@ -152,47 +163,47 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.PointSizeRangeMin = p.limits.pointSizeRange[0];
|
caps.PointSizeRangeMin = p.limits.pointSizeRange[0];
|
||||||
caps.PointSizeRangeMax = p.limits.pointSizeRange[1];
|
caps.PointSizeRangeMax = p.limits.pointSizeRange[1];
|
||||||
caps.PointSizeGranularity = p.limits.pointSizeGranularity;
|
caps.PointSizeGranularity = p.limits.pointSizeGranularity;
|
||||||
caps.Max3DTextureSize = static_cast<Int>(p.limits.maxImageDimension3D);
|
caps.Max3DTextureSize = SaturateToInt(p.limits.maxImageDimension3D);
|
||||||
caps.MaxArrayTextureLayers = static_cast<Int>(p.limits.maxImageArrayLayers);
|
caps.MaxArrayTextureLayers = SaturateToInt(p.limits.maxImageArrayLayers);
|
||||||
caps.MaxCubeMapTextureSize = static_cast<Int>(p.limits.maxImageDimensionCube);
|
caps.MaxCubeMapTextureSize = SaturateToInt(p.limits.maxImageDimensionCube);
|
||||||
caps.MaxFramebufferWidth = static_cast<Int>(p.limits.maxFramebufferWidth);
|
caps.MaxFramebufferWidth = SaturateToInt(p.limits.maxFramebufferWidth);
|
||||||
caps.MaxFramebufferHeight = static_cast<Int>(p.limits.maxFramebufferHeight);
|
caps.MaxFramebufferHeight = SaturateToInt(p.limits.maxFramebufferHeight);
|
||||||
caps.MaxFramebufferLayers = static_cast<Int>(p.limits.maxFramebufferLayers);
|
caps.MaxFramebufferLayers = SaturateToInt(p.limits.maxFramebufferLayers);
|
||||||
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(p.limits);
|
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(p.limits);
|
||||||
caps.MaxTextureSize = static_cast<Int>(p.limits.maxImageDimension2D);
|
caps.MaxTextureSize = SaturateToInt(p.limits.maxImageDimension2D);
|
||||||
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageColorSampleCounts);
|
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageColorSampleCounts);
|
||||||
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageDepthSampleCounts);
|
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageDepthSampleCounts);
|
||||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
|
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
|
||||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
|
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
|
||||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||||
caps.MaxSampleMaskWords = static_cast<Int>(p.limits.maxSampleMaskWords);
|
caps.MaxSampleMaskWords = SaturateToInt(p.limits.maxSampleMaskWords);
|
||||||
caps.MaxTextureImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxVertexTextureImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxVertexTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxComputeTextureImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxComputeTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxCombinedTextureImageUnits = static_cast<Int>(p.limits.maxDescriptorSetSampledImages);
|
caps.MaxCombinedTextureImageUnits = SaturateToInt(p.limits.maxDescriptorSetSampledImages);
|
||||||
caps.MaxVertexAttribs = static_cast<Int>(p.limits.maxVertexInputAttributes);
|
caps.MaxVertexAttribs = SaturateToInt(p.limits.maxVertexInputAttributes);
|
||||||
caps.MaxComputeShaderStorageBlocks = static_cast<Int>(p.limits.maxPerStageDescriptorStorageBuffers);
|
caps.MaxComputeShaderStorageBlocks = SaturateToInt(p.limits.maxPerStageDescriptorStorageBuffers);
|
||||||
caps.MaxCombinedShaderStorageBlocks = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxComputeUniformBlocks = static_cast<Int>(p.limits.maxPerStageDescriptorUniformBuffers);
|
caps.MaxComputeUniformBlocks = SaturateToInt(p.limits.maxPerStageDescriptorUniformBuffers);
|
||||||
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(p.limits.maxComputeWorkGroupInvocations);
|
caps.MaxComputeWorkGroupInvocations = SaturateToInt(p.limits.maxComputeWorkGroupInvocations);
|
||||||
caps.MaxShaderStorageBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxShaderStorageBufferBindings = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxTextureBufferSize = static_cast<Int>(p.limits.maxTexelBufferElements);
|
caps.MaxTextureBufferSize = SaturateToInt(p.limits.maxTexelBufferElements);
|
||||||
caps.TextureBufferOffsetAlignment =
|
caps.TextureBufferOffsetAlignment =
|
||||||
static_cast<Int>(std::max<VkDeviceSize>(1, p.limits.minTexelBufferOffsetAlignment));
|
static_cast<Int>(std::max<VkDeviceSize>(1, p.limits.minTexelBufferOffsetAlignment));
|
||||||
caps.MaxUniformBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetUniformBuffers);
|
caps.MaxUniformBufferBindings = SaturateToInt(p.limits.maxDescriptorSetUniformBuffers);
|
||||||
caps.MaxUniformBlockSize = static_cast<Int>(p.limits.maxUniformBufferRange);
|
caps.MaxUniformBlockSize = SaturateToInt(p.limits.maxUniformBufferRange);
|
||||||
caps.MaxImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxCombinedImageUniforms = static_cast<Int>(p.limits.maxDescriptorSetStorageImages);
|
caps.MaxCombinedImageUniforms = SaturateToInt(p.limits.maxDescriptorSetStorageImages);
|
||||||
caps.MaxComputeImageUniforms = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxComputeImageUniforms = SaturateToInt(p.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxDrawBuffers = static_cast<Int>(p.limits.maxFragmentOutputAttachments);
|
caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments);
|
||||||
caps.MaxColorAttachments = static_cast<Int>(p.limits.maxColorAttachments);
|
caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments);
|
||||||
caps.MaxClipDistances = static_cast<Int>(p.limits.maxClipDistances);
|
caps.MaxClipDistances = SaturateToInt(p.limits.maxClipDistances);
|
||||||
caps.MaxViewports = static_cast<Int>(p.limits.maxViewports);
|
caps.MaxViewports = SaturateToInt(p.limits.maxViewports);
|
||||||
caps.MaxViewportWidth = static_cast<Int>(p.limits.maxViewportDimensions[0]);
|
caps.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]);
|
||||||
caps.MaxViewportHeight = static_cast<Int>(p.limits.maxViewportDimensions[1]);
|
caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]);
|
||||||
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = SaturateToInt(p.limits.viewportSubPixelBits);
|
||||||
FillFragmentInterpolationLimits(caps, p.limits);
|
FillFragmentInterpolationLimits(caps, p.limits);
|
||||||
|
|
||||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||||
@@ -269,47 +280,47 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.PointSizeRangeMin = properties.limits.pointSizeRange[0];
|
caps.PointSizeRangeMin = properties.limits.pointSizeRange[0];
|
||||||
caps.PointSizeRangeMax = properties.limits.pointSizeRange[1];
|
caps.PointSizeRangeMax = properties.limits.pointSizeRange[1];
|
||||||
caps.PointSizeGranularity = properties.limits.pointSizeGranularity;
|
caps.PointSizeGranularity = properties.limits.pointSizeGranularity;
|
||||||
caps.Max3DTextureSize = static_cast<Int>(properties.limits.maxImageDimension3D);
|
caps.Max3DTextureSize = SaturateToInt(properties.limits.maxImageDimension3D);
|
||||||
caps.MaxArrayTextureLayers = static_cast<Int>(properties.limits.maxImageArrayLayers);
|
caps.MaxArrayTextureLayers = SaturateToInt(properties.limits.maxImageArrayLayers);
|
||||||
caps.MaxCubeMapTextureSize = static_cast<Int>(properties.limits.maxImageDimensionCube);
|
caps.MaxCubeMapTextureSize = SaturateToInt(properties.limits.maxImageDimensionCube);
|
||||||
caps.MaxFramebufferWidth = static_cast<Int>(properties.limits.maxFramebufferWidth);
|
caps.MaxFramebufferWidth = SaturateToInt(properties.limits.maxFramebufferWidth);
|
||||||
caps.MaxFramebufferHeight = static_cast<Int>(properties.limits.maxFramebufferHeight);
|
caps.MaxFramebufferHeight = SaturateToInt(properties.limits.maxFramebufferHeight);
|
||||||
caps.MaxFramebufferLayers = static_cast<Int>(properties.limits.maxFramebufferLayers);
|
caps.MaxFramebufferLayers = SaturateToInt(properties.limits.maxFramebufferLayers);
|
||||||
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(properties.limits);
|
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(properties.limits);
|
||||||
caps.MaxTextureSize = static_cast<Int>(properties.limits.maxImageDimension2D);
|
caps.MaxTextureSize = SaturateToInt(properties.limits.maxImageDimension2D);
|
||||||
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageColorSampleCounts);
|
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageColorSampleCounts);
|
||||||
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageDepthSampleCounts);
|
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageDepthSampleCounts);
|
||||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
|
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
|
||||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
|
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
|
||||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||||
caps.MaxSampleMaskWords = static_cast<Int>(properties.limits.maxSampleMaskWords);
|
caps.MaxSampleMaskWords = SaturateToInt(properties.limits.maxSampleMaskWords);
|
||||||
caps.MaxTextureImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxVertexTextureImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxVertexTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxComputeTextureImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxComputeTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxCombinedTextureImageUnits = static_cast<Int>(properties.limits.maxDescriptorSetSampledImages);
|
caps.MaxCombinedTextureImageUnits = SaturateToInt(properties.limits.maxDescriptorSetSampledImages);
|
||||||
caps.MaxVertexAttribs = static_cast<Int>(properties.limits.maxVertexInputAttributes);
|
caps.MaxVertexAttribs = SaturateToInt(properties.limits.maxVertexInputAttributes);
|
||||||
caps.MaxComputeShaderStorageBlocks = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageBuffers);
|
caps.MaxComputeShaderStorageBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorStorageBuffers);
|
||||||
caps.MaxCombinedShaderStorageBlocks = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxComputeUniformBlocks = static_cast<Int>(properties.limits.maxPerStageDescriptorUniformBuffers);
|
caps.MaxComputeUniformBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorUniformBuffers);
|
||||||
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(properties.limits.maxComputeWorkGroupInvocations);
|
caps.MaxComputeWorkGroupInvocations = SaturateToInt(properties.limits.maxComputeWorkGroupInvocations);
|
||||||
caps.MaxShaderStorageBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxShaderStorageBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxTextureBufferSize = static_cast<Int>(properties.limits.maxTexelBufferElements);
|
caps.MaxTextureBufferSize = SaturateToInt(properties.limits.maxTexelBufferElements);
|
||||||
caps.TextureBufferOffsetAlignment =
|
caps.TextureBufferOffsetAlignment =
|
||||||
static_cast<Int>(std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment));
|
static_cast<Int>(std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment));
|
||||||
caps.MaxUniformBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetUniformBuffers);
|
caps.MaxUniformBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetUniformBuffers);
|
||||||
caps.MaxUniformBlockSize = static_cast<Int>(properties.limits.maxUniformBufferRange);
|
caps.MaxUniformBlockSize = SaturateToInt(properties.limits.maxUniformBufferRange);
|
||||||
caps.MaxImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxCombinedImageUniforms = static_cast<Int>(properties.limits.maxDescriptorSetStorageImages);
|
caps.MaxCombinedImageUniforms = SaturateToInt(properties.limits.maxDescriptorSetStorageImages);
|
||||||
caps.MaxComputeImageUniforms = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxComputeImageUniforms = SaturateToInt(properties.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxDrawBuffers = static_cast<Int>(properties.limits.maxFragmentOutputAttachments);
|
caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments);
|
||||||
caps.MaxColorAttachments = static_cast<Int>(properties.limits.maxColorAttachments);
|
caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments);
|
||||||
caps.MaxClipDistances = static_cast<Int>(properties.limits.maxClipDistances);
|
caps.MaxClipDistances = SaturateToInt(properties.limits.maxClipDistances);
|
||||||
caps.MaxViewports = static_cast<Int>(properties.limits.maxViewports);
|
caps.MaxViewports = SaturateToInt(properties.limits.maxViewports);
|
||||||
caps.MaxViewportWidth = static_cast<Int>(properties.limits.maxViewportDimensions[0]);
|
caps.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]);
|
||||||
caps.MaxViewportHeight = static_cast<Int>(properties.limits.maxViewportDimensions[1]);
|
caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]);
|
||||||
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = SaturateToInt(properties.limits.viewportSubPixelBits);
|
||||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||||
caps.SupportsWideLines = false;
|
caps.SupportsWideLines = false;
|
||||||
caps.SupportsShaderFloat64 = false;
|
caps.SupportsShaderFloat64 = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user