mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82244e9048 | ||
|
|
7bd2f08313 | ||
|
|
121b99f8c3 | ||
|
|
e8d79344d6 | ||
|
|
88be35c9ba | ||
|
|
60808b6cf2 | ||
|
|
eb2f14e55a | ||
|
|
0eb5d54bb8 | ||
|
|
6a2e9dc791 | ||
|
|
d5aceebd7b | ||
|
|
473d9951b7 | ||
|
|
13783e3aec | ||
|
|
6bb844b1c1 | ||
|
|
6162603072 | ||
|
|
666f150202 | ||
|
|
2f62970dd5 | ||
|
|
dcb568d445 | ||
|
|
a5f36c8f8d | ||
|
|
1ebe9d11c5 | ||
|
|
a8bb63950d |
@@ -310,6 +310,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
|
||||
@@ -334,6 +335,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
|
||||
@@ -391,6 +393,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||
|
||||
@@ -69,6 +69,13 @@ namespace MobileGL::MG_Config {
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
|
||||
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
|
||||
// there, because the view's ES internalformat is normalized independently of the storage
|
||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||
// so that work can be done without editing the gate.
|
||||
Bool EnableGlesTextureView = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EnableGlesTextureView = QueryEnvFlag("MOBILEGL_ENABLE_GLES_TEXTURE_VIEW");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
|
||||
@@ -749,11 +749,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLVersion = {4, 3, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -777,7 +777,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
|
||||
capabilities.SupportsTextureView);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -990,9 +991,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||
@@ -1017,6 +1024,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// has had the same texture parameter since ES 3.1, which every device MobileGL
|
||||
// runs on provides.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
|
||||
// object-label table are MobileGL's own state, not the host driver's - so it is as
|
||||
// available here as it is on DirectVulkan, which has advertised it all along.
|
||||
E_GL_KHR_debug,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
@@ -1065,6 +1087,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
|
||||
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
|
||||
// over the SAME storage, so that writes through either are visible through the other and
|
||||
// the two carry independent per-texture parameters at the same time - which is exactly
|
||||
// what applications use it for (Better Clouds samples one D24S8 through its own name with
|
||||
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
|
||||
// a single shading pass). A copy-based fallback satisfies neither half, and fails
|
||||
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
|
||||
// the only behaviour that cannot be mistaken for success.
|
||||
//
|
||||
// The host extension is necessary and NOT sufficient, which is why this second gate
|
||||
// exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
|
||||
// KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
|
||||
// view_sampling Fail and view_classes crashes, while only the two pure-API cases
|
||||
// (errors, gettexparameter - neither of which touches the host view) pass. The cause is
|
||||
// known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
|
||||
// VIEW's ES internalformat independently of the storage it aliases, so whenever the two
|
||||
// land on different renderability carriers the host rejects the pair, the error is
|
||||
// swallowed, and the view is left as a storage-less name that samples as zeros.
|
||||
// DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
|
||||
// seam - it passes 5 of the 7 cases on the same device - so the string stays there.
|
||||
//
|
||||
// Until that reconciliation exists, advertising here would be the same lie the comment
|
||||
// above refuses to tell, just with an extra prerequisite met. Set
|
||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
|
||||
extensions.push_back(E_GL_ARB_texture_view);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
// state is accepted regardless, but forwarding it would be a no-op without the extension,
|
||||
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
|
||||
|
||||
@@ -78,11 +78,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -1082,19 +1082,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get());
|
||||
auto& backendObj = backendTextureSlot ? *backendTextureSlot
|
||||
auto& backendSlot = backendTextureSlot ? *backendTextureSlot
|
||||
: g_backendTextureObjects.GetOrCreate(textureObject);
|
||||
if (!backendObj) {
|
||||
backendObj = MakeShared<BackendTextureObject>();
|
||||
if (!backendSlot) {
|
||||
backendSlot = MakeShared<BackendTextureObject>();
|
||||
}
|
||||
|
||||
// A by-VALUE copy of the twin for the duration of the syncs below. `backendSlot` is a
|
||||
// reference INTO the open-addressed registry, and syncing can RE-ENTER this function:
|
||||
// a texture created by glTextureView has to sync the texture whose storage it views
|
||||
// first (SyncTextureViewToBackend), and that nested call may insert, grow the map and
|
||||
// relocate every entry - leaving the reference dangling. Holding the object itself
|
||||
// keeps the calls below working on the right twin regardless; the slot is re-resolved
|
||||
// at the end for the reference this function returns.
|
||||
const SharedPtr<BackendTextureObject> backendObj = backendSlot;
|
||||
|
||||
if (imageBindableStorageRequired) {
|
||||
backendObj->RequireImageBindableStorage(textureObject);
|
||||
}
|
||||
backendObj->SyncTextureParamsToBackend(textureObject);
|
||||
backendObj->SyncBuiltinSamplerToBackend(textureObject);
|
||||
backendObj->SyncMipmapsToBackend(textureObject);
|
||||
// The storage sync may RE-MINT the driver texture - a fresh glTexStorage after a
|
||||
// shape change, an image-bindable widening, or the glTextureView that an
|
||||
// ARB_texture_view view is created on - which discards every parameter the two calls
|
||||
// above just pushed. Re-push them here rather than leaving it to the next sync: the
|
||||
// very next thing that happens is usually the draw this sync was run for, and until
|
||||
// the filters land the new texture is at the ES defaults, which for a single-level or
|
||||
// integer texture is not merely mis-filtered but INCOMPLETE, i.e. it samples zero.
|
||||
if (backendObj->NeedsParameterResync()) {
|
||||
backendObj->SyncTextureParamsToBackend(textureObject);
|
||||
backendObj->SyncBuiltinSamplerToBackend(textureObject);
|
||||
}
|
||||
|
||||
return backendObj;
|
||||
auto* refreshedSlot = g_backendTextureObjects.Find(textureObject.get());
|
||||
auto& refreshedBackendObj = refreshedSlot ? *refreshedSlot
|
||||
: g_backendTextureObjects.GetOrCreate(textureObject);
|
||||
if (!refreshedBackendObj) {
|
||||
// A collection ran during the nested sync and took this slot with it; put the
|
||||
// twin the caller is about to use back, rather than handing back an empty one.
|
||||
refreshedBackendObj = backendObj;
|
||||
}
|
||||
return refreshedBackendObj;
|
||||
}
|
||||
|
||||
// Identity snapshot of what one texture unit has bound: the object in every binding
|
||||
|
||||
@@ -2495,6 +2495,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TextureSwizzleParam::Alpha};
|
||||
m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
||||
m_forceTextureParamsResync = true;
|
||||
// The filter/wrap/LOD cache belongs to the name that just went away, and its gate is
|
||||
// the frontend SAMPLER's version, which a backend re-mint does not move - so without
|
||||
// this the new driver texture keeps the ES defaults for life. See m_forceSamplerResync.
|
||||
m_cacheSamplerParameters = SamplerParameters{};
|
||||
m_forceSamplerResync = true;
|
||||
}
|
||||
|
||||
// Sets the backend GL unpack state to MobileGL's upload default for the scope,
|
||||
@@ -3117,6 +3122,122 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought.
|
||||
// Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability
|
||||
// is the extension AND the pointer, because eglGetProcAddress hands back live-looking
|
||||
// stubs (see AcquireGLESFunctions).
|
||||
static MG_External::GLES::glTextureViewEXT_PTR ResolveTextureViewEntryPoint() {
|
||||
if (g_GLESFuncs.glTextureViewEXT != nullptr) {
|
||||
return g_GLESFuncs.glTextureViewEXT;
|
||||
}
|
||||
return reinterpret_cast<MG_External::GLES::glTextureViewEXT_PTR>(g_GLESFuncs.glTextureViewOES);
|
||||
}
|
||||
|
||||
// Stamps the same per-draw clean-gate keys a completed storage sync stamps, so a view
|
||||
// that needs no work costs the same nothing per draw that any other synced texture does.
|
||||
void BackendTextureObject::StampViewSyncKeys(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (MG_State::pGLContext) {
|
||||
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
||||
}
|
||||
m_syncedContentVersion = stateTextureObject->GetContentVersion();
|
||||
}
|
||||
|
||||
void BackendTextureObject::SyncTextureViewToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
const auto& storageObject = stateTextureObject->GetViewStorageOwner();
|
||||
if (!storageObject) {
|
||||
MGLOG_E_ONCE("Texture %u claims to be a view but names no storage owner.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
if (!g_GLESCapabilities.SupportsTextureView) {
|
||||
// Unreachable through the API: the frontend refuses glTextureView with
|
||||
// GL_INVALID_OPERATION when the backend does not advertise GL_ARB_texture_view,
|
||||
// and DirectGLES only advertises it when this capability is set.
|
||||
MGLOG_E_ONCE("Texture view %u reached the backend on a driver without "
|
||||
"EXT/OES_texture_view.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
// Deliberately a by-VALUE copy of the SharedPtr: SyncTextureObjectToBackend hands back
|
||||
// a reference INTO the open-addressed registry map, and the params/sampler syncs below
|
||||
// (plus any nested growth) can rehash it out from under a reference.
|
||||
const SharedPtr<BackendTextureObject> storageBackendObject =
|
||||
SyncTextureObjectToBackend(storageObject, m_imageBindableStorageRequired);
|
||||
if (!storageBackendObject) {
|
||||
MGLOG_E_ONCE("Failed to sync the storage texture of view %u.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId();
|
||||
if (storageBackendTextureId == 0) {
|
||||
MGLOG_D("Storage texture of view %u has no ES name yet.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) {
|
||||
StampViewSyncKeys(stateTextureObject);
|
||||
return;
|
||||
}
|
||||
// Either the first sync, or the storage was re-minted underneath us. A name that has
|
||||
// already been through glTextureView cannot be viewed again, so start from a fresh
|
||||
// one (this also scrubs the binding caches and bumps the FBO attachment generation).
|
||||
RecreateBackendTexture();
|
||||
|
||||
GLenum glInternalFormat = 0;
|
||||
GLenum glFormat = 0;
|
||||
GLenum glType = 0;
|
||||
TextureImpl::GenerateTextureFormatInfo(stateTextureObject->GetFormat(), &glInternalFormat, &glFormat,
|
||||
&glType, stateTextureObject->GetTarget());
|
||||
const GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
|
||||
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
ResolveTextureViewEntryPoint()(m_backendTextureId, target, storageBackendTextureId, glInternalFormat,
|
||||
stateTextureObject->GetViewMinLevel(),
|
||||
stateTextureObject->GetViewNumLevels(),
|
||||
stateTextureObject->GetViewMinLayer(),
|
||||
stateTextureObject->GetViewNumLayers());
|
||||
const GLenum error = g_GLESFuncs.glGetError();
|
||||
if (error != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("glTextureView(view=%u target=%s origtexture=%u internalformat=%s levels=[%u,%u) "
|
||||
"layers=[%u,%u)) failed: %s",
|
||||
m_backendTextureId, MG_Util::ConvertGLEnumToString(target).c_str(),
|
||||
storageBackendTextureId, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
|
||||
stateTextureObject->GetViewMinLevel(),
|
||||
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
|
||||
stateTextureObject->GetViewMinLayer(),
|
||||
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers(),
|
||||
MG_Util::ConvertGLEnumToString(error).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
m_viewSourceBackendTextureId = storageBackendTextureId;
|
||||
m_isInitialized = true;
|
||||
// A view's storage is immutable by construction (its origtexture had to be), which is
|
||||
// what keeps the respecify paths away from this name.
|
||||
m_backendStorageImmutable = true;
|
||||
const auto baseSize = stateTextureObject->GetBaseSize();
|
||||
m_prevTextureInfo = {stateTextureObject->GetFormat(),
|
||||
static_cast<SizeT>(baseSize.x()),
|
||||
static_cast<SizeT>(baseSize.y()),
|
||||
static_cast<SizeT>(baseSize.z()),
|
||||
static_cast<SizeT>(stateTextureObject->GetViewNumLevels()),
|
||||
0,
|
||||
stateTextureObject->GetSamples(),
|
||||
stateTextureObject->HasFixedSampleLocations()};
|
||||
MGLOG_D("Texture view %u (ES %u) now views storage texture %u (ES %u), levels [%u,%u) layers [%u,%u)",
|
||||
stateTextureObject->GetExternalIndex(), m_backendTextureId, storageObject->GetExternalIndex(),
|
||||
storageBackendTextureId, stateTextureObject->GetViewMinLevel(),
|
||||
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
|
||||
stateTextureObject->GetViewMinLayer(),
|
||||
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers());
|
||||
StampViewSyncKeys(stateTextureObject);
|
||||
}
|
||||
|
||||
void BackendTextureObject::SyncMipmapsToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (!stateTextureObject) {
|
||||
@@ -3124,6 +3245,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
// A texture created by glTextureView owns no storage: the levels, the format and
|
||||
// every texel belong to the texture it views, and this name only has to be made to
|
||||
// ALIAS them. Everything below - storage allocation, respecification, per-level
|
||||
// uploads - would be re-doing the storage texture's work on the wrong name.
|
||||
if (stateTextureObject->IsTextureView()) {
|
||||
SyncTextureViewToBackend(stateTextureObject);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -3977,12 +4107,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
auto* samplerObject = stateTextureObject->GetSamplerObject().get();
|
||||
Uint currentSamplerVersion = samplerObject->GetVersion();
|
||||
if (m_syncedSamplerVersion == currentSamplerVersion) {
|
||||
if (m_syncedSamplerVersion == currentSamplerVersion && !m_forceSamplerResync) {
|
||||
MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
||||
return;
|
||||
}
|
||||
|
||||
m_syncedSamplerVersion = currentSamplerVersion;
|
||||
m_forceSamplerResync = false;
|
||||
|
||||
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
|
||||
m_backendTextureId, stateTextureObject->GetExternalIndex());
|
||||
|
||||
@@ -792,6 +792,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendTextureObject(const BackendTextureObject&) = delete;
|
||||
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// The storage half of the sync for a texture created by glTextureView. Instead of
|
||||
// allocating storage and replaying uploads, it makes this object's ES name BE a view
|
||||
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
|
||||
// two names one image and independent per-texture parameters at the same time. The
|
||||
// parameter and sampler halves are unchanged and run on this name as on any other.
|
||||
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// The storage half of the sync for a texture created by glTextureView. Instead of
|
||||
// allocating storage and replaying uploads, it makes this object's ES name BE a view
|
||||
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
|
||||
// two names one image and independent per-texture parameters at the same time. The
|
||||
// parameter and sampler halves are unchanged and run on this name as on any other.
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
||||
@@ -824,6 +836,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// `contextId`/`samplingGeneration` are the frontend context's current
|
||||
// values, hoisted by the caller so a per-draw list walk reads them once
|
||||
// instead of per texture. `t` must be the live frontend texture.
|
||||
// True while a driver-side re-mint has left the parameter caches describing a texture
|
||||
// that no longer exists; SyncTextureObjectToBackend re-pushes them in the same sync.
|
||||
Bool NeedsParameterResync() const { return m_forceTextureParamsResync || m_forceSamplerResync; }
|
||||
|
||||
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
||||
Uint64 samplingGeneration) const {
|
||||
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
||||
@@ -867,6 +883,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// it is only the IMAGE binding ES cannot spell - and the private name below carries
|
||||
// the split the shader was rewritten against. 0 when this texture takes no split.
|
||||
Uint m_bufferImageSplitViewId = 0;
|
||||
// For a texture created by glTextureView: the ES name of the storage texture this
|
||||
// one was last made a view OF. EXT_texture_view may be called only once per name, so
|
||||
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
|
||||
// detected here and answered with a fresh name for the view as well - otherwise the
|
||||
// view would keep aliasing storage that no longer exists.
|
||||
Uint m_viewSourceBackendTextureId = 0;
|
||||
// For a texture created by glTextureView: the ES name of the storage texture this
|
||||
// one was last made a view OF. EXT_texture_view may be called only once per name, so
|
||||
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
|
||||
// detected here and answered with a fresh name for the view as well - otherwise the
|
||||
// view would keep aliasing storage that no longer exists.
|
||||
// ES context generation the id was created under; a dtor running after
|
||||
// that context died must not delete a foreign (recycled) name.
|
||||
Uint m_contextGeneration = 0;
|
||||
@@ -915,6 +942,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// parameter already pushed onto it: the params-version early-out has to be overridden
|
||||
// once, or an unchanged version would skip the re-push forever.
|
||||
Bool m_forceTextureParamsResync = false;
|
||||
// The same problem for the FILTER state, which lives in m_cacheSamplerParameters and
|
||||
// is gated on the frontend sampler's version rather than on the params version. A
|
||||
// re-mint leaves that cache describing values the new driver texture never received,
|
||||
// and an unchanged sampler version would then skip re-pushing them forever. This
|
||||
// matters more than mis-filtering: ES makes a texture INCOMPLETE when its filters do
|
||||
// not suit its level set (any integer texture with a non-NEAREST filter, or a
|
||||
// single-level texture with a mipmapping filter), and an incomplete texture samples
|
||||
// (0, 0, 0, 1) rather than its contents.
|
||||
Bool m_forceSamplerResync = false;
|
||||
};
|
||||
|
||||
void ActivateTextureUnit(Uint unit);
|
||||
|
||||
@@ -500,7 +500,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.RendererName = "Magma",
|
||||
.BackendName = "Direct (Vulkan)",
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 3, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
@@ -514,7 +514,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
@@ -533,6 +538,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
|
||||
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
|
||||
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
|
||||
// every immutable-storage texture (see the comment there).
|
||||
E_GL_ARB_texture_view,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
|
||||
@@ -2302,6 +2302,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case GL_INT_IMAGE_2D_RECT:
|
||||
case GL_UNSIGNED_INT_IMAGE_2D_RECT:
|
||||
return TextureTarget::TextureRectangle;
|
||||
case GL_SAMPLER_CUBE_MAP_ARRAY:
|
||||
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW:
|
||||
case GL_INT_SAMPLER_CUBE_MAP_ARRAY:
|
||||
case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY:
|
||||
case GL_IMAGE_CUBE_MAP_ARRAY:
|
||||
case GL_INT_IMAGE_CUBE_MAP_ARRAY:
|
||||
case GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY:
|
||||
return TextureTarget::TextureCubeMapArray;
|
||||
case GL_SAMPLER_2D:
|
||||
case GL_SAMPLER_2D_SHADOW:
|
||||
case GL_INT_SAMPLER_2D:
|
||||
|
||||
@@ -47,7 +47,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto attachedTexture = attachment.GetTexture();
|
||||
if (attachedTexture && attachedTexture.get() == &texture) {
|
||||
outAttachment = attachmentType;
|
||||
outLevel = attachment.GetTextureLevel();
|
||||
outLevel = static_cast<Int>(ToStorageMipLevel(attachment.GetTexture().get(),
|
||||
attachment.GetTextureLevel()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -416,16 +417,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
||||
SamplerResolveMemo* viewFormatMemo =
|
||||
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
|
||||
// The format this GL texture presents to the shader. For a texture created by
|
||||
// glTextureView that is the format the VIEW reinterpreted its storage as (GL 4.6 core
|
||||
// 8.18), not the storage image's own - resolving the numeric domain against the latter
|
||||
// would pick a sampled view for a format the shader never declared. The probe is behind
|
||||
// IsTextureView() so nothing about the ordinary per-draw path changes.
|
||||
const VkFormat sampledSourceFormat =
|
||||
texture->IsTextureView()
|
||||
? m_textureManager->ResolveTextureViewWindow(*texture, *resource).format
|
||||
: resource->format;
|
||||
VkFormat sampledViewFormat;
|
||||
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
|
||||
viewFormatMemo->viewFormatSource == resource->format &&
|
||||
viewFormatMemo->viewFormatSource == sampledSourceFormat &&
|
||||
viewFormatMemo->viewFormatDomain == numericDomain) {
|
||||
sampledViewFormat = viewFormatMemo->viewFormat;
|
||||
} else {
|
||||
sampledViewFormat =
|
||||
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
|
||||
VkTextureManager::ResolveSampledImageViewFormat(sampledSourceFormat, numericDomain);
|
||||
if (viewFormatMemo != nullptr) {
|
||||
viewFormatMemo->viewFormatSource = resource->format;
|
||||
viewFormatMemo->viewFormatSource = sampledSourceFormat;
|
||||
viewFormatMemo->viewFormatDomain = numericDomain;
|
||||
viewFormatMemo->viewFormat = sampledViewFormat;
|
||||
viewFormatMemo->viewFormatValid = true;
|
||||
@@ -440,9 +450,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
|
||||
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
|
||||
// already produced instead of re-entering GetOrCreateSampledImageView's sync path. A GL
|
||||
// texture view is excluded because resource->sampledView belongs to the texture it VIEWS
|
||||
// - same image, but the storage texture's level range and depth/stencil aspect, which is
|
||||
// exactly the state a view exists to differ on.
|
||||
const VkImageView sampledImageView =
|
||||
sampledViewFormat == resource->format
|
||||
(!texture->IsTextureView() && sampledViewFormat == resource->format)
|
||||
? resource->sampledView
|
||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||
if (sampledImageView == VK_NULL_HANDLE) {
|
||||
@@ -547,7 +560,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->sampledLevelCount),
|
||||
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
||||
samplerBindingOverride.imageView :
|
||||
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
||||
// Same reason as in ResolveSamplerDescriptor: the resource's own views describe
|
||||
// the storage texture, so a view has to be asked for its own.
|
||||
(samplerBindingOverride.texture->IsTextureView()
|
||||
? m_textureManager->GetOrCreateSampledImageView(*samplerBindingOverride.texture,
|
||||
VK_FORMAT_UNDEFINED)
|
||||
: (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView)),
|
||||
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
|
||||
samplerBindingOverride.imageLayout : resource->layout,
|
||||
};
|
||||
@@ -1020,8 +1038,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
binding);
|
||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
|
||||
// The storage's own VkFormat is the wrong reference for a GL texture view: the view
|
||||
// reinterprets it (GL 4.6 core table 8.21), and it is the VIEW's format the shader's
|
||||
// image declaration was written against. Same correction the sampled path makes above.
|
||||
const VkFormat storageImageSourceFormat =
|
||||
imageBinding.Texture->IsTextureView()
|
||||
? m_textureManager->ResolveTextureViewWindow(*imageBinding.Texture, *resource).format
|
||||
: resource->format;
|
||||
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||
reflectedFormat, imageBinding.Format, storageImageSourceFormat, useBindingFormat);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||
@@ -1029,8 +1054,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
useBindingFormat ? "true" : "false");
|
||||
return false;
|
||||
}
|
||||
// glBindImageTexture named a level and a layer of the bound texture; on a GL texture
|
||||
// view both are relative to the view, and the storage image is what the descriptor
|
||||
// actually points at (see ToStorageMipLevel).
|
||||
const Int32 storageImageLayer =
|
||||
imageBinding.Layered != GL_FALSE
|
||||
? imageBinding.Layer
|
||||
: static_cast<Int32>(ToStorageArrayLayer(imageBinding.Texture.get(), imageBinding.Layer));
|
||||
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||
*imageBinding.Texture, ToStorageMipLevel(imageBinding.Texture.get(), static_cast<Int>(mipLevel)),
|
||||
viewFormat, imageBinding.Layered != GL_FALSE, storageImageLayer);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||
|
||||
@@ -122,8 +122,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &attachment;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
|
||||
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
|
||||
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
|
||||
// by - the storage texture; keying it on the view instead left the clear invisible to every
|
||||
// materialisation done through the parent's name (and vice versa), so the image stayed in
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
|
||||
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
return storageOwner ? storageOwner.get() : texture;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount) {
|
||||
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
|
||||
if (rawTexture != nullptr && texture != rawTexture) {
|
||||
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
|
||||
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
|
||||
// of a view's level 0 would collide with a clear of the storage's level 0 even when
|
||||
// the view opened onto level 1.
|
||||
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
|
||||
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
|
||||
}
|
||||
return PendingClearKey {
|
||||
.texture = texture,
|
||||
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -157,6 +179,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
|
||||
// the storage it views. A clear posted against a view and one posted against its parent
|
||||
// target the same image, so they have to coalesce rather than queue independently.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity {
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -286,9 +317,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(texture.get());
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -305,8 +338,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(attachment);
|
||||
// The alive entry must hold the STORAGE object, because the key names it:
|
||||
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
|
||||
// storage's identity made every lookup of this clear fail that check and silently report
|
||||
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -321,6 +360,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -411,6 +451,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
|
||||
@@ -114,7 +114,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkClearManager {
|
||||
public:
|
||||
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
|
||||
// Resolves a GL texture view to the storage it views before keying; see the definition.
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
Bool Initialize();
|
||||
|
||||
@@ -67,19 +67,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
|
||||
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
|
||||
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
|
||||
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
|
||||
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
|
||||
// they resolve the layer through their own copies of this helper, which do shift.
|
||||
const auto* texture = attachment.GetTexture().get();
|
||||
if (attachment.IsLayered()) {
|
||||
return 0;
|
||||
return ToStorageArrayLayer(texture, 0);
|
||||
}
|
||||
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
||||
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
|
||||
}
|
||||
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
const Int face =
|
||||
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
|
||||
return ToStorageArrayLayer(texture, face);
|
||||
}
|
||||
|
||||
// The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the
|
||||
// state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this
|
||||
// remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a
|
||||
// geometry shader writing gl_Layer = 1..n had its output silently dropped and the parent's
|
||||
// upper layers were never written at all.
|
||||
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
@@ -633,11 +649,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (att.IsTexture()) {
|
||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
const Int textureLevel = att.GetTextureLevel();
|
||||
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
|
||||
att.GetTextureLevel()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
||||
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
const Int textureLayer = att.GetTextureLayer();
|
||||
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
|
||||
att.GetTextureLayer()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
||||
const Bool textureLayered = att.IsLayered();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
||||
@@ -1006,7 +1024,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
|
||||
auto& att = fbo.GetAttachment(drawbuf);
|
||||
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0));
|
||||
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
|
||||
const auto textureTarget = texture->GetTarget();
|
||||
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
||||
attachmentDescriptions.emplace_back();
|
||||
@@ -1049,8 +1067,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
const IntVec2 attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, att.GetSize(), swapchainExtent);
|
||||
// Same remap as ResolveAttachmentLayerCount, for the same reason: a
|
||||
// 1D-array attachment's GL height is its layer count, and using it as the
|
||||
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
|
||||
// image it is built over.
|
||||
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
|
||||
if (width == 0)
|
||||
width = attachmentExtent.x();
|
||||
if (height == 0)
|
||||
@@ -1144,7 +1166,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (a.IsTexture() && b.IsTexture()) {
|
||||
return a.GetTexture().get() == b.GetTexture().get() &&
|
||||
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
|
||||
a.GetTextureLevel() == b.GetTextureLevel();
|
||||
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
|
||||
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
|
||||
}
|
||||
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
|
||||
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
|
||||
@@ -1199,8 +1222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthAttachmentDescription.format = depthTextureResource->format;
|
||||
depthAttachmentSampleCount = depthTextureResource->sampleCount;
|
||||
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
|
||||
attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(),
|
||||
attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo,
|
||||
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
|
||||
swapchainExtent);
|
||||
} else {
|
||||
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
||||
@@ -1254,7 +1278,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
const Uint32 attachmentMipLevel =
|
||||
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0));
|
||||
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
|
||||
selectedDepthStencilAttachment->GetTextureLevel());
|
||||
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
|
||||
|
||||
@@ -220,6 +220,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity(
|
||||
MG_State::GLState::ITextureObject* texture) {
|
||||
// A GL texture view (ARB_texture_view) is identified by the texture whose STORAGE it
|
||||
// views, not by itself. Everything this identity keys - the TextureResource, the tracked
|
||||
// image layout, the alive-object weak reference, the storage-usage marks, the per-draw
|
||||
// sync memos - is a property of the IMAGE, and a view shares that image exactly. Doing
|
||||
// the resolution here rather than at each call site is what makes it impossible to miss
|
||||
// one: a layout update posted against a view's own identity would have found no resource
|
||||
// at all, which is precisely how an attached view came back blank.
|
||||
//
|
||||
// One hop suffices and cannot recurse: glTextureView composes a view-of-a-view onto the
|
||||
// root at creation, so a storage owner is never itself a view.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity{
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -694,6 +710,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
||||
m_viewRequestedImageFlags.erase(identity);
|
||||
m_viewRequestedFormats.erase(identity);
|
||||
auto resourceIt = m_textureResources.find(identity);
|
||||
if (resourceIt != m_textureResources.end()) {
|
||||
DeferResourceRelease(Move(resourceIt->second));
|
||||
@@ -737,9 +755,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_drawSyncedThisDraw.clear();
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
|
||||
|
||||
// A GL texture view has no image of its own; it resolves to - and shares - the resource
|
||||
// of the texture whose storage it views, so that there is exactly one VkImage, one
|
||||
// tracked layout and one upload path per storage. Everything that makes the view a
|
||||
// different texture (format, level/layer window, sampled aspect) is applied where the
|
||||
// VkImageViews are built, keyed in alternateSampledViews / attachmentViews.
|
||||
MG_State::GLState::ITextureObject& texture = StorageTextureOf(textureOrView);
|
||||
if (&texture != &textureOrView) {
|
||||
NoteTextureViewImageRequirements(textureOrView, texture);
|
||||
}
|
||||
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
|
||||
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
|
||||
@@ -838,7 +866,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A GL texture view shares this resource with the texture it views, so it must not touch
|
||||
// perMipViews: that vector is indexed by mip level alone and holds views built with the
|
||||
// STORAGE texture's format and full layer range. Route it through the keyed attachment
|
||||
// cache instead, where its own window is part of the key.
|
||||
if (texture.IsTextureView()) {
|
||||
const TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
return GetOrCreateAttachmentViewAtMipLevel(texture, mipLevel, window.baseArrayLayer, window.layerCount,
|
||||
window.viewType);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -866,7 +906,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 layerCount,
|
||||
VkImageViewType viewType) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// mipLevel and baseArrayLayer arrive in STORAGE space - every caller runs them through
|
||||
// ToStorageMipLevel / ToStorageArrayLayer at the GL attachment boundary. What a GL texture
|
||||
// view still contributes here is its own internal format, which may reinterpret the
|
||||
// storage's (GL 4.6 core table 8.21) and is what the attachment must actually be written
|
||||
// through.
|
||||
VkFormat viewFormatOverride = VK_FORMAT_UNDEFINED;
|
||||
if (texture.IsTextureView()) {
|
||||
viewFormatOverride = ResolveTextureViewWindow(texture, *resource).format;
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
|
||||
@@ -894,10 +946,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
|
||||
const VkFormat baseAttachmentFormat =
|
||||
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
|
||||
const VkFormat attachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(baseAttachmentFormat, framebufferSrgbEnabled);
|
||||
|
||||
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType) {
|
||||
// The shortcut back to the per-mip vector is only sound for the storage texture itself;
|
||||
// for a view every field below is part of what distinguishes it from its parent.
|
||||
if (viewFormatOverride == VK_FORMAT_UNDEFINED && attachmentFormat == resource->format &&
|
||||
baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -932,7 +989,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's
|
||||
// own format and aspect, so a GL view has to go to the keyed cache.
|
||||
if (texture.IsTextureView()) {
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
// Storage space already (see ToStorageMipLevel); only the level COUNT narrows.
|
||||
window.baseMipLevel = mipLevel;
|
||||
window.levelCount = 1;
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -960,14 +1032,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return perMipSampledView;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
|
||||
resource->sampledView == VK_NULL_HANDLE) {
|
||||
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
|
||||
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
|
||||
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
|
||||
// in the same cache while both name the same image, the same levels and the same layers.
|
||||
VkImageView VkTextureManager::GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource,
|
||||
const TextureViewWindow& window) {
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = window.baseMipLevel,
|
||||
.levelCount = window.levelCount,
|
||||
.baseArrayLayer = window.baseArrayLayer,
|
||||
.layerCount = window.layerCount,
|
||||
.viewType = window.viewType,
|
||||
.format = window.format,
|
||||
.aspect = window.sampledAspect,
|
||||
.componentSwizzle = PackComponentSwizzle(window.components),
|
||||
};
|
||||
const auto existing = resource.alternateSampledViews.find(key);
|
||||
if (existing != resource.alternateSampledViews.end()) {
|
||||
return existing->second;
|
||||
}
|
||||
|
||||
if (window.format != resource.format &&
|
||||
(resource.imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs a mutable-format image to be viewed as format=%d "
|
||||
"(image format=%d)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
|
||||
static_cast<Int>(resource.format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const VkImageView view =
|
||||
CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType,
|
||||
window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount,
|
||||
&window.components);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("%s: failed to create sampled view for textureId=%d format=%d aspect=0x%x "
|
||||
"mips=[%u,%u) layers=[%u,%u)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
|
||||
static_cast<Uint32>(window.sampledAspect), window.baseMipLevel,
|
||||
window.baseMipLevel + window.levelCount, window.baseArrayLayer,
|
||||
window.baseArrayLayer + window.layerCount);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
resource.alternateSampledViews.emplace(key, view);
|
||||
return view;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// A GL texture view never has a sampledView of its own on this resource - that one
|
||||
// belongs to the storage texture, with the storage texture's format, level range and
|
||||
// depth/stencil aspect. The window is the view's whole identity, so it always goes to the
|
||||
// keyed cache, even when the requested format happens to match the image's.
|
||||
if (texture.IsTextureView()) {
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
if (format != VK_FORMAT_UNDEFINED) {
|
||||
window.format = format;
|
||||
}
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
|
||||
if (resource->sampledView == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
|
||||
return resource->sampledView;
|
||||
}
|
||||
@@ -987,8 +1121,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = resource->sampledBaseMipLevel,
|
||||
.levelCount = resource->sampledLevelCount,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = resource->arrayLayers,
|
||||
.viewType = resource->viewType,
|
||||
.format = format,
|
||||
.aspect = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.componentSwizzle = PackComponentSwizzle(
|
||||
ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()))),
|
||||
};
|
||||
const auto existing = resource->alternateSampledViews.find(key);
|
||||
if (existing != resource->alternateSampledViews.end()) {
|
||||
@@ -1029,6 +1168,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel, VkFormat format,
|
||||
Bool layered, Int32 layer) {
|
||||
// mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts
|
||||
// the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer.
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
|
||||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
@@ -1053,8 +1194,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = resource->arrayLayers;
|
||||
// A GL texture view opens onto a WINDOW of the storage's layers; a layered image
|
||||
// binding of it must not reach past that window into the parent's other layers.
|
||||
Uint32 baseArrayLayer = ToStorageArrayLayer(&texture, 0);
|
||||
Uint32 layerCount = texture.IsTextureView()
|
||||
? std::min(static_cast<Uint32>(texture.GetViewNumLayers()),
|
||||
resource->arrayLayers - baseArrayLayer)
|
||||
: resource->arrayLayers;
|
||||
VkImageViewType viewType = resource->viewType;
|
||||
if (!layered) {
|
||||
switch (resource->viewType) {
|
||||
@@ -1087,7 +1233,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType;
|
||||
if (format == resource->format && isFullResourceView) {
|
||||
if (format == resource->format && isFullResourceView && !texture.IsTextureView()) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -1613,7 +1759,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool storageUpgradePending =
|
||||
!outResource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||
// Same shape for a GL texture view's demands on the image (MUTABLE_FORMAT for a
|
||||
// format-reinterpreting view, CUBE_COMPATIBLE for a cube view of an array texture):
|
||||
// nothing about the texture itself changed, but the live image cannot carry the view.
|
||||
// Masked by what this format can actually be given: MUTABLE_FORMAT is deliberately
|
||||
// withheld from formats the driver already refused it for (see SyncTextureResource), and
|
||||
// without this mask the "upgrade still pending" test below could never come true again -
|
||||
// costing every later sync of that texture the whole slow path, forever.
|
||||
VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
|
||||
if (m_mutableFormatUnsupported.find(outResource.format) != m_mutableFormatUnsupported.end()) {
|
||||
requestedViewFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
const Bool viewFlagUpgradePending =
|
||||
(outResource.imageCreateFlags & requestedViewFlags) != requestedViewFlags;
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && !viewFlagUpgradePending &&
|
||||
outResource.syncedContentVersion == syncingContentVersion &&
|
||||
outResource.syncedShapeVersion == syncingShapeVersion &&
|
||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||
@@ -1807,6 +1966,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
// Flags a GL texture view over this storage asked for (see NoteTextureViewImageRequirements).
|
||||
// MUTABLE_FORMAT is still withheld from formats the driver has already refused it for, so a
|
||||
// reinterpreting view degrades to no view rather than to no texture.
|
||||
const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
|
||||
if (requestedViewFlags != 0) {
|
||||
imageCreateFlags |= requestedViewFlags;
|
||||
if (m_mutableFormatUnsupported.find(format) != m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
}
|
||||
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
|
||||
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
|
||||
// views - multisample sRGB render targets included.
|
||||
@@ -1967,6 +2136,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
// ...plus every format a glTextureView over this storage reinterprets it as. Those
|
||||
// are NOT enumerable from ResolveSampledImageViewFormat - an application may name any
|
||||
// member of the format's view class (GL 4.6 core table 8.21) - so without this the
|
||||
// list would forbid the very view the MUTABLE_FORMAT bit was requested for.
|
||||
AppendViewRequestedFormats(texture, viewFormats);
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
@@ -2392,6 +2566,164 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredViewReleases[m_currentFrameIndex].push_back(view);
|
||||
}
|
||||
|
||||
MG_State::GLState::ITextureObject& VkTextureManager::StorageTextureOf(
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
const auto& storageOwner = texture.GetViewStorageOwner();
|
||||
return storageOwner ? *storageOwner : texture;
|
||||
}
|
||||
|
||||
// The VkImageViewType a GL texture view's own target asks for. Deliberately derived from the
|
||||
// GL target rather than inherited from the storage image: a 2D view of a 2D-array texture is
|
||||
// a VK_IMAGE_VIEW_TYPE_2D over one layer, and a cube view of the same image is a
|
||||
// VK_IMAGE_VIEW_TYPE_CUBE over six - which is the whole reason table 8.20 lists those pairs.
|
||||
static VkImageViewType ResolveTextureViewImageViewType(TextureTarget target,
|
||||
VkImageViewType storageViewType) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return VK_IMAGE_VIEW_TYPE_1D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return VK_IMAGE_VIEW_TYPE_CUBE;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
default:
|
||||
return storageViewType;
|
||||
}
|
||||
}
|
||||
|
||||
VkTextureManager::TextureViewWindow VkTextureManager::ResolveTextureViewWindow(
|
||||
MG_State::GLState::ITextureObject& texture, const TextureResource& resource) const {
|
||||
TextureViewWindow window{};
|
||||
window.format = resource.format;
|
||||
window.viewType = resource.viewType;
|
||||
window.baseArrayLayer = 0;
|
||||
window.layerCount = resource.arrayLayers;
|
||||
window.sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
|
||||
window.components = ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()));
|
||||
ResolveViewMipRange(texture, resource.mipLevels, window.baseMipLevel, window.levelCount);
|
||||
if (!texture.IsTextureView()) {
|
||||
return window;
|
||||
}
|
||||
|
||||
window.isTextureView = true;
|
||||
// GL 4.6 core 8.18: the view's TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL are relative to the
|
||||
// view, so ResolveViewMipRange above already clamped them against the view's own level
|
||||
// count (TextureObjectView reports it); shifting by TEXTURE_VIEW_MIN_LEVEL puts them back
|
||||
// into the storage image's numbering.
|
||||
window.baseMipLevel += static_cast<Uint32>(texture.GetViewMinLevel());
|
||||
window.baseArrayLayer = static_cast<Uint32>(texture.GetViewMinLayer());
|
||||
window.layerCount = static_cast<Uint32>(texture.GetViewNumLayers());
|
||||
window.viewType = ResolveTextureViewImageViewType(texture.GetTarget(), resource.viewType);
|
||||
// The view's OWN internalformat, which may reinterpret the storage's (table 8.21).
|
||||
const VkFormat viewFormat = ResolveTextureFormatInfo(texture.GetFormat()).format;
|
||||
if (viewFormat != VK_FORMAT_UNDEFINED) {
|
||||
window.format = viewFormat;
|
||||
}
|
||||
// Recomputed against the view's own format: a depth/stencil storage viewed as
|
||||
// depth/stencil still has to honour the VIEW's DEPTH_STENCIL_TEXTURE_MODE, which is the
|
||||
// one parameter Better Clouds deliberately sets differently on the two names.
|
||||
window.sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(GetAspectMaskForFormat(window.format) != VK_IMAGE_ASPECT_NONE
|
||||
? GetAspectMaskForFormat(window.format)
|
||||
: resource.aspect,
|
||||
texture.GetDepthStencilTextureMode());
|
||||
|
||||
// Clamp to what the image actually has; a malformed view must degrade to an empty range
|
||||
// rather than reach vkCreateImageView with an out-of-bounds subresource.
|
||||
if (window.baseMipLevel >= resource.mipLevels) {
|
||||
window.baseMipLevel = resource.mipLevels - 1;
|
||||
window.levelCount = 1;
|
||||
} else {
|
||||
window.levelCount = std::min(window.levelCount, resource.mipLevels - window.baseMipLevel);
|
||||
}
|
||||
if (window.levelCount == 0) window.levelCount = 1;
|
||||
if (window.baseArrayLayer >= resource.arrayLayers) {
|
||||
window.baseArrayLayer = resource.arrayLayers - 1;
|
||||
window.layerCount = 1;
|
||||
} else {
|
||||
window.layerCount = std::min(window.layerCount, resource.arrayLayers - window.baseArrayLayer);
|
||||
}
|
||||
if (window.layerCount == 0) window.layerCount = 1;
|
||||
return window;
|
||||
}
|
||||
|
||||
// The extra VkImageCreateFlags a GL texture view needs on the image it views. Recorded
|
||||
// BEFORE the storage texture is synced (see SyncTextureAndGetDescriptor) so the very first
|
||||
// resolve of a view already creates - or recreates and copies forward - an image the view can
|
||||
// legally be built over, instead of handing back VK_NULL_HANDLE for a frame.
|
||||
void VkTextureManager::NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture) {
|
||||
const TextureIdentity storageIdentity = MakeTextureIdentity(&storageTexture);
|
||||
VkImageCreateFlags required = 0;
|
||||
const VkFormat viewFormat = ResolveTextureFormatInfo(viewTexture.GetFormat()).format;
|
||||
const VkFormat storageFormat = ResolveTextureFormatInfo(storageTexture.GetFormat()).format;
|
||||
if (viewFormat != VK_FORMAT_UNDEFINED && storageFormat != VK_FORMAT_UNDEFINED &&
|
||||
viewFormat != storageFormat) {
|
||||
required |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
// The image may be created with a NARROWED format list (see SyncTextureResource), and
|
||||
// that list is a promise about every format the image will ever be viewed as. Record
|
||||
// this one so the promise stays true.
|
||||
m_viewRequestedFormats[storageIdentity].insert(viewFormat);
|
||||
}
|
||||
const TextureTarget viewTarget = viewTexture.GetTarget();
|
||||
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
|
||||
// Only when the storage could legally carry the bit. VK_IMAGE_CREATE_CUBE_COMPATIBLE
|
||||
// demands a 2D image with square levels and at least six array layers
|
||||
// (VUID-VkImageCreateInfo-flags-00954), and asking for it on a storage that has fewer
|
||||
// would fail vkCreateImage - which, because SyncTextureResource has already released
|
||||
// the old resource by then, would leave the PARENT texture with no image at all. A
|
||||
// degenerate view must not be able to destroy the texture it views; let its own view
|
||||
// creation fail instead.
|
||||
const IntVec3 storageSize = storageTexture.GetBaseSize();
|
||||
const Bool storageCanBeCube = storageSize.x() == storageSize.y() &&
|
||||
storageTexture.GetViewNumLayers() >= 6 &&
|
||||
storageTexture.GetTarget() != TextureTarget::Texture3D;
|
||||
if (storageCanBeCube) {
|
||||
required |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||
} else {
|
||||
MGLOG_W_ONCE("Texture view %d wants a cube view of texture %d, whose storage is %dx%d with %u "
|
||||
"layers and cannot be cube-compatible; the view will have no image view.",
|
||||
viewTexture.GetExternalIndex(), storageTexture.GetExternalIndex(), storageSize.x(),
|
||||
storageSize.y(), storageTexture.GetViewNumLayers());
|
||||
}
|
||||
}
|
||||
if (required == 0) {
|
||||
return;
|
||||
}
|
||||
VkImageCreateFlags& stored = m_viewRequestedImageFlags[storageIdentity];
|
||||
stored |= required;
|
||||
}
|
||||
|
||||
VkImageCreateFlags VkTextureManager::GetViewRequestedImageFlags(
|
||||
const MG_State::GLState::ITextureObject& storageTexture) const {
|
||||
const auto it = m_viewRequestedImageFlags.find(
|
||||
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
|
||||
return it == m_viewRequestedImageFlags.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
void VkTextureManager::AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const {
|
||||
const auto it = m_viewRequestedFormats.find(
|
||||
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
|
||||
if (it == m_viewRequestedFormats.end()) {
|
||||
return;
|
||||
}
|
||||
for (const VkFormat viewFormat : it->second) {
|
||||
if (std::find(outFormats.begin(), outFormats.end(), viewFormat) == outFormats.end()) {
|
||||
outFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) {
|
||||
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE");
|
||||
|
||||
|
||||
@@ -41,6 +41,27 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
|
||||
return glTexelSize;
|
||||
}
|
||||
|
||||
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
|
||||
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
|
||||
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
|
||||
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
|
||||
// storage texture's (VkTextureManager::StorageTextureOf).
|
||||
//
|
||||
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
|
||||
// GetOrCreate*View entry point below expects values that have already been through here, and so
|
||||
// does everything that reads or copies an attachment directly. Both are identity on a plain
|
||||
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
|
||||
// and there is no second, view-only code path to keep in step.
|
||||
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
|
||||
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
|
||||
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
|
||||
}
|
||||
|
||||
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
|
||||
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
|
||||
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
|
||||
}
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||
@@ -139,17 +160,35 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
|
||||
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
|
||||
// through two GL names in one draw, the parent with the stencil aspect and the view with
|
||||
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
|
||||
// same image. Without these two fields those views would alias each other in the cache.
|
||||
struct SampledImageViewKey {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
// GL_TEXTURE_SWIZZLE_* is per-texture state, so two views over one storage with the
|
||||
// same window but different swizzles are different views. Baked into the key because
|
||||
// a GL texture view's ONLY sampled view lives in this cache: unlike the storage
|
||||
// texture's own sampledView, which SyncTextureViews rebuilds whenever the params
|
||||
// version moves, nothing else would ever notice a swizzle change on a view.
|
||||
Uint32 componentSwizzle = 0;
|
||||
|
||||
Bool operator==(const SampledImageViewKey& other) const {
|
||||
return baseMipLevel == other.baseMipLevel &&
|
||||
levelCount == other.levelCount &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
format == other.format;
|
||||
format == other.format &&
|
||||
aspect == other.aspect &&
|
||||
componentSwizzle == other.componentSwizzle;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,10 +196,15 @@ public:
|
||||
SizeT operator()(const SampledImageViewKey& key) const {
|
||||
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
||||
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.componentSwizzle) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -358,6 +402,58 @@ public:
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
|
||||
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
|
||||
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
|
||||
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
|
||||
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
|
||||
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
|
||||
// layout field, so a second resource aliasing the same image would desynchronise the moment
|
||||
// either of them transitioned it.
|
||||
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// The window a GL texture object opens onto its storage image. For a plain texture this is
|
||||
// the resource's own full extent; for a view it is the sub-range, format and aspect
|
||||
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
|
||||
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
|
||||
// the storage texture's own defaults.
|
||||
struct TextureViewWindow {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B,
|
||||
VK_COMPONENT_SWIZZLE_A};
|
||||
Bool isTextureView = false;
|
||||
};
|
||||
|
||||
// The four component swizzles packed into one value, for the sampled-view cache key.
|
||||
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
|
||||
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
|
||||
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
|
||||
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
|
||||
}
|
||||
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
|
||||
const TextureResource& resource) const;
|
||||
// Records what a GL texture view needs of the image it views, so the next sync of the
|
||||
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
|
||||
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
|
||||
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture);
|
||||
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
|
||||
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with.
|
||||
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const;
|
||||
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
|
||||
// image. Shared back end of every GL-texture-view sampled path.
|
||||
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource, const TextureViewWindow& window);
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
@@ -572,6 +668,19 @@ private:
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
|
||||
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
|
||||
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
|
||||
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
|
||||
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
|
||||
// feature almost none of them use. A SAME-format view - which is the common case, and the
|
||||
// Better Clouds case - needs no flag at all and therefore costs nothing.
|
||||
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
|
||||
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
|
||||
// that NO other format will ever be viewed, and building a view outside it is
|
||||
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
|
||||
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
|
||||
@@ -1258,7 +1258,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
return depthAttachment.GetTexture().get() != stencilAttachment.GetTexture().get() ||
|
||||
depthAttachment.GetTextureUploadTarget() != stencilAttachment.GetTextureUploadTarget() ||
|
||||
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
|
||||
ToStorageMipLevel(depthAttachment.GetTexture().get(), depthAttachment.GetTextureLevel()) !=
|
||||
ToStorageMipLevel(stencilAttachment.GetTexture().get(), stencilAttachment.GetTextureLevel());
|
||||
}
|
||||
|
||||
static Bool IsColorAttachment(FramebufferAttachmentType attachmentType) {
|
||||
@@ -1475,11 +1476,15 @@ void main() {
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
||||
if (IsCubeMapFaceUploadTarget(uploadTarget)) {
|
||||
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
// The face index IS the layer index, so it takes the same view shift as one that
|
||||
// arrived through GetTextureLayer (see ToStorageArrayLayer).
|
||||
const Int face = static_cast<Int>(uploadTarget) -
|
||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
|
||||
return ToStorageArrayLayer(attachment.GetTexture().get(), face);
|
||||
}
|
||||
// Every other layered attachment names its layer directly. Returning 0 regardless made
|
||||
// every blit, copy and ReadPixels against such an attachment read layer zero.
|
||||
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
return ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
|
||||
}
|
||||
|
||||
// A 3D image has arrayLayers == 1: its "layer" is a z slice, which has to travel as an
|
||||
@@ -1755,10 +1760,10 @@ void main() {
|
||||
outBinding.sampleCount = resource->sampleCount;
|
||||
const auto attachmentExtent = attachment.GetSize();
|
||||
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
|
||||
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
|
||||
outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
|
||||
outBinding.mipLevelCount = resource->mipLevels;
|
||||
if (AttachmentIsDepthSlice(attachment)) {
|
||||
outBinding.depthOffset = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
|
||||
outBinding.baseArrayLayer = 0;
|
||||
} else {
|
||||
outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
|
||||
@@ -1871,10 +1876,10 @@ void main() {
|
||||
outBinding.sampleCount = resource->sampleCount;
|
||||
const auto attachmentExtent = attachment.GetSize();
|
||||
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
|
||||
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
|
||||
outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
|
||||
outBinding.mipLevelCount = resource->mipLevels;
|
||||
if (AttachmentIsDepthSlice(attachment)) {
|
||||
outBinding.depthOffset = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
|
||||
outBinding.baseArrayLayer = 0;
|
||||
} else {
|
||||
outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
|
||||
@@ -2020,10 +2025,10 @@ void main() {
|
||||
outBinding.sampleCount = resource->sampleCount;
|
||||
const auto attachmentExtent = attachment.GetSize();
|
||||
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
|
||||
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
|
||||
outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
|
||||
outBinding.mipLevelCount = 1;
|
||||
if (AttachmentIsDepthSlice(attachment)) {
|
||||
outBinding.depthOffset = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
|
||||
outBinding.baseArrayLayer = 0;
|
||||
} else {
|
||||
outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
|
||||
@@ -9020,7 +9025,15 @@ void main() {
|
||||
// and an overlap check). Refused outright, and refused for real rather than through an
|
||||
// assertion the release build drops: recording the pair anyway is a validation error and,
|
||||
// on a tiler, a copy whose source has already been overwritten.
|
||||
if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
|
||||
// Compared by STORAGE, not by GL object: a texture view and the texture it views are two
|
||||
// different objects over one VkImage (ARB_texture_view), and GL 4.6 core 8.18 explicitly
|
||||
// permits copying between them - so an object-identity test would let exactly the case
|
||||
// this guard exists for through.
|
||||
const auto* srcStorageTexture =
|
||||
srcEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*srcEndpoint.Texture) : nullptr;
|
||||
const auto* dstStorageTexture =
|
||||
dstEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*dstEndpoint.Texture) : nullptr;
|
||||
if (srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
|
||||
MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
|
||||
CopyImageEndpointName(srcEndpoint));
|
||||
return;
|
||||
@@ -9090,6 +9103,15 @@ void main() {
|
||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
||||
return;
|
||||
}
|
||||
// Storage space from here down. srcImage/dstImage are the STORAGE textures' resources
|
||||
// (SyncTextureAndGetDescriptor resolves a view to the texture it views), while srcLevel /
|
||||
// dstLevel and the z origins below arrived relative to whichever name the application
|
||||
// passed - so a view's level 0 has to become the parent level it opened onto before it
|
||||
// can index a subresource, exactly as at every other attachment boundary.
|
||||
srcLevel = static_cast<GLint>(ToStorageMipLevel(srcEndpoint.Texture.get(), srcLevel));
|
||||
dstLevel = static_cast<GLint>(ToStorageMipLevel(dstEndpoint.Texture.get(), dstLevel));
|
||||
srcZ = static_cast<GLint>(ToStorageArrayLayer(srcEndpoint.Texture.get(), srcZ));
|
||||
dstZ = static_cast<GLint>(ToStorageArrayLayer(dstEndpoint.Texture.get(), dstZ));
|
||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
|
||||
static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
|
||||
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
|
||||
@@ -9828,8 +9850,8 @@ void main() {
|
||||
vkFormat = resource->format;
|
||||
trackedLayout = &resource->layout;
|
||||
imageAspect = resource->aspect;
|
||||
mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
|
||||
baseArrayLayer = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
|
||||
baseArrayLayer = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
|
||||
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
const auto& renderbufferObject = attachment.GetRenderbuffer();
|
||||
const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, renderbufferObject);
|
||||
@@ -10192,10 +10214,14 @@ void main() {
|
||||
textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
const Bool isCubeFace = textureUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
textureUploadTarget <= TextureUploadTarget::CubeMapNegativeZ;
|
||||
const Uint32 arrayLayer = isCubeFace
|
||||
? static_cast<Uint32>(textureUploadTarget) -
|
||||
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
|
||||
// Storage space: `resource` is the storage texture's, so a view's level and
|
||||
// layer have to be shifted into its numbering (see ToStorageMipLevel).
|
||||
const Int glArrayLayer = isCubeFace
|
||||
? static_cast<Int>(textureUploadTarget) -
|
||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX)
|
||||
: 0;
|
||||
const Uint32 arrayLayer = ToStorageArrayLayer(textureObject.get(), glArrayLayer);
|
||||
const Uint32 storageLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
|
||||
// GL wants back - but in Vulkan they are array layers of a one-row image, not
|
||||
// rows of layer 0, so the read has to be told which of the two it is looking at.
|
||||
@@ -10204,7 +10230,7 @@ void main() {
|
||||
? static_cast<Uint32>(std::max<Int>(levelSize.y(), 1))
|
||||
: 1u;
|
||||
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
|
||||
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
|
||||
storageLevel, arrayLayer, 0, 0, levelSize.x(),
|
||||
levelSize.y(), format, type, pixels,
|
||||
/*defaultFramebufferOrientation=*/false, sourceLayers);
|
||||
} else {
|
||||
@@ -10281,13 +10307,15 @@ void main() {
|
||||
frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect,
|
||||
static_cast<Uint32>(level), 1);
|
||||
ToStorageMipLevel(textureObject.get(), level), 1);
|
||||
MOBILEGL_ASSERT(ok, "%s: failed to transition texture image", __func__);
|
||||
|
||||
VkBufferImageCopy copyRegion{};
|
||||
copyRegion.imageSubresource.aspectMask = resource->aspect;
|
||||
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
||||
copyRegion.imageSubresource.baseArrayLayer = 0;
|
||||
// Storage space, as above: a texture view reads its own level 0 out of whichever level
|
||||
// and layer of the parent it opened onto.
|
||||
copyRegion.imageSubresource.mipLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||
copyRegion.imageSubresource.baseArrayLayer = ToStorageArrayLayer(textureObject.get(), 0);
|
||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
||||
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
||||
@@ -10302,7 +10330,7 @@ void main() {
|
||||
frame.commandBuffer, resource->image, resource->layout, originalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, resource->aspect,
|
||||
static_cast<Uint32>(level), 1);
|
||||
ToStorageMipLevel(textureObject.get(), level), 1);
|
||||
MOBILEGL_ASSERT(ok, "%s: failed to restore texture image layout", __func__);
|
||||
|
||||
if (!SubmitReadbackCommandsAndWait(frame)) {
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Debug.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
// Must agree with what GL_Getter answers for GL_MAX_DEBUG_GROUP_STACK_DEPTH and
|
||||
// GL_MAX_DEBUG_MESSAGE_LENGTH / GL_MAX_LABEL_LENGTH; an application that sizes a buffer
|
||||
// off the query and then trips a different limit here would have no way to explain it.
|
||||
constexpr SizeT kMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLsizei kMaxDebugMessageLength = 1024;
|
||||
constexpr GLsizei kMaxLabelLength = 256;
|
||||
|
||||
// The debug state KHR_debug makes per-context. Held here rather than on GLContext because
|
||||
// nothing else in MobileGL reads it, and it is keyed on the context id so a
|
||||
// destroyed-and-recreated context starts with an empty stack and no labels - which the
|
||||
// unit tests, which recreate the context between cases, depend on.
|
||||
struct DebugState {
|
||||
Uint64 contextId = 0;
|
||||
// The messages pushed with glPushDebugGroup, innermost last. The base group GL creates
|
||||
// the context with is implicit and is what makes the reported depth start at 1.
|
||||
Vector<String> groupStack;
|
||||
// Keyed by (identifier, name); see MakeObjectLabelKey.
|
||||
UnorderedMap<Uint64, String> objectLabels;
|
||||
};
|
||||
|
||||
DebugState& State() {
|
||||
static DebugState state;
|
||||
const Uint64 contextId = MG_State::pGLContext ? MG_State::pGLContext->GetTextureContextId() : 0;
|
||||
if (state.contextId != contextId) {
|
||||
state.contextId = contextId;
|
||||
state.groupStack.clear();
|
||||
state.objectLabels.clear();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Uint64 MakeObjectLabelKey(GLenum identifier, GLuint name) {
|
||||
return (static_cast<Uint64>(identifier) << 32) | static_cast<Uint64>(name);
|
||||
}
|
||||
|
||||
void RecordDebugError(ErrorCode code, const char* caller, const String& message) {
|
||||
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
|
||||
}
|
||||
|
||||
// GL 4.6 core 20.2: only an APPLICATION or THIRD_PARTY source may be injected; the rest
|
||||
// are reserved for the implementation itself.
|
||||
Bool ValidateInjectedSource(GLenum source, const char* caller) {
|
||||
if (source == GL_DEBUG_SOURCE_APPLICATION || source == GL_DEBUG_SOURCE_THIRD_PARTY) {
|
||||
return true;
|
||||
}
|
||||
RecordDebugError(ErrorCode::InvalidEnum, caller,
|
||||
std::format("source {} is not GL_DEBUG_SOURCE_APPLICATION or "
|
||||
"GL_DEBUG_SOURCE_THIRD_PARTY.",
|
||||
MG_Util::ConvertGLEnumToString(source)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A negative length means the string is NUL-terminated (GL 4.6 core 20.2), which is how
|
||||
// every one of these entry points spells "just use the whole thing".
|
||||
Bool ValidateDebugStringLength(GLsizei length, const GLchar* text, GLsizei limit, const char* caller,
|
||||
const char* what) {
|
||||
const GLsizei effective =
|
||||
length < 0 ? static_cast<GLsizei>(text != nullptr ? std::strlen(text) : 0) : length;
|
||||
if (effective < limit) {
|
||||
return true;
|
||||
}
|
||||
RecordDebugError(ErrorCode::InvalidValue, caller,
|
||||
std::format("{} length {} is not less than the {} limit of {}.", what, effective, what,
|
||||
limit));
|
||||
return false;
|
||||
}
|
||||
|
||||
String MakeDebugString(GLsizei length, const GLchar* text) {
|
||||
if (text == nullptr) return {};
|
||||
return length < 0 ? String(text) : String(text, static_cast<SizeT>(length));
|
||||
}
|
||||
|
||||
// Whether `name` currently names an object of `identifier`'s type. GL 4.6 core 20.5 makes
|
||||
// labelling something that does not exist INVALID_VALUE, and every type KHR_debug lists
|
||||
// has a frontend name check - so this is answered exactly rather than waved through.
|
||||
// GL_DISPLAY_LIST is deliberately absent: it exists only in the compatibility profile,
|
||||
// which MobileGL does not expose, so it falls to the INVALID_ENUM path below.
|
||||
Bool ValidateLabelledObject(GLenum identifier, GLuint name, Bool& outIdentifierKnown) {
|
||||
outIdentifierKnown = true;
|
||||
auto* context = MG_State::pGLContext.get();
|
||||
switch (identifier) {
|
||||
case GL_BUFFER:
|
||||
return context->ValidateBufferName(name);
|
||||
case GL_SHADER:
|
||||
return context->ValidateShaderName(name);
|
||||
case GL_PROGRAM:
|
||||
return context->ValidateProgramName(name);
|
||||
case GL_VERTEX_ARRAY:
|
||||
return context->ValidateVertexArrayName(name);
|
||||
case GL_QUERY:
|
||||
return IsQuery(name) == GL_TRUE;
|
||||
case GL_PROGRAM_PIPELINE:
|
||||
return context->ValidateProgramPipelineName(name);
|
||||
case GL_TRANSFORM_FEEDBACK:
|
||||
return context->ValidateTransformFeedbackName(name);
|
||||
case GL_SAMPLER:
|
||||
return context->ValidateSamplerName(name);
|
||||
case GL_TEXTURE:
|
||||
return context->ValidateTextureName(name);
|
||||
case GL_RENDERBUFFER:
|
||||
return context->ValidateRenderbufferName(name);
|
||||
case GL_FRAMEBUFFER:
|
||||
// Name 0 is the default framebuffer, which is a real, labellable object.
|
||||
return name == 0 || context->ValidateFramebufferName(name);
|
||||
default:
|
||||
outIdentifierKnown = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLint GetDebugGroupStackDepth() {
|
||||
// GL 4.6 core 20.6: the context is created with one group already on the stack, so the
|
||||
// reported depth is one more than the number of pushes the application has made.
|
||||
return static_cast<GLint>(State().groupStack.size()) + 1;
|
||||
}
|
||||
|
||||
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message) {
|
||||
static_cast<void>(id);
|
||||
if (!ValidateInjectedSource(source, __func__)) return;
|
||||
if (!ValidateDebugStringLength(length, message, kMaxDebugMessageLength, __func__, "message")) return;
|
||||
|
||||
auto& state = State();
|
||||
if (state.groupStack.size() + 1 >= kMaxDebugGroupStackDepth) {
|
||||
// Not INVALID_*: KHR_debug gives the group stack its own error code.
|
||||
RecordDebugError(ErrorCode::StackOverflow, __func__,
|
||||
std::format("the debug group stack is already {} deep, which is its maximum.",
|
||||
kMaxDebugGroupStackDepth));
|
||||
return;
|
||||
}
|
||||
state.groupStack.push_back(MakeDebugString(length, message));
|
||||
MGLOG_D("glPushDebugGroup(%s) -> depth %d", state.groupStack.back().c_str(), GetDebugGroupStackDepth());
|
||||
}
|
||||
|
||||
void PopDebugGroup() {
|
||||
auto& state = State();
|
||||
if (state.groupStack.empty()) {
|
||||
// The base group the context was created with may not be popped (GL 4.6 core 20.6).
|
||||
RecordDebugError(ErrorCode::StackUnderflow, __func__,
|
||||
"the debug group stack holds only the group the context was created with.");
|
||||
return;
|
||||
}
|
||||
MGLOG_D("glPopDebugGroup(%s)", state.groupStack.back().c_str());
|
||||
state.groupStack.pop_back();
|
||||
}
|
||||
|
||||
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
||||
const GLchar* buf) {
|
||||
static_cast<void>(id);
|
||||
if (!ValidateInjectedSource(source, __func__)) return;
|
||||
switch (type) {
|
||||
case GL_DEBUG_TYPE_ERROR:
|
||||
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
|
||||
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
|
||||
case GL_DEBUG_TYPE_PORTABILITY:
|
||||
case GL_DEBUG_TYPE_PERFORMANCE:
|
||||
case GL_DEBUG_TYPE_MARKER:
|
||||
case GL_DEBUG_TYPE_PUSH_GROUP:
|
||||
case GL_DEBUG_TYPE_POP_GROUP:
|
||||
case GL_DEBUG_TYPE_OTHER:
|
||||
break;
|
||||
default:
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("type {} is not a debug message type.",
|
||||
MG_Util::ConvertGLEnumToString(type)));
|
||||
return;
|
||||
}
|
||||
switch (severity) {
|
||||
case GL_DEBUG_SEVERITY_HIGH:
|
||||
case GL_DEBUG_SEVERITY_MEDIUM:
|
||||
case GL_DEBUG_SEVERITY_LOW:
|
||||
case GL_DEBUG_SEVERITY_NOTIFICATION:
|
||||
break;
|
||||
default:
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("severity {} is not a debug message severity.",
|
||||
MG_Util::ConvertGLEnumToString(severity)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateDebugStringLength(length, buf, kMaxDebugMessageLength, __func__, "message")) return;
|
||||
|
||||
// No callback is ever invoked and the message log is empty by construction
|
||||
// (GL_MAX_DEBUG_LOGGED_MESSAGES is 1 and glGetDebugMessageLog returns nothing), so the
|
||||
// application-visible effect is exactly the error checking above. The text still reaches
|
||||
// MobileGL's own log, where it is worth having next to the calls it annotates - at debug
|
||||
// level, so an application that inserts a message per draw costs nothing in a release build.
|
||||
MGLOG_D("glDebugMessageInsert: %s", MakeDebugString(length, buf).c_str());
|
||||
}
|
||||
|
||||
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
|
||||
Bool identifierKnown = false;
|
||||
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
|
||||
if (!identifierKnown) {
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("identifier {} is not a labellable object type.",
|
||||
MG_Util::ConvertGLEnumToString(identifier)));
|
||||
return;
|
||||
}
|
||||
if (!objectExists) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__,
|
||||
std::format("{} {} is not the name of an existing object.",
|
||||
MG_Util::ConvertGLEnumToString(identifier), name));
|
||||
return;
|
||||
}
|
||||
if (!ValidateDebugStringLength(length, label, kMaxLabelLength, __func__, "label")) return;
|
||||
|
||||
auto& labels = State().objectLabels;
|
||||
const Uint64 key = MakeObjectLabelKey(identifier, name);
|
||||
if (label == nullptr) {
|
||||
// GL 4.6 core 20.5: a NULL label removes any label the object had.
|
||||
labels.erase(key);
|
||||
return;
|
||||
}
|
||||
labels[key] = MakeDebugString(length, label);
|
||||
}
|
||||
|
||||
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
|
||||
if (bufSize < 0) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__, "bufSize must not be negative.");
|
||||
return;
|
||||
}
|
||||
Bool identifierKnown = false;
|
||||
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
|
||||
if (!identifierKnown) {
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("identifier {} is not a labellable object type.",
|
||||
MG_Util::ConvertGLEnumToString(identifier)));
|
||||
return;
|
||||
}
|
||||
if (!objectExists) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__,
|
||||
std::format("{} {} is not the name of an existing object.",
|
||||
MG_Util::ConvertGLEnumToString(identifier), name));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& labels = State().objectLabels;
|
||||
const auto it = labels.find(MakeObjectLabelKey(identifier, name));
|
||||
const String& text = it != labels.end() ? it->second : String{};
|
||||
// GL 4.6 core 20.5: the returned length excludes the NUL, and an unlabelled object hands
|
||||
// back an empty string with length 0 rather than an error.
|
||||
SizeT copied = 0;
|
||||
if (label != nullptr && bufSize > 0) {
|
||||
copied = std::min(text.size(), static_cast<SizeT>(bufSize) - 1);
|
||||
std::memcpy(label, text.data(), copied);
|
||||
label[copied] = '\0';
|
||||
}
|
||||
if (length != nullptr) {
|
||||
*length = static_cast<GLsizei>(copied);
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// KHR_debug, core since GL 4.3 (GL 4.6 core 20). Applications use these to annotate a capture
|
||||
// and to name their objects; Better Clouds calls all four for exactly that.
|
||||
//
|
||||
// MobileGL implements the STATE and the ERRORS, and deliberately does not forward the calls to
|
||||
// the host driver. Two independent reasons:
|
||||
//
|
||||
// * glObjectLabel names a FRONTEND object. MobileGL's texture 5 is not the ES driver's
|
||||
// texture 5 (and under DirectVulkan it is not a driver object at all), so forwarding the
|
||||
// pair verbatim would label an unrelated object or a nonexistent one - worse than not
|
||||
// labelling.
|
||||
// * A debug GROUP is only meaningful if it brackets the commands the application issued
|
||||
// inside it. Neither backend emits its work at the moment the GL call arrives: DirectGLES
|
||||
// defers and reorders state sync and uploads around draws, and DirectVulkan is usually not
|
||||
// even recording a command buffer here. A forwarded push/pop would therefore enclose the
|
||||
// wrong commands, which is a misleading capture rather than a helpful one.
|
||||
//
|
||||
// What the application can rely on is the observable contract: the group stack depth is real
|
||||
// (GL_DEBUG_GROUP_STACK_DEPTH tracks it, and over/underflow raise the errors KHR_debug
|
||||
// specifies), and a label written with glObjectLabel comes back from glGetObjectLabel.
|
||||
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
|
||||
void PopDebugGroup();
|
||||
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
||||
const GLchar* buf);
|
||||
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
|
||||
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label);
|
||||
|
||||
// Current depth of the debug group stack, for GL_DEBUG_GROUP_STACK_DEPTH. The base group the
|
||||
// context is created with counts, so this is never below 1 (GL 4.6 core 20.6).
|
||||
GLint GetDebugGroupStackDepth();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -382,6 +382,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.9: every DrawElements-family count is a sizei and "if count is negative, an
|
||||
// INVALID_VALUE error is generated". The same sentence covers instancecount and the
|
||||
// MultiDraw* drawcount, so one helper serves all of them; the parameter is named for the
|
||||
// caller so the message says which argument the application actually got wrong.
|
||||
static Bool ValidateNonNegativeDrawArgument(const char* functionName, const char* argumentName, GLsizei value) {
|
||||
if (value >= 0) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(argumentName) + " must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.9 for DrawRangeElements*: "if end < start, an INVALID_VALUE error is
|
||||
// generated". Both are uints, so a caller that passes -1 for start arrives here as
|
||||
// 0xFFFFFFFF and is caught by the same comparison - which is exactly what
|
||||
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_count_argument checks.
|
||||
static Bool ValidateDrawElementsRange(const char* functionName, GLuint start, GLuint end) {
|
||||
if (end >= start) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "end must not be less than start."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
|
||||
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
|
||||
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
|
||||
@@ -836,6 +861,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
if (!ValidateDrawElementsRange(__func__, start, end)) return;
|
||||
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
@@ -860,6 +888,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "instancecount", instancecount)) return;
|
||||
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
|
||||
@@ -914,6 +945,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||
}
|
||||
@@ -952,6 +985,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
|
||||
// GL 4.6 core 10.5 defines MultiDrawElementsBaseVertex as drawcount separate
|
||||
// DrawElementsBaseVertex calls, so each element of the count array carries the same
|
||||
// non-negative requirement the single-draw entry point applies to its own count. The
|
||||
// whole call is rejected before any sub-draw is issued, which is what makes the error
|
||||
// observable at all - a driver that drew the valid prefix first would leave the
|
||||
// framebuffer half-written.
|
||||
if (count != nullptr) {
|
||||
for (GLsizei draw = 0; draw < drawcount; ++draw) {
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "every element of count", count[draw])) return;
|
||||
}
|
||||
}
|
||||
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../Framebuffer/GL_Framebuffer.h"
|
||||
#include "../VertexArray/GL_VertexArray.h"
|
||||
#include "../Sync/GL_Sync.h"
|
||||
#include "../Debug/GL_Debug.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
@@ -378,27 +379,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexBindingDivisor, GLuint bindingindex, GLuint
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyImageSubData, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopDebugGroup)
|
||||
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
|
||||
(void)identifier;
|
||||
(void)name;
|
||||
(void)length;
|
||||
(void)label;
|
||||
}
|
||||
MOBILEGL_GL_API void glGetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
|
||||
(void)identifier;
|
||||
(void)name;
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
if (label && bufSize > 0) {
|
||||
label[0] = '\0';
|
||||
}
|
||||
}
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PopDebugGroup)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ObjectLabel, GLenum identifier, GLuint name, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ObjectLabel, identifier, name, length, label)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetObjectLabel, GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetObjectLabel, identifier, name, bufSize, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params)
|
||||
@@ -996,7 +983,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirect, GLenum mode, const void*
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cmath>
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
@@ -1427,19 +1428,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: 0;
|
||||
return;
|
||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||
// KHR_debug floors this at 64 even when the group entry points are stubs: the
|
||||
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
|
||||
// KHR_debug floors this at 64. It must agree with what GL_Debug.cpp actually enforces,
|
||||
// or an application that nests to the reported limit would take a STACK_OVERFLOW.
|
||||
*params = kFrontendMaxDebugGroupStackDepth;
|
||||
return;
|
||||
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
||||
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
||||
*params = 1024; // agrees with GL_Debug.cpp's kMaxDebugMessageLength
|
||||
return;
|
||||
case GL_MAX_DEBUG_LOGGED_MESSAGES:
|
||||
// Size of the message log ring; KHR_debug requires at least 1.
|
||||
*params = kFrontendMaxDebugLoggedMessages;
|
||||
return;
|
||||
case GL_DEBUG_GROUP_STACK_DEPTH:
|
||||
*params = 0; // debug-group entrypoints are stubbed
|
||||
// The live depth, which is never 0: GL 4.6 core 20.6 creates the context with one
|
||||
// group already on the stack, and that is the one glPopDebugGroup may not pop.
|
||||
*params = GetDebugGroupStackDepth();
|
||||
return;
|
||||
case GL_CONTEXT_FLAGS: {
|
||||
*params = MG_State::pEGLContext ? MG_State::pEGLContext->GetCurrentContextFlags() : 0;
|
||||
|
||||
@@ -186,6 +186,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return TextureImpl::ValidateTextureInternalFormat(textureInternalFormat);
|
||||
}
|
||||
|
||||
// How many LAYERS a texture of this target has, given its base level's state-side extent.
|
||||
// GL keeps a 1D array's layer count in the height and every other layered target's in the
|
||||
// depth; a cube map has exactly six and a 3D texture has one (its depth is spatial).
|
||||
Uint LayerCountOfImmutableTexture(TextureTarget target, const IntVec3& baseSize) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1DArray:
|
||||
return static_cast<Uint>(std::max(baseSize.y(), 1));
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return static_cast<Uint>(std::max(baseSize.z(), 1));
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return 6;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.19: TexStorage* leaves the texture describing itself as a full-extent view
|
||||
// of its own storage - TEXTURE_VIEW_MIN_LEVEL 0, TEXTURE_VIEW_NUM_LEVELS <levels>,
|
||||
// TEXTURE_VIEW_MIN_LAYER 0, TEXTURE_VIEW_NUM_LAYERS the layer count. That is not just a
|
||||
// query detail: glTextureView COMPOSES onto these ("<numlevels> and the value of
|
||||
// TEXTURE_VIEW_NUM_LEVELS from the original texture minus <minlevel>", 8.18), so leaving
|
||||
// them at the mutable-texture default of 0 would clamp every view to zero levels.
|
||||
void SeedImmutableViewState(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Uint levels) {
|
||||
if (!textureObject) return;
|
||||
textureObject->SetViewLevelLayerRange(
|
||||
0, levels, 0,
|
||||
LayerCountOfImmutableTexture(textureObject->GetTarget(), textureObject->GetBaseSize()));
|
||||
}
|
||||
|
||||
Bool ValidateTextureMutable(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller) {
|
||||
if (!textureObject || !textureObject->IsImmutable()) return true;
|
||||
@@ -1373,15 +1404,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
|
||||
*params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE;
|
||||
break;
|
||||
// Texture views are not implemented; a texture that is not a view reports the defaults
|
||||
// GL 4.6 core table 23.17 gives (0 layers/levels of offset, and its own extent).
|
||||
// GL 4.6 core table 23.17. All four start at 0 and stay there on a mutable texture;
|
||||
// TexStorage* seeds them with the texture's full extent and glTextureView composes onto
|
||||
// that (see SeedImmutableViewState and TextureView).
|
||||
case GL_TEXTURE_VIEW_MIN_LEVEL:
|
||||
*params = static_cast<GLint>(textureObject->GetViewMinLevel());
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_MIN_LAYER:
|
||||
*params = 0;
|
||||
*params = static_cast<GLint>(textureObject->GetViewMinLayer());
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_NUM_LEVELS:
|
||||
*params = static_cast<GLint>(textureObject->GetViewNumLevels());
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_NUM_LAYERS:
|
||||
*params = 0;
|
||||
*params = static_cast<GLint>(textureObject->GetViewNumLayers());
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -2845,6 +2881,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(textureObject->GetImmutableLevels());
|
||||
}
|
||||
break;
|
||||
// GL 4.6 core table 23.17. Zero on a mutable texture; TexStorage* seeds the full extent
|
||||
// and glTextureView composes onto it (SeedImmutableViewState / TextureView).
|
||||
case GL_TEXTURE_VIEW_MIN_LEVEL:
|
||||
if (params) {
|
||||
*params = static_cast<GLint>(textureObject->GetViewMinLevel());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_NUM_LEVELS:
|
||||
if (params) {
|
||||
*params = static_cast<GLint>(textureObject->GetViewNumLevels());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_MIN_LAYER:
|
||||
if (params) {
|
||||
*params = static_cast<GLint>(textureObject->GetViewMinLayer());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_NUM_LAYERS:
|
||||
if (params) {
|
||||
*params = static_cast<GLint>(textureObject->GetViewNumLayers());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_BORDER_COLOR:
|
||||
if (params) {
|
||||
const auto& borderColor = textureObject->GetBorderColor();
|
||||
@@ -3003,6 +3061,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLfloat>(textureObject->GetImmutableLevels());
|
||||
}
|
||||
break;
|
||||
// GL 4.6 core table 23.17; the float form answers the same state as the integer one
|
||||
// (KHR-GL43.texture_view.gettexparameter queries both).
|
||||
case GL_TEXTURE_VIEW_MIN_LEVEL:
|
||||
if (params) {
|
||||
*params = static_cast<GLfloat>(textureObject->GetViewMinLevel());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_NUM_LEVELS:
|
||||
if (params) {
|
||||
*params = static_cast<GLfloat>(textureObject->GetViewNumLevels());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_MIN_LAYER:
|
||||
if (params) {
|
||||
*params = static_cast<GLfloat>(textureObject->GetViewMinLayer());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_VIEW_NUM_LAYERS:
|
||||
if (params) {
|
||||
*params = static_cast<GLfloat>(textureObject->GetViewNumLayers());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_BORDER_COLOR:
|
||||
if (params) {
|
||||
const auto& borderColor = textureObject->GetBorderColor();
|
||||
@@ -4779,6 +4859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// longer pre-existing chain has to be dropped explicitly.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
void TextureStorage2D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) {
|
||||
@@ -4830,7 +4911,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (const auto uploadTarget : textureObject->GetUploadTargets()) {
|
||||
for (GLsizei level = 0; level < levels; ++level) {
|
||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
||||
// GL 4.6 core 8.19: for GL_TEXTURE_1D_ARRAY the state-side HEIGHT is the LAYER
|
||||
// COUNT, and layers do not halve down the mip chain - level i is
|
||||
// (max(1, width >> i), height). Shrinking it made every mipmapped 1D array
|
||||
// level report fewer layers than it has.
|
||||
const Bool heightIsLayerCount = textureObject->GetTarget() == TextureTarget::Texture1DArray;
|
||||
const GLsizei levelHeight =
|
||||
heightIsLayerCount ? height : std::max<GLsizei>(1, height >> level);
|
||||
const SizeT byteSize =
|
||||
static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
|
||||
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||
@@ -4853,6 +4940,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
||||
}
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
@@ -4927,6 +5015,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
// Shared front half of glTextureStorage2DMultisample/3DMultisample. The target forms are reached
|
||||
@@ -5007,6 +5096,211 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
void RecordTextureViewError(ErrorCode code, const String& message) {
|
||||
MG_State::pGLContext->RecordError(code,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "TextureView", message));
|
||||
}
|
||||
|
||||
// The internalformat the view-compatibility rule has to compare against, which is NOT
|
||||
// always ConvertTextureInternalFormatToGLEnum(GetFormat()): MobileGL answers every
|
||||
// compressed request with uncompressed storage and only remembers the requested enum on
|
||||
// the side, so a BPTC parent would otherwise present itself as RGBA8 and admit an RGBA8
|
||||
// view that table 8.21 forbids.
|
||||
GLenum ResolveTextureViewSourceFormat(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (mipmapTexture != nullptr && !textureObject->GetUploadTargets().empty()) {
|
||||
const TextureUploadTarget uploadTarget = textureObject->GetUploadTargets()[0];
|
||||
const GLenum stored = mipmapTexture->GetMipmapCompressedFormat(uploadTarget, 0);
|
||||
if (stored != GL_NONE) return stored;
|
||||
const GLenum requested = mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTarget, 0);
|
||||
if (requested != GL_NONE) return requested;
|
||||
}
|
||||
return MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
|
||||
}
|
||||
|
||||
Bool BackendSupportsTextureViews() {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) return false;
|
||||
// Deliberately the ADVERTISED extension list rather than a separate capability bit:
|
||||
// it makes "MobileGL claims GL_ARB_texture_view" and "glTextureView actually works"
|
||||
// the same fact by construction. DirectVulkan always advertises it; DirectGLES only
|
||||
// does when the driver has EXT/OES_texture_view, because ES cannot otherwise give two
|
||||
// texture names one storage (see the no-EXT discussion in BackendObject_DirectGLES).
|
||||
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
|
||||
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_texture_view) != extensions.end();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// glTextureView - ARB_texture_view, core since GL 4.3 (GL 4.6 core 8.18).
|
||||
//
|
||||
// Creates a texture whose STORAGE is another texture's, optionally reinterpreting the format
|
||||
// and narrowing the level/layer range. The error list below is the spec's, in the order the
|
||||
// conformance suite (KHR-GL43.texture_view.errors, cases a..s) walks it.
|
||||
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
|
||||
GLuint numlevels, GLuint minlayer, GLuint numlayers) {
|
||||
if (!BackendSupportsTextureViews()) {
|
||||
// The honest answer when the backend cannot share one storage between two texture
|
||||
// names. Raising an error - and withholding the GL_ARB_texture_view string - is the
|
||||
// only alternative to a silent no-op that leaves the view with no storage at all,
|
||||
// which is indistinguishable from success at the call site and renders garbage.
|
||||
MGLOG_W_ONCE("glTextureView: the active backend has no texture-view support "
|
||||
"(GL_EXT_texture_view / GL_OES_texture_view absent); raising GL_INVALID_OPERATION");
|
||||
RecordTextureViewError(ErrorCode::InvalidOperation,
|
||||
"The active backend does not support texture views.");
|
||||
return;
|
||||
}
|
||||
|
||||
const TextureTarget viewTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
if (!TextureImpl::ValidateTextureTarget(viewTarget)) return;
|
||||
|
||||
// a) <texture> is 0.
|
||||
if (texture == 0) {
|
||||
RecordTextureViewError(ErrorCode::InvalidValue, "texture must not be zero.");
|
||||
return;
|
||||
}
|
||||
// b) <texture> is not a name returned by glGenTextures.
|
||||
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
|
||||
RecordTextureViewError(ErrorCode::InvalidOperation,
|
||||
std::format("texture {} is not a name returned by glGenTextures.", texture));
|
||||
return;
|
||||
}
|
||||
// c) <texture> has already been bound and given a target. A name that any bind (or
|
||||
// glCreateTextures, or an earlier glTextureView) has instantiated owns a texture object;
|
||||
// only a still-uninstantiated reservation may become a view.
|
||||
if (MG_State::pGLContext->ValidateTextureObject(texture)) {
|
||||
RecordTextureViewError(ErrorCode::InvalidOperation,
|
||||
std::format("texture {} has already been bound and given a target.", texture));
|
||||
return;
|
||||
}
|
||||
// d) <origtexture> is not the name of a texture object. Note the error code differs from
|
||||
// (b): INVALID_VALUE here, INVALID_OPERATION there.
|
||||
auto origTextureObject = MG_State::pGLContext->GetTextureObject(origtexture);
|
||||
if (origtexture == 0 || !origTextureObject) {
|
||||
RecordTextureViewError(ErrorCode::InvalidValue,
|
||||
std::format("origtexture {} is not the name of a texture object.", origtexture));
|
||||
return;
|
||||
}
|
||||
// e) <origtexture> is a mutable texture object. A view aliases storage that can never be
|
||||
// respecified underneath it, so only immutable storage qualifies.
|
||||
if (!origTextureObject->IsImmutable()) {
|
||||
RecordTextureViewError(ErrorCode::InvalidOperation,
|
||||
std::format("origtexture {} does not have immutable storage.", origtexture));
|
||||
return;
|
||||
}
|
||||
// f) target is incompatible with origtexture's target (table 8.20).
|
||||
const TextureTarget origTarget = origTextureObject->GetTarget();
|
||||
if (!TextureImpl::IsLegalTextureViewTargetPair(origTarget, viewTarget)) {
|
||||
RecordTextureViewError(
|
||||
ErrorCode::InvalidOperation,
|
||||
std::format("target {} is not a legal texture-view target for an origtexture whose target is {}.",
|
||||
MG_Util::ConvertGLEnumToString(target),
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertTextureTargetToGLEnum(origTarget))));
|
||||
return;
|
||||
}
|
||||
// k)..q) the per-target <numlayers> constraints, all INVALID_VALUE.
|
||||
const Uint requiredLayers = TextureImpl::RequiredTextureViewLayerCount(viewTarget);
|
||||
if (requiredLayers != 0 && numlayers != requiredLayers) {
|
||||
RecordTextureViewError(ErrorCode::InvalidValue,
|
||||
std::format("target {} requires numlayers to be {}, but it is {}.",
|
||||
MG_Util::ConvertGLEnumToString(target), requiredLayers, numlayers));
|
||||
return;
|
||||
}
|
||||
if (viewTarget == TextureTarget::TextureCubeMapArray && (numlayers == 0 || numlayers % 6 != 0)) {
|
||||
RecordTextureViewError(
|
||||
ErrorCode::InvalidValue,
|
||||
std::format("GL_TEXTURE_CUBE_MAP_ARRAY requires numlayers to be a multiple of 6, but it is {}.",
|
||||
numlayers));
|
||||
return;
|
||||
}
|
||||
// g)/h) the format-compatibility rule (table 8.21). A format WITH a view class may be
|
||||
// reinterpreted as any other format in the same class; a format with NO entry in the
|
||||
// table - every depth, stencil and depth/stencil format among them - may only ever be
|
||||
// viewed as itself, which is why the Better Clouds D24S8 view must name
|
||||
// GL_DEPTH24_STENCIL8 exactly.
|
||||
const GLenum origFormat = ResolveTextureViewSourceFormat(origTextureObject);
|
||||
const auto origViewClass = TextureImpl::GetTextureViewClass(origFormat);
|
||||
if (origViewClass == TextureImpl::TextureViewClass::None) {
|
||||
if (internalformat != origFormat) {
|
||||
RecordTextureViewError(
|
||||
ErrorCode::InvalidOperation,
|
||||
std::format("origtexture's internal format {} has no view class, so internalformat must be "
|
||||
"identical to it, but it is {}.",
|
||||
MG_Util::ConvertGLEnumToString(origFormat),
|
||||
MG_Util::ConvertGLEnumToString(internalformat)));
|
||||
return;
|
||||
}
|
||||
} else if (TextureImpl::GetTextureViewClass(internalformat) != origViewClass) {
|
||||
RecordTextureViewError(
|
||||
ErrorCode::InvalidOperation,
|
||||
std::format("internalformat {} is not in the same view class as origtexture's internal format {}.",
|
||||
MG_Util::ConvertGLEnumToString(internalformat),
|
||||
MG_Util::ConvertGLEnumToString(origFormat)));
|
||||
return;
|
||||
}
|
||||
const TextureInternalFormat viewInternalFormat =
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(viewInternalFormat)) return;
|
||||
|
||||
// i)/j) the range checks, both against the ORIGINAL's view state rather than its raw
|
||||
// level/layer counts. On a plain immutable texture TexStorage* seeded those with the full
|
||||
// extent, so the two agree; on a view-of-a-view they are what bounds the child to the
|
||||
// parent's already-narrowed window.
|
||||
const Uint origNumLevels = origTextureObject->GetViewNumLevels();
|
||||
const Uint origNumLayers = origTextureObject->GetViewNumLayers();
|
||||
if (minlevel >= origNumLevels) {
|
||||
RecordTextureViewError(ErrorCode::InvalidValue,
|
||||
std::format("minlevel {} is larger than origtexture's greatest level {}.", minlevel,
|
||||
origNumLevels == 0 ? 0 : origNumLevels - 1));
|
||||
return;
|
||||
}
|
||||
if (minlayer >= origNumLayers) {
|
||||
RecordTextureViewError(ErrorCode::InvalidValue,
|
||||
std::format("minlayer {} is larger than origtexture's greatest layer {}.", minlayer,
|
||||
origNumLayers == 0 ? 0 : origNumLayers - 1));
|
||||
return;
|
||||
}
|
||||
// r)/s) a cube-map or cube-map-array view demands square levels, because its faces are
|
||||
// square by definition and the storage it borrows is not reshaped.
|
||||
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
|
||||
const IntVec3 baseSize = origTextureObject->GetBaseSize();
|
||||
if (baseSize.x() != baseSize.y()) {
|
||||
RecordTextureViewError(
|
||||
ErrorCode::InvalidOperation,
|
||||
std::format("a cube-map texture view requires origtexture's width and height to match, but "
|
||||
"they are {}x{}.",
|
||||
baseSize.x(), baseSize.y()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.18, verbatim:
|
||||
// TEXTURE_VIEW_MIN_LEVEL = <minlevel> + origtexture's TEXTURE_VIEW_MIN_LEVEL
|
||||
// TEXTURE_VIEW_NUM_LEVELS = min(<numlevels>, origtexture's TEXTURE_VIEW_NUM_LEVELS - <minlevel>)
|
||||
// TEXTURE_VIEW_MIN_LAYER = <minlayer> + origtexture's TEXTURE_VIEW_MIN_LAYER
|
||||
// TEXTURE_VIEW_NUM_LAYERS = min(<numlayers>, origtexture's TEXTURE_VIEW_NUM_LAYERS - <minlayer>)
|
||||
// Because the offsets ADD all the way down, the composed values are already expressed in
|
||||
// the ROOT's coordinates - which is exactly what lets the view point straight at the root
|
||||
// and skip the chain.
|
||||
const auto& storageOwner =
|
||||
origTextureObject->IsTextureView() ? origTextureObject->GetViewStorageOwner() : origTextureObject;
|
||||
const Uint composedMinLevel = minlevel + origTextureObject->GetViewMinLevel();
|
||||
const Uint composedNumLevels = std::min(numlevels, origNumLevels - minlevel);
|
||||
const Uint composedMinLayer = minlayer + origTextureObject->GetViewMinLayer();
|
||||
const Uint composedNumLayers = std::min(numlayers, origNumLayers - minlayer);
|
||||
|
||||
const auto& viewObject = MG_State::pGLContext->CreateTextureViewObject(
|
||||
texture, viewTarget, storageOwner, composedMinLevel, composedNumLevels, composedMinLayer,
|
||||
composedNumLayers);
|
||||
if (!viewObject) {
|
||||
RecordTextureViewError(ErrorCode::InvalidOperation, "Failed to create the texture view object.");
|
||||
return;
|
||||
}
|
||||
viewObject->SetInternalFormat(viewInternalFormat);
|
||||
viewObject->SetSamples(storageOwner->GetSamples());
|
||||
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
|
||||
}
|
||||
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
|
||||
@@ -5112,6 +5406,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
|
||||
if (!textureObject) return;
|
||||
textureObject->SetImmutableLevels(1);
|
||||
SeedImmutableViewState(textureObject, 1);
|
||||
}
|
||||
|
||||
void TexStorage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
|
||||
|
||||
@@ -60,6 +60,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
|
||||
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
|
||||
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
|
||||
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
|
||||
GLuint numlevels, GLuint minlayer, GLuint numlayers);
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
|
||||
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
|
||||
@@ -623,4 +623,144 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core table 8.21 ("Compatible internal formats for TextureView"), transcribed whole.
|
||||
// Written against the raw GLenum rather than TextureInternalFormat on purpose: MobileGL's own
|
||||
// enum collapses every compressed format onto uncompressed storage and drops formats it
|
||||
// cannot carry, so classifying the converted value would silently widen the compatibility
|
||||
// rule - GL_COMPRESSED_RG_RGTC2 and GL_RGBA8 would end up in the same class.
|
||||
TextureViewClass GetTextureViewClass(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case GL_RGBA32F:
|
||||
case GL_RGBA32UI:
|
||||
case GL_RGBA32I:
|
||||
return TextureViewClass::Bits128;
|
||||
case GL_RGB32F:
|
||||
case GL_RGB32UI:
|
||||
case GL_RGB32I:
|
||||
return TextureViewClass::Bits96;
|
||||
case GL_RGBA16F:
|
||||
case GL_RG32F:
|
||||
case GL_RGBA16UI:
|
||||
case GL_RG32UI:
|
||||
case GL_RGBA16I:
|
||||
case GL_RG32I:
|
||||
case GL_RGBA16:
|
||||
case GL_RGBA16_SNORM:
|
||||
return TextureViewClass::Bits64;
|
||||
case GL_RGB16:
|
||||
case GL_RGB16_SNORM:
|
||||
case GL_RGB16F:
|
||||
case GL_RGB16UI:
|
||||
case GL_RGB16I:
|
||||
return TextureViewClass::Bits48;
|
||||
case GL_RG16F:
|
||||
case GL_R11F_G11F_B10F:
|
||||
case GL_R32F:
|
||||
case GL_RGB10_A2UI:
|
||||
case GL_RGBA8UI:
|
||||
case GL_RG16UI:
|
||||
case GL_R32UI:
|
||||
case GL_RGBA8I:
|
||||
case GL_RG16I:
|
||||
case GL_R32I:
|
||||
case GL_RGB10_A2:
|
||||
case GL_RGBA8:
|
||||
case GL_RG16:
|
||||
case GL_RGBA8_SNORM:
|
||||
case GL_RG16_SNORM:
|
||||
case GL_SRGB8_ALPHA8:
|
||||
case GL_RGB9_E5:
|
||||
return TextureViewClass::Bits32;
|
||||
case GL_RGB8:
|
||||
case GL_RGB8_SNORM:
|
||||
case GL_SRGB8:
|
||||
case GL_RGB8UI:
|
||||
case GL_RGB8I:
|
||||
return TextureViewClass::Bits24;
|
||||
case GL_R16F:
|
||||
case GL_RG8UI:
|
||||
case GL_R16UI:
|
||||
case GL_RG8I:
|
||||
case GL_R16I:
|
||||
case GL_RG8:
|
||||
case GL_R16:
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R16_SNORM:
|
||||
return TextureViewClass::Bits16;
|
||||
case GL_R8UI:
|
||||
case GL_R8I:
|
||||
case GL_R8:
|
||||
case GL_R8_SNORM:
|
||||
return TextureViewClass::Bits8;
|
||||
case GL_COMPRESSED_RED_RGTC1:
|
||||
case GL_COMPRESSED_SIGNED_RED_RGTC1:
|
||||
return TextureViewClass::Rgtc1Red;
|
||||
case GL_COMPRESSED_RG_RGTC2:
|
||||
case GL_COMPRESSED_SIGNED_RG_RGTC2:
|
||||
return TextureViewClass::Rgtc2Rg;
|
||||
case GL_COMPRESSED_RGBA_BPTC_UNORM:
|
||||
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
|
||||
return TextureViewClass::BptcUnorm;
|
||||
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
|
||||
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
|
||||
return TextureViewClass::BptcFloat;
|
||||
default:
|
||||
// Every depth/stencil format, every S3TC/ETC/ASTC format and every unsized format
|
||||
// reaches here. The caller must then demand an EXACT format match.
|
||||
return TextureViewClass::None;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core table 8.20 ("Legal texture targets for TextureView").
|
||||
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget) {
|
||||
switch (origTarget) {
|
||||
case TextureTarget::Texture1D:
|
||||
return viewTarget == TextureTarget::Texture1D || viewTarget == TextureTarget::Texture1DArray;
|
||||
case TextureTarget::Texture2D:
|
||||
return viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::Texture2DArray;
|
||||
case TextureTarget::Texture3D:
|
||||
return viewTarget == TextureTarget::Texture3D;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::Texture2D ||
|
||||
viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::TextureCubeMapArray;
|
||||
case TextureTarget::TextureRectangle:
|
||||
return viewTarget == TextureTarget::TextureRectangle;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return viewTarget == TextureTarget::Texture1DArray || viewTarget == TextureTarget::Texture1D;
|
||||
case TextureTarget::Texture2DArray:
|
||||
return viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::Texture2D ||
|
||||
viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return viewTarget == TextureTarget::TextureCubeMapArray || viewTarget == TextureTarget::Texture2DArray ||
|
||||
viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::TextureCubeMap;
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return viewTarget == TextureTarget::Texture2DMultisample ||
|
||||
viewTarget == TextureTarget::Texture2DMultisampleArray;
|
||||
case TextureTarget::TextureBuffer:
|
||||
// The table lists no legal target for a buffer texture: its storage is a buffer
|
||||
// object, and there is nothing to make a view of.
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget) {
|
||||
switch (viewTarget) {
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return 6;
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return 1;
|
||||
default:
|
||||
// 1D/2D array, cube-map array, 2D multisample array: any count (the cube-map array's
|
||||
// "multiple of 6" is checked by the caller).
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -79,4 +79,33 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
// 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);
|
||||
|
||||
// ---- glTextureView (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// Table 8.21's view classes. `None` is not a class - it means the format has NO entry in the
|
||||
// table, which the spec turns into a much stricter rule than "same class": such a format can
|
||||
// only ever be viewed as ITSELF. Every depth, stencil and depth/stencil format lands here,
|
||||
// which is why the Better Clouds D24S8 view must name GL_DEPTH24_STENCIL8 exactly.
|
||||
enum class TextureViewClass {
|
||||
None = 0,
|
||||
Bits128,
|
||||
Bits96,
|
||||
Bits64,
|
||||
Bits48,
|
||||
Bits32,
|
||||
Bits24,
|
||||
Bits16,
|
||||
Bits8,
|
||||
Rgtc1Red,
|
||||
Rgtc2Rg,
|
||||
BptcUnorm,
|
||||
BptcFloat,
|
||||
};
|
||||
TextureViewClass GetTextureViewClass(GLenum internalformat);
|
||||
// Table 8.20: which <target> values glTextureView accepts for a given origtexture target.
|
||||
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget);
|
||||
// Table 8.20 again, read the other way: how many layers <target> requires. Returns 0 for the
|
||||
// targets whose layer count is unconstrained (the array targets), 6 for GL_TEXTURE_CUBE_MAP,
|
||||
// and 1 for every single-layer target. GL_TEXTURE_CUBE_MAP_ARRAY is special-cased by the
|
||||
// caller because its constraint is "a multiple of 6", not an exact count.
|
||||
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -98,6 +98,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/TextureViewScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||
|
||||
@@ -518,5 +518,62 @@ void main() {
|
||||
ExpectSameImage(batched, unrolled, "a batch with zero-count sub-draws");
|
||||
}
|
||||
|
||||
// The base-vertex family's argument checks (GL 4.6 core 10.3.9). These are what
|
||||
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_* assert, and the reason the group sat
|
||||
// NotSupported for so long hid the fact that the entry points forwarded any argument
|
||||
// straight to the backend: a negative count reached the emulation as a huge unsigned
|
||||
// size. Each case drains the error queue first so the assertion names the call it made.
|
||||
TEST_F(MultiDrawScenario, BaseVertexDrawsRejectMalformedArguments) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 0;
|
||||
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||
// A bound program and VAO are prerequisites, not decoration: the entry points check
|
||||
// "is there something to execute" (GL_INVALID_OPERATION) before they look at any
|
||||
// argument, so without these every case below would pass for the wrong reason.
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
|
||||
const auto expectError = [&](const char* what, GLenum expected) {
|
||||
EXPECT_EQ(FirstGLError(), expected) << what;
|
||||
// FirstGLError stops at the first one; make sure nothing else is queued so the
|
||||
// next case starts clean.
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
};
|
||||
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, -1, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawElementsBaseVertex with a negative count", GL_INVALID_VALUE);
|
||||
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 3, GL_NONE, nullptr, 0);
|
||||
expectError("glDrawElementsBaseVertex with a non-index type", GL_INVALID_ENUM);
|
||||
|
||||
glDrawRangeElementsBaseVertex(GL_TRIANGLES, 3, 0, 3, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawRangeElementsBaseVertex with end < start", GL_INVALID_VALUE);
|
||||
|
||||
// start = -1 arrives as 0xFFFFFFFF, so this is the same end < start rule seen from
|
||||
// the other side - and it is the shape the CTS's invalid_count case actually uses.
|
||||
glDrawRangeElementsBaseVertex(GL_TRIANGLES, static_cast<GLuint>(-1), 2, 1, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawRangeElementsBaseVertex with a wrapped start", GL_INVALID_VALUE);
|
||||
|
||||
glDrawElementsInstancedBaseVertex(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr, -1, 0);
|
||||
expectError("glDrawElementsInstancedBaseVertex with a negative instancecount", GL_INVALID_VALUE);
|
||||
|
||||
const GLsizei negativeCount = -1;
|
||||
const void* offsets[1] = {reinterpret_cast<const void*>(0)};
|
||||
const GLint baseVertices[1] = {0};
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &negativeCount, GL_UNSIGNED_INT, offsets, 1, baseVertices);
|
||||
expectError("glMultiDrawElementsBaseVertex with a negative element of count", GL_INVALID_VALUE);
|
||||
|
||||
const GLsizei validCount = 6;
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &validCount, GL_UNSIGNED_INT, offsets, -1, baseVertices);
|
||||
expectError("glMultiDrawElementsBaseVertex with a negative drawcount", GL_INVALID_VALUE);
|
||||
|
||||
// The well-formed call still has to go through, or the checks above would be
|
||||
// indistinguishable from a blanket rejection.
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &validCount, GL_UNSIGNED_INT, offsets, 1, baseVertices);
|
||||
expectError("a well-formed glMultiDrawElementsBaseVertex", GL_NO_ERROR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,908 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.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
|
||||
//
|
||||
// glTextureView (ARB_texture_view / GL 4.6 core 8.18) end to end on both backends.
|
||||
//
|
||||
// THE DEFECT. glTextureView was a stub that logged once and returned. That is worse than not
|
||||
// having the function: MobileGL advertises GL 4.6, so LWJGL resolves a non-null pointer, an
|
||||
// application's capability check passes, it takes the texture-view path, and the view texture it
|
||||
// then samples has no storage at all. Nothing errors; the picture is simply wrong. The Better
|
||||
// Clouds Minecraft mod is exactly this shape - its GLCompat gates `supportsTextureView` on
|
||||
// `caps.glTextureView != NULL`, which was already true, so it ran its FULL path against a view
|
||||
// that aliased nothing.
|
||||
//
|
||||
// WHAT A VIEW IS, and why a copy cannot stand in for one. A view is a second texture NAME over
|
||||
// the SAME storage. Two consequences the tests below pin, both of which a copy fails:
|
||||
// * writes through either name are visible through the other (CoherencyIsBidirectional), and
|
||||
// * the two names carry INDEPENDENT per-texture parameters at the same time - which is the
|
||||
// entire point for Better Clouds: one D24S8 image, sampled in ONE shading pass through the
|
||||
// parent with DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX and through the view with
|
||||
// GL_DEPTH_COMPONENT (BetterCloudsCoveragePipeline below).
|
||||
//
|
||||
// MECHANISM PER BACKEND. DirectVulkan: the view resolves to the storage texture's ONE
|
||||
// TextureResource - one VkImage, one tracked layout, one upload path - and its own VkImageViews
|
||||
// (sub-range, reinterpreted VkFormat, its own aspect) are cached in alternateSampledViews /
|
||||
// attachmentViews keyed by the whole window. DirectGLES: the view gets its own ES name minted by
|
||||
// EXT/OES_texture_view over the storage texture's name, so the driver supplies the aliasing and
|
||||
// per-name parameters come for free. Without that extension the frontend refuses glTextureView
|
||||
// with GL_INVALID_OPERATION and withholds GL_ARB_texture_view rather than emulate by copying -
|
||||
// see NoExtensionSupportIsRefusedRatherThanFaked.
|
||||
//
|
||||
// CONTROLS. Every case here would pass on a stub for at least one of its assertions, so each one
|
||||
// also asserts something the stub cannot produce: a non-zero sampled value, a DIFFERENT value
|
||||
// through the two names, or a value that changed after a write through the other name.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr int kSize = 64;
|
||||
// The lower strip no cloud quad covers, so coverage 0 / depth 0 is asserted too - a
|
||||
// uniform image would otherwise pass a test that only ever looked at covered texels.
|
||||
constexpr int kUncoveredTop = 16;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform vec4 uRect; // x0, y0, x1, y1 in NDC
|
||||
uniform float uDepth; // NDC z
|
||||
void main() {
|
||||
vec2 p = mix(uRect.xy, uRect.zw, aPos);
|
||||
gl_Position = vec4(p, uDepth, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Mirrors betterclouds_coverage.fsh's shape: a second fragment output at location 1 whose
|
||||
// draw buffer is GL_NONE. The mod declares and writes it while glDrawBuffers names only
|
||||
// COLOR_ATTACHMENT0, so a layer that mishandles a write to a NONE draw buffer would either
|
||||
// error or clobber attachment 0.
|
||||
constexpr const char* kCoverageFragmentSource = R"(#version 330 core
|
||||
layout (location = 0) out vec4 outColor;
|
||||
layout (location = 1) out float outUnused;
|
||||
void main() {
|
||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
outUnused = 1.0 / 255.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The Better Clouds shading pass, reduced to its sampling. Both fetches name the SAME
|
||||
// D24S8 image through two GL texture names bound to two units in this one invocation.
|
||||
// `ivec2(gl_FragCoord)` (a truncating vec4 -> ivec2 constructor) is the mod's own spelling
|
||||
// at betterclouds_shading.fsh:56, kept verbatim because a strict GLSL front end can reject
|
||||
// it; the depth fetch uses the conventional `.xy` form the mod uses at line 117.
|
||||
constexpr const char* kShadingFragmentSource = R"(#version 330 core
|
||||
uniform usampler2D uCoverage; // the PARENT, DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX
|
||||
uniform sampler2D uDepthView; // the VIEW, DEPTH_STENCIL_TEXTURE_MODE = GL_DEPTH_COMPONENT
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
uint coverage = texelFetch(uCoverage, ivec2(gl_FragCoord), 0).r;
|
||||
float depth = texelFetch(uDepthView, ivec2(gl_FragCoord.xy), 0).r;
|
||||
outColor = vec4(float(coverage) * 0.25, depth, 0.0, 1.0);
|
||||
gl_FragDepth = depth;
|
||||
}
|
||||
)";
|
||||
|
||||
// Reads a reinterpreting view (GL_R32UI over GL_RGBA8 storage - both VIEW_CLASS_32_BITS)
|
||||
// and unpacks the word back into the four bytes it was written as.
|
||||
constexpr const char* kDecodeWordFragmentSource = R"(#version 330 core
|
||||
uniform usampler2D uWords;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
uint word = texelFetch(uWords, ivec2(gl_FragCoord.xy), 0).r;
|
||||
outColor = vec4(float((word ) & 0xFFu) / 255.0,
|
||||
float((word >> 8) & 0xFFu) / 255.0,
|
||||
float((word >> 16) & 0xFFu) / 255.0,
|
||||
float((word >> 24) & 0xFFu) / 255.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kSampleFragmentSource = R"(#version 330 core
|
||||
uniform sampler2D uTexture;
|
||||
uniform float uLod;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
outColor = textureLod(uTexture, gl_FragCoord.xy / 64.0, uLod);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string Describe(const Rgba8& c) {
|
||||
return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + "," +
|
||||
std::to_string(c.a) + ")";
|
||||
}
|
||||
|
||||
class TextureViewScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!TextureViewUsable()) {
|
||||
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName()
|
||||
<< " (GL_ARB_texture_view not advertised)";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint rbo : m_rbos) {
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
}
|
||||
m_rbos.clear();
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
if (m_vao != 0) {
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
if (m_vbo != 0) {
|
||||
glDeleteBuffers(1, &m_vbo);
|
||||
m_vbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial same-format full-range view. It exercises nothing the cases below test,
|
||||
// so a backend that simply does not have the feature skips instead of failing every
|
||||
// one of them - the same shape CopyImageLayeredScenario uses for glCopyImageSubData.
|
||||
bool TextureViewUsable() {
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
GLuint view = 0;
|
||||
glGenTextures(1, &view);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(1, &view);
|
||||
glDeleteTextures(1, &storage);
|
||||
return usable;
|
||||
}
|
||||
|
||||
GLuint MakeVao() {
|
||||
if (m_vao != 0) return m_vao;
|
||||
// A unit quad; the vertex shader maps it onto whatever NDC rect uRect names, so
|
||||
// one buffer serves every draw here.
|
||||
static constexpr float kQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
|
||||
1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
return m_vao;
|
||||
}
|
||||
|
||||
GLuint MakeProgram(const char* vertexSource, const char* fragmentSource) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(vertexSource, fragmentSource, &error);
|
||||
EXPECT_NE(program, 0u) << "program failed to build: " << error;
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeTexture() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint MakeFbo() {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// A 2D texture with immutable storage and NEAREST filtering, i.e. what every case
|
||||
// here views. Levels beyond 1 stay undefined until a caller fills them.
|
||||
GLuint MakeImmutable2D(GLenum internalFormat, int levels, int width, int height) {
|
||||
const GLuint texture = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_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);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void DrawQuad(GLuint program, float x0, float y0, float x1, float y1, float depth) {
|
||||
glUseProgram(program);
|
||||
glUniform4f(glGetUniformLocation(program, "uRect"), x0, y0, x1, y1);
|
||||
const GLint depthLocation = glGetUniformLocation(program, "uDepth");
|
||||
if (depthLocation >= 0) glUniform1f(depthLocation, depth);
|
||||
glBindVertexArray(MakeVao());
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
// Reads the colour texture currently attached to `fbo` as COLOR_ATTACHMENT0.
|
||||
Image ReadFbo(GLuint fbo, int width, int height) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
return ReadPixels(width, height);
|
||||
}
|
||||
|
||||
// Every pixel of the inclusive region must match `expected` within `tolerance` per
|
||||
// channel. Whole-region rather than a spot check, for the reason HeadlessGL.h gives:
|
||||
// three of four vertices carrying stale data still paints a correct centre pixel.
|
||||
void ExpectRegion(const Image& image, int x0, int x1, int y0, int y1, Rgba8 expected, int tolerance,
|
||||
const char* what) {
|
||||
int offenders = 0;
|
||||
Rgba8 firstOffender{};
|
||||
int firstX = -1;
|
||||
int firstY = -1;
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const Rgba8 actual = image.At(x, y);
|
||||
const bool ok = std::abs(int(actual.r) - int(expected.r)) <= tolerance &&
|
||||
std::abs(int(actual.g) - int(expected.g)) <= tolerance &&
|
||||
std::abs(int(actual.b) - int(expected.b)) <= tolerance &&
|
||||
std::abs(int(actual.a) - int(expected.a)) <= tolerance;
|
||||
if (!ok) {
|
||||
if (offenders == 0) {
|
||||
firstOffender = actual;
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
}
|
||||
++offenders;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of "
|
||||
<< (x1 - x0 + 1) * (y1 - y0 + 1) << " pixels disagree; first at (" << firstX
|
||||
<< ", " << firstY << ") is " << Describe(firstOffender) << ", expected "
|
||||
<< Describe(expected) << " +/- " << tolerance;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
std::vector<GLuint> m_rbos;
|
||||
std::vector<GLuint> m_programs;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// The driving case: the Better Clouds full-mode pipeline, in its real order.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, BetterCloudsCoveragePipeline) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// --- Resources.java:230-251, in order ---------------------------------------------
|
||||
const GLuint coverageColor = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
const GLuint coverage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
|
||||
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_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
|
||||
const GLuint coverageFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, coverageColor, 0);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, coverage, 0);
|
||||
const GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
|
||||
glDrawBuffers(1, drawBuffers);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "the coverage framebuffer is incomplete; the mod would silently demote to its "
|
||||
"fallback configuration here (Resources.java:187-209)";
|
||||
|
||||
// The view is made from a name glGenTextures has only RESERVED - it has never been
|
||||
// bound, so glTextureView has to instantiate the texture object itself.
|
||||
const GLuint coverageDepthView = MakeTexture();
|
||||
glTextureView(coverageDepthView, GL_TEXTURE_2D, coverage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glTextureView raised an error";
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
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_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "setting up the view raised an error";
|
||||
|
||||
// The two names must be distinguishable through the queries, or nothing below proves
|
||||
// which one produced a sample.
|
||||
GLint parentMode = 0;
|
||||
GLint viewMode = 0;
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &parentMode);
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &viewMode);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(parentMode, GL_STENCIL_INDEX) << "the parent must keep the stencil aspect";
|
||||
EXPECT_EQ(viewMode, GL_DEPTH_COMPONENT)
|
||||
<< "the view must carry its OWN depth-stencil mode; sharing one parameter set with "
|
||||
"the parent is precisely what a texture view exists to avoid";
|
||||
|
||||
// --- OpenGLRenderer.java:244-344, the coverage pass -------------------------------
|
||||
const GLuint coverageProgram = MakeProgram(kQuadVertexSource, kCoverageFragmentSource);
|
||||
ASSERT_NE(coverageProgram, 0u);
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
// Reverse-Z, as the mod runs it (OpenGLRenderer.java:236/241).
|
||||
glClearDepth(0.0);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilMask(0xff);
|
||||
glClearStencil(0);
|
||||
// The coverage COUNT: one increment per depth-passing cloud fragment.
|
||||
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
|
||||
glStencilFunc(GL_ALWAYS, 0xff, 0xff);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Quad 1 covers everything above the uncovered strip, at window depth 0.25.
|
||||
const float stripTop = 2.0f * (float(kUncoveredTop) / float(kSize)) - 1.0f;
|
||||
DrawQuad(coverageProgram, -1.0f, stripTop, 1.0f, 1.0f, -0.5f);
|
||||
// Quad 2 covers the right half of that, at window depth 0.75 - nearer under GEQUAL,
|
||||
// so it both passes the depth test and increments the stencil a second time.
|
||||
DrawQuad(coverageProgram, 0.0f, stripTop, 1.0f, 1.0f, 0.5f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the coverage pass raised an error";
|
||||
|
||||
// --- OpenGLRenderer.java:393-464, the shading pass --------------------------------
|
||||
// A different draw framebuffer, exactly as the mod does (it hands the frame back to
|
||||
// Blaze3D before shading). The coverage texture stays ATTACHED to coverageFbo while
|
||||
// being sampled here, which is the shape a lazy/deferred FBO binding gets wrong.
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
GLuint destinationDepth = 0;
|
||||
glGenRenderbuffers(1, &destinationDepth);
|
||||
m_rbos.push_back(destinationDepth);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, destinationDepth);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, destinationDepth);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
// The mod's own indexed/non-indexed colour-mask pair (OpenGLRenderer.java:411-412).
|
||||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
||||
glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
const GLuint shadingProgram = MakeProgram(kQuadVertexSource, kShadingFragmentSource);
|
||||
ASSERT_NE(shadingProgram, 0u);
|
||||
glUseProgram(shadingProgram);
|
||||
// Unit 1 = the view (depth aspect), unit 3 = the parent (stencil aspect), the mod's
|
||||
// own unit assignment (Resources.java:309/311).
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glUniform1i(glGetUniformLocation(shadingProgram, "uDepthView"), 1);
|
||||
glUniform1i(glGetUniformLocation(shadingProgram, "uCoverage"), 3);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
|
||||
DrawQuad(shadingProgram, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the shading pass raised an error";
|
||||
|
||||
const Image shaded = ReadFbo(destination.fbo, kSize, kSize);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// R = coverage * 0.25 (so 1 -> 64, 2 -> 128), G = the depth read THROUGH THE VIEW.
|
||||
// A stub view samples (0,0,0,1), which fails the green channel of both covered
|
||||
// regions; a view that inherited the parent's stencil aspect fails them too.
|
||||
constexpr int kTolerance = 3;
|
||||
ExpectRegion(shaded, 1, kSize - 2, 1, kUncoveredTop - 2, Rgba8{0, 0, 0, 255}, kTolerance,
|
||||
"the uncovered strip must read coverage 0 and cleared depth 0");
|
||||
ExpectRegion(shaded, 1, kSize / 2 - 2, kUncoveredTop + 1, kSize - 2, Rgba8{64, 64, 0, 255}, kTolerance,
|
||||
"one cloud quad: stencil 1 through the parent, window depth 0.25 through the view");
|
||||
ExpectRegion(shaded, kSize / 2 + 1, kSize - 2, kUncoveredTop + 1, kSize - 2, Rgba8{128, 191, 0, 255},
|
||||
kTolerance,
|
||||
"two overlapping cloud quads: stencil 2 through the parent, window depth 0.75 "
|
||||
"through the view");
|
||||
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Storage sharing, in both directions. This is the assertion a copy-based emulation
|
||||
// fails, and the reason the no-EXT path refuses rather than emulates.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, CoherencyIsBidirectional) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Render red through the PARENT's name...
|
||||
const GLuint fbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// ...and read it back through the VIEW's.
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "a texture view must be attachable like any other texture";
|
||||
Image throughView = ReadFbo(viewFbo, kSize, kSize);
|
||||
ExpectRegion(throughView, 0, kSize - 1, 0, kSize - 1, Rgba8{255, 0, 0, 255}, 1,
|
||||
"a write through the parent must be visible through the view");
|
||||
|
||||
// Now the other direction: write green through the VIEW, read through the PARENT.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const Image throughParent = ReadFbo(fbo, kSize, kSize);
|
||||
ExpectRegion(throughParent, 0, kSize - 1, 0, kSize - 1, Rgba8{0, 255, 0, 255}, 1,
|
||||
"a write through the view must be visible through the parent - they are one "
|
||||
"storage, not two");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Format reinterpretation within a view class (GL 4.6 core table 8.21).
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ReinterpretingViewReadsTheSameBitsThroughAnotherFormat) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// GL_RGBA8 and GL_R32UI are both VIEW_CLASS_32_BITS, so one may be viewed as the
|
||||
// other. Filling the RGBA8 storage with a known byte pattern makes the R32UI view's
|
||||
// answer a fact about the BITS rather than about the colour.
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = 0x40;
|
||||
texels[i + 1] = 0x80;
|
||||
texels[i + 2] = 0xC0;
|
||||
texels[i + 3] = 0xFF;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the storage raised an error";
|
||||
|
||||
// NEGATIVE CONTROL. Everything below reads the storage through a REINTERPRETING view,
|
||||
// so a test that only asserted the view's answer could not tell "the reinterpret is
|
||||
// wrong" from "the seed never reached the GPU at all". Read the same texels through
|
||||
// the parent's own format first.
|
||||
const GLuint parentFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, parentFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image seeded = ReadFbo(parentFbo, kSize, kSize);
|
||||
ExpectRegion(seeded, 0, kSize - 1, 0, kSize - 1, Rgba8{0x40, 0x80, 0xC0, 0xFF}, 1,
|
||||
"control: the storage must hold the seeded byte pattern before any view reads it");
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "an in-class reinterpret must be accepted";
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
GLint viewFormat = 0;
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &viewFormat);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(viewFormat, GL_R32UI) << "the view must report its OWN internal format";
|
||||
|
||||
// A REAL GL_R32UI texture holding the very word the storage's bytes spell. The
|
||||
// assertion below is that the view and this texture sample IDENTICALLY.
|
||||
//
|
||||
// Comparing against a reference texture rather than against a hard-coded colour is
|
||||
// deliberate. Sampling a 32-bit integer texture is not itself what this scenario is
|
||||
// about, and llvmpipe's ES driver does it inconsistently (verified outside MobileGL,
|
||||
// with a raw-EGL program that reproduces the same wrong decode with NO view in play).
|
||||
// Holding both sides to the same driver factors that out completely: whatever the
|
||||
// driver makes of a usampler2D fetch, the view has to make the same thing of it, or
|
||||
// it is not delivering the storage's bits. A view that samples zero, that lands on
|
||||
// the wrong texels, or that lost its format still fails.
|
||||
constexpr std::uint32_t kExpectedWord = 0xFFC08040u; // little-endian A,B,G,R
|
||||
const GLuint reference = MakeImmutable2D(GL_R32UI, 1, kSize, kSize);
|
||||
std::vector<std::uint32_t> words(static_cast<std::size_t>(kSize) * kSize, kExpectedWord);
|
||||
glBindTexture(GL_TEXTURE_2D, reference);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RED_INTEGER, GL_UNSIGNED_INT, words.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the reference texture failed";
|
||||
|
||||
const GLuint program = MakeProgram(kQuadVertexSource, kDecodeWordFragmentSource);
|
||||
ASSERT_NE(program, 0u);
|
||||
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
const auto decodeThrough = [&](GLuint texture) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUniform1i(glGetUniformLocation(program, "uWords"), 0);
|
||||
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "sampling raised an error";
|
||||
return ReadFbo(destination.fbo, kSize, kSize);
|
||||
};
|
||||
|
||||
const Image throughReference = decodeThrough(reference);
|
||||
const Image throughView = decodeThrough(view);
|
||||
|
||||
// Guard against the degenerate agreement of two black images: the reference must
|
||||
// itself carry something, or "identical" would prove nothing.
|
||||
const Rgba8 referenceTexel = throughReference.At(kSize / 2, kSize / 2);
|
||||
ASSERT_FALSE(referenceTexel == (Rgba8{0, 0, 0, 0}))
|
||||
<< "the reference GL_R32UI texture sampled as nothing, so the comparison below is vacuous";
|
||||
|
||||
std::size_t mismatches = 0;
|
||||
for (int y = 0; y < kSize; ++y) {
|
||||
for (int x = 0; x < kSize; ++x) {
|
||||
if (!(throughView.At(x, y) == throughReference.At(x, y))) ++mismatches;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(mismatches, 0u)
|
||||
<< "the GL_R32UI view of GL_RGBA8 storage must sample exactly what a real GL_R32UI texture "
|
||||
"holding the same word does; view centre is " << Describe(throughView.At(kSize / 2, kSize / 2))
|
||||
<< ", reference centre is " << Describe(referenceTexel);
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Sub-ranges: one mip level of two, and one layer of an array.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ViewOfOneMipLevelAddressesThatLevelAsItsOwnLevelZero) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 2, kSize, kSize);
|
||||
// Level 0 red, level 1 blue, so the view's answer names the level it opened onto.
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 1);
|
||||
glViewport(0, 0, kSize / 2, kSize / 2);
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the mip chain raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
GLint minLevel = -1;
|
||||
GLint numLevels = -1;
|
||||
GLint immutableLevels = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &immutableLevels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLevel, 1);
|
||||
EXPECT_EQ(numLevels, 1);
|
||||
// GL 4.6 core 8.18: inherited from the ORIGINAL, not set to <numlevels>.
|
||||
EXPECT_EQ(immutableLevels, 2) << "TEXTURE_IMMUTABLE_LEVELS is the original texture's value";
|
||||
|
||||
// The view's level 0 IS the parent's level 1: attaching level 0 of the view must find
|
||||
// the blue half-size image.
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image levelOne = ReadFbo(viewFbo, kSize / 2, kSize / 2);
|
||||
ExpectRegion(levelOne, 0, kSize / 2 - 1, 0, kSize / 2 - 1, Rgba8{0, 0, 255, 255}, 1,
|
||||
"the view's level 0 must be the parent's level 1 (blue), not its level 0 (red)");
|
||||
}
|
||||
|
||||
TEST_F(TextureViewScenario, ViewOfOneArrayLayerAddressesThatLayer) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kChosenLayer = 2;
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// A different colour per layer, so a view that lost its layer offset reads the wrong
|
||||
// one rather than merely reading nothing.
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
|
||||
glClearColor(float(layer) / 8.0f, 1.0f - float(layer) / 8.0f, 0.5f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array layers raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, kChosenLayer, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "2D_ARRAY -> 2D is a legal view pair";
|
||||
|
||||
GLint minLayer = -1;
|
||||
GLint numLayers = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER, &minLayer);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS, &numLayers);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLayer, kChosenLayer);
|
||||
EXPECT_EQ(numLayers, 1);
|
||||
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image sliced = ReadFbo(viewFbo, kSize, kSize);
|
||||
const Rgba8 expected{static_cast<std::uint8_t>(kChosenLayer * 255 / 8),
|
||||
static_cast<std::uint8_t>(255 - kChosenLayer * 255 / 8), 128, 255};
|
||||
ExpectRegion(sliced, 0, kSize - 1, 0, kSize - 1, expected, 2,
|
||||
"a single-layer 2D view of an array must address the layer it named");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Writing THROUGH a layer-sliced view. The read direction is covered above; this is the
|
||||
// write direction, and it is the one that can corrupt the parent rather than merely
|
||||
// return the wrong pixels - a view whose texel path forgot its layer origin writes over
|
||||
// the parent's layer 0 while the application believes it addressed layer minLayer.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, WritingThroughALayerSlicedViewLandsOnItsOwnLayers) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kViewMinLayer = 2;
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const auto layerFill = [](int layer) {
|
||||
return Rgba8{static_cast<std::uint8_t>(10 + layer * 20),
|
||||
static_cast<std::uint8_t>(200 - layer * 20), 30, 255};
|
||||
};
|
||||
// Seeded by CPU sub-image rather than by rendering, deliberately: this scenario is
|
||||
// about the view's LAYER ORIGIN, and seeding through the GPU would additionally
|
||||
// depend on a CPU sub-image reaching a layer whose content the GPU wrote - which
|
||||
// DirectVulkan does not currently do even for a plain array texture (no view
|
||||
// involved), and which would make a failure here unattributable.
|
||||
const auto uploadLayer = [&](GLuint texture, int layer, Rgba8 colour) {
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = colour.r;
|
||||
texels[i + 1] = colour.g;
|
||||
texels[i + 2] = colour.b;
|
||||
texels[i + 3] = colour.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
};
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
uploadLayer(storage, layer, layerFill(layer));
|
||||
}
|
||||
const GLuint fbo = MakeFbo();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the layers raised an error";
|
||||
|
||||
// A two-layer window starting at layer 2, so a lost offset lands on layer 0 - which
|
||||
// the assertions below would see as an untouched layer that moved.
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, kViewMinLayer, 2);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Write the view's OWN layer 0, i.e. the storage's layer 2.
|
||||
constexpr Rgba8 kPainted{255, 0, 255, 255};
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = kPainted.r;
|
||||
texels[i + 1] = kPainted.g;
|
||||
texels[i + 2] = kPainted.b;
|
||||
texels[i + 3] = kPainted.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, view);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "writing through the view raised an error";
|
||||
|
||||
// POSITIVE CONTROL, through the parent's own name and into a layer outside the view's
|
||||
// window. It makes the assertions below able to tell "the view lost its layer origin"
|
||||
// from "a CPU sub-image into this array does not reach the GPU at all", which is a
|
||||
// different question and not one a texture view can answer.
|
||||
constexpr Rgba8 kControl{0, 0, 255, 255};
|
||||
std::vector<std::uint8_t> controlTexels(texels.size());
|
||||
for (std::size_t i = 0; i < controlTexels.size(); i += 4) {
|
||||
controlTexels[i + 0] = kControl.r;
|
||||
controlTexels[i + 1] = kControl.g;
|
||||
controlTexels[i + 2] = kControl.b;
|
||||
controlTexels[i + 3] = kControl.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
controlTexels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the control write raised an error";
|
||||
|
||||
// Read every layer of the PARENT back: only the one the view's layer 0 maps to may
|
||||
// have changed.
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
const Image image = ReadPixels(kSize, kSize);
|
||||
Rgba8 expected = layerFill(layer);
|
||||
const char* what = "a layer outside the view's window must not have been written";
|
||||
if (layer == kViewMinLayer) {
|
||||
expected = kPainted;
|
||||
what = "the view's layer 0 must be the storage layer it named";
|
||||
} else if (layer == 1) {
|
||||
expected = kControl;
|
||||
what = "control: a sub-image written through the PARENT must reach its layer";
|
||||
}
|
||||
ExpectRegion(image, 0, kSize - 1, 0, kSize - 1, expected, 2, what);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Views of views compose; the composed view still reaches the ROOT storage.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ViewOfAViewComposesTheLevelRanges) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLevels = 3;
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, kLevels, kSize, kSize);
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
for (int level = 0; level < kLevels; ++level) {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, level);
|
||||
glViewport(0, 0, kSize >> level, kSize >> level);
|
||||
glClearColor(0.0f, 0.0f, float(level + 1) / 4.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// First view opens onto levels [1, 3); the second takes level 1 OF THAT, which is the
|
||||
// root's level 2. GL 4.6 core 8.18 makes the offsets add.
|
||||
const GLuint firstView = MakeTexture();
|
||||
glTextureView(firstView, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
const GLuint secondView = MakeTexture();
|
||||
glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "origtexture may itself be a view";
|
||||
|
||||
GLint minLevel = -1;
|
||||
GLint numLevels = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, secondView);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLevel, 2) << "TEXTURE_VIEW_MIN_LEVEL adds the original's";
|
||||
EXPECT_EQ(numLevels, 1);
|
||||
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, secondView, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image composed = ReadFbo(viewFbo, kSize >> 2, kSize >> 2);
|
||||
ExpectRegion(composed, 0, (kSize >> 2) - 1, 0, (kSize >> 2) - 1, Rgba8{0, 0, 191, 255}, 2,
|
||||
"the composed view must land on the root's level 2");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// GL name-deletion semantics: the storage outlives the original's NAME.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, DeletingTheOriginalKeepsTheViewUsable) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const GLuint fbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The NAME goes; the storage may not, because a view still references it
|
||||
// (GL 4.6 core 5.1.2 - an object is not deleted while anything still refers to it).
|
||||
glDeleteTextures(1, &storage);
|
||||
EXPECT_EQ(glIsTexture(storage), static_cast<GLboolean>(GL_FALSE));
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Sample the view through a shader, so the answer comes from a live descriptor rather
|
||||
// than from an attachment the frontend might have kept alive by other means.
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
const GLuint program = MakeProgram(kQuadVertexSource, kSampleFragmentSource);
|
||||
ASSERT_NE(program, 0u);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glUniform1i(glGetUniformLocation(program, "uTexture"), 0);
|
||||
glUniform1f(glGetUniformLocation(program, "uLod"), 0.0f);
|
||||
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const Image sampled = ReadFbo(destination.fbo, kSize, kSize);
|
||||
ExpectRegion(sampled, 1, kSize - 2, 1, kSize - 2, Rgba8{0, 255, 255, 255}, 2,
|
||||
"the view must still reach its storage after the original's name was deleted");
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -267,6 +267,13 @@ namespace MobileGL::MG_State {
|
||||
return m_textureState.CreateTextureObject(index, target);
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
|
||||
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
||||
Uint numLevels, Uint minLayer, Uint numLayers) {
|
||||
return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
|
||||
numLayers);
|
||||
}
|
||||
|
||||
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
||||
// GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
|
||||
// that is currently bound acts as if FramebufferTexture* had been called with texture
|
||||
|
||||
@@ -111,6 +111,11 @@ namespace MobileGL {
|
||||
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
|
||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
// See TextureState::CreateTextureViewObject (glTextureView, GL 4.6 core 8.18).
|
||||
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
|
||||
const SharedPtr<ITextureObject>& storageOwner,
|
||||
Uint minLevel, Uint numLevels, Uint minLayer,
|
||||
Uint numLayers);
|
||||
void MarkTextureObjectForDeletion(Uint index);
|
||||
TextureUnit& GetTextureUnitObject(Int unit);
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||
|
||||
@@ -627,7 +627,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
||||
.explicitFragmentOutIndices = in.explicitFragDataIndex,
|
||||
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
|
||||
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
|
||||
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding,
|
||||
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding};
|
||||
|
||||
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
|
||||
auto result = ShaderCompiler::LinkProgram(attrib);
|
||||
@@ -1573,7 +1574,25 @@ namespace MobileGL::MG_State::GLState {
|
||||
// (DirectGLES.cpp / UniformManager.cpp), all 14 elements also read the same
|
||||
// buffer. This is the rule the storage-block path in ProgramInterface.cpp
|
||||
// already applies, and whose comment there claims uniform blocks follow.
|
||||
const Int declaredBinding = ubo.getBinding();
|
||||
//
|
||||
// "Declared" cannot be read back off the reflection, though. MobileGL asks glslang
|
||||
// to auto-map bindings, so mapIO writes an invented one into every block's
|
||||
// qualifier before reflection ever runs and ubo.getBinding() is never negative;
|
||||
// worse, glslang packs uniform blocks into the SAME slot space as samplers and
|
||||
// images (setEnvClient(EShClientVulkan) leaves spvVersion.openGl at 0, so
|
||||
// TDefaultGlslIoResolver::resolveBinding keys every resource kind on set 0), so a
|
||||
// block declared after an unbound image gets 1. GL 4.6 core 7.6.2 says an
|
||||
// unqualified block reports ZERO. The set below is the shader's own answer,
|
||||
// captured during mapIO while the qualifier still meant it - the same mechanism
|
||||
// SeedDefaultStorageBlockBindings uses for storage blocks, and the aliasing at 0
|
||||
// that results is GL's, not a bug: unqualified blocks collide there until the
|
||||
// application rebinds them.
|
||||
//
|
||||
// Only this GL-visible binding POINT changes. The backends' descriptor lookups run
|
||||
// off glslang's assignment through uniformBlockIndexByBinding, which is untouched.
|
||||
const String blockTypeName = StripArrayElementSuffix(ubo.name);
|
||||
const Int declaredBinding =
|
||||
artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding();
|
||||
artifacts.uniformBlockBinding[i] =
|
||||
declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name);
|
||||
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
||||
|
||||
@@ -369,6 +369,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// link, so a stale set would otherwise default a block the new sources do declare a
|
||||
// binding for.
|
||||
artifacts.storageBlocksWithoutBinding.clear();
|
||||
artifacts.uniformBlocksWithoutBinding.clear();
|
||||
artifacts.attribs.clear();
|
||||
artifacts.attribTypes.clear();
|
||||
artifacts.activeUniformCount = 0;
|
||||
|
||||
@@ -1024,8 +1024,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint32 GetBlockBindingVersion() const { return m_blockBindingVersion; }
|
||||
|
||||
// Set by glUniformBlockBinding. The vector is seeded at link with each block's DECLARED
|
||||
// binding (layout(binding=N), else -1), so an untouched program already reports what its
|
||||
// shaders asked for.
|
||||
// binding (layout(binding=N)), and with GL's default of 0 for a block that declared none
|
||||
// - which the reflection cannot tell apart on its own, so the seeder consults
|
||||
// uniformBlocksWithoutBinding. Either way an untouched program already reports what GL
|
||||
// says it should.
|
||||
void SetUniformBlockBinding(Uint index, Uint binding) {
|
||||
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
|
||||
return;
|
||||
@@ -1287,6 +1289,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
|
||||
// auto-mapper assigns every uniform block a binding whether or not the shader asked
|
||||
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
|
||||
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
Uint maxUniformLocation = 0;
|
||||
|
||||
@@ -291,6 +291,13 @@ namespace MobileGL {
|
||||
return m_lifetimeId;
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& TextureObjectBase::GetViewStorageOwner() const {
|
||||
// A plain texture owns its own storage. Only TextureObjectView overrides this,
|
||||
// which is what IsTextureView() keys on everywhere else.
|
||||
static const SharedPtr<ITextureObject> noStorageOwner = nullptr;
|
||||
return noStorageOwner;
|
||||
}
|
||||
|
||||
Uint TextureObjectWithOneMipmap::GetMipmapLevelCount() const {
|
||||
return m_textureStorage.GetLevelCount();
|
||||
}
|
||||
@@ -434,8 +441,28 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
|
||||
const Bool mipmapped =
|
||||
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
// A multisample texture is fetched, never filtered. GL 4.6 core 8.17 gives it exactly
|
||||
// one level and says its sampler state is not used at all - texelFetch is the only way
|
||||
// a shader can read it - so 8.14's filter-completeness rules, which is what the
|
||||
// `mipmapped` branch below asks about, never apply to it.
|
||||
//
|
||||
// Deriving `mipmapped` from that unused sampler is what made EVERY multisample texture
|
||||
// look incomplete: MIN_FILTER's initial value is NEAREST_MIPMAP_LINEAR, and a texture
|
||||
// that can only ever have one level never satisfies the mip-chain check. Both backends
|
||||
// treat "samples as incomplete" as "do not bind it" (DirectGLES's per-unit walk in
|
||||
// ResolveAndBindUnitTextures, DirectVulkan's UniformManager), so the sampler2DMS the
|
||||
// shader declared was left pointing at nothing and every texelFetch read zero. That is
|
||||
// the sampler2DMS/sampler2DMSArray half of KHR-GL43.compute_shader.resource-texture,
|
||||
// which fails at the first data7 element with the multisample texture correctly
|
||||
// cleared and simply never bound.
|
||||
//
|
||||
// IsCopyImageEndpointComplete already spells the same guard as
|
||||
// CopyImageTargetHasMipmapChain; this was the one place that asked without it.
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
const Bool filtered = target != TextureTarget::Texture2DMultisample &&
|
||||
target != TextureTarget::Texture2DMultisampleArray;
|
||||
const Bool mipmapped = filtered && effectiveSampler != nullptr &&
|
||||
effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
return !IsMipmapCompleteForFilter(texture, mipmapped);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,29 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual GLenum GetDepthStencilTextureMode() const = 0;
|
||||
virtual void SetDepthStencilTextureMode(GLenum mode) = 0;
|
||||
|
||||
// ---- Texture views (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// The texture object whose immutable storage this one's texels actually live in, or
|
||||
// nullptr when this texture owns its storage. It is itself NEVER a view: glTextureView
|
||||
// composes a view-of-a-view onto the ROOT at creation, which is exactly what the spec's
|
||||
// additive "<minlevel> plus the value of TEXTURE_VIEW_MIN_LEVEL from the original
|
||||
// texture" rule describes, so one hop always reaches the storage.
|
||||
//
|
||||
// Holding it as a SharedPtr is what gives GL's name-deletion semantics for free: after
|
||||
// glDeleteTextures(origtexture) the name is gone and TextureState has dropped its entry,
|
||||
// but the object - and therefore the storage and every backend resource keyed on it -
|
||||
// stays alive as long as some view still references it (GL 4.6 core 5.1.2).
|
||||
virtual const SharedPtr<ITextureObject>& GetViewStorageOwner() const = 0;
|
||||
Bool IsTextureView() const { return GetViewStorageOwner() != nullptr; }
|
||||
// GL 4.6 core table 23.17, expressed in the storage owner's level/layer coordinates
|
||||
// (see above - composition makes the two the same number). All four are 0 on a mutable
|
||||
// texture; TexStorage* seeds them with (0, levels, 0, layers) because the spec makes an
|
||||
// immutable texture a full-extent view of itself, and glTextureView composes onto those.
|
||||
virtual Uint GetViewMinLevel() const = 0;
|
||||
virtual Uint GetViewNumLevels() const = 0;
|
||||
virtual Uint GetViewMinLayer() const = 0;
|
||||
virtual Uint GetViewNumLayers() const = 0;
|
||||
virtual void SetViewLevelLayerRange(Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) = 0;
|
||||
|
||||
protected:
|
||||
virtual Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const = 0;
|
||||
};
|
||||
@@ -123,6 +146,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool HasFixedSampleLocations() const override;
|
||||
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
|
||||
Uint64 GetLifetimeId() const override;
|
||||
// A plain texture owns its storage; TextureObjectView overrides this.
|
||||
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override;
|
||||
Uint GetViewMinLevel() const override { return m_viewMinLevel; }
|
||||
Uint GetViewNumLevels() const override { return m_viewNumLevels; }
|
||||
Uint GetViewMinLayer() const override { return m_viewMinLayer; }
|
||||
Uint GetViewNumLayers() const override { return m_viewNumLayers; }
|
||||
void SetViewLevelLayerRange(Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) override {
|
||||
m_viewMinLevel = minLevel;
|
||||
m_viewNumLevels = numLevels;
|
||||
m_viewMinLayer = minLayer;
|
||||
m_viewNumLayers = numLayers;
|
||||
}
|
||||
GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; }
|
||||
// Bumps the params version like every other backend-visible texture parameter: the mode
|
||||
// decides which ASPECT of a packed depth/stencil image a sampler reads, which DirectGLES
|
||||
@@ -165,6 +200,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
// matches before its first sync. Bumped only on dirty=true in MarkStorageDirty.
|
||||
Uint64 m_contentVersion = 1;
|
||||
GLenum m_depthStencilTextureMode = GL_DEPTH_COMPONENT;
|
||||
// GL 4.6 core table 23.17: all four are 0 until immutable storage exists, which is what
|
||||
// makes glGetTexParameteriv(GL_TEXTURE_VIEW_NUM_LEVELS) answer 0 on a mutable texture.
|
||||
Uint m_viewMinLevel = 0;
|
||||
Uint m_viewNumLevels = 0;
|
||||
Uint m_viewMinLayer = 0;
|
||||
Uint m_viewNumLayers = 0;
|
||||
Int m_samples = 0;
|
||||
Bool m_fixedSampleLocations = true;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "TextureObjectView.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
namespace {
|
||||
// Where a target keeps its LAYER count. GL puts a 1D array's layers in the state-side
|
||||
// height (that is what glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and what
|
||||
// TextureObject.cpp's completeness walk assumes); every other layered target keeps them
|
||||
// in z. GL_TEXTURE_3D is deliberately None: its depth is a spatial axis, not layers, and
|
||||
// ARB_texture_view forbids anything but a full-depth 3D->3D view of it.
|
||||
enum class LayerAxis { None, Y, Z };
|
||||
|
||||
LayerAxis LayerAxisOf(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1DArray:
|
||||
return LayerAxis::Y;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return LayerAxis::Z;
|
||||
default:
|
||||
return LayerAxis::None;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<TextureUploadTarget> UploadTargetsForViewTarget(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return {TextureUploadTarget::Texture1D};
|
||||
case TextureTarget::Texture2D:
|
||||
return {TextureUploadTarget::Texture2D};
|
||||
case TextureTarget::Texture3D:
|
||||
return {TextureUploadTarget::Texture3D};
|
||||
case TextureTarget::TextureRectangle:
|
||||
return {TextureUploadTarget::TextureRectangle};
|
||||
case TextureTarget::Texture1DArray:
|
||||
return {TextureUploadTarget::Texture1DArray};
|
||||
case TextureTarget::Texture2DArray:
|
||||
return {TextureUploadTarget::Texture2DArray};
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return {TextureUploadTarget::CubeMapArray};
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return {TextureUploadTarget::Texture2DMultisample};
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return {TextureUploadTarget::Texture2DMultisampleArray};
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return {TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
|
||||
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
|
||||
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ};
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "TextureObjectView: target %d cannot be a texture view", (int)target);
|
||||
return {TextureUploadTarget::Texture2D};
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TextureObjectView::TextureObjectView(Uint externalIndex, TextureTarget target,
|
||||
SharedPtr<ITextureObject> storageOwner, Uint minLevel, Uint numLevels,
|
||||
Uint minLayer, Uint numLayers)
|
||||
: TextureObjectMipmap(target, externalIndex), m_storageOwner(Move(storageOwner)),
|
||||
m_uploadTargets(UploadTargetsForViewTarget(target)) {
|
||||
MOBILEGL_ASSERT(m_storageOwner != nullptr, "TextureObjectView: storage owner is null");
|
||||
MOBILEGL_ASSERT(!m_storageOwner->IsTextureView(),
|
||||
"TextureObjectView: storage owner must be a root texture, not another view");
|
||||
m_ownerMipmap = AsMipmapTexture(m_storageOwner.get());
|
||||
SetViewLevelLayerRange(minLevel, numLevels, minLayer, numLayers);
|
||||
// Held rather than forwarded so the base class's level-range clamp works against the
|
||||
// view's OWN level count - TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL on a view are relative
|
||||
// to the view. GetImmutableLevels() forwards to the owner for the actual GL query, which
|
||||
// GL 4.6 core 8.18 defines as the ORIGINAL texture's value.
|
||||
SetImmutableLevels(numLevels);
|
||||
}
|
||||
|
||||
Uint TextureObjectView::GetImmutableLevels() const {
|
||||
return m_storageOwner->GetImmutableLevels();
|
||||
}
|
||||
|
||||
Uint64 TextureObjectView::GetContentVersion() const {
|
||||
return m_storageOwner->GetContentVersion();
|
||||
}
|
||||
|
||||
Int TextureObjectView::GetSamples() const {
|
||||
return m_storageOwner->GetSamples();
|
||||
}
|
||||
|
||||
Bool TextureObjectView::HasFixedSampleLocations() const {
|
||||
return m_storageOwner->HasFixedSampleLocations();
|
||||
}
|
||||
|
||||
TextureUploadTarget TextureObjectView::ToOwnerUploadTarget(TextureUploadTarget viewTarget) const {
|
||||
const auto& ownerTargets = m_storageOwner->GetUploadTargets();
|
||||
MOBILEGL_ASSERT(!ownerTargets.empty(), "TextureObjectView: storage owner has no upload target");
|
||||
if (ownerTargets.size() == 1) {
|
||||
// The owner keeps every layer in one blob, so there is nothing to choose.
|
||||
return ownerTargets[0];
|
||||
}
|
||||
// The owner is a cube map: six independent blobs, one per face, and the view's layer
|
||||
// index selects among them. A cube-map view of a cube map maps face to face; any other
|
||||
// view target addresses layers, which for a cube-map owner ARE its faces.
|
||||
const Uint faceCount = static_cast<Uint>(ownerTargets.size());
|
||||
Uint face = m_viewMinLayer;
|
||||
if (GetTarget() == TextureTarget::TextureCubeMap) {
|
||||
for (Uint i = 0; i < m_uploadTargets.size(); ++i) {
|
||||
if (m_uploadTargets[i] == viewTarget) {
|
||||
face = m_viewMinLayer + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ownerTargets[std::min(face, faceCount - 1)];
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectView::ToViewLevelSize(const IntVec3& ownerLevelSize) const {
|
||||
IntVec3 size = ownerLevelSize;
|
||||
// Collapse whichever axis the OWNER stored its layers in down to a single slice, then
|
||||
// impose this view's own layer count on whichever axis THIS target stores layers in.
|
||||
// Doing it in that order makes every legal target pair fall out: 2D_ARRAY->2D clears z,
|
||||
// 2D->2D_ARRAY sets it, 2D_ARRAY->2D_ARRAY replaces it, and 3D->3D touches neither
|
||||
// (LayerAxis::None on both sides), which is what keeps a 3D view's full depth intact.
|
||||
switch (LayerAxisOf(m_storageOwner->GetTarget())) {
|
||||
case LayerAxis::Y:
|
||||
size.y() = 1;
|
||||
break;
|
||||
case LayerAxis::Z:
|
||||
size.z() = 1;
|
||||
break;
|
||||
case LayerAxis::None:
|
||||
break;
|
||||
}
|
||||
switch (LayerAxisOf(GetTarget())) {
|
||||
case LayerAxis::Y:
|
||||
size.y() = static_cast<Int>(m_viewNumLayers);
|
||||
break;
|
||||
case LayerAxis::Z:
|
||||
size.z() = static_cast<Int>(m_viewNumLayers);
|
||||
break;
|
||||
case LayerAxis::None:
|
||||
break;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
SizeT TextureObjectView::LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const {
|
||||
if (m_viewMinLayer == 0 || m_ownerMipmap == nullptr) return 0;
|
||||
const LayerAxis ownerAxis = LayerAxisOf(m_storageOwner->GetTarget());
|
||||
if (ownerAxis == LayerAxis::None) {
|
||||
// A cube-map owner keeps each face in its OWN blob, and ToOwnerUploadTarget already
|
||||
// picked the right one; a 3D or plain 2D owner has no layers to skip.
|
||||
return 0;
|
||||
}
|
||||
const TextureUploadTarget ownerTarget = ToOwnerUploadTarget(viewTarget);
|
||||
const Uint ownerLevel = ToOwnerLevel(mipmapLevel);
|
||||
const IntVec3 ownerSize = m_ownerMipmap->GetMipmapTexelSize(ownerTarget, ownerLevel);
|
||||
const SizeT ownerBytes = m_ownerMipmap->GetMipmapByteSize(ownerTarget, ownerLevel);
|
||||
const SizeT ownerTexels = static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(ownerSize.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(ownerSize.z(), 1));
|
||||
if (ownerTexels == 0 || ownerBytes == 0) return 0;
|
||||
const SizeT bytesPerTexel = ownerBytes / ownerTexels;
|
||||
// One "layer" is a whole x*y slice for a 2D/cube array, and a single row of `width`
|
||||
// texels for a 1D array (whose layer count lives in the state-side height).
|
||||
const SizeT layerTexels = ownerAxis == LayerAxis::Y
|
||||
? static_cast<SizeT>(std::max(ownerSize.x(), 0))
|
||||
: static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(ownerSize.y(), 0));
|
||||
const SizeT offset = static_cast<SizeT>(m_viewMinLayer) * layerTexels * bytesPerTexel;
|
||||
return offset < ownerBytes ? offset : 0;
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectView::ToOwnerRegionOffset(const IntVec3& viewOffset) const {
|
||||
if (m_viewMinLayer == 0) return viewOffset;
|
||||
IntVec3 offset = viewOffset;
|
||||
// The dirty region is recorded in the OWNER's blob coordinates - that is the space its
|
||||
// upload path walks - so the view's layer origin has to be added here even though
|
||||
// MapMipmapData hands back an already-shifted POINTER. The two are not double-counting:
|
||||
// one moves the bytes, the other tells the owner which of its layers moved.
|
||||
switch (LayerAxisOf(m_storageOwner->GetTarget())) {
|
||||
case LayerAxis::Y:
|
||||
offset.y() += static_cast<Int>(m_viewMinLayer);
|
||||
break;
|
||||
case LayerAxis::Z:
|
||||
offset.z() += static_cast<Int>(m_viewMinLayer);
|
||||
break;
|
||||
case LayerAxis::None:
|
||||
break;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
Uint TextureObjectView::GetMipmapLevelCount() const {
|
||||
if (m_ownerMipmap == nullptr) return 0;
|
||||
const Uint ownerLevels = m_ownerMipmap->GetMipmapLevelCount();
|
||||
if (m_viewMinLevel >= ownerLevels) return 0;
|
||||
return std::min(m_viewNumLevels, ownerLevels - m_viewMinLevel);
|
||||
}
|
||||
|
||||
const IntVec3 TextureObjectView::GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return {0, 0, 0};
|
||||
return ToViewLevelSize(
|
||||
m_ownerMipmap->GetMipmapTexelSize(ToOwnerUploadTarget(target), ToOwnerLevel(mipmapLevel)));
|
||||
}
|
||||
|
||||
const SizeT TextureObjectView::GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return 0;
|
||||
const TextureUploadTarget ownerTarget = ToOwnerUploadTarget(target);
|
||||
const Uint ownerLevel = ToOwnerLevel(mipmapLevel);
|
||||
const IntVec3 ownerSize = m_ownerMipmap->GetMipmapTexelSize(ownerTarget, ownerLevel);
|
||||
const SizeT ownerBytes = m_ownerMipmap->GetMipmapByteSize(ownerTarget, ownerLevel);
|
||||
const SizeT ownerTexels = static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(ownerSize.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(ownerSize.z(), 1));
|
||||
if (ownerTexels == 0 || ownerBytes == 0) return 0;
|
||||
// Scaled rather than recomputed from a format table: the view's internalformat is
|
||||
// required to be in the same view class as the owner's (GL 4.6 core table 8.21), i.e. to
|
||||
// have the identical texel size, so bytes-per-texel is shared by construction and the
|
||||
// only difference is how many texels the view addresses.
|
||||
const IntVec3 viewSize = ToViewLevelSize(ownerSize);
|
||||
const SizeT viewTexels = static_cast<SizeT>(std::max(viewSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(viewSize.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(viewSize.z(), 1));
|
||||
const SizeT viewBytes = (ownerBytes / ownerTexels) * viewTexels;
|
||||
// Clamped against what remains of the owner's blob past this view's layer origin. A view
|
||||
// whose layer window the shadow cannot lay out contiguously - several faces of a cube-map
|
||||
// owner, which are separate blobs - would otherwise advertise more bytes than
|
||||
// MapMipmapData can hand back, and a caller sizing a copy off this would overrun.
|
||||
const SizeT layerOffset = LayerByteOffset(target, mipmapLevel);
|
||||
const SizeT available = layerOffset < ownerBytes ? ownerBytes - layerOffset : 0;
|
||||
return std::min(viewBytes, available);
|
||||
}
|
||||
|
||||
void TextureObjectView::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) {
|
||||
// Unreachable through the API: a view is immutable from birth (GL 4.6 core 8.18 sets its
|
||||
// TEXTURE_IMMUTABLE_FORMAT), and every entry point that would allocate is gated on
|
||||
// ValidateTextureMutable. Forwarded rather than asserted so an internal caller that
|
||||
// re-specifies the storage still hits the one real allocation.
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->AllocateStorage(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), input);
|
||||
}
|
||||
|
||||
void TextureObjectView::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->TruncateMipmapLevels(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(levelCount));
|
||||
}
|
||||
|
||||
void TextureObjectView::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
const TextureUploadTarget ownerTarget = ToOwnerUploadTarget(uploadTarget);
|
||||
const Uint ownerLevel = ToOwnerLevel(mipmapLevel);
|
||||
const SizeT layerOffset = LayerByteOffset(uploadTarget, mipmapLevel);
|
||||
if (layerOffset == 0) {
|
||||
m_ownerMipmap->UpdateMipmapSubData(ownerTarget, ownerLevel, input);
|
||||
return;
|
||||
}
|
||||
// The owner's whole-level write starts at ITS level origin, which for a layer-sliced view
|
||||
// is the wrong place: writing there would silently overwrite the parent's layers 0..n
|
||||
// instead of the window this view opened. Write through the shifted pointer instead, and
|
||||
// mark exactly the layers that moved.
|
||||
auto* destination = static_cast<Uint8*>(m_ownerMipmap->MapMipmapData(ownerTarget, ownerLevel));
|
||||
if (destination == nullptr || input.data == nullptr || input.size == 0) return;
|
||||
const SizeT capacity = GetMipmapByteSize(uploadTarget, mipmapLevel);
|
||||
std::memcpy(destination + layerOffset, input.data, std::min(input.size, capacity));
|
||||
const IntVec3 viewSize = GetMipmapTexelSize(uploadTarget, mipmapLevel);
|
||||
MarkStorageDirtyRegion(uploadTarget, mipmapLevel, IntVec3{0, 0, 0},
|
||||
IntVec3{viewSize.x(), viewSize.y(), std::max(viewSize.z(), 1)});
|
||||
}
|
||||
|
||||
void* TextureObjectView::MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
|
||||
if (m_ownerMipmap == nullptr) return nullptr;
|
||||
auto* data = static_cast<Uint8*>(
|
||||
m_ownerMipmap->MapMipmapData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel)));
|
||||
if (data == nullptr) return nullptr;
|
||||
// Shifted to the view's first LAYER, so that a caller which maps this pointer and then
|
||||
// offsets into it using the extents GetMipmapTexelSize reports - which is what every
|
||||
// glTexSubImage*/glGetTexImage path does - lands on the layers this view addresses rather
|
||||
// than on the parent's first ones.
|
||||
return data + LayerByteOffset(uploadTarget, mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectView::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->MarkStorageDirty(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), dirty);
|
||||
}
|
||||
|
||||
Bool TextureObjectView::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return false;
|
||||
return m_ownerMipmap->IsStorageDirty(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
|
||||
}
|
||||
|
||||
void TextureObjectView::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
|
||||
ToOwnerRegionOffset(offset), size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObjectView::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return {};
|
||||
return m_ownerMipmap->GetStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
|
||||
}
|
||||
|
||||
void TextureObjectView::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->SetMipmapCompressedImage(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
|
||||
internalFormat, data, size);
|
||||
}
|
||||
|
||||
GLenum TextureObjectView::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return GL_NONE;
|
||||
return m_ownerMipmap->GetMipmapCompressedFormat(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
|
||||
}
|
||||
|
||||
SizeT TextureObjectView::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return 0;
|
||||
return m_ownerMipmap->GetMipmapCompressedByteSize(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
|
||||
}
|
||||
|
||||
const void* TextureObjectView::MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return nullptr;
|
||||
return m_ownerMipmap->MapMipmapCompressedImage(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
|
||||
}
|
||||
|
||||
void TextureObjectView::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->SetMipmapRequestedCompressedFormat(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
|
||||
internalFormat);
|
||||
}
|
||||
|
||||
GLenum TextureObjectView::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
if (m_ownerMipmap == nullptr) return GL_NONE;
|
||||
return m_ownerMipmap->GetMipmapRequestedCompressedFormat(ToOwnerUploadTarget(uploadTarget),
|
||||
ToOwnerLevel(mipmapLevel));
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectView::GetBaseSize() const {
|
||||
if (GetMipmapLevelCount() == 0) return {0, 0, 0};
|
||||
return GetMipmapTexelSize(m_uploadTargets[0], 0);
|
||||
}
|
||||
|
||||
Bool TextureObjectView::IsComplete() const {
|
||||
if (!TextureObjectBase::IsComplete()) return false;
|
||||
// The view's own level set is what sampling walks, and it can be shorter than the
|
||||
// owner's. Everything below it - that the owner has real storage at all - is the owner's
|
||||
// answer, because these texels are its texels.
|
||||
if (GetMipmapLevelCount() == 0) return false;
|
||||
return m_storageOwner->IsComplete();
|
||||
}
|
||||
|
||||
Uint TextureObjectView::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
|
||||
for (Uint i = 0; i < static_cast<Uint>(m_uploadTargets.size()); ++i) {
|
||||
if (m_uploadTargets[i] == target) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
@@ -0,0 +1,126 @@
|
||||
// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObjectView.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
#include "TextureObject.h"
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// A texture created by glTextureView (ARB_texture_view / GL 4.6 core 8.18): a texture object
|
||||
// in every respect - own name, own target, own internal format, own sampler and own
|
||||
// per-texture parameters - whose TEXELS are somebody else's. That last part is the whole
|
||||
// point of the extension, and the reason this cannot be a plain TextureObject2D with a copy:
|
||||
// the application samples the view and the original SIMULTANEOUSLY, reading different aspects
|
||||
// or different formats out of one storage, and writes through either name must be visible
|
||||
// through the other.
|
||||
//
|
||||
// So this class owns no MipmapStorage at all. Every storage question is answered by
|
||||
// m_storageOwner, shifted by the view's level offset; every parameter question is answered
|
||||
// by this object's own TextureObjectBase state. The owner is held by SharedPtr, which is
|
||||
// exactly GL's name-deletion rule (5.1.2): glDeleteTextures on the original frees the NAME
|
||||
// immediately, but the storage - and every backend resource keyed on the owner object -
|
||||
// lives until the last view referencing it is gone too.
|
||||
//
|
||||
// m_storageOwner is guaranteed never to be a view itself. glTextureView composes a
|
||||
// view-of-a-view onto the root at creation time, which is what the spec's additive
|
||||
// "<minlevel> plus the value of TEXTURE_VIEW_MIN_LEVEL from the original texture" rule
|
||||
// means; one hop therefore always reaches real storage and no recursion is possible.
|
||||
//
|
||||
// LEVEL offsets are applied by shifting the level index; LAYER offsets cannot be, because the
|
||||
// TextureObjectMipmap interface addresses storage as (upload target, level) and a layer lives
|
||||
// INSIDE a level's blob. They are applied two other ways instead, and the pair is what keeps
|
||||
// a layer-sliced view from corrupting its parent:
|
||||
// * MapMipmapData returns a pointer already advanced to the view's first layer, so a caller
|
||||
// that maps it and then offsets using the extents GetMipmapTexelSize reports - which is
|
||||
// what every glTexSubImage*/glGetTexImage path does - writes the layers it meant to; and
|
||||
// * MarkStorageDirtyRegion moves the region's origin into the OWNER's layer space, which is
|
||||
// the space its upload path walks.
|
||||
// Those two are not double-counting: one moves the bytes, the other names which of the
|
||||
// owner's layers moved.
|
||||
class TextureObjectView : public TextureObjectMipmap {
|
||||
public:
|
||||
TextureObjectView(Uint externalIndex, TextureTarget target, SharedPtr<ITextureObject> storageOwner,
|
||||
Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers);
|
||||
|
||||
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override { return m_storageOwner; }
|
||||
const Vector<TextureUploadTarget>& GetUploadTargets() const override { return m_uploadTargets; }
|
||||
|
||||
// A view is immutable from birth (GL 4.6 core 8.18 sets its TEXTURE_IMMUTABLE_FORMAT), and
|
||||
// unconditionally so: the base class infers immutability from a non-zero level count, and
|
||||
// a degenerate view - one the spec's min() composition narrowed to zero levels - would
|
||||
// otherwise report GL_FALSE, walk straight past ValidateTextureMutable and let
|
||||
// glTexImage2D respecify the PARENT's immutable storage through AllocateStorage.
|
||||
Bool IsImmutable() const override { return true; }
|
||||
|
||||
// GL 4.6 core 8.18: "TEXTURE_IMMUTABLE_LEVELS is set to the value of
|
||||
// TEXTURE_IMMUTABLE_LEVELS from the original texture" - NOT to <numlevels>. Kept as a
|
||||
// forward rather than in m_immutableLevels so that the base class's level-range clamp
|
||||
// keeps using the view's own level count, which is what TEXTURE_BASE_LEVEL /
|
||||
// TEXTURE_MAX_LEVEL on a view are relative to.
|
||||
Uint GetImmutableLevels() const override;
|
||||
|
||||
// Both follow the storage, not this object: a backend that memoised on the view's own
|
||||
// counter would keep serving stale texels after the owner was written through its own
|
||||
// name (KHR-GL43.texture_view.coherency is exactly this test).
|
||||
Uint64 GetContentVersion() const override;
|
||||
Int GetSamples() const override;
|
||||
Bool HasFixedSampleLocations() const override;
|
||||
|
||||
Uint GetMipmapLevelCount() const override;
|
||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||
Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
||||
const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) override;
|
||||
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
protected:
|
||||
Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const override;
|
||||
|
||||
private:
|
||||
// The owner-side upload target a given view-side one addresses. Only GL_TEXTURE_CUBE_MAP
|
||||
// stores its six faces as six separate blobs (MipmapUploadTargetArray<6>); every other
|
||||
// target - arrays and cube-map arrays included - keeps all its layers in one blob, so
|
||||
// the mapping is "the owner's only target" unless one of the two sides is a cube map.
|
||||
TextureUploadTarget ToOwnerUploadTarget(TextureUploadTarget viewTarget) const;
|
||||
Uint ToOwnerLevel(Uint viewLevel) const { return m_viewMinLevel + viewLevel; }
|
||||
// The owner's level extent rewritten into this view's shape: the owner's layer axis is
|
||||
// collapsed to one slice and the view's own layer count is imposed on the view's layer
|
||||
// axis. A GL 1D array carries its layer count in the state-side HEIGHT while every other
|
||||
// layered target carries it in z, so the axis is target-dependent.
|
||||
IntVec3 ToViewLevelSize(const IntVec3& ownerLevelSize) const;
|
||||
// Where this view's first LAYER starts inside the owner's level blob. The layer axis a
|
||||
// level's bytes are laid out along is the OWNER's, so this is a slice for a 2D/cube array
|
||||
// and a single row for a 1D array; a cube-map owner returns 0 because its faces are
|
||||
// separate blobs that ToOwnerUploadTarget already selects between.
|
||||
SizeT LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const;
|
||||
// A dirty-region origin moved from the view's layer space into the owner's.
|
||||
IntVec3 ToOwnerRegionOffset(const IntVec3& viewOffset) const;
|
||||
|
||||
SharedPtr<ITextureObject> m_storageOwner;
|
||||
// Non-owning; m_storageOwner keeps it alive and is never a view, so this is set once in
|
||||
// the constructor and is null only for the (rejected at creation) buffer-texture case.
|
||||
TextureObjectMipmap* m_ownerMipmap = nullptr;
|
||||
Vector<TextureUploadTarget> m_uploadTargets;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "TextureObject2DCube.h"
|
||||
#include "TextureObjectBuffer.h"
|
||||
#include "TextureObjectStubs.h"
|
||||
#include "TextureObjectView.h"
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
static std::atomic<Uint64> s_nextTextureStateContextId = 1;
|
||||
@@ -104,6 +105,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& TextureState::CreateTextureViewObject(
|
||||
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
||||
Uint numLevels, Uint minLayer, Uint numLayers) {
|
||||
MOBILEGL_ASSERT(storageOwner != nullptr, "CreateTextureViewObject: storage owner is null");
|
||||
auto& textureObject = m_textureObjects[index];
|
||||
textureObject = MakeShared<TextureObjectView>(index, target, storageOwner, minLevel, numLevels, minLayer,
|
||||
numLayers);
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
|
||||
if (m_indexGenerator.IsValid(index)) {
|
||||
auto it = m_textureObjects.find(index);
|
||||
|
||||
@@ -48,6 +48,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
TextureState();
|
||||
void GenerateNames(Uint number, Vector<Uint>& textures);
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
// glTextureView (GL 4.6 core 8.18). `storageOwner` must already be a texture with
|
||||
// immutable storage and must NOT itself be a view - the caller composes a view-of-a-view
|
||||
// onto the root first, and passes the composed (root-relative) level/layer range here.
|
||||
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
|
||||
const SharedPtr<ITextureObject>& storageOwner,
|
||||
Uint minLevel, Uint numLevels, Uint minLayer,
|
||||
Uint numLayers);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
|
||||
// zero names a real, per-target texture object shared by every texture unit; binding 0
|
||||
|
||||
@@ -1087,11 +1087,11 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
@@ -1113,17 +1113,17 @@ TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
|
||||
};
|
||||
|
||||
const auto esWithoutIndirect =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false);
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false);
|
||||
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithBoth =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false);
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
|
||||
@@ -516,14 +516,14 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
|
||||
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
}
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
|
||||
@@ -240,7 +240,7 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
||||
const auto& extensions = rendererInfo.Extensions;
|
||||
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 0);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
|
||||
|
||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
|
||||
@@ -263,6 +263,63 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
||||
extensions.end());
|
||||
}
|
||||
|
||||
// A multisample texture is fetched, never filtered, so the mip-chain completeness rules never
|
||||
// apply to it (GL 4.6 core 8.17). It has exactly one level and MIN_FILTER's initial value is
|
||||
// NEAREST_MIPMAP_LINEAR, so asking those rules anyway calls EVERY multisample texture incomplete
|
||||
// - and both backends express "incomplete" as "leave the native target unbound", which makes the
|
||||
// shader's sampler2DMS read zero from a texture that was written correctly.
|
||||
//
|
||||
// That is KHR-GL43.compute_shader.resource-texture: it clears its 2DMS texture to 123.0 through
|
||||
// an FBO (which succeeds - the ES clear is issued on a COMPLETE 4-sample framebuffer with no
|
||||
// error) and then fails at the first sampler2DMS element because the texture was never bound.
|
||||
TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) {
|
||||
using namespace MobileGL;
|
||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||
|
||||
ScopedDirectGLESTextureBindings state;
|
||||
|
||||
GLuint frontendTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
|
||||
ASSERT_NE(frontendTexture, 0u);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, frontendTexture);
|
||||
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2DMultisample)
|
||||
.GetBoundObject();
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
|
||||
textureObject->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
textureObject->SetSamples(4);
|
||||
textureObject->SetFixedSampleLocations(false);
|
||||
// One level, 4x4 - the shape glTexImage2DMultisample produces, and a size whose mip chain
|
||||
// would need three levels if the filter rules were (wrongly) applied.
|
||||
MG_State::GLState::AsMipmapTexture(textureObject.get())
|
||||
->AllocateStorage(TextureUploadTarget::Texture2DMultisample, 0, {{4, 4, 1}, 4});
|
||||
|
||||
// The precondition that used to poison it, asserted rather than assumed: the texture's own
|
||||
// sampler still reports a mipmapping filter, because GL's initial MIN_FILTER is
|
||||
// NEAREST_MIPMAP_LINEAR and a multisample texture has no way (and no reason) to change it.
|
||||
// If a future default made this None the test would pass without covering anything.
|
||||
const auto& sampler = textureObject->GetSamplerObject();
|
||||
ASSERT_NE(sampler, nullptr);
|
||||
ASSERT_NE(sampler->GetMipmapMode(), SamplerMipmapMode::None)
|
||||
<< "fixture is stale: the default sampler no longer asks for mipmapping, so this test "
|
||||
"would not exercise the multisample guard";
|
||||
|
||||
EXPECT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(), sampler.get()))
|
||||
<< "a multisample texture is never filter-incomplete";
|
||||
|
||||
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
// The symptom itself: the per-unit walk has to actually bind it.
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u)
|
||||
<< "the multisample texture was not bound; every texelFetch against it reads zero";
|
||||
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D_MULTISAMPLE);
|
||||
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
using namespace MobileGL;
|
||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||
@@ -530,7 +587,7 @@ TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
||||
const auto& extensions = rendererInfo.Extensions;
|
||||
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 0);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
|
||||
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
|
||||
|
||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
|
||||
|
||||
@@ -15,5 +15,21 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
DriverBugProbesTest
|
||||
DriverBugProbesTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(DriverBugProbesTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(DriverBugProbesTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,660 @@
|
||||
// MobileGL - MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::SelfTest::CollectGlesKnownDriverBugs;
|
||||
using MobileGL::MG_Util::SelfTest::DriverBugVerdict;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeCrossStageImageQualifierMergeDropsWrites;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||
|
||||
namespace {
|
||||
// A driver table with nothing resolved. Every probe has to treat this as "cannot tell",
|
||||
// never as "affected".
|
||||
MG_External::GLESFunctionsTable EmptyFunctionTable() {
|
||||
return MG_External::GLESFunctionsTable{};
|
||||
}
|
||||
|
||||
// ===================== THE FAKE DRIVER =====================
|
||||
//
|
||||
// Same idea as the fake GLES table BackendLoaderTest drives the gl_InstanceID probe with:
|
||||
// captureless lambdas over one file-scope state, with per-test knobs that turn each defect
|
||||
// on and off. It is deliberately a MODEL of the defect rather than a canned answer - the
|
||||
// fake reads the shader text the probe actually submitted and reproduces what the affected
|
||||
// driver does with it, so a probe that stopped building the triggering shape would stop
|
||||
// detecting, which is exactly what these tests are for.
|
||||
//
|
||||
// These tests call the Probe* functions directly rather than through
|
||||
// CollectGlesKnownDriverBugs(): the collector goes through the once-per-process memos, and a
|
||||
// memo latched by one test would decide the answer for every later one.
|
||||
|
||||
// The exact text an affected Adreno driver puts in the info log for this refusal.
|
||||
const char* const kImageLocationLinkLog =
|
||||
"Error: Image Image location or component exceeds max allowed.\nError: Linking failed.";
|
||||
|
||||
struct FakeDriver {
|
||||
// ---- limits the probes gate on -------------------------------------
|
||||
GLint maxColorTextureSamples = 4;
|
||||
GLint maxImageUnits = 8;
|
||||
GLint maxVertexImageUniforms = 8;
|
||||
GLint maxFragmentImageUniforms = 8;
|
||||
GLint maxGeometryImageUniforms = 3;
|
||||
// The landed geometry probe reads this; zero keeps it inert so it cannot interfere.
|
||||
GLint maxGeometrySsboBlocks = 0;
|
||||
bool geometryImageLimitQueryRaisesError = false;
|
||||
bool colorTextureSamplesQueryRaisesError = false;
|
||||
|
||||
// ---- defect knobs ---------------------------------------------------
|
||||
// Probe 1: a swizzled-alpha, non-zero-sample .w fetch reads garbage from the second
|
||||
// sampling program onward.
|
||||
bool msaaSwizzledAlphaCorrupted = false;
|
||||
// Probe 1's inconclusive path: EVERY sampled read is wrong, including the controls.
|
||||
bool msaaEveryReadWrong = false;
|
||||
// Probe 2: the link fails once the program declares more distinct image uniform NAMES
|
||||
// than this.
|
||||
int distinctImageNameBudget = 1000;
|
||||
// Probe 3: a same-name coherent writeonly/readonly pair loses the writing stage's store.
|
||||
bool sameNameImagePairDropsWrites = false;
|
||||
// Probe 3's inconclusive path: the renamed control loses it too.
|
||||
bool everyVertexImageWriteDropped = false;
|
||||
// Probe 4: how many texels the in-invocation dependent read misses under the STRONGEST
|
||||
// shape, how many it misses under the shape MobileGL emits today, and whether the
|
||||
// two-draw control misses them too.
|
||||
int coherencyStrongestShapeFailedTexels = 0;
|
||||
int coherencyEmittedShapeFailedTexels = 0;
|
||||
int coherencyControlFailedTexels = 0;
|
||||
|
||||
// ---- object bookkeeping ---------------------------------------------
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
GLuint nextShaderId = 1;
|
||||
GLuint nextProgramId = 1;
|
||||
GLuint nextTextureId = 1;
|
||||
GLuint nextFramebufferId = 1;
|
||||
GLuint nextVertexArrayId = 1;
|
||||
int aliveShaders = 0;
|
||||
int alivePrograms = 0;
|
||||
int aliveTextures = 0;
|
||||
int aliveFramebuffers = 0;
|
||||
int aliveVertexArrays = 0;
|
||||
|
||||
std::map<GLuint, std::string> shaderSources;
|
||||
std::map<GLuint, std::vector<GLuint>> programShaders;
|
||||
std::map<GLuint, bool> programLinked;
|
||||
std::map<GLuint, std::string> programInfoLogs;
|
||||
// texture id -> GL_TEXTURE_SWIZZLE_A
|
||||
std::map<GLuint, GLenum> multisampleAlphaSwizzle;
|
||||
|
||||
GLuint boundMultisampleTexture = 0;
|
||||
GLuint currentProgram = 0;
|
||||
// How many programs that sample a multisample texture have been linked so far. The
|
||||
// corruption starts at the second.
|
||||
int sampledMultisampleProgramCount = 0;
|
||||
// Set by glDrawArrays, consumed by glReadPixels.
|
||||
GLfloat lastSampledValue = 1.0f;
|
||||
int lastFailedTexelCount = 0;
|
||||
};
|
||||
|
||||
FakeDriver g_fake;
|
||||
|
||||
void ResetFakeDriver() { g_fake = FakeDriver{}; }
|
||||
|
||||
const std::string& SourceOf(GLuint shader) {
|
||||
static const std::string empty;
|
||||
const auto it = g_fake.shaderSources.find(shader);
|
||||
return it == g_fake.shaderSources.end() ? empty : it->second;
|
||||
}
|
||||
|
||||
bool Contains(const std::string& haystack, const char* needle) {
|
||||
return haystack.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
// Every `image2D <name>` the program declares, across all its stages.
|
||||
std::vector<std::string> DeclaredImageNames(GLuint program) {
|
||||
std::vector<std::string> names;
|
||||
const auto attached = g_fake.programShaders.find(program);
|
||||
if (attached == g_fake.programShaders.end()) return names;
|
||||
for (const GLuint shader : attached->second) {
|
||||
const std::string& source = SourceOf(shader);
|
||||
std::size_t at = 0;
|
||||
while ((at = source.find("image2D ", at)) != std::string::npos) {
|
||||
at += std::strlen("image2D ");
|
||||
const std::size_t end = source.find_first_of(";,)", at);
|
||||
if (end == std::string::npos) break;
|
||||
std::string name = source.substr(at, end - at);
|
||||
while (!name.empty() && (name.back() == ' ' || name.back() == '\t')) name.pop_back();
|
||||
if (std::find(names.begin(), names.end(), name) == names.end()) {
|
||||
names.push_back(name);
|
||||
}
|
||||
at = end;
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
std::string StageSourceContaining(GLuint program, const char* needle) {
|
||||
const auto attached = g_fake.programShaders.find(program);
|
||||
if (attached == g_fake.programShaders.end()) return {};
|
||||
for (const GLuint shader : attached->second) {
|
||||
const std::string& source = SourceOf(shader);
|
||||
if (Contains(source, needle)) return source;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The uniform name in `... image2D <name>;` of the first declaration in `source`.
|
||||
std::string FirstImageNameIn(const std::string& source) {
|
||||
const std::size_t at = source.find("image2D ");
|
||||
if (at == std::string::npos) return {};
|
||||
const std::size_t start = at + std::strlen("image2D ");
|
||||
const std::size_t end = source.find(';', start);
|
||||
if (end == std::string::npos) return {};
|
||||
return source.substr(start, end - start);
|
||||
}
|
||||
|
||||
// Whatever the sampling vertex shader asked for: `texelFetch(mg_probeSampler, ivec2(0), N).C`.
|
||||
void ParseSampledFetch(const std::string& source, int& sampleIndex, char& component) {
|
||||
sampleIndex = -1;
|
||||
component = '?';
|
||||
const std::size_t at = source.find("texelFetch(mg_probeSampler, ivec2(0), ");
|
||||
if (at == std::string::npos) return;
|
||||
const std::size_t start = at + std::strlen("texelFetch(mg_probeSampler, ivec2(0), ");
|
||||
sampleIndex = std::atoi(source.c_str() + start);
|
||||
const std::size_t dot = source.find(").", start);
|
||||
if (dot != std::string::npos && dot + 2 < source.size()) component = source[dot + 2];
|
||||
}
|
||||
|
||||
MG_External::GLESFunctionsTable MakeFakeGLESFunctions() {
|
||||
MG_External::GLESFunctionsTable funcs{};
|
||||
|
||||
funcs.glGetError = []() -> GLenum {
|
||||
const GLenum error = g_fake.pendingError;
|
||||
g_fake.pendingError = GL_NO_ERROR;
|
||||
return error;
|
||||
};
|
||||
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
|
||||
switch (pname) {
|
||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||
if (g_fake.colorTextureSamplesQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.maxColorTextureSamples;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_IMAGE_UNITS:
|
||||
*data = g_fake.maxImageUnits;
|
||||
break;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxVertexImageUniforms;
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxFragmentImageUniforms;
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||
if (g_fake.geometryImageLimitQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.maxGeometryImageUniforms;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
*data = g_fake.maxGeometrySsboBlocks;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) { *data = 0; };
|
||||
funcs.glGetFloatv = [](GLenum, GLfloat* data) {
|
||||
data[0] = 0.0f;
|
||||
data[1] = 0.0f;
|
||||
data[2] = 0.0f;
|
||||
data[3] = 0.0f;
|
||||
};
|
||||
funcs.glIsEnabled = [](GLenum) -> GLboolean { return GL_FALSE; };
|
||||
funcs.glEnable = [](GLenum) {};
|
||||
funcs.glDisable = [](GLenum) {};
|
||||
funcs.glFinish = []() {};
|
||||
funcs.glMemoryBarrier = [](GLbitfield) {};
|
||||
funcs.glPixelStorei = [](GLenum, GLint) {};
|
||||
funcs.glViewport = [](GLint, GLint, GLsizei, GLsizei) {};
|
||||
funcs.glClear = [](GLbitfield) {};
|
||||
funcs.glClearColor = [](GLfloat, GLfloat, GLfloat, GLfloat) {};
|
||||
funcs.glActiveTexture = [](GLenum) {};
|
||||
|
||||
// ---- shaders and programs -------------------------------------------
|
||||
funcs.glCreateShader = [](GLenum) -> GLuint {
|
||||
++g_fake.aliveShaders;
|
||||
return g_fake.nextShaderId++;
|
||||
};
|
||||
funcs.glShaderSource = [](GLuint shader, GLsizei count, const GLchar* const* strings,
|
||||
const GLint*) {
|
||||
std::string source;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (strings[i] != nullptr) source += strings[i];
|
||||
}
|
||||
g_fake.shaderSources[shader] = std::move(source);
|
||||
};
|
||||
funcs.glCompileShader = [](GLuint) {};
|
||||
funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) {
|
||||
if (pname == GL_COMPILE_STATUS) *params = GL_TRUE;
|
||||
};
|
||||
funcs.glGetShaderInfoLog = [](GLuint, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
|
||||
if (bufSize > 0) infoLog[0] = '\0';
|
||||
};
|
||||
funcs.glDeleteShader = [](GLuint shader) {
|
||||
if (shader != 0) --g_fake.aliveShaders;
|
||||
};
|
||||
funcs.glCreateProgram = []() -> GLuint {
|
||||
++g_fake.alivePrograms;
|
||||
return g_fake.nextProgramId++;
|
||||
};
|
||||
funcs.glAttachShader = [](GLuint program, GLuint shader) {
|
||||
g_fake.programShaders[program].push_back(shader);
|
||||
};
|
||||
funcs.glLinkProgram = [](GLuint program) {
|
||||
const std::vector<std::string> names = DeclaredImageNames(program);
|
||||
const bool overBudget = static_cast<int>(names.size()) > g_fake.distinctImageNameBudget;
|
||||
g_fake.programLinked[program] = !overBudget;
|
||||
g_fake.programInfoLogs[program] = overBudget ? kImageLocationLinkLog : "";
|
||||
if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) {
|
||||
++g_fake.sampledMultisampleProgramCount;
|
||||
}
|
||||
};
|
||||
funcs.glGetProgramiv = [](GLuint program, GLenum pname, GLint* params) {
|
||||
if (pname != GL_LINK_STATUS) return;
|
||||
const auto it = g_fake.programLinked.find(program);
|
||||
*params = (it == g_fake.programLinked.end() || it->second) ? GL_TRUE : GL_FALSE;
|
||||
};
|
||||
funcs.glGetProgramInfoLog = [](GLuint program, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
|
||||
if (bufSize <= 0) return;
|
||||
const auto it = g_fake.programInfoLogs.find(program);
|
||||
const std::string& log = it == g_fake.programInfoLogs.end() ? std::string() : it->second;
|
||||
const GLsizei copied = static_cast<GLsizei>(
|
||||
std::min<std::size_t>(log.size(), static_cast<std::size_t>(bufSize - 1)));
|
||||
std::memcpy(infoLog, log.data(), static_cast<std::size_t>(copied));
|
||||
infoLog[copied] = '\0';
|
||||
};
|
||||
funcs.glDeleteProgram = [](GLuint program) {
|
||||
if (program != 0) --g_fake.alivePrograms;
|
||||
};
|
||||
funcs.glUseProgram = [](GLuint program) { g_fake.currentProgram = program; };
|
||||
funcs.glGetUniformLocation = [](GLuint, const GLchar*) -> GLint { return 0; };
|
||||
funcs.glUniform1i = [](GLint, GLint) {};
|
||||
|
||||
// ---- textures, framebuffers, vertex arrays ---------------------------
|
||||
funcs.glGenTextures = [](GLsizei n, GLuint* textures) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
textures[i] = g_fake.nextTextureId++;
|
||||
++g_fake.aliveTextures;
|
||||
}
|
||||
};
|
||||
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture;
|
||||
};
|
||||
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (textures[i] != 0) --g_fake.aliveTextures;
|
||||
}
|
||||
};
|
||||
funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) {
|
||||
if (target == GL_TEXTURE_2D_MULTISAMPLE && pname == GL_TEXTURE_SWIZZLE_A) {
|
||||
g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] =
|
||||
static_cast<GLenum>(param);
|
||||
}
|
||||
};
|
||||
funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum,
|
||||
const void*) {};
|
||||
funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
||||
const void*) {};
|
||||
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
||||
funcs.glTexStorage2DMultisample = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei, GLboolean) {};
|
||||
funcs.glGenFramebuffers = [](GLsizei n, GLuint* framebuffers) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
framebuffers[i] = g_fake.nextFramebufferId++;
|
||||
++g_fake.aliveFramebuffers;
|
||||
}
|
||||
};
|
||||
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
|
||||
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
||||
funcs.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; };
|
||||
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||
}
|
||||
};
|
||||
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
arrays[i] = g_fake.nextVertexArrayId++;
|
||||
++g_fake.aliveVertexArrays;
|
||||
}
|
||||
};
|
||||
funcs.glBindVertexArray = [](GLuint) {};
|
||||
funcs.glDeleteVertexArrays = [](GLsizei n, const GLuint* arrays) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (arrays[i] != 0) --g_fake.aliveVertexArrays;
|
||||
}
|
||||
};
|
||||
funcs.glBindImageTexture = [](GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum) {};
|
||||
|
||||
// ---- the draw, where the defects live --------------------------------
|
||||
funcs.glDrawArrays = [](GLenum, GLint, GLsizei) {
|
||||
const GLuint program = g_fake.currentProgram;
|
||||
const std::string sampling = StageSourceContaining(program, "texelFetch(mg_probeSampler");
|
||||
if (!sampling.empty()) {
|
||||
int sampleIndex = -1;
|
||||
char component = '?';
|
||||
ParseSampledFetch(sampling, sampleIndex, component);
|
||||
const GLenum swizzle = g_fake.multisampleAlphaSwizzle.count(
|
||||
g_fake.boundMultisampleTexture) != 0
|
||||
? g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture]
|
||||
: GL_ALPHA;
|
||||
// An R32F texel filled with (1, 0, 0, -) reads 1.0 through both the ALPHA and the
|
||||
// RED swizzle sources, which is why one expected constant covers every shape.
|
||||
g_fake.lastSampledValue = 1.0f;
|
||||
if (g_fake.msaaEveryReadWrong) {
|
||||
g_fake.lastSampledValue = 0.0f;
|
||||
} else if (g_fake.msaaSwizzledAlphaCorrupted && swizzle == GL_RED && component == 'w' &&
|
||||
sampleIndex != 0 && g_fake.sampledMultisampleProgramCount >= 2) {
|
||||
// Uninitialised memory: a value that is neither the answer nor the clear.
|
||||
g_fake.lastSampledValue = -1.34954e-17f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Matched on the access qualifier alone, not on "coherent writeonly": the strongest
|
||||
// coherency shape spells it "coherent volatile writeonly".
|
||||
const std::string writeStage = StageSourceContaining(program, "writeonly");
|
||||
const std::string readStage = StageSourceContaining(program, "readonly");
|
||||
if (!writeStage.empty() && !readStage.empty() && Contains(readStage, "memoryBarrierImage")) {
|
||||
// The coherency probe: one invocation stores and then reads back. `volatile` is
|
||||
// what tells the strongest shape apart from the one MobileGL emits today, and
|
||||
// giving them separate knobs is what lets a test pin the case where only the
|
||||
// emitted shape is wrong - a fixable defect that must not be reported here.
|
||||
g_fake.lastFailedTexelCount = Contains(readStage, "coherent volatile")
|
||||
? g_fake.coherencyStrongestShapeFailedTexels
|
||||
: g_fake.coherencyEmittedShapeFailedTexels;
|
||||
return;
|
||||
}
|
||||
if (!writeStage.empty() && readStage.empty()) {
|
||||
// The coherency control's store half; the load half decides the result.
|
||||
g_fake.lastFailedTexelCount = 0;
|
||||
return;
|
||||
}
|
||||
if (writeStage.empty() && !readStage.empty()) {
|
||||
g_fake.lastFailedTexelCount = g_fake.coherencyControlFailedTexels;
|
||||
return;
|
||||
}
|
||||
if (!writeStage.empty() && !readStage.empty()) {
|
||||
// The qualifier-merge pair: the stores are lost when the two halves share a name.
|
||||
const bool sharedName =
|
||||
FirstImageNameIn(writeStage) == FirstImageNameIn(readStage) &&
|
||||
!FirstImageNameIn(writeStage).empty();
|
||||
const bool lost = g_fake.everyVertexImageWriteDropped ||
|
||||
(g_fake.sameNameImagePairDropsWrites && sharedName);
|
||||
g_fake.lastFailedTexelCount = lost ? 1 << 20 : 0;
|
||||
return;
|
||||
}
|
||||
g_fake.lastFailedTexelCount = 0;
|
||||
};
|
||||
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels) {
|
||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
if (format == GL_RED && type == GL_FLOAT) {
|
||||
GLfloat* out = static_cast<GLfloat*>(pixels);
|
||||
for (std::size_t i = 0; i < texels; ++i) out[i] = g_fake.lastSampledValue;
|
||||
return;
|
||||
}
|
||||
GLubyte* out = static_cast<GLubyte*>(pixels);
|
||||
const std::size_t failed =
|
||||
std::min<std::size_t>(texels, static_cast<std::size_t>(g_fake.lastFailedTexelCount));
|
||||
for (std::size_t i = 0; i < texels; ++i) {
|
||||
const bool ok = i >= failed;
|
||||
out[i * 4 + 0] = ok ? 0 : 255;
|
||||
out[i * 4 + 1] = ok ? 255 : 0;
|
||||
out[i * 4 + 2] = 0;
|
||||
out[i * 4 + 3] = 255;
|
||||
}
|
||||
};
|
||||
|
||||
return funcs;
|
||||
}
|
||||
|
||||
void ExpectProbeReleasedEverything() {
|
||||
EXPECT_EQ(g_fake.aliveShaders, 0);
|
||||
EXPECT_EQ(g_fake.alivePrograms, 0);
|
||||
EXPECT_EQ(g_fake.aliveTextures, 0);
|
||||
EXPECT_EQ(g_fake.aliveFramebuffers, 0);
|
||||
EXPECT_EQ(g_fake.aliveVertexArrays, 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The rule the whole section depends on: a probe that cannot run reports NO bug. If an
|
||||
// unrunnable probe answered "affected", every device without the entry points - every desktop
|
||||
// build, every unit-test process - would grow a driver-bug row it has no evidence for, and the
|
||||
// section would stop meaning "this device has these bugs".
|
||||
TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
||||
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
|
||||
EXPECT_FALSE(ProbeGeometryStageSsboWriteAfterEmitDropped(gl))
|
||||
<< "a probe with no entry points to call must not claim the driver is affected";
|
||||
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
}
|
||||
|
||||
// The section lists only bugs the device HAS, so a driver nothing could be probed on renders
|
||||
// nothing at all rather than a list of reassurances.
|
||||
TEST(DriverBugProbes, CollectsNoFindingsWhenNothingCanBeProbed) {
|
||||
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
|
||||
EXPECT_TRUE(CollectGlesKnownDriverBugs(gl).empty());
|
||||
}
|
||||
|
||||
// Every finding the table can produce is a bug that is PRESENT, which is why the vocabulary is
|
||||
// FIXED/UNFIXABLE and not PASS/FAIL. This latches that no probe can smuggle in a "not affected"
|
||||
// row by returning a finding with an empty name or detail - the screen renders both.
|
||||
TEST(DriverBugProbes, EveryFindingCarriesANameAndAnExplanation) {
|
||||
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
|
||||
for (const auto& finding : CollectGlesKnownDriverBugs(gl)) {
|
||||
EXPECT_FALSE(finding.name.empty());
|
||||
EXPECT_FALSE(finding.detail.empty()) << finding.name << " must say what MobileGL does about it";
|
||||
EXPECT_TRUE(finding.verdict == DriverBugVerdict::Fixed ||
|
||||
finding.verdict == DriverBugVerdict::Unfixable);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== R32F MULTISAMPLE SWIZZLE =====================
|
||||
|
||||
TEST(DriverBugProbes, R32FMultisampleSwizzleIsCleanOnAConformingDriver) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, R32FMultisampleSwizzleIsDetectedFromTheSecondProgramOnward) {
|
||||
ResetFakeDriver();
|
||||
g_fake.msaaSwizzledAlphaCorrupted = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_TRUE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The control rule, made executable: a driver on which even the default-swizzle, sample-zero and
|
||||
// .x reads are wrong is broken in some larger way, and the probe may not name the alpha swizzle
|
||||
// as the cause.
|
||||
TEST(DriverBugProbes, R32FMultisampleSwizzleReportsNothingWhenTheControlsAreWrongToo) {
|
||||
ResetFakeDriver();
|
||||
g_fake.msaaSwizzledAlphaCorrupted = true;
|
||||
g_fake.msaaEveryReadWrong = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl))
|
||||
<< "with every read wrong the probe has no evidence that the alpha swizzle is the variable";
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, R32FMultisampleSwizzleNeedsMoreThanOneSample) {
|
||||
ResetFakeDriver();
|
||||
g_fake.msaaSwizzledAlphaCorrupted = true;
|
||||
g_fake.maxColorTextureSamples = 1;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||
}
|
||||
|
||||
// ===================== IMAGE LOCATION PER NAME =====================
|
||||
|
||||
TEST(DriverBugProbes, ImageLocationBudgetIsCleanWhenNamesDoNotCost) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeImageLocationPerNameBudget(gl);
|
||||
EXPECT_FALSE(measurement.detected);
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, ImageLocationBudgetIsDetectedWhenOnlyTheSharedNamesLink) {
|
||||
ResetFakeDriver();
|
||||
// Four image uniforms per stage: twelve distinct names in the subject, four in the control.
|
||||
g_fake.distinctImageNameBudget = 5;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeImageLocationPerNameBudget(gl);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_EQ(measurement.perStageImageUniforms, g_fake.maxGeometryImageUniforms + 1);
|
||||
EXPECT_EQ(measurement.subjectDistinctNames, measurement.perStageImageUniforms * 3);
|
||||
EXPECT_EQ(measurement.controlDistinctNames, measurement.perStageImageUniforms);
|
||||
EXPECT_NE(measurement.driverMessage.find("exceeds max allowed"), String::npos)
|
||||
<< "the report quotes the driver rather than paraphrasing it";
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The control rule again: when the shared-name program is refused too, the shape is simply too
|
||||
// big for this driver and the refusal is honest.
|
||||
TEST(DriverBugProbes, ImageLocationBudgetReportsNothingWhenTheControlAlsoFails) {
|
||||
ResetFakeDriver();
|
||||
g_fake.distinctImageNameBudget = 2;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, ImageLocationBudgetNeedsAGeometryStageThatCanHoldImages) {
|
||||
ResetFakeDriver();
|
||||
g_fake.distinctImageNameBudget = 5;
|
||||
g_fake.maxGeometryImageUniforms = 0;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, ImageLocationBudgetStaysSilentOnAContextWithoutTheGeometryLimit) {
|
||||
ResetFakeDriver();
|
||||
g_fake.distinctImageNameBudget = 5;
|
||||
g_fake.geometryImageLimitQueryRaisesError = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected)
|
||||
<< "a pre-ES-3.2 context has no geometry stage to build the shape out of";
|
||||
}
|
||||
|
||||
// ===================== CROSS-STAGE QUALIFIER MERGE =====================
|
||||
|
||||
TEST(DriverBugProbes, QualifierMergeIsCleanWhenTheDriverKeepsTheStore) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, QualifierMergeIsDetectedWhenOnlyTheSharedNameLosesTheStore) {
|
||||
ResetFakeDriver();
|
||||
g_fake.sameNameImagePairDropsWrites = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_TRUE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// A driver that loses the RENAMED store too cannot write images from the vertex stage at all -
|
||||
// a different and much larger claim, which this probe may not make.
|
||||
TEST(DriverBugProbes, QualifierMergeReportsNothingWhenTheRenamedControlAlsoFails) {
|
||||
ResetFakeDriver();
|
||||
g_fake.sameNameImagePairDropsWrites = true;
|
||||
g_fake.everyVertexImageWriteDropped = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, QualifierMergeNeedsVertexStageImageUniforms) {
|
||||
ResetFakeDriver();
|
||||
g_fake.sameNameImagePairDropsWrites = true;
|
||||
g_fake.maxVertexImageUniforms = 0;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
}
|
||||
|
||||
// ===================== IMAGE COHERENCY RESIDUAL =====================
|
||||
|
||||
TEST(DriverBugProbes, ImageCoherencyIsCleanWhenTheDependentReadObservesTheStore) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeImageWriteReadCoherencyResidual(gl);
|
||||
EXPECT_FALSE(measurement.detected);
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, ImageCoherencyResidualIsDetectedAndQuantified) {
|
||||
ResetFakeDriver();
|
||||
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||
g_fake.coherencyEmittedShapeFailedTexels = 418;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
const auto measurement = ProbeImageWriteReadCoherencyResidual(gl);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_EQ(measurement.mismatchedTexels, 376);
|
||||
EXPECT_EQ(measurement.emittedShapeMismatchedTexels, 418)
|
||||
<< "the row reports what applications get, not only what is theoretically reachable";
|
||||
EXPECT_GT(measurement.totalTexels, 418) << "the report needs a denominator to quote a rate";
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The reason the subject is the STRONGEST shape and not the one MobileGL emits. Mesa llvmpipe
|
||||
// misses every texel with `coherent` + memoryBarrierImage() and none once the pair is also
|
||||
// `volatile` - a defect MobileGL could fix by emitting a different shape, which is not what
|
||||
// UNFIXABLE means and does not belong in this section.
|
||||
TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenAStrongerShapeWouldFixIt) {
|
||||
ResetFakeDriver();
|
||||
g_fake.coherencyStrongestShapeFailedTexels = 0;
|
||||
g_fake.coherencyEmittedShapeFailedTexels = 4096;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected)
|
||||
<< "a driver the volatile shape satisfies has a fixable defect, not an unfixable one";
|
||||
}
|
||||
|
||||
// The control rule once more: a driver whose glFinish-separated two-draw dependency is ALSO
|
||||
// dirty has a bigger defect than an in-invocation ordering residual, and this probe must not
|
||||
// dress that up as one.
|
||||
TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenTheFinishSeparatedControlIsDirtyToo) {
|
||||
ResetFakeDriver();
|
||||
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||
g_fake.coherencyControlFailedTexels = 4096;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
|
||||
ResetFakeDriver();
|
||||
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||
g_fake.maxFragmentImageUniforms = 1;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -156,6 +157,53 @@ void main() {
|
||||
g_out.value[0] = 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// glslang emits constants in FIRST-USE order, so a shader that does not use the flattened
|
||||
// array's length until after it has declared the counter block leaves that constant BELOW the
|
||||
// block. The pass needs the length to build `uint[length]` immediately before the block (SPIR-V
|
||||
// forbids forward type references), and it used to decline the whole block in that case - which
|
||||
// left the offsets in place and made SPIRV-Cross refuse the stage outright:
|
||||
//
|
||||
// Push constant block cannot be expressed as neither std430 nor std140.
|
||||
//
|
||||
// That is KHR-GL43.compute_shader.pipeline-compute-chain: its first kernel declares two counters
|
||||
// at offset 8 (so the flattened array is 4 elements) and first uses the value 4 after the block,
|
||||
// so the kernel never reached the driver and every buffer, image and counter it writes kept its
|
||||
// initial value. Here `i < 4u` is what puts `uint 4` below the block; the ordering assertion
|
||||
// below is the fixture's own latch, so a future glslang that emits constants differently reports
|
||||
// a stale fixture rather than silently testing nothing.
|
||||
constexpr const char* kLateLengthConstantCounters = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 1, offset = 8) uniform atomic_uint g_counter[2];
|
||||
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
|
||||
void main() {
|
||||
uint i = atomicCounterIncrement(g_counter[1]);
|
||||
if (i < 4u) { g_out.value[0] = i; }
|
||||
}
|
||||
)";
|
||||
|
||||
// Index of the first OpConstant of type uint with value |value|, and of struct |structId|, in
|
||||
// the module's instruction order. -1 when absent.
|
||||
std::pair<Int64, Int64> UintConstantAndStructOrder(const Vector<Uint32>& spirv, Uint32 structId,
|
||||
Uint32 value) {
|
||||
Int64 index = 0, constantIndex = -1, structIndex = -1;
|
||||
Uint32 uintTypeId = 0;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpTypeInt && wordCount >= 4u && words[2] == 32u && words[3] == 0u) {
|
||||
uintTypeId = words[1];
|
||||
}
|
||||
if (opcode == spv::Op::OpConstant && wordCount >= 4u && words[1] == uintTypeId &&
|
||||
words[3] == value && constantIndex < 0) {
|
||||
constantIndex = index;
|
||||
}
|
||||
if (opcode == spv::Op::OpTypeStruct && wordCount >= 2u && words[1] == structId) {
|
||||
structIndex = index;
|
||||
}
|
||||
++index;
|
||||
});
|
||||
return {constantIndex, structIndex};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(FlattenAtomicCounterBlockPass, MovesTheBlockToOffsetZeroAndGrowsTheArray) {
|
||||
@@ -212,3 +260,44 @@ TEST(FlattenAtomicCounterBlockPass, IsIdempotent) {
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
// The block must still flatten when the module already declares the flattened array's length
|
||||
// constant BELOW the block. The pass relocates that constant instead of declining; declining
|
||||
// left the offsets in place and cost the whole stage its transpile.
|
||||
TEST(FlattenAtomicCounterBlockPass, FlattensWhenTheLengthConstantIsDeclaredAfterTheBlock) {
|
||||
const Vector<Uint32> input = CompileCompute(kLateLengthConstantCounters);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
const Uint32 structId = FindAtomicCounterBlockStructId(input);
|
||||
ASSERT_NE(structId, 0u);
|
||||
ASSERT_EQ(MemberOffsetOf(input, structId, 0u), 8);
|
||||
|
||||
// The fixture's precondition, asserted rather than assumed: two counters at offset 8 need a
|
||||
// 4-element array, and this shader's `uint 4` really does sit below the block.
|
||||
const auto [constantIndex, structIndex] = UintConstantAndStructOrder(input, structId, 4u);
|
||||
ASSERT_GE(constantIndex, 0) << "fixture is stale: the module no longer declares a uint 4";
|
||||
ASSERT_GE(structIndex, 0);
|
||||
ASSERT_GT(constantIndex, structIndex)
|
||||
<< "fixture is stale: `uint 4` is no longer declared after the counter block, so this "
|
||||
"test would pass without exercising the relocation at all";
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
ASSERT_NE(output, input) << "the block was declined; the offsets are still in the module and "
|
||||
"SPIRV-Cross will refuse the stage";
|
||||
|
||||
const Uint32 outStructId = FindAtomicCounterBlockStructId(output);
|
||||
ASSERT_EQ(outStructId, structId);
|
||||
EXPECT_EQ(MemberCountOf(output, outStructId), 1u);
|
||||
EXPECT_EQ(MemberOffsetOf(output, outStructId, 0u), 0);
|
||||
EXPECT_EQ(ArrayLengthOf(output, MemberTypeOf(output, outStructId, 0u)), 4);
|
||||
// The relocation moved a definition; the module has to still be well-ordered.
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
// The symptom the CTS case actually failed on: with the block declined this throws.
|
||||
MG_Util::ShaderTranspiler::SpvcSession session(
|
||||
output, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << "ESSL transpile failed: " << (essl ? String{} : essl.error().log);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ namespace {
|
||||
String log;
|
||||
UnorderedMap<String, Uint> opaqueBindings;
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
UnorderedMap<String, Int> uniformLocations;
|
||||
};
|
||||
|
||||
@@ -145,6 +146,7 @@ namespace {
|
||||
if (captureEnabled) {
|
||||
programAttrib.explicitOpaqueUniformBindings = &capture.opaqueBindings;
|
||||
programAttrib.storageBlocksWithoutBinding = &capture.storageBlocksWithoutBinding;
|
||||
programAttrib.uniformBlocksWithoutBinding = &capture.uniformBlocksWithoutBinding;
|
||||
}
|
||||
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
@@ -561,11 +563,49 @@ void main() {
|
||||
<< "a declared binding must never be defaulted away";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BoundFirst"), 0u)
|
||||
<< "the binding may appear anywhere in the layout list, not only last";
|
||||
// A UNIFORM block is a different binding space with its own glUniformBlockBinding path, and
|
||||
// its default is already handled where uniformBlockBinding is seeded. Naming it here would
|
||||
// make the seeder default a resource it does not own.
|
||||
// A UNIFORM block is a different binding space with its own glUniformBlockBinding path, so it
|
||||
// must not reach the storage-block seeder - it has a capture set of its own (see
|
||||
// UnqualifiedUniformBlocksAreCapturedSeparatelyFromStorageBlocks below).
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("InputBuffer"), 0u)
|
||||
<< "uniform blocks are out of scope";
|
||||
<< "uniform blocks belong to the other set";
|
||||
}
|
||||
|
||||
// The uniform-block half of the same capture, and the reason it exists: glslang packs uniform
|
||||
// blocks into the same auto-mapped slot space as samplers and images, so an unqualified block
|
||||
// declared AFTER an unbound image comes back carrying binding 1 while GL 4.6 core 7.6.2 requires
|
||||
// it to report 0. Reflection cannot tell the invented number from a declared one, so the shader's
|
||||
// own answer has to be captured here, during mapIO, and applied at reflection time.
|
||||
// KHR-GL4{2,3}.shading_language_420pack.binding_uniform_default is exactly this shader shape.
|
||||
TEST_F(GlslangCaptureProbeTest, UnqualifiedUniformBlocksAreCapturedSeparatelyFromStorageBlocks) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
writeonly uniform image2D uni_image;
|
||||
layout(std140) uniform GOKU { vec4 gohan; vec4 goten; } goku;
|
||||
layout(std140, binding = 3) uniform VEGETA { vec4 trunks; } vegeta;
|
||||
layout(std430) buffer OutputBuffer { vec4 data0[]; } g_out_buffer;
|
||||
void main() {
|
||||
g_out_buffer.data0[0] = goku.gohan + goku.goten + vegeta.trunks;
|
||||
imageStore(uni_image, ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
const LinkCapture capture = CaptureFromCompute(source);
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
EXPECT_EQ(capture.uniformBlocksWithoutBinding.count("GOKU"), 1u)
|
||||
<< "an unqualified uniform block declared after an unbound image is the regressing shape";
|
||||
EXPECT_EQ(capture.uniformBlocksWithoutBinding.count("VEGETA"), 0u)
|
||||
<< "a declared binding must never be defaulted away";
|
||||
EXPECT_EQ(capture.uniformBlocksWithoutBinding.count("OutputBuffer"), 0u)
|
||||
<< "storage blocks belong to the other set";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("GOKU"), 0u)
|
||||
<< "the two sets must not cross-contaminate";
|
||||
|
||||
// The negative control every capture case here carries: with the OUT pointer left null the
|
||||
// resolver must write nothing at all.
|
||||
const LinkCapture off = CaptureFromCompute(source, /*captureEnabled=*/false);
|
||||
ASSERT_TRUE(off.linked) << off.log;
|
||||
EXPECT_TRUE(off.uniformBlocksWithoutBinding.empty());
|
||||
}
|
||||
|
||||
// The capture must not mistake a buffer-typed SAMPLER or a member qualifier for a block, and
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
add_executable(
|
||||
DebugTest
|
||||
DebugTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(DebugTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
DebugTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DebugTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
add_executable(
|
||||
ObjectLifetimeIdTest
|
||||
ObjectLifetimeIdTest.cpp
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// MobileGL - MobileGL/MG_Test/State/DebugTest.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
|
||||
//
|
||||
// KHR_debug (GL 4.6 core 20), the part MobileGL actually implements: the debug group stack and
|
||||
// object labels. These were silent stubs - glPushDebugGroup logged once and returned, glObjectLabel
|
||||
// discarded its argument and glGetObjectLabel always answered with an empty string - which meant
|
||||
// GL_DEBUG_GROUP_STACK_DEPTH reported 0 (not a legal value; the context is created with one group
|
||||
// already on the stack) and a label never survived being written.
|
||||
//
|
||||
// The calls are deliberately NOT forwarded to the host driver; GL_Debug.h explains why. What the
|
||||
// tests below pin is the observable contract that remains: the stack depth is real and its
|
||||
// over/underflow errors are the ones KHR_debug names, and a label written comes back.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
namespace {
|
||||
class DebugTest : public ::testing::Test {
|
||||
protected:
|
||||
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();
|
||||
// The group stack is context state and this binary shares one context across cases,
|
||||
// so unwind whatever a previous case left pushed.
|
||||
while (StackDepth() > 1) {
|
||||
MG_Impl::GLImpl::PopDebugGroup();
|
||||
}
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
while (StackDepth() > 1) {
|
||||
MG_Impl::GLImpl::PopDebugGroup();
|
||||
}
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
static GLint StackDepth() {
|
||||
GLint depth = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_DEBUG_GROUP_STACK_DEPTH, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
static GLuint GenTexture() {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
return texture;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DebugTest, StackDepthStartsAtOneAndTracksPushesAndPops) {
|
||||
// GL 4.6 core 20.6: the context is created with one group on the stack, so 0 is never a
|
||||
// legal answer - which is what the old stub reported.
|
||||
EXPECT_EQ(StackDepth(), 1);
|
||||
|
||||
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 1, -1, "outer");
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(StackDepth(), 2);
|
||||
|
||||
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_THIRD_PARTY, 2, -1, "inner");
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(StackDepth(), 3);
|
||||
|
||||
MG_Impl::GLImpl::PopDebugGroup();
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(StackDepth(), 2);
|
||||
|
||||
MG_Impl::GLImpl::PopDebugGroup();
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(StackDepth(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, PoppingTheBaseGroupIsStackUnderflow) {
|
||||
ASSERT_EQ(StackDepth(), 1);
|
||||
MG_Impl::GLImpl::PopDebugGroup();
|
||||
ExpectSingleGlError(GL_STACK_UNDERFLOW);
|
||||
EXPECT_EQ(StackDepth(), 1) << "a refused pop must not move the stack";
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, PushingPastTheAdvertisedLimitIsStackOverflow) {
|
||||
GLint limit = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_GROUP_STACK_DEPTH, &limit);
|
||||
ASSERT_GE(limit, 64) << "KHR_debug floors GL_MAX_DEBUG_GROUP_STACK_DEPTH at 64";
|
||||
|
||||
// Nesting exactly to the advertised limit must WORK - an implementation whose real limit
|
||||
// is lower than the one it reports is worse than one that reports a lower limit.
|
||||
for (GLint i = 1; i < limit; ++i) {
|
||||
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, "deep");
|
||||
}
|
||||
DrainPendingGlErrors();
|
||||
EXPECT_EQ(StackDepth(), limit);
|
||||
|
||||
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, "too deep");
|
||||
ExpectSingleGlError(GL_STACK_OVERFLOW);
|
||||
EXPECT_EQ(StackDepth(), limit) << "a refused push must not move the stack";
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, OnlyApplicationAndThirdPartySourcesMayBePushed) {
|
||||
// 20.2 reserves every other source for the implementation.
|
||||
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_API, 0, -1, "not mine to push");
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
EXPECT_EQ(StackDepth(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, DebugMessageInsertValidatesItsEnums) {
|
||||
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0,
|
||||
GL_DEBUG_SEVERITY_NOTIFICATION, -1, "hello");
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_MARKER, 0,
|
||||
GL_DEBUG_SEVERITY_NOTIFICATION, -1, "bad source");
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
|
||||
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_TEXTURE_2D, 0,
|
||||
GL_DEBUG_SEVERITY_NOTIFICATION, -1, "bad type");
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
|
||||
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0, GL_TEXTURE_2D, -1,
|
||||
"bad severity");
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, AMessageLongerThanTheAdvertisedLimitIsInvalidValue) {
|
||||
GLint limit = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_MESSAGE_LENGTH, &limit);
|
||||
ASSERT_GT(limit, 0);
|
||||
const std::string tooLong(static_cast<std::size_t>(limit) + 1, 'x');
|
||||
|
||||
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0,
|
||||
GL_DEBUG_SEVERITY_NOTIFICATION, -1, tooLong.c_str());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, ALabelWrittenComesBack) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "coverage_stencil");
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLchar buffer[64] = {};
|
||||
GLsizei length = -1;
|
||||
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
// 20.5: the returned length excludes the terminator.
|
||||
EXPECT_EQ(length, static_cast<GLsizei>(std::string("coverage_stencil").size()));
|
||||
EXPECT_STREQ(buffer, "coverage_stencil");
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, LabelsAreScopedToTheObjectAndItsType) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "the texture");
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_BUFFER, buffer, -1, "the buffer");
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLchar textureLabel[32] = {};
|
||||
GLchar bufferLabel[32] = {};
|
||||
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(textureLabel), nullptr, textureLabel);
|
||||
MG_Impl::GLImpl::GetObjectLabel(GL_BUFFER, buffer, sizeof(bufferLabel), nullptr, bufferLabel);
|
||||
DrainPendingGlErrors();
|
||||
// The two names may collide numerically - they are separate namespaces - so a label store
|
||||
// keyed on the name alone would hand one object's label to the other.
|
||||
EXPECT_STREQ(textureLabel, "the texture");
|
||||
EXPECT_STREQ(bufferLabel, "the buffer");
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, AnUnlabelledObjectAnswersWithAnEmptyString) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLchar buffer[8] = {'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x'};
|
||||
GLsizei length = -1;
|
||||
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(length, 0);
|
||||
EXPECT_STREQ(buffer, "");
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, ALabelIsTruncatedToTheBufferAndStaysTerminated) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "abcdefgh");
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLchar buffer[4] = {};
|
||||
GLsizei length = -1;
|
||||
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(length, 3) << "bufSize includes the terminator, so only bufSize-1 characters fit";
|
||||
EXPECT_STREQ(buffer, "abc");
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, LabellingSomethingThatDoesNotExistIsInvalidValue) {
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, 0xFFFFFFFFu, -1, "nothing");
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, LabellingANonObjectTypeIsInvalidEnum) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE_2D, texture, -1, "not an object type");
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
TEST_F(DebugTest, ANullLabelRemovesTheLabel) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "temporary");
|
||||
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, 0, nullptr);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLsizei length = -1;
|
||||
GLchar buffer[16] = {};
|
||||
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(length, 0);
|
||||
}
|
||||
} // namespace
|
||||
@@ -19,6 +19,24 @@ target_link_libraries(
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(TextureTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
add_executable(
|
||||
TextureViewTest
|
||||
TextureViewTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(TextureViewTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
TextureViewTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
gtest_discover_tests(TextureViewTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
add_executable(
|
||||
VkClearManagerTest
|
||||
VkClearManagerTest.cpp
|
||||
|
||||
@@ -1642,6 +1642,38 @@ TEST_F(TextureTest, TexStorage2DTrimsALongerPreExistingMipChain) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.19: for GL_TEXTURE_1D_ARRAY the `height` argument of glTexStorage2D is the LAYER
|
||||
// COUNT, and an array texture's layer count "stays put all the way down the chain" (8.14.3) - only
|
||||
// the image's own axes halve. Shrinking it made level i report height >> i layers, which is also
|
||||
// what ComputeMipmapCompleteForFilter reads (it holds component 1 constant for this target), so
|
||||
// every mipmapped 1D array texture judged itself incomplete.
|
||||
TEST_F(TextureTest, TexStorage2DKeepsA1DArrayLayerCountConstantDownTheMipChain) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, texture);
|
||||
|
||||
constexpr GLsizei kLevels = 3;
|
||||
constexpr GLsizei kWidth = 4;
|
||||
constexpr GLsizei kLayers = 4;
|
||||
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_1D_ARRAY, kLevels, GL_RGBA8, kWidth, kLayers);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
ASSERT_NE(mipmapObject, nullptr);
|
||||
ASSERT_EQ(mipmapObject->GetMipmapLevelCount(), static_cast<Uint>(kLevels));
|
||||
|
||||
for (GLsizei level = 0; level < kLevels; ++level) {
|
||||
const IntVec3 size =
|
||||
mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture1DArray, static_cast<Uint>(level));
|
||||
EXPECT_EQ(size.x(), std::max<GLsizei>(1, kWidth >> level)) << "level " << level << " width";
|
||||
EXPECT_EQ(size.y(), kLayers) << "level " << level << " must keep every layer";
|
||||
}
|
||||
|
||||
// The completeness walk is the reason this matters beyond the reported extent.
|
||||
EXPECT_TRUE(textureObject->IsComplete());
|
||||
}
|
||||
|
||||
// glTexImage2D used to reject every GL_COMPRESSED_* internal format with GL_INVALID_ENUM, because
|
||||
// none of them mapped to a TextureInternalFormat and the "unknown format" gate fired. They now
|
||||
// resolve to the uncompressed storage that backs them - what GL prescribes for the generic formats,
|
||||
@@ -4033,6 +4065,8 @@ TEST_F(TextureTest, ThreeChannelWideningRetargetsInternalFormatAndTransferPairTo
|
||||
const Flags<PixelFormatNormalizeOptionBit> widenNoSnorm16 =
|
||||
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget |
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
const Flags<PixelFormatNormalizeOptionBit> widenNoNorm16 =
|
||||
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget | PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
|
||||
const Case cases[] = {
|
||||
// Complementary's colortex1 and colortex2. The transfer pair used to stay three-channel
|
||||
@@ -4047,10 +4081,16 @@ TEST_F(TextureTest, ThreeChannelWideningRetargetsInternalFormatAndTransferPairTo
|
||||
// cannot render to the encoding gets the 32-bit float rather than the half.
|
||||
{GL_RGB16_SNORM, widen, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16.
|
||||
{GL_RGB16, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB10, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB12, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16. The same-width sibling
|
||||
// whenever the driver has EXT_texture_norm16 - which is what keeps the whole 48-bit
|
||||
// ARB_texture_view class on one ES view class, so a GL_RGB16 texture can be viewed as
|
||||
// GL_RGB16UI - and the 32-bit float only when it does not.
|
||||
{GL_RGB16, widen, GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT},
|
||||
{GL_RGB10, widen, GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT},
|
||||
{GL_RGB12, widen, GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT},
|
||||
{GL_RGB16, widenNoNorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB10, widenNoNorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB12, widenNoNorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// sRGB and the integer formats: the base format has to move to the four-channel one of the
|
||||
// right class, GL_RGBA_INTEGER included.
|
||||
{GL_SRGB8, widen, GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
// MobileGL - MobileGL/MG_Test/Texture/TextureViewTest.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 frontend half of glTextureView (ARB_texture_view / GL 4.6 core 8.18): its error surface and
|
||||
// the state it derives. The cases below are the conformance suite's own list
|
||||
// (KHR-GL43.texture_view.errors, a..s) plus the composition rules 8.18 spells out, which
|
||||
// KHR-GL43.texture_view.gettexparameter checks.
|
||||
//
|
||||
// No backend is involved: glTextureView creates a frontend texture object whose storage is
|
||||
// another object's, and everything asserted here is decided before any driver sees it. The one
|
||||
// backend fact that matters is whether the active backend can share storage between two texture
|
||||
// names at all - MobileGL keys that on the advertised GL_ARB_texture_view string, so the fixture
|
||||
// installs a backend that advertises it (and one test removes it again, to pin the refusal).
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_Util/GLExtensions.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
namespace {
|
||||
// A backend that claims exactly one thing: whether it can back a texture view. Everything
|
||||
// else about it is inert, because nothing else in this file reaches a backend.
|
||||
class TextureViewCapabilityBackend final : public MG_Backend::BackendObject {
|
||||
public:
|
||||
explicit TextureViewCapabilityBackend(Bool advertiseTextureView) {
|
||||
m_info.RendererName = "TextureViewTest";
|
||||
if (advertiseTextureView) {
|
||||
m_info.RendererGLInfo.Extensions.push_back(E_GL_ARB_texture_view);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize() override {}
|
||||
Bool InitCapabilities() override { return true; }
|
||||
Bool InitWindowSurface() override { return true; }
|
||||
const RendererInfo& GetRendererInfo() const override { return m_info; }
|
||||
String GetBackendAPIVersionString() const override { return {}; }
|
||||
const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override {
|
||||
static MG_Backend::GlobalBackendFunctionsTable table = {};
|
||||
return table;
|
||||
}
|
||||
const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
|
||||
static MG_Backend::DynamicBackendParameters params = {};
|
||||
return params;
|
||||
}
|
||||
BackendType GetBackendType() const override { return BackendType::Unknown; }
|
||||
|
||||
private:
|
||||
RendererInfo m_info;
|
||||
};
|
||||
|
||||
class ScopedBackendOverride {
|
||||
public:
|
||||
explicit ScopedBackendOverride(Bool advertiseTextureView)
|
||||
: m_previous(Move(MG_Backend::pActiveBackendObject)) {
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<TextureViewCapabilityBackend>(advertiseTextureView);
|
||||
}
|
||||
~ScopedBackendOverride() { MG_Backend::pActiveBackendObject = Move(m_previous); }
|
||||
|
||||
private:
|
||||
UniquePtr<MG_Backend::BackendObject> m_previous;
|
||||
};
|
||||
|
||||
class TextureViewTest : public ::testing::Test {
|
||||
protected:
|
||||
// GL error flags are sticky per error code and the context outlives an individual test in
|
||||
// this binary, so anything an earlier test left pending would be handed to the next
|
||||
// GetError() call - which silently turns error-code assertions into reads of someone
|
||||
// else's error. Bounded because there is one flag per code.
|
||||
static void DrainPendingGlErrors() {
|
||||
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
|
||||
// The call under test must raise exactly the expected error and nothing more: a second
|
||||
// pending error means one entry point queued several, which GetError() would hand out at
|
||||
// an unrelated call site later on.
|
||||
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();
|
||||
m_backend = MakeUnique<ScopedBackendOverride>(true);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
m_backend.reset();
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||
}
|
||||
|
||||
static GLuint GenTexture() {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A bound, immutable GL_TEXTURE_2D. `levels` levels of `size` x `size` RGBA8 unless a
|
||||
// caller wants otherwise.
|
||||
static GLuint MakeImmutable2D(GLsizei levels = 2, GLsizei width = 16, GLsizei height = 16,
|
||||
GLenum internalFormat = GL_RGBA8) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static GLuint MakeImmutable2DArray(GLsizei levels = 1, GLsizei size = 16, GLsizei layers = 6) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, levels, GL_RGBA8, size, size, layers);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static GLint GetViewParameter(GLuint texture, GLenum target, GLenum pname) {
|
||||
GLint value = -1;
|
||||
MG_Impl::GLImpl::BindTexture(target, texture);
|
||||
MG_Impl::GLImpl::GetTexParameteriv(target, pname, &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
UniquePtr<ScopedBackendOverride> m_backend;
|
||||
};
|
||||
|
||||
// ============================ the state TexStorage* seeds ============================
|
||||
// GL 4.6 core 8.19 leaves an immutable texture describing itself as a full-extent view of its
|
||||
// own storage. That is not cosmetic: glTextureView COMPOSES onto these values, so if they
|
||||
// stayed at the mutable default of 0 every view would clamp to zero levels.
|
||||
|
||||
TEST_F(TextureViewTest, MutableTextureReportsNoViewState) {
|
||||
const GLuint texture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 0);
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 0);
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER), 0);
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS), 0);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, TexStorageSeedsTheFullExtentAsViewState) {
|
||||
const GLuint texture = MakeImmutable2D(3, 16, 16);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 0);
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 3);
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER), 0);
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS), 1);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, TexStorageOnAnArrayReportsItsLayerCount) {
|
||||
const GLuint texture = MakeImmutable2DArray(1, 16, 6);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_VIEW_NUM_LAYERS), 6);
|
||||
}
|
||||
|
||||
// ============================ the derived view state ============================
|
||||
|
||||
TEST_F(TextureViewTest, ViewDerivesItsRangeAndInheritsImmutableLevels) {
|
||||
const GLuint storage = MakeImmutable2D(3, 16, 16);
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 1);
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 2);
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER), 0);
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS), 1);
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_FORMAT), GL_TRUE);
|
||||
// 8.18: "TEXTURE_IMMUTABLE_LEVELS is set to the value of TEXTURE_IMMUTABLE_LEVELS from
|
||||
// the ORIGINAL texture" - not to <numlevels>.
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 3);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, ViewClampsItsLevelCountToWhatRemains) {
|
||||
const GLuint storage = MakeImmutable2D(3, 16, 16);
|
||||
const GLuint view = GenTexture();
|
||||
// 8.18: NUM_LEVELS is "the lesser of <numlevels> and TEXTURE_VIEW_NUM_LEVELS from the
|
||||
// original minus <minlevel>", so an over-large request clamps rather than erroring.
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 2, 10, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 2);
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 1);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, ViewOfAViewComposesOntoTheOriginalRatherThanRestarting) {
|
||||
const GLuint storage = MakeImmutable2D(4, 32, 32);
|
||||
const GLuint first = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(first, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 3, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
const GLuint second = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(second, GL_TEXTURE_2D, first, GL_RGBA8, 2, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
// 8.18: MIN_LEVEL is "<minlevel> plus TEXTURE_VIEW_MIN_LEVEL from the original".
|
||||
EXPECT_EQ(GetViewParameter(second, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 3);
|
||||
EXPECT_EQ(GetViewParameter(second, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 1);
|
||||
EXPECT_EQ(GetViewParameter(second, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 4);
|
||||
|
||||
// And the composed view points at the ROOT, not at the intermediate one - which is what
|
||||
// lets both backends resolve a view's storage in a single hop.
|
||||
const auto& secondObject = MG_State::pGLContext->GetTextureObject(second);
|
||||
const auto& storageObject = MG_State::pGLContext->GetTextureObject(storage);
|
||||
ASSERT_TRUE(secondObject != nullptr);
|
||||
EXPECT_TRUE(secondObject->IsTextureView());
|
||||
EXPECT_EQ(secondObject->GetViewStorageOwner().get(), storageObject.get());
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, ViewCarriesItsOwnParametersAndFormat) {
|
||||
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_DEPTH24_STENCIL8);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, view);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// This divergence IS the feature (it is what Better Clouds uses glTextureView for): one
|
||||
// storage read through two names with two different aspects in the same shading pass.
|
||||
EXPECT_EQ(GetViewParameter(storage, GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE), GL_STENCIL_INDEX);
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE), GL_DEPTH_COMPONENT);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, DeletingTheOriginalLeavesTheViewsStorageAlive) {
|
||||
const GLuint storage = MakeImmutable2D(1, 16, 16);
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const auto storageObject = MG_State::pGLContext->GetTextureObject(storage);
|
||||
ASSERT_TRUE(storageObject != nullptr);
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &storage);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// GL 4.6 core 5.1.2: the NAME is gone, the object is not - something still refers to it.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsTexture(storage), static_cast<GLboolean>(GL_FALSE));
|
||||
const auto& viewObject = MG_State::pGLContext->GetTextureObject(view);
|
||||
ASSERT_TRUE(viewObject != nullptr);
|
||||
EXPECT_EQ(viewObject->GetViewStorageOwner().get(), storageObject.get());
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_FORMAT), GL_TRUE);
|
||||
}
|
||||
|
||||
// ============================ the error surface ============================
|
||||
// KHR-GL43.texture_view.errors walks these in this order; the letters are its own.
|
||||
|
||||
TEST_F(TextureViewTest, ZeroTextureIsInvalidValue) { // (a)
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(0, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, TextureThatGenTexturesNeverReturnedIsInvalidOperation) { // (b)
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(0xFFFFFFFFu, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, AlreadyBoundTextureIsInvalidOperation) { // (c)
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
const GLuint alreadyBound = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, alreadyBound);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::TextureView(alreadyBound, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, OrigTextureThatIsNotATextureObjectIsInvalidValue) { // (d)
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
// Note the code differs from (b): INVALID_VALUE here, INVALID_OPERATION there.
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, 0xFFFFFFFFu, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, MutableOrigTextureIsInvalidOperation) { // (e)
|
||||
const GLuint mutableTexture = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, mutableTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, mutableTexture, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, IncompatibleTargetPairIsInvalidOperation) { // (f)
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
// Table 8.20 admits 2D -> 2D and 2D -> 2D_ARRAY, and nothing else.
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_3D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, LegalTargetPairsAreAccepted) { // (f), the other way round
|
||||
const GLuint storage = MakeImmutable2D(1, 16, 16);
|
||||
const GLuint sameTarget = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(sameTarget, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
const GLuint arrayView = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(arrayView, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, FormatFromAnotherViewClassIsInvalidOperation) { // (g)
|
||||
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_RGBA8);
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
// GL_RGBA8 is VIEW_CLASS_32_BITS; GL_R8 is VIEW_CLASS_8_BITS.
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_R8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, FormatFromTheSameViewClassIsAccepted) { // (g), the other way round
|
||||
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_RGBA8);
|
||||
const GLuint view = GenTexture();
|
||||
// Both VIEW_CLASS_32_BITS.
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLint internalFormat = 0;
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, view);
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
|
||||
DrainPendingGlErrors();
|
||||
EXPECT_EQ(internalFormat, GL_R32UI) << "the view must take the format it was asked for, not its parent's";
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, ClasslessFormatMayOnlyBeViewedAsItself) { // (h)
|
||||
// Depth, stencil and depth/stencil formats have NO entry in table 8.21, which the spec
|
||||
// turns into a stricter rule than "same class": the view's format must be IDENTICAL.
|
||||
// This is the rule the Better Clouds D24S8 view depends on being permissive enough.
|
||||
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_DEPTH24_STENCIL8);
|
||||
const GLuint sameFormat = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(sameFormat, GL_TEXTURE_2D, storage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const GLuint otherFormat = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(otherFormat, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, MinLevelPastTheLastLevelIsInvalidValue) { // (i)
|
||||
const GLuint storage = MakeImmutable2D(2, 16, 16);
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 2, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, MinLayerPastTheLastLayerIsInvalidValue) { // (j)
|
||||
const GLuint storage = MakeImmutable2DArray(1, 16, 4);
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 4, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, CubeMapViewDemandsExactlySixLayers) { // (k)
|
||||
const GLuint storage = MakeImmutable2DArray(1, 16, 6);
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, 0, 5);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, CubeMapArrayViewDemandsAMultipleOfSixLayers) { // (l)
|
||||
const GLuint storage = MakeImmutable2DArray(1, 16, 12);
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP_ARRAY, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, SingleLayerTargetsRejectMoreThanOneLayer) { // (m).. (q)
|
||||
const GLuint storage = MakeImmutable2DArray(1, 16, 4);
|
||||
DrainPendingGlErrors();
|
||||
for (const GLenum target : {GL_TEXTURE_2D}) {
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, target, storage, GL_RGBA8, 0, 1, 0, 2);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
// The 1D and 3D forms take the same rule; check one of them from a legal parent so the
|
||||
// target-pair rule cannot be what is rejecting it.
|
||||
const GLuint texture3D = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture3D);
|
||||
MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 8, 8, 8);
|
||||
DrainPendingGlErrors();
|
||||
const GLuint view3D = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view3D, GL_TEXTURE_3D, texture3D, GL_RGBA8, 0, 1, 0, 2);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, CubeMapViewDemandsSquareLevels) { // (r)
|
||||
const GLuint storage = GenTexture();
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 32, 33, 6);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, 0, 6);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, BufferTextureHasNoLegalViewTarget) {
|
||||
// Table 8.20 lists nothing for GL_TEXTURE_BUFFER: its storage is a buffer object, so
|
||||
// there is no image to view.
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_BUFFER, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, NonTextureTargetIsInvalidEnum) {
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_ARRAY_BUFFER, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, AFailedCallLeavesTheNameUninstantiated) {
|
||||
// The spec's "texture must not already have a target" rule means a rejected call has to
|
||||
// leave the name exactly as GenTextures left it, or a retry would then fail with (c).
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
const GLuint view = GenTexture();
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_3D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(view));
|
||||
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(view));
|
||||
}
|
||||
|
||||
// ============================ the no-support contract ============================
|
||||
|
||||
TEST_F(TextureViewTest, BackendWithoutTextureViewSupportRaisesInvalidOperation) {
|
||||
const GLuint storage = MakeImmutable2D();
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// A backend that cannot give two texture names one storage - ES without
|
||||
// EXT/OES_texture_view - withholds GL_ARB_texture_view, and glTextureView must then FAIL
|
||||
// rather than quietly produce a view with no storage. A silent no-op is the one
|
||||
// unacceptable answer: it is indistinguishable from success at the call site, and the
|
||||
// application renders from a texture that aliases nothing.
|
||||
const ScopedBackendOverride noTextureView(false);
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(view));
|
||||
}
|
||||
} // namespace
|
||||
@@ -514,6 +514,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// Optional: absent on an ES 3.2 core driver, and absent on ES 3.1 without the
|
||||
// matching extension. The tier resolution below picks whichever spelling the
|
||||
// driver's own support actually comes from.
|
||||
// Optional by nature: ES never made texture views core, so both spellings are
|
||||
// absent on plenty of drivers and neither absence is an error.
|
||||
INIT_GLES_FUNC_OPTIONAL(glTextureViewEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glTextureViewOES)
|
||||
INIT_GLES_FUNC_OPTIONAL(glTexBufferEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glTexBufferOES)
|
||||
INIT_GLES_FUNC_OPTIONAL(glTexBufferRangeEXT)
|
||||
@@ -889,6 +893,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// Resolved into caps.TextureBufferSupport below, once the ES version is also known.
|
||||
Bool hasExtTextureBuffer = false;
|
||||
Bool hasOesTextureBuffer = false;
|
||||
// Resolved into caps.SupportsTextureView below. Two spellings of one extension; the
|
||||
// entry points differ only in suffix, so unlike the buffer-texture tier there is nothing
|
||||
// downstream that needs to know WHICH one answered.
|
||||
Bool hasExtTextureView = false;
|
||||
Bool hasOesTextureView = false;
|
||||
// Combined with the three entry points below; DirectGLES emulates baseInstance when this
|
||||
// comes out false, so a stub pointer counting as support would silently break the draws.
|
||||
Bool hasBaseInstanceExtension = false;
|
||||
@@ -925,6 +934,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
std::strcmp(extension, "GL_OES_texture_cube_map_array") == 0) {
|
||||
caps.SupportsTextureCubeMapArray = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_texture_view") == 0) {
|
||||
hasExtTextureView = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_OES_texture_view") == 0) {
|
||||
hasOesTextureView = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_texture_buffer") == 0) {
|
||||
hasExtTextureBuffer = true;
|
||||
}
|
||||
@@ -985,6 +1000,18 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glDrawArraysInstancedBaseInstanceEXT != nullptr &&
|
||||
glesFuncs.glDrawElementsInstancedBaseInstanceEXT != nullptr &&
|
||||
glesFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT != nullptr;
|
||||
// ES has no core texture views at any version, so this is extension-only by nature.
|
||||
// Each spelling must bring its OWN entry point: a driver that advertises the OES string
|
||||
// is not required to export glTextureViewEXT.
|
||||
caps.SupportsTextureView = (hasExtTextureView && glesFuncs.glTextureViewEXT != nullptr) ||
|
||||
(hasOesTextureView && glesFuncs.glTextureViewOES != nullptr);
|
||||
// Escape hatch for the integration suite: the no-extension path is the one MobileGL has
|
||||
// to refuse honestly rather than emulate, and on a driver that HAS the extension there
|
||||
// would otherwise be no way to exercise that refusal (see TextureViewScenario).
|
||||
if (std::getenv("MOBILEGL_DISABLE_TEXTURE_VIEW") != nullptr) {
|
||||
MGLOG_I("MOBILEGL_DISABLE_TEXTURE_VIEW is set; reporting no EXT/OES_texture_view support");
|
||||
caps.SupportsTextureView = false;
|
||||
}
|
||||
// Core from ES 3.2 on, so an extension string is not required there; below 3.2 the
|
||||
// extension is, and the pointer still has to have resolved either way.
|
||||
const Bool esAtLeast32 = caps.GLESVersion.Major > 3 ||
|
||||
|
||||
@@ -604,6 +604,12 @@ namespace MobileGL {
|
||||
// support comes from GL_EXT_texture_buffer or GL_OES_texture_buffer exports the
|
||||
// suffixed spellings instead, and a strict eglGetProcAddress returns NULL for the core
|
||||
// one there - so resolving only the core name makes both extension tiers look absent.
|
||||
GL_FUNC_TYPEDEF(void, glTextureViewEXT, GLuint texture, GLenum target, GLuint origtexture,
|
||||
GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer,
|
||||
GLuint numlayers)
|
||||
GL_FUNC_TYPEDEF(void, glTextureViewOES, GLuint texture, GLenum target, GLuint origtexture,
|
||||
GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer,
|
||||
GLuint numlayers)
|
||||
GL_FUNC_TYPEDEF(void, glTexBufferEXT, GLenum target, GLenum internalformat, GLuint buffer)
|
||||
GL_FUNC_TYPEDEF(void, glTexBufferOES, GLenum target, GLenum internalformat, GLuint buffer)
|
||||
GL_FUNC_TYPEDEF(void, glTexBufferRangeEXT, GLenum target, GLenum internalformat, GLuint buffer,
|
||||
@@ -1023,6 +1029,8 @@ namespace MobileGL {
|
||||
GL_FUNC_DECL(glGetSamplerParameterIuiv)
|
||||
GL_FUNC_DECL(glTexBuffer)
|
||||
GL_FUNC_DECL(glTexBufferRange)
|
||||
GL_FUNC_DECL(glTextureViewEXT)
|
||||
GL_FUNC_DECL(glTextureViewOES)
|
||||
GL_FUNC_DECL(glTexBufferEXT)
|
||||
GL_FUNC_DECL(glTexBufferOES)
|
||||
GL_FUNC_DECL(glTexBufferRangeEXT)
|
||||
@@ -1086,6 +1094,14 @@ namespace MobileGL {
|
||||
Bool SupportsTextureBorderClamp = false;
|
||||
// GL_TEXTURE_CUBE_MAP_ARRAY: ES 3.2 core, or EXT/OES_texture_cube_map_array before it.
|
||||
Bool SupportsTextureCubeMapArray = false;
|
||||
// GL_EXT_texture_view / GL_OES_texture_view: two ES texture names sharing one
|
||||
// storage, i.e. the only way DirectGLES can answer glTextureView at all. ES never
|
||||
// made this core - not even in 3.2 - so unlike every other capability here there is
|
||||
// no version that implies it, and a driver without it leaves MobileGL with no honest
|
||||
// implementation (a copy is not a view: writes through one name must be visible
|
||||
// through the other). Gate on this, never on the entry points: eglGetProcAddress
|
||||
// hands back live-looking stubs (see AcquireGLESFunctions).
|
||||
Bool SupportsTextureView = false;
|
||||
// Which spelling of buffer-texture support the host driver has. Desktop GL makes buffer
|
||||
// textures core from 3.1 on, so the frontend advertises them unconditionally and an app
|
||||
// may call glTexBuffer at any time; ES only gained them in 3.2, and before that only
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverBugProbes.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
// ===================== KNOWN DRIVER BUGS =====================
|
||||
//
|
||||
// THIS IS THE DESIGNATED HOME FOR DRIVER-CAPABILITY LIES.
|
||||
//
|
||||
// The rest of the POST suite answers a different question: does the extension exist, and
|
||||
// does a simple probe show it working. The entries here are not extension questions at
|
||||
// all - they are CORE functionality that a driver advertises, accepts without error, and
|
||||
// then does not perform. Nothing in an extension string or a limit query says so, which
|
||||
// is exactly why each one needs its own executable probe.
|
||||
//
|
||||
// The inventory comes from CAMPAIGN FINDINGS, not from anything the driver reports.
|
||||
//
|
||||
// EVERY PROBE MUST CARRY A CONTROL. The geometry entry below is why the rule is written
|
||||
// down: the same defect was first characterised as "this driver drops all geometry-stage
|
||||
// storage-buffer writes", which would have justified withdrawing
|
||||
// GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS entirely. A control showed geometry-stage writes
|
||||
// land perfectly well when they precede EmitVertex(), so the limit is not a lie and
|
||||
// withdrawing it would have broken shaders that work today. A probe without a control
|
||||
// measures a symptom and invites exactly that over-correction.
|
||||
//
|
||||
// ADDING A SIBLING IS ONE FUNCTION: write an `Optional<DriverBugFinding> ProbeXxx(gl)`
|
||||
// that returns nullopt when the driver is not affected, and add it to the table in
|
||||
// CollectGlesKnownDriverBugs().
|
||||
|
||||
// What MobileGL can do about a bug this device HAS. There is deliberately no "not
|
||||
// affected" member: a driver that passes the probe produces no finding at all, so the
|
||||
// report only ever lists bugs actually present on this device.
|
||||
enum class DriverBugVerdict : Uint8 {
|
||||
// A MobileGL quirk repairs or substitutes for the defect and the application sees
|
||||
// correct behaviour.
|
||||
Fixed,
|
||||
// There is no substitute. `detail` says what MobileGL does defensively instead, and
|
||||
// what an application can still rely on.
|
||||
Unfixable,
|
||||
};
|
||||
|
||||
struct DriverBugFinding {
|
||||
// Short name of the bug, not of the feature.
|
||||
String name;
|
||||
DriverBugVerdict verdict = DriverBugVerdict::Unfixable;
|
||||
// One line: what the driver does wrong, and what MobileGL does about it.
|
||||
String detail;
|
||||
};
|
||||
|
||||
// Draws one point through VS+GS+FS whose geometry stage writes two storage buffers: one
|
||||
// BEFORE its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the
|
||||
// before-emit write lands and the after-emit write does not.
|
||||
//
|
||||
// The before-emit write is the control, and it is the whole point of the probe. Adreno 830
|
||||
// discards geometry-stage storage writes issued after the last emit while performing the
|
||||
// identical write issued before it (measured both ways, and for both point and triangle
|
||||
// geometry shaders, so the primitive shape is not the variable). Reading only the
|
||||
// after-emit half would say "geometry storage writes do not work on this driver", which is
|
||||
// false and would justify withdrawing a limit applications legitimately use.
|
||||
//
|
||||
// Deterministic by construction - the write either reaches memory or the driver
|
||||
// structurally discards it - so the answer is latched, not sampled. Returns false when the
|
||||
// driver advertises no geometry storage blocks, when an entry point is missing, or when
|
||||
// anything about the probe fails to set up: an inconclusive probe must never be reported
|
||||
// as a bug. Restores every piece of GL state it touches.
|
||||
Bool ProbeGeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeGeometryStageSsboWriteAfterEmitDropped(), evaluated at most once per process.
|
||||
Bool GeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Samples one R32F GL_TEXTURE_2D_MULTISAMPLE texel through a swizzled alpha channel, twice,
|
||||
// with a separately linked program each time. Returns true only when the swizzled read goes
|
||||
// wrong while every control read stays right.
|
||||
//
|
||||
// Adreno 830 returns uninitialised memory - a different value every run - for
|
||||
// texelFetch(sampler2DMS, ..., sampleIndex != 0).w on an R32F multisample texture whose
|
||||
// GL_TEXTURE_SWIZZLE_A is not the default, from the SECOND such program in the context
|
||||
// onward. The first program reads correctly, which is why the probe links two.
|
||||
//
|
||||
// THREE CONTROLS, each identical to the subject but for one variable, and all three must
|
||||
// read correctly for a wrong subject to count: (1) the same fetch with
|
||||
// GL_TEXTURE_SWIZZLE_A left at its default, (2) the same fetch at sample index 0, and
|
||||
// (3) the same swizzled texture read through .x instead of .w. Without them a driver that
|
||||
// simply cannot render R32F, or cannot sample multisample textures at all, would be
|
||||
// reported as having this very specific corruption.
|
||||
//
|
||||
// Returns false when the driver cannot host the shape (no multisample R32F colour target,
|
||||
// fewer than two samples, a missing entry point, an incomplete framebuffer): an
|
||||
// inconclusive probe must never be reported as a bug. Restores every piece of GL state it
|
||||
// touches.
|
||||
Bool ProbeR32FMultisampleSwizzleCorruption(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeR32FMultisampleSwizzleCorruption(), evaluated at most once per process.
|
||||
Bool R32FMultisampleSwizzleCorrupted(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// What the image-location budget probe measured. `detected` is the only field the verdict
|
||||
// depends on; the rest exist so the report can say what the shape was instead of asserting
|
||||
// a number that was true on one device in one campaign.
|
||||
struct ImageLocationBudgetMeasurement {
|
||||
Bool detected = false;
|
||||
// Image uniforms declared per stage in both the subject and the control - one more than
|
||||
// GL_MAX_GEOMETRY_IMAGE_UNIFORMS, which is the smallest of the three stages' budgets.
|
||||
Int perStageImageUniforms = 0;
|
||||
// Distinct uniform NAMES in the subject (per-stage-unique) and in the control (shared).
|
||||
Int subjectDistinctNames = 0;
|
||||
Int controlDistinctNames = 0;
|
||||
// The first line of the driver's info log for the failing link, so the report quotes the
|
||||
// driver rather than paraphrasing it.
|
||||
String driverMessage;
|
||||
};
|
||||
|
||||
// Links the same three-stage (vertex, geometry, fragment) program twice: once with every
|
||||
// stage naming its image uniforms uniquely, once with all three stages sharing one set of
|
||||
// names. Both declare the same number of image uniforms per stage, on the same bindings,
|
||||
// with the same qualifier and the same stores - the names are the only difference.
|
||||
//
|
||||
// Adreno 830 charges its image-location budget per distinct NAME, so the shared-name program
|
||||
// links while the per-stage-named one is rejected with "Image location or component exceeds
|
||||
// max allowed", even though nothing about the image USAGE changed. That is what makes the
|
||||
// shared-name link the control: it proves the driver can host this exact amount of image
|
||||
// work and that only the naming moved the answer.
|
||||
//
|
||||
// `detected` is false unless the subject fails AND the control links. Both failing means the
|
||||
// shape is simply too large for the driver (an honest refusal); both linking means the
|
||||
// driver does not have this bug.
|
||||
ImageLocationBudgetMeasurement ProbeImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeImageLocationPerNameBudget(), evaluated at most once per process.
|
||||
const ImageLocationBudgetMeasurement& ImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Draws one quad whose vertex stage stores to a `coherent writeonly` image and whose
|
||||
// fragment stage reads the same image declared `coherent readonly` under the SAME name, then
|
||||
// checks every fragment saw the store. Returns true only when the same-name program loses
|
||||
// the store while the different-name control keeps it.
|
||||
//
|
||||
// Adreno 830 merges the two declarations into one uniform and silently discards the writing
|
||||
// stage's stores. The control is the identical pair of shaders with the two halves renamed -
|
||||
// exactly what MobileGL's image-uniform repair emits - which keeps every store. Without it
|
||||
// the probe would be indistinguishable from "this driver cannot store to images from the
|
||||
// vertex stage", which is a different and much larger claim.
|
||||
//
|
||||
// Returns false when the driver advertises no vertex-stage image uniforms, when an entry
|
||||
// point is missing, or when the setup fails.
|
||||
Bool ProbeCrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeCrossStageImageQualifierMergeDropsWrites(), evaluated at most once per process.
|
||||
Bool CrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// What the image coherency probe measured. The residual is reported rather than hard-coded:
|
||||
// it is a rate, it differs between devices, and a report that quotes a number measured
|
||||
// somewhere else is worse than no number at all.
|
||||
struct ImageCoherencyResidualMeasurement {
|
||||
Bool detected = false;
|
||||
// Texels the STRONGEST in-shader shape missed - that is what makes the defect unfixable.
|
||||
Int mismatchedTexels = 0;
|
||||
// Texels the shape MobileGL emits today missed, on the same driver in the same run. It
|
||||
// is what applications actually get, and it is not always the same number.
|
||||
Int emittedShapeMismatchedTexels = 0;
|
||||
Int totalTexels = 0;
|
||||
};
|
||||
|
||||
// Counts the texels whose dependent imageLoad() did not observe the imageStore() that
|
||||
// precedes it in the same fragment invocation.
|
||||
//
|
||||
// THE SUBJECT IS THE STRONGEST SHAPE THE LANGUAGE OFFERS - a `coherent volatile`
|
||||
// readonly/writeonly pair on one binding with BOTH memoryBarrierImage() and memoryBarrier()
|
||||
// between the store and the read - and that choice is the whole reason the row can say
|
||||
// "unfixable". Probing only the shape MobileGL emits today (`coherent` plus
|
||||
// memoryBarrierImage()) reports a bug on drivers where simply adding `volatile` makes the
|
||||
// read correct, which is a defect MobileGL could fix rather than one it cannot: measured on
|
||||
// Mesa llvmpipe, the emitted shape misses every texel while the `volatile` shape misses
|
||||
// none. Only a driver that fails even the strongest shape has no in-shader substitute left.
|
||||
//
|
||||
// The control is the same dependency split across TWO draws with a glMemoryBarrier and a
|
||||
// glFinish between them. It separates "this driver cannot make image writes visible at all"
|
||||
// (control also dirty - a far worse defect, and the probe declines to call it this one) from
|
||||
// the finding, which is about ordering inside one invocation.
|
||||
//
|
||||
// `detected` is false unless the strongest shape is dirty AND the control is clean. The
|
||||
// shape MobileGL emits is measured either way, so the report can say what applications get.
|
||||
ImageCoherencyResidualMeasurement ProbeImageWriteReadCoherencyResidual(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeImageWriteReadCoherencyResidual(), evaluated at most once per process.
|
||||
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Every known driver bug this GLES driver actually has. Bugs it does not have are absent,
|
||||
// so an unaffected device renders an empty section rather than a wall of "not affected".
|
||||
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -37,17 +37,19 @@
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
namespace {
|
||||
// Display ranks for PostCheck::displayRank: within one backend section, FAIL
|
||||
// rows render first, then WARN, PASS, INFO, then the device-driver identity
|
||||
// rows render first, then WARN, then PASS, then the device-driver identity
|
||||
// strings, and always last (regardless of status) the strings MobileGL itself
|
||||
// reports to applications. Rows are stable-sorted, so relative order within a
|
||||
// rank is preserved. Purely cosmetic: the verdict computation is unaffected.
|
||||
//
|
||||
// There is no rank between PASS and the identity blocks because there are no INFO
|
||||
// capability rows any more - see the taxonomy on ReportBuilder below.
|
||||
enum DisplayRank : Int {
|
||||
RankFail = 0,
|
||||
RankWarn = 1,
|
||||
RankPass = 2,
|
||||
RankInfo = 3,
|
||||
RankDriverReported = 4,
|
||||
RankMobileGLReported = 5,
|
||||
RankDriverReported = 3,
|
||||
RankMobileGLReported = 4,
|
||||
};
|
||||
|
||||
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
|
||||
@@ -68,6 +70,28 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"to advertise it anyway";
|
||||
}
|
||||
|
||||
// ===================== THE ROW VERDICT TAXONOMY =====================
|
||||
//
|
||||
// EVERY CAPABILITY ROW IS PASS, WARN OR FAIL. INFO IS FOR IDENTITY ONLY - renderer
|
||||
// names, version strings, driver strings - and there is deliberately no way to emit an
|
||||
// INFO capability row from here: the only INFO emitters are the two identity helpers at
|
||||
// the bottom of this struct. A row that says "not supported; no impact today" tells a
|
||||
// reader nothing about whether their application will work, which is the one question
|
||||
// the screen exists to answer.
|
||||
//
|
||||
// PASS - the backend supports the capability directly.
|
||||
// WARN - the backend does NOT support it directly, but a MobileGL quirk substitutes
|
||||
// and the application still sees correct behaviour. The detail names the
|
||||
// substitute and whatever it costs.
|
||||
// FAIL - unsupported, with no substitute: an application that uses it gets wrong
|
||||
// output, a failed draw, or nothing at all. The detail says what breaks.
|
||||
//
|
||||
// FAIL comes in two flavours, and the difference is about the BACKEND, not the row.
|
||||
// Fail() is for a capability the backend cannot start without, and it drives the
|
||||
// backend summary to UNSUPPORTED. FailOptional() is for a capability that is just as
|
||||
// unusable but that the backend runs fine without, so the summary stays DEGRADED - a
|
||||
// device with no dual-source blend still plays Minecraft, and reporting the whole
|
||||
// backend as unusable because of it would be a lie in the other direction.
|
||||
struct ReportBuilder {
|
||||
BackendPostReport report;
|
||||
Bool fatalFailed = false;
|
||||
@@ -77,20 +101,27 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
report.checks.push_back({Move(name), "PASS", Move(detail), RankPass});
|
||||
}
|
||||
|
||||
// FAIL on a capability the backend cannot run without: the backend summary becomes
|
||||
// UNSUPPORTED.
|
||||
void Fail(String name, String detail) {
|
||||
fatalFailed = true;
|
||||
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
|
||||
}
|
||||
|
||||
// FAIL on a capability with no substitute that the backend can nonetheless run
|
||||
// without. The row is as red as any other FAIL - an application using it does not
|
||||
// work - but the backend summary degrades rather than declaring the whole backend
|
||||
// unusable.
|
||||
void FailOptional(String name, String detail) {
|
||||
warnUnmet = true;
|
||||
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
|
||||
}
|
||||
|
||||
void Warn(String name, String detail) {
|
||||
warnUnmet = true;
|
||||
report.checks.push_back({Move(name), "WARN", Move(detail), RankWarn});
|
||||
}
|
||||
|
||||
void Info(String name, String detail) {
|
||||
report.checks.push_back({Move(name), "INFO", Move(detail), RankInfo});
|
||||
}
|
||||
|
||||
// A "Backend driver reported ..." identity string straight from the device
|
||||
// driver; rendered after the regular rows.
|
||||
void DriverReported(String name, String detail) {
|
||||
@@ -158,17 +189,19 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// applications DO, not just what they can do: with the extension advertised, Iris
|
||||
// and Sodium batch their pipeline compiles and poll GL_COMPLETION_STATUS_KHR.
|
||||
//
|
||||
// PASS when it is on (the intended configuration once the default flips), INFO when
|
||||
// it is off - "off" is a supported configuration, not a degradation, so it must not
|
||||
// colour the verdict. Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a
|
||||
// user reading a POST page can tell which side of the switch they are on and how to
|
||||
// change it.
|
||||
// PASS when it is on (the intended configuration once the default flips), WARN when it
|
||||
// is off: the capability is not advertised, and what stands in for it - compiling on
|
||||
// the calling thread - produces exactly the same programs, just without the overlap.
|
||||
// Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a user reading a POST page
|
||||
// can tell which side of the switch they are on and how to change it.
|
||||
void AppendAsyncShaderCompileRow(ReportBuilder& builder) {
|
||||
constexpr const char* rowName = "Asynchronous shader compilation";
|
||||
if (!MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
builder.Info(rowName,
|
||||
"off; glCompileShader and glLinkProgram run on the calling thread and "
|
||||
"GL_KHR_parallel_shader_compile is not advertised (set environment variable "
|
||||
builder.Warn(rowName,
|
||||
"off; GL_KHR_parallel_shader_compile is not advertised and "
|
||||
"glCompileShader/glLinkProgram run on the calling thread instead. The "
|
||||
"programs are identical - only the overlap is lost, so a shaderpack load "
|
||||
"takes as long as its compiles do (set environment variable "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1 to enable it)");
|
||||
return;
|
||||
}
|
||||
@@ -301,22 +334,30 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("Polygon mode",
|
||||
"glPolygonMode GL_LINE/GL_POINT available via GL_NV/ANGLE_polygon_mode");
|
||||
} else {
|
||||
builder.Warn("Polygon mode",
|
||||
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL");
|
||||
builder.FailOptional("Polygon mode",
|
||||
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT silently "
|
||||
"falls back to GL_FILL. There is no substitute - wireframe and point "
|
||||
"rasterization would have to be rebuilt out of line/point primitives - "
|
||||
"so an application asking for either gets solid triangles instead");
|
||||
}
|
||||
if (caps.SupportsIndexedColorMask) {
|
||||
builder.Pass("Indexed color mask",
|
||||
"per-draw-buffer glColorMaski available (ES 3.2 core or draw_buffers_indexed)");
|
||||
} else {
|
||||
builder.Warn("Indexed color mask",
|
||||
"no indexed glColorMaski; per-draw-buffer color masks fall back to draw buffer 0");
|
||||
builder.FailOptional("Indexed color mask",
|
||||
"no indexed glColorMaski; every per-draw-buffer colour mask collapses "
|
||||
"onto draw buffer 0's, so an MRT pass that masks its attachments "
|
||||
"differently writes the wrong channels to all but one of them, with "
|
||||
"nothing to substitute");
|
||||
}
|
||||
if (caps.SupportsDualSourceBlend) {
|
||||
builder.Pass("Dual-source blend",
|
||||
"GL_SRC1_* dual-source blend factors available via GL_EXT_blend_func_extended");
|
||||
} else {
|
||||
builder.Warn("Dual-source blend",
|
||||
"no GL_EXT_blend_func_extended; GL_SRC1_* dual-source blend factors hard-fail at draw");
|
||||
builder.FailOptional("Dual-source blend",
|
||||
"no GL_EXT_blend_func_extended; a draw using a GL_SRC1_* blend factor "
|
||||
"hard-fails, and a second fragment output cannot be produced any other "
|
||||
"way");
|
||||
}
|
||||
|
||||
if (es31) {
|
||||
@@ -328,9 +369,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("Vertex shader storage blocks",
|
||||
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}", maxVertexSsboBlocks));
|
||||
} else {
|
||||
builder.Warn("Vertex shader storage blocks",
|
||||
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the Flywheel/Create indirect draw "
|
||||
"machinery cannot read indirect command buffers from the vertex stage",
|
||||
builder.FailOptional(
|
||||
"Vertex shader storage blocks",
|
||||
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the vertex stage cannot read a "
|
||||
"storage buffer at all, and there is nothing to read one with instead - the "
|
||||
"Flywheel/Create indirect draw machinery, which fetches its per-instance data "
|
||||
"from a vertex-stage SSBO, cannot run",
|
||||
maxVertexSsboBlocks));
|
||||
}
|
||||
|
||||
@@ -350,14 +394,15 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (caps.SupportsPersistentMapping) {
|
||||
builder.Pass("GL_EXT_buffer_storage", "supported (persistent buffer mapping)");
|
||||
} else {
|
||||
builder.Info("GL_EXT_buffer_storage",
|
||||
"not supported; no impact today: the frontend fully emulates persistent "
|
||||
"mapping regardless of this extension");
|
||||
builder.Warn("GL_EXT_buffer_storage",
|
||||
"not supported; the frontend emulates persistent mapping with its own "
|
||||
"shadow storage instead, so glBufferStorage and a GL_MAP_PERSISTENT_BIT "
|
||||
"mapping behave correctly - at the cost of the shadow copy");
|
||||
}
|
||||
if (caps.SupportsBaseInstance) {
|
||||
builder.Pass("GL_EXT_base_instance", "supported (native baseInstance draws)");
|
||||
} else {
|
||||
builder.Info("GL_EXT_base_instance",
|
||||
builder.Warn("GL_EXT_base_instance",
|
||||
"not supported; direct baseInstance draws are emulated by shifting the "
|
||||
"instanced arrays' attribute offsets, and gl_BaseInstance by a uniform. "
|
||||
"The one gap is an INDIRECT draw whose command carries a non-zero "
|
||||
@@ -366,15 +411,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// Both multi-draw rows gate on the capability flags, not the entry-point pointers:
|
||||
// eglGetProcAddress may hand back a non-NULL stub for these on drivers without the
|
||||
// extension (NVIDIA ES does, and its glMultiDrawElementsBaseVertexEXT stub silently
|
||||
// drops every draw), so the pointers prove nothing. Absence is INFO in both cases
|
||||
// because MobileGL falls back to an equivalent per-draw loop.
|
||||
// drops every draw), so the pointers prove nothing. Absence is WARN in both cases:
|
||||
// MobileGL falls back to an equivalent per-draw loop, so the output is identical and
|
||||
// only the command count changes.
|
||||
if (caps.SupportsMultiDrawIndirect) {
|
||||
builder.Pass("Multi-draw indirect",
|
||||
"glMultiDrawArrays/ElementsIndirectEXT available via GL_EXT_multi_draw_indirect");
|
||||
} else {
|
||||
builder.Info("Multi-draw indirect",
|
||||
"GL_EXT_multi_draw_indirect not supported; no impact today: multi-draw "
|
||||
"indirect is decomposed into per-command indirect draws regardless");
|
||||
builder.Warn("Multi-draw indirect",
|
||||
"GL_EXT_multi_draw_indirect not supported; MobileGL decomposes a multi-draw "
|
||||
"indirect batch into per-command indirect draws, which renders the same "
|
||||
"thing for one driver call per command instead of one per batch");
|
||||
}
|
||||
if (caps.SupportsMultiDrawElementsBaseVertex) {
|
||||
builder.Pass("Multi-draw base vertex",
|
||||
@@ -382,7 +429,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"with GL_EXT_multi_draw_arrays); glMultiDrawElementsBaseVertex batches into one "
|
||||
"driver call");
|
||||
} else {
|
||||
builder.Info("Multi-draw base vertex",
|
||||
builder.Warn("Multi-draw base vertex",
|
||||
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
|
||||
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch "
|
||||
"takes the next emulation tier instead, with identical output - see "
|
||||
@@ -407,9 +454,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"available (ES 3.1 core); the opt-in \"compute\" multi-draw tier can flatten a "
|
||||
"whole batch into one draw");
|
||||
} else {
|
||||
builder.Info("Compute shaders",
|
||||
"not available (pre-ES 3.1); no impact on the default multi-draw tiers, which "
|
||||
"never use compute");
|
||||
builder.FailOptional("Compute shaders",
|
||||
"not available (pre-ES 3.1); MobileGL advertises "
|
||||
"GL_ARB_compute_shader on an OpenGL 4.x context and there is no way to "
|
||||
"run a glDispatchCompute without the ES counterpart, so a program with "
|
||||
"a compute shader cannot be built at all. The default multi-draw tiers "
|
||||
"never use compute, so nothing else is lost");
|
||||
}
|
||||
{
|
||||
// The same resolution the backend runs, over the capabilities probed here.
|
||||
@@ -420,45 +470,58 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// was consulted.
|
||||
using MG_Backend::DirectGLES::MultiDrawImpl::ResolveTier;
|
||||
String resolution;
|
||||
const MG_Config::GLESMultiDrawMode tier =
|
||||
ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution);
|
||||
builder.Info("Multi-draw elements tier",
|
||||
"glMultiDrawElements(BaseVertex) emulation: " + resolution +
|
||||
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
const String detail = "glMultiDrawElements(BaseVertex) emulation: " + resolution +
|
||||
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE";
|
||||
// PASS only on the tier that hands the whole batch to the driver in one call.
|
||||
// Every other tier is a MobileGL substitute: the output is identical, the
|
||||
// command count is not.
|
||||
if (tier == MG_Config::GLESMultiDrawMode::Ext) {
|
||||
builder.Pass("Multi-draw elements tier", detail);
|
||||
} else {
|
||||
builder.Warn("Multi-draw elements tier",
|
||||
detail + " - the batch is replayed rather than handed over whole, "
|
||||
"which renders the same thing for more driver calls");
|
||||
}
|
||||
}
|
||||
if (caps.SupportsTextureBorderClamp) {
|
||||
builder.Pass("Texture border clamp",
|
||||
"supported (GL_TEXTURE_BORDER_COLOR reaches the driver, so "
|
||||
"GL_CLAMP_TO_BORDER samples the colour the application set)");
|
||||
} else {
|
||||
builder.Warn("Texture border clamp",
|
||||
builder.FailOptional(
|
||||
"Texture border clamp",
|
||||
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_border_clamp); "
|
||||
"GL_TEXTURE_BORDER_COLOR is not synced to the driver at all, so anything "
|
||||
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
|
||||
"border instead of the requested colour");
|
||||
"border instead of the requested colour, and no wrap mode substitutes for it");
|
||||
}
|
||||
if (caps.SupportsTextureCubeMapArray) {
|
||||
builder.Pass("Texture cube map array",
|
||||
"supported (GL_TEXTURE_CUBE_MAP_ARRAY textures get real storage and can be "
|
||||
"attached to a framebuffer)");
|
||||
} else {
|
||||
builder.Warn("Texture cube map array",
|
||||
builder.FailOptional(
|
||||
"Texture cube map array",
|
||||
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
|
||||
"map array texture gets no driver storage at all, so sampling one reads nothing "
|
||||
"and rendering to one does not reach the screen");
|
||||
"and rendering to one does not reach the screen. Nothing substitutes: the "
|
||||
"shaders that declare a samplerCubeArray do not compile either");
|
||||
}
|
||||
// WARN, not FAIL, and the choice is deliberate. The consequence is severe - buffer
|
||||
// textures are CORE in OpenGL 3.1 and MobileGL advertises a 4.x context, so an
|
||||
// application may use one without asking, and nothing degrades gracefully: the
|
||||
// texture gets no driver storage, and every shader declaring a samplerBuffer fails
|
||||
// to compile outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer :
|
||||
// require` for it below ESSL 320, so the program never links and every draw using it
|
||||
// silently draws nothing. That is how Minecraft 26.3, whose cloud layer is built
|
||||
// entirely from gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its
|
||||
// clouds. But FAIL means "this backend cannot run on this driver", and that is not
|
||||
// true: such a device runs everything that does not touch a buffer texture. It is
|
||||
// also exactly the shape of the "Texture cube map array" row above, which loses its
|
||||
// shaders to the same SPIRV-Cross `: require` mechanism and is a WARN - two adjacent
|
||||
// rows with one consequence must not carry two severities.
|
||||
// FAIL, and specifically FailOptional. The consequence is severe - buffer textures
|
||||
// are CORE in OpenGL 3.1 and MobileGL advertises a 4.x context, so an application
|
||||
// may use one without asking, and nothing degrades gracefully: the texture gets no
|
||||
// driver storage, and every shader declaring a samplerBuffer fails to compile
|
||||
// outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require`
|
||||
// for it below ESSL 320, so the program never links and every draw using it silently
|
||||
// draws nothing. That is how Minecraft 26.3, whose cloud layer is built entirely
|
||||
// from gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its clouds.
|
||||
// There is no substitute, which is what makes the row FAIL; the backend still RUNS
|
||||
// everything that does not touch a buffer texture, which is what keeps the failure
|
||||
// out of the backend summary. It is exactly the shape of the "Texture cube map
|
||||
// array" row above, which loses its shaders to the same SPIRV-Cross `: require`
|
||||
// mechanism - two adjacent rows with one consequence must carry one severity.
|
||||
// The limit is stated on every tier because it is the one number an application can
|
||||
// read, and on the None tier it is knowingly a fiction (see below).
|
||||
{
|
||||
@@ -495,7 +558,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
break;
|
||||
case Tier::None:
|
||||
default:
|
||||
builder.Warn("Buffer textures",
|
||||
builder.FailOptional(
|
||||
"Buffer textures",
|
||||
format("not supported (pre-ES 3.2 without GL_EXT/OES_texture_buffer); "
|
||||
"glTexBuffer does not exist, so a buffer texture gets no storage, "
|
||||
"and any shader declaring a samplerBuffer fails to compile and "
|
||||
@@ -513,7 +577,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// either answer, and the rows exist so the two halves of the loss are named at
|
||||
// startup instead of discovered as a shader that will not compile or an
|
||||
// unexplained GL_INVALID_OPERATION at draw setup.
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
// WARN, not PASS: ESSL has no 64-bit float type, so this backend does not support
|
||||
// fp64 directly at all. What it has is a complete substitute - the shaders build and
|
||||
// run - which is exactly what WARN means.
|
||||
builder.Warn("fp64", AppendFp64AdvertisementNote(
|
||||
"demoted to fp32 - ESSL has no 64-bit float type, so every double / "
|
||||
"dvec / dmat in a shader is narrowed to 32 bits before transpilation "
|
||||
"(DemoteFloat64Pass). Such shaders COMPILE AND RUN, at single "
|
||||
@@ -531,10 +598,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("Tessellation patch parameters",
|
||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||
} else {
|
||||
builder.Warn("Tessellation patch parameters",
|
||||
"glPatchParameteri missing (pre-ES 3.2 without GL_EXT_tessellation_shader); "
|
||||
"GL_PATCH_VERTICES stays at the driver default of 3 and a patch draw of any "
|
||||
"other size renders nothing");
|
||||
builder.FailOptional("Tessellation patch parameters",
|
||||
"glPatchParameteri missing (pre-ES 3.2 without "
|
||||
"GL_EXT_tessellation_shader); GL_PATCH_VERTICES stays at the driver "
|
||||
"default of 3 and a patch draw of any other size renders nothing - "
|
||||
"the patch size cannot be communicated any other way");
|
||||
}
|
||||
if (glesFuncs.glGenTransformFeedbacks != nullptr && glesFuncs.glBindTransformFeedback != nullptr &&
|
||||
glesFuncs.glPauseTransformFeedback != nullptr && glesFuncs.glResumeTransformFeedback != nullptr) {
|
||||
@@ -550,7 +618,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("GL_EXT_texture_norm16", "supported");
|
||||
} else {
|
||||
builder.Warn("GL_EXT_texture_norm16",
|
||||
"not supported; 16-bit normalized texture formats need emulation");
|
||||
"not supported; MobileGL substitutes a wider format for every 16-bit "
|
||||
"normalized texture, so the texels are still readable at their declared "
|
||||
"precision at the cost of the extra storage");
|
||||
}
|
||||
if (caps.SupportsRenderSnorm) {
|
||||
builder.Pass("GL_EXT_render_snorm",
|
||||
@@ -583,23 +653,31 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"render targets (Iris reports GL_FRAMEBUFFER_UNSUPPORTED and refuses to load)");
|
||||
}
|
||||
|
||||
// INFO, never WARN: this is the HOST driver's ability to compile its own ESSL on
|
||||
// its own threads, and MobileGL's asynchronous compilation does not depend on it
|
||||
// in the slightest - the pool parallelises GLSL -> SPIR-V -> ESSL translation,
|
||||
// which is where a shaderpack load actually spends its time, and it does that on
|
||||
// a driver that has never heard of the extension. The row exists so that the day
|
||||
// the driver-side half is overlapped too, the POST already says which devices can.
|
||||
builder.Info("Driver GL_KHR_parallel_shader_compile",
|
||||
caps.SupportsParallelShaderCompile
|
||||
? "supported; the device driver can also compile the translated ESSL off-thread"
|
||||
: "not supported; the device driver compiles the translated ESSL on the calling "
|
||||
"thread (MobileGL's own compile pool is unaffected)");
|
||||
// WARN and never FAIL when it is absent: this is the HOST driver's ability to
|
||||
// compile its own ESSL on its own threads, and MobileGL's own compile pool stands in
|
||||
// for all of it that matters - the pool parallelises GLSL -> SPIR-V -> ESSL
|
||||
// translation, which is where a shaderpack load actually spends its time, and it
|
||||
// does that on a driver that has never heard of the extension. The row exists so
|
||||
// that the day the driver-side half is overlapped too, the POST already says which
|
||||
// devices can.
|
||||
if (caps.SupportsParallelShaderCompile) {
|
||||
builder.Pass("Driver GL_KHR_parallel_shader_compile",
|
||||
"supported; the device driver can also compile the translated ESSL off-thread");
|
||||
} else {
|
||||
builder.Warn("Driver GL_KHR_parallel_shader_compile",
|
||||
"not supported; the device driver compiles the translated ESSL on the calling "
|
||||
"thread. MobileGL's own compile pool substitutes for the expensive half of the "
|
||||
"work (GLSL -> SPIR-V -> ESSL) and is unaffected, so loads still overlap");
|
||||
}
|
||||
|
||||
builder.Info("Indirect gl_InstanceID semantics",
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance
|
||||
? "includes baseInstance (ANGLE-style; MobileGL's shader rewrite keeps gl_InstanceID "
|
||||
"zero-based)"
|
||||
: "conforming (zero-based)");
|
||||
if (caps.IndirectDrawInstanceIdIncludesBaseInstance) {
|
||||
builder.Warn("Indirect gl_InstanceID semantics",
|
||||
"includes baseInstance (ANGLE-style), which is not what GL promises; "
|
||||
"MobileGL's shader rewrite subtracts it back out so gl_InstanceID stays "
|
||||
"zero-based and instanced indirect draws index their arrays correctly");
|
||||
} else {
|
||||
builder.Pass("Indirect gl_InstanceID semantics", "conforming (zero-based)");
|
||||
}
|
||||
|
||||
builder.DriverReported("Backend driver reported GL_VENDOR", caps.GLESVendorString);
|
||||
builder.DriverReported("Backend driver reported GL_RENDERER", caps.GLESRendererString);
|
||||
@@ -614,17 +692,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
const MG_External::GLESFunctionsTable& glesFuncs) {
|
||||
const String disabledNote = TimerQueryDisabledNote();
|
||||
if (!caps.SupportsDisjointTimerQuery) {
|
||||
builder.Warn("Timer queries",
|
||||
"GL_EXT_disjoint_timer_query not supported; timer queries unavailable; "
|
||||
"Minecraft F3 GPU% will not show" +
|
||||
builder.FailOptional("Timer queries",
|
||||
"GL_EXT_disjoint_timer_query not supported; there is no way to time "
|
||||
"GPU work from the client, so glBeginQuery(GL_TIME_ELAPSED) has "
|
||||
"nothing to stand in for it and Minecraft's F3 GPU% will not show" +
|
||||
disabledNote);
|
||||
return;
|
||||
}
|
||||
// Every emit carries the extension-presence fact the old standalone
|
||||
// GL_EXT_disjoint_timer_query row showed, plus the probe outcome.
|
||||
const String extensionPresent = "GL_EXT_disjoint_timer_query extension present";
|
||||
// FailOptional: a driver that advertises the extension and then cannot serve a
|
||||
// query is broken in a way nothing substitutes for, but timing GPU work is not
|
||||
// something the backend needs in order to run.
|
||||
const auto fail = [&](const String& detail) {
|
||||
builder.Fail("Timer queries", extensionPresent + "; but " + detail + disabledNote);
|
||||
builder.FailOptional("Timer queries", extensionPresent + "; but " + detail + disabledNote);
|
||||
};
|
||||
|
||||
if (!glesFuncs.glGenQueries || !glesFuncs.glDeleteQueries || !glesFuncs.glBeginQuery ||
|
||||
@@ -758,8 +840,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
const String pathNote = native ? "GL_NV_shader_noperspective_interpolation present (native path)"
|
||||
: "GL_NV_shader_noperspective_interpolation absent (gl_Position.w / "
|
||||
"gl_FragCoord.w emulation path)";
|
||||
// FailOptional: a shaderpack that declares a noperspective varying renders it wrong
|
||||
// and nothing stands in for the interpolation, but everything that does not use one
|
||||
// is unaffected, so the backend still runs.
|
||||
const auto fail = [&](const String& detail) {
|
||||
builder.Fail("noperspective interpolation", pathNote + "; " + detail);
|
||||
builder.FailOptional("noperspective interpolation", pathNote + "; " + detail);
|
||||
};
|
||||
|
||||
if (!g.glCreateShader || !g.glShaderSource || !g.glCompileShader || !g.glGetShaderiv ||
|
||||
@@ -1157,6 +1242,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
||||
glesFuncs, caps, builder.report.formatCapabilities.value());
|
||||
ReportThreeChannelColorAttachments(builder, caps, builder.report.formatCapabilities.value());
|
||||
// The "Known Driver Bugs" section. Deliberately last, and deliberately not a
|
||||
// builder.Pass/Warn/Fail row: these are not capability checks and they must not move
|
||||
// the backend verdict, which is about whether the backend can RUN on this driver.
|
||||
// Only bugs the device actually has come back, so a clean driver adds nothing here.
|
||||
builder.report.knownDriverBugs = CollectGlesKnownDriverBugs(glesFuncs);
|
||||
} while (false);
|
||||
}
|
||||
|
||||
@@ -1178,7 +1268,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
|
||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
|
||||
summary.caps.SupportsDrawIndirect,
|
||||
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance));
|
||||
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance,
|
||||
summary.caps.SupportsTextureView));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
@@ -1252,8 +1343,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
const String timestampFacts =
|
||||
format("timestampValidBits = {} on the graphics queue family; timestampPeriod = {} ns per tick",
|
||||
timestampValidBits, timestampPeriod);
|
||||
// FailOptional, for the same reason as the GLES row: the backend does not need to
|
||||
// time GPU work in order to run.
|
||||
const auto fail = [&](const String& detail) {
|
||||
builder.Fail("Timer queries", timestampFacts + "; but " + detail + disabledNote);
|
||||
builder.FailOptional("Timer queries", timestampFacts + "; but " + detail + disabledNote);
|
||||
};
|
||||
const auto vkCreateDeviceFn =
|
||||
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||
@@ -1473,7 +1566,13 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
Bool subgroupPropertiesAvailable,
|
||||
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
|
||||
constexpr const char* RowName = "Subgroup first-reduction witness";
|
||||
const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); };
|
||||
// FailOptional, not Fail. The witness reports whether the NATIVE subgroup
|
||||
// first-reduction works; when it does not, the renderer takes its non-subgroup
|
||||
// iteration path and draws the same image. Both an Adreno 830 and Mesa lavapipe
|
||||
// fail this row's topology check today while running the DirectVulkan backend
|
||||
// perfectly well, so a fatal verdict here would have the screen announce that a
|
||||
// backend the user is looking at through that very backend cannot run.
|
||||
const auto fail = [&](String detail) { builder.FailOptional(RowName, Move(detail)); };
|
||||
|
||||
if (!subgroupPropertiesAvailable) {
|
||||
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
|
||||
@@ -1500,7 +1599,13 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
|
||||
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
|
||||
builder.Info(RowName, eligibility.detail);
|
||||
// WARN, not FAIL: there is nothing to witness on a device with no native
|
||||
// subgroup contract, and the renderer takes its non-subgroup iteration path,
|
||||
// which produces the same image.
|
||||
builder.Warn(RowName,
|
||||
eligibility.detail +
|
||||
"; the renderer takes its non-subgroup iteration path instead, which "
|
||||
"renders the same thing without the first-reduction shortcut");
|
||||
return;
|
||||
}
|
||||
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
|
||||
@@ -2199,20 +2304,23 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (features.multiDrawIndirect == VK_TRUE) {
|
||||
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
||||
} else {
|
||||
builder.Info("multiDrawIndirect",
|
||||
"unsupported; multi-draw batches fall back to one draw per command (tier "
|
||||
"\"indirect\" of the multi-draw dispatch is unavailable)");
|
||||
builder.Warn("multiDrawIndirect",
|
||||
"unsupported; MobileGL unrolls a multi-draw batch into one draw per command "
|
||||
"(tier \"indirect\" of the multi-draw dispatch is unavailable), which renders "
|
||||
"the same thing for more commands");
|
||||
}
|
||||
if (features.drawIndirectFirstInstance == VK_TRUE) {
|
||||
builder.Pass("drawIndirectFirstInstance", "indirect commands may carry a non-zero firstInstance");
|
||||
} else {
|
||||
builder.Warn("drawIndirectFirstInstance",
|
||||
"unsupported; indirect commands with a non-zero baseInstance cannot run natively");
|
||||
builder.FailOptional("drawIndirectFirstInstance",
|
||||
"unsupported; an indirect command carrying a non-zero baseInstance "
|
||||
"cannot run, and the offset cannot be folded into the command from the "
|
||||
"CPU because the command is on the GPU");
|
||||
}
|
||||
// Multi-draw dispatch tiers (ext -> indirect -> unroll). INFO on the missing
|
||||
// pieces: every tier has a fallback, nothing is lost, only batched into more
|
||||
// commands. The renderer resolves the same chain at device creation, clamped
|
||||
// by MOBILEGL_MAGMA_MULTIDRAW_MODE.
|
||||
// Multi-draw dispatch tiers (ext -> indirect -> unroll). WARN on the missing
|
||||
// pieces: every tier has a fallback that renders the same thing, only batched
|
||||
// into more commands. The renderer resolves the same chain at device creation,
|
||||
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE.
|
||||
{
|
||||
Bool multiDrawExtUsable = false;
|
||||
if (HasVkExtension(deviceExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME) &&
|
||||
@@ -2229,8 +2337,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("VK_EXT_multi_draw",
|
||||
"supported; a glMultiDraw* batch runs as one vkCmdDrawMulti(Indexed)EXT");
|
||||
} else {
|
||||
builder.Info("VK_EXT_multi_draw",
|
||||
"unsupported; glMultiDraw* batches use the indirect or unrolled tier");
|
||||
builder.Warn("VK_EXT_multi_draw",
|
||||
"unsupported; glMultiDraw* batches take the indirect or unrolled tier "
|
||||
"instead, with identical output");
|
||||
}
|
||||
const char* resolvedTier = multiDrawExtUsable ? "ext"
|
||||
: features.multiDrawIndirect == VK_TRUE ? "indirect"
|
||||
@@ -2243,32 +2352,46 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
: multiDrawMode == MG_Config::MultiDrawMode::Indirect ? "indirect"
|
||||
: "unroll");
|
||||
}
|
||||
builder.Info("Multi-draw dispatch tier", tierDetail);
|
||||
// PASS only on the tier that hands the whole batch to the driver in one command.
|
||||
if (multiDrawExtUsable) {
|
||||
builder.Pass("Multi-draw dispatch tier", tierDetail);
|
||||
} else {
|
||||
builder.Warn("Multi-draw dispatch tier",
|
||||
tierDetail + "; the batch is replayed rather than handed over whole, which "
|
||||
"renders the same thing for more commands");
|
||||
}
|
||||
}
|
||||
if (features.vertexPipelineStoresAndAtomics == VK_TRUE) {
|
||||
builder.Pass("vertexPipelineStoresAndAtomics",
|
||||
"supported by driver (not currently enabled by the DirectVulkan backend)");
|
||||
} else {
|
||||
builder.Warn("vertexPipelineStoresAndAtomics",
|
||||
"unsupported; shaders that write storage buffers from the vertex stage will not work");
|
||||
builder.FailOptional("vertexPipelineStoresAndAtomics",
|
||||
"unsupported; a shader that writes a storage buffer or runs an atomic "
|
||||
"from the vertex stage cannot build a pipeline, and the write cannot be "
|
||||
"moved to another stage without changing what the shader does");
|
||||
}
|
||||
if (features.fillModeNonSolid == VK_TRUE) {
|
||||
builder.Pass("fillModeNonSolid", "glPolygonMode GL_LINE/GL_POINT rasterization supported");
|
||||
} else {
|
||||
builder.Warn("fillModeNonSolid",
|
||||
"unsupported; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL (no wireframe/point "
|
||||
"rasterization)");
|
||||
builder.FailOptional("fillModeNonSolid",
|
||||
"unsupported; glPolygonMode GL_LINE/GL_POINT silently falls back to "
|
||||
"GL_FILL, and wireframe/point rasterization cannot be rebuilt out of "
|
||||
"the triangle pipeline");
|
||||
}
|
||||
if (features.independentBlend == VK_TRUE) {
|
||||
builder.Pass("independentBlend", "per-draw-buffer glColorMaski and indexed blend state supported");
|
||||
} else {
|
||||
builder.Warn("independentBlend",
|
||||
"unsupported; per-draw-buffer glColorMaski falls back to draw buffer 0 for all attachments");
|
||||
builder.FailOptional("independentBlend",
|
||||
"unsupported; every attachment takes draw buffer 0's colour mask and "
|
||||
"blend state, so an MRT pass that configures them separately writes the "
|
||||
"wrong channels to all but one attachment");
|
||||
}
|
||||
if (features.dualSrcBlend == VK_TRUE) {
|
||||
builder.Pass("dualSrcBlend", "GL_SRC1_* dual-source blend factors supported");
|
||||
} else {
|
||||
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
|
||||
builder.FailOptional("dualSrcBlend",
|
||||
"unsupported; a draw using a GL_SRC1_* blend factor hard-fails, and a "
|
||||
"second fragment output cannot be produced any other way");
|
||||
}
|
||||
// The Magma counterpart of the GLES "Buffer textures" row, so the two sections can be
|
||||
// read side by side. Vulkan has no optional-feature bit here: a uniform texel buffer is
|
||||
@@ -2308,10 +2431,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"back on its own; a format that refuses the flag is detected at image "
|
||||
"creation and declines per-slice attachment)");
|
||||
} else {
|
||||
builder.Warn("2D-array-compatible 3D images",
|
||||
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour attachments; "
|
||||
"glFramebufferTextureLayer on a GL_TEXTURE_3D texture is declined for every "
|
||||
"slice past the first");
|
||||
builder.FailOptional("2D-array-compatible 3D images",
|
||||
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour "
|
||||
"attachments; glFramebufferTextureLayer on a GL_TEXTURE_3D texture "
|
||||
"is declined for every slice past the first, and a 3D slice cannot "
|
||||
"be rendered into any other way");
|
||||
}
|
||||
}
|
||||
if (features.imageCubeArray == VK_TRUE) {
|
||||
@@ -2319,9 +2443,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
|
||||
"attached to a framebuffer per layer");
|
||||
} else {
|
||||
builder.Warn("imageCubeArray",
|
||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
|
||||
"one reads nothing and glFramebufferTextureLayer on one is declined");
|
||||
builder.FailOptional("imageCubeArray",
|
||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, "
|
||||
"so sampling one reads nothing and glFramebufferTextureLayer on one is "
|
||||
"declined - there is no substitute image type");
|
||||
}
|
||||
// MobileGL follows the device here: shaderFloat64 decides whether a module keeps its
|
||||
// 64-bit floats or has them narrowed before pipeline creation (DemoteFloat64Pass). Adreno
|
||||
@@ -2336,7 +2461,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"FETCH here, so such a program is narrowed whole exactly as it would be on a "
|
||||
"device without the feature"));
|
||||
} else {
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
// WARN rather than PASS: the device does not support fp64 at all here, and what
|
||||
// stands in for it is a MobileGL pass that narrows the shader. It runs, at single
|
||||
// precision - the definition of a substitute.
|
||||
builder.Warn("fp64", AppendFp64AdvertisementNote(
|
||||
"demoted to fp32 (device shaderFloat64 = unsupported) - every double / dvec "
|
||||
"/ dmat in a shader is narrowed to 32 bits before pipeline creation, so such "
|
||||
"shaders BUILD AND RUN at single precision instead of failing to create a "
|
||||
@@ -2369,8 +2497,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (shaderDrawParameters) {
|
||||
builder.Pass("shaderDrawParameters", "gl_DrawID/gl_BaseVertex/gl_BaseInstance shaders supported");
|
||||
} else {
|
||||
builder.Warn("shaderDrawParameters",
|
||||
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
||||
builder.FailOptional("shaderDrawParameters",
|
||||
"unavailable; a shader reading gl_DrawID, gl_BaseVertex or "
|
||||
"gl_BaseInstance has no SPIR-V builtin to read them from, so such "
|
||||
"shaders do not work and nothing supplies the values instead");
|
||||
}
|
||||
summary.shaderDrawParametersSupported = shaderDrawParameters;
|
||||
|
||||
@@ -2407,10 +2537,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"supported; flat varyings take GL's last vertex and transform feedback records "
|
||||
"strip/fan triangles in GL's vertex order");
|
||||
} else {
|
||||
builder.Warn("provokingVertexLast",
|
||||
"unsupported; flat-shaded varyings take a primitive's first vertex instead of GL's "
|
||||
"last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated "
|
||||
"(e.g. 0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
|
||||
builder.FailOptional("provokingVertexLast",
|
||||
"unsupported; flat-shaded varyings take a primitive's first vertex "
|
||||
"instead of GL's last, and transform feedback records "
|
||||
"TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated (e.g. 0,1,2 / 1,3,2 "
|
||||
"instead of 0,1,2 / 2,1,3). Rewriting the convention would mean "
|
||||
"reordering every index buffer, which MobileGL does not do");
|
||||
}
|
||||
if (provokingVertexLast && !transformFeedbackPreservesProvokingVertex) {
|
||||
builder.Warn("transformFeedbackPreservesProvokingVertex",
|
||||
@@ -2439,9 +2571,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("primitiveTopologyListRestart",
|
||||
"primitive restart supported on list topologies (GL_PRIMITIVE_RESTART)");
|
||||
} else {
|
||||
builder.Warn("primitiveTopologyListRestart",
|
||||
"unsupported; primitive restart works on strip/fan topologies only, list-topology restart "
|
||||
"hard-fails at draw");
|
||||
builder.FailOptional("primitiveTopologyListRestart",
|
||||
"unsupported; primitive restart works on strip/fan topologies only, and "
|
||||
"a list-topology draw with GL_PRIMITIVE_RESTART enabled hard-fails - "
|
||||
"splitting the index stream on the CPU is not done");
|
||||
}
|
||||
|
||||
// Core 1.0 features the backend turns GL stages into pipeline stages with.
|
||||
@@ -2451,9 +2584,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("tessellationShader",
|
||||
"supported (GL_PATCHES draws run the tessellation control/evaluation stages)");
|
||||
} else {
|
||||
builder.Warn("tessellationShader",
|
||||
"unsupported; a program with a tessellation control/evaluation shader cannot build a "
|
||||
"pipeline, so GL_PATCHES draws render nothing");
|
||||
builder.FailOptional("tessellationShader",
|
||||
"unsupported; a program with a tessellation control/evaluation shader "
|
||||
"cannot build a pipeline, so GL_PATCHES draws render nothing and there "
|
||||
"is no stage to run the tessellation on instead");
|
||||
}
|
||||
|
||||
Bool vertexAttributeInstanceRateDivisor = false;
|
||||
@@ -2471,10 +2605,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("vertexAttributeInstanceRateDivisor",
|
||||
"supported (glVertexAttribDivisor advances an attribute every N instances)");
|
||||
} else {
|
||||
builder.Warn("vertexAttributeInstanceRateDivisor",
|
||||
"unsupported; Vulkan's instance input rate can only advance once per instance, so "
|
||||
"every non-zero glVertexAttribDivisor behaves as 1 and instanced attributes meant to "
|
||||
"change every N instances change every one");
|
||||
builder.FailOptional("vertexAttributeInstanceRateDivisor",
|
||||
"unsupported; Vulkan's instance input rate can only advance once per "
|
||||
"instance, so every non-zero glVertexAttribDivisor behaves as 1 and "
|
||||
"instanced attributes meant to change every N instances change every "
|
||||
"one - silently wrong geometry, with no substitute fetch rate");
|
||||
}
|
||||
|
||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
||||
@@ -2498,11 +2633,18 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
format("basic subgroup operations in compute, subgroup size {}",
|
||||
subgroupProperties.subgroupSize));
|
||||
} else {
|
||||
builder.Warn("Compute shader subgroup",
|
||||
"basic subgroup operations are not usable from compute shaders");
|
||||
builder.FailOptional("Compute shader subgroup",
|
||||
"basic subgroup operations are not usable from compute shaders, so "
|
||||
"MobileGL withholds GL_KHR_shader_subgroup and the subgroup "
|
||||
"iteration-render-pass path cannot run; there is no scalar rewrite "
|
||||
"that stands in for a subgroup reduction");
|
||||
}
|
||||
} else {
|
||||
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
|
||||
builder.FailOptional("Compute shader subgroup",
|
||||
"subgroup properties could not be queried (no "
|
||||
"vkGetPhysicalDeviceProperties2, or a pre-1.1 device), so MobileGL "
|
||||
"withholds GL_KHR_shader_subgroup and the subgroup paths are "
|
||||
"unavailable whatever the hardware can actually do");
|
||||
}
|
||||
|
||||
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||
@@ -2522,9 +2664,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (indexTypeUint8) {
|
||||
builder.Pass("Index type uint8", "supported (native GL_UNSIGNED_BYTE index buffers)");
|
||||
} else {
|
||||
builder.Warn("Index type uint8",
|
||||
"not supported; GL_UNSIGNED_BYTE index buffers cannot be drawn (the backend "
|
||||
"has no conversion fallback and asserts on uint8 index draws)");
|
||||
builder.FailOptional("Index type uint8",
|
||||
"not supported; a GL_UNSIGNED_BYTE index buffer cannot be drawn - the "
|
||||
"backend has no widening conversion and asserts on uint8 index draws");
|
||||
}
|
||||
builder.DriverReported("Backend driver reported device", String(properties.deviceName));
|
||||
builder.DriverReported("Backend driver reported driver version", driverVersionString + " (vendor-encoded)");
|
||||
@@ -2542,10 +2684,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
ProbeVulkanTimerQuery(builder, getInstanceProcAddr, instance, physicalDevice,
|
||||
graphicsQueueFamilyIndex, graphicsQueueTimestampValidBits, timestampPeriod);
|
||||
} else {
|
||||
builder.Warn("Timer queries",
|
||||
builder.FailOptional(
|
||||
"Timer queries",
|
||||
format("timestampValidBits = 0 on the graphics queue family; timestampPeriod = {} ns "
|
||||
"per tick; timestamps unsupported on the graphics queue; timer queries "
|
||||
"unavailable",
|
||||
"per tick; the graphics queue cannot write a timestamp at all, so there is "
|
||||
"nothing to time GPU work with and glBeginQuery(GL_TIME_ELAPSED) has no "
|
||||
"substitute",
|
||||
timestampPeriod) +
|
||||
TimerQueryDisabledNote());
|
||||
}
|
||||
|
||||
@@ -7,26 +7,43 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "DriverBugProbes.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
// One row of a backend power-on self-test (POST) report.
|
||||
//
|
||||
// EVERY CAPABILITY ROW IS PASS, WARN OR FAIL; INFO IS FOR IDENTITY ONLY.
|
||||
// PASS - the backend supports the capability directly.
|
||||
// WARN - not directly, but a MobileGL quirk substitutes and the application still sees
|
||||
// correct behaviour; the detail names the substitute and what it costs.
|
||||
// FAIL - unsupported with no substitute; an application that uses it gets wrong output, a
|
||||
// failed draw, or nothing.
|
||||
// INFO - identity only: renderer name, API version, driver strings, and the strings
|
||||
// MobileGL itself reports to applications. Never a capability answer.
|
||||
// A FAIL row does not by itself mean the backend cannot run - see BackendPostReport::verdict.
|
||||
struct PostCheck {
|
||||
String name;
|
||||
String status; // "PASS" | "WARN" | "FAIL" | "INFO"
|
||||
String detail;
|
||||
// Display ordering rank within a backend section (lower renders first): FAIL,
|
||||
// WARN, PASS, INFO, then the device-driver identity strings, then the strings
|
||||
// WARN, PASS, then the device-driver identity strings, then the strings
|
||||
// MobileGL itself reports to applications. Rows are stable-sorted by this rank
|
||||
// before the report is returned; it is not serialized to JSON.
|
||||
Int displayRank = 0;
|
||||
};
|
||||
|
||||
// Verdict for one backend's device driver.
|
||||
// - UNSUPPORTED: a fatal check failed; the backend cannot run on this driver.
|
||||
// - DEGRADED: every fatal check passed but at least one soft expectation is unmet.
|
||||
// - OK: all expectations met.
|
||||
// Verdict for one backend's device driver, derived from the rows.
|
||||
// - UNSUPPORTED: a REQUIRED capability failed; the backend cannot run on this driver.
|
||||
// - DEGRADED: every required capability is present, but at least one row is WARN or is a
|
||||
// FAIL on an optional capability - the backend runs, and something an application might
|
||||
// ask for is substituted or missing.
|
||||
// - OK: every row passed.
|
||||
// So a section can carry FAIL rows and still be DEGRADED rather than UNSUPPORTED: a device
|
||||
// with no dual-source blend still runs. Which capabilities are required is decided at the
|
||||
// row (ReportBuilder::Fail vs ReportBuilder::FailOptional in DriverPost.cpp).
|
||||
// available is false when no probeable driver exists at all (library missing, display
|
||||
// uninitializable, zero Vulkan physical devices, ...).
|
||||
struct BackendPostReport {
|
||||
@@ -34,6 +51,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED"
|
||||
String rendererInfo;
|
||||
Vector<PostCheck> checks;
|
||||
// The "Known Driver Bugs" section, kept apart from `checks` on purpose. `checks` asks
|
||||
// whether a feature is there and roughly works; these are core features the driver
|
||||
// claims, accepts, and then does not perform - a separate question, from a separate
|
||||
// inventory (campaign findings, not the extension string). See DriverBugProbes.h.
|
||||
//
|
||||
// Only bugs this device ACTUALLY HAS appear here: a probe that comes back clean
|
||||
// contributes no entry, so an unaffected driver renders the section empty rather than
|
||||
// as a list of reassurances. That is also why the verdict vocabulary is FIXED /
|
||||
// UNFIXABLE rather than PASS / FAIL - every row is a bug that is present, and the
|
||||
// verdict says whether MobileGL can do anything about it.
|
||||
Vector<DriverBugFinding> knownDriverBugs;
|
||||
Optional<MG_Backend::FormatCapabilityCache> formatCapabilities;
|
||||
};
|
||||
|
||||
|
||||
@@ -144,6 +144,27 @@ namespace {
|
||||
out << '}';
|
||||
}
|
||||
out << ']';
|
||||
// The "Known Driver Bugs" section, separate from "checks" because it answers a
|
||||
// different question and uses a different verdict vocabulary (FIXED | UNFIXABLE).
|
||||
// Every entry is a bug the device HAS - a clean probe contributes nothing - so an
|
||||
// unaffected driver serializes an empty array and the screen renders no section.
|
||||
out << ",\"knownDriverBugs\":[";
|
||||
for (SizeT i = 0; i < report.knownDriverBugs.size(); ++i) {
|
||||
const MobileGL::MG_Util::SelfTest::DriverBugFinding& bug = report.knownDriverBugs[i];
|
||||
if (i != 0) {
|
||||
out << ',';
|
||||
}
|
||||
out << "{\"name\":";
|
||||
AppendJsonString(out, bug.name);
|
||||
out << ",\"verdict\":";
|
||||
AppendJsonString(out, bug.verdict == MobileGL::MG_Util::SelfTest::DriverBugVerdict::Fixed
|
||||
? "FIXED"
|
||||
: "UNFIXABLE");
|
||||
out << ",\"detail\":";
|
||||
AppendJsonString(out, bug.detail);
|
||||
out << '}';
|
||||
}
|
||||
out << ']';
|
||||
if (report.formatCapabilities.has_value()) {
|
||||
AppendFormatCapabilitiesJson(out, report.formatCapabilities.value());
|
||||
}
|
||||
|
||||
@@ -486,7 +486,8 @@ namespace MobileGL {
|
||||
attrib.explicitFragmentOutLocations,
|
||||
attrib.explicitFragmentOutIndices,
|
||||
attrib.explicitOpaqueUniformBindings,
|
||||
attrib.storageBlocksWithoutBinding);
|
||||
attrib.storageBlocksWithoutBinding,
|
||||
attrib.uniformBlocksWithoutBinding);
|
||||
break;
|
||||
}
|
||||
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
||||
|
||||
@@ -325,9 +325,31 @@ namespace MobileGL {
|
||||
// appending it at the end of the section would make the module invalid. A
|
||||
// duplicate OpTypeArray is legal (SPIR-V 2.8 exempts aggregates from the
|
||||
// uniqueness rule, and so does spirv-val), so no search for an existing one is
|
||||
// needed; the LENGTH CONSTANT is not exempt, and if the module already declares
|
||||
// it after the block there is nowhere legal to put the array - the block is then
|
||||
// declined and keeps today's behaviour. Returns 0 for that.
|
||||
// needed; the LENGTH CONSTANT is not exempt, so when the module already declares
|
||||
// it the pass has to work with the one instruction that exists.
|
||||
//
|
||||
// That instruction is not always in a usable place. GetDefiningInstruction only
|
||||
// honours `position` when it MINTS the constant; when the module already has one
|
||||
// it hands back the existing instruction wherever it happens to sit, and glslang
|
||||
// emits constants in first-use order, so a shader whose first use of the value is
|
||||
// below the counter block declares it below the block. The flattened array would
|
||||
// then forward-reference its own length.
|
||||
//
|
||||
// KHR-GL43.compute_shader.pipeline-compute-chain is exactly that shader: two
|
||||
// counters at offset 8 need a 4-element array, and its `%uint_4` is first used by
|
||||
// a later declaration, so it lands AFTER gl_AtomicCounterBlock_1. Declining there
|
||||
// - which is what this used to do - left the offsets in place, and SPIRV-Cross
|
||||
// then refused the whole stage with "Push constant block cannot be expressed as
|
||||
// neither std430 nor std140", so the chain's first kernel never reached the
|
||||
// driver and every resource it writes stayed at its initial value.
|
||||
//
|
||||
// Moving the constant UP to just before the block is always legal, which is why
|
||||
// this is a relocation and not a second declaration: an OpConstant's only operand
|
||||
// is its result TYPE, and that type already precedes the block (it is the element
|
||||
// type of the counter array the block declares). Every existing use sits after
|
||||
// the constant's old position and therefore after its new one too, so no use is
|
||||
// left dangling - moving a definition earlier in the types/constants section
|
||||
// cannot invalidate anything. Ordering is all that changes; def-use is untouched.
|
||||
uint32_t CreateCounterArrayTypeBefore(IRContext* context, Instruction* structType,
|
||||
uint32_t uintTypeId, uint32_t length) {
|
||||
auto* constantMgr = context->get_constant_mgr();
|
||||
@@ -341,7 +363,12 @@ namespace MobileGL {
|
||||
if (position == context->types_values_end()) return 0;
|
||||
Instruction* lengthInst = constantMgr->GetDefiningInstruction(lengthConstant, 0, &position);
|
||||
if (lengthInst == nullptr) return 0;
|
||||
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) return 0;
|
||||
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) {
|
||||
// Pre-existing constant, declared below the block. Relocate it; see above
|
||||
// for why that is sound. InsertBefore unlinks it from its current spot
|
||||
// first, so this is a move rather than an aliasing second entry.
|
||||
lengthInst->InsertBefore(structType);
|
||||
}
|
||||
|
||||
const uint32_t arrayTypeId = context->TakeNextId();
|
||||
if (arrayTypeId == 0) return 0;
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace MobileGL {
|
||||
// assigned - see the comment on TMglGlslIoResolver::reserverResourceSlot.
|
||||
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr;
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr;
|
||||
};
|
||||
|
||||
struct ProgramBinaryAttrib {
|
||||
|
||||
@@ -196,6 +196,17 @@ namespace MobileGL {
|
||||
m_storageBlocksWithoutBinding->insert(name.c_str());
|
||||
}
|
||||
|
||||
// A UNIFORM block that declared no binding. Same capture point and same union-across-
|
||||
// stages reasoning as the storage-block set above, and the same reason it cannot be
|
||||
// asked later: mapIO is about to write an auto-assigned binding into this very
|
||||
// qualifier. MGL_GLOBAL_UBO is MobileGL's own synthesized block, not an application
|
||||
// one - it never reaches the GL block space and must not be seeded here.
|
||||
if (m_uniformBlocksWithoutBinding != nullptr && type.getBasicType() == glslang::EbtBlock &&
|
||||
qualifier.storage == glslang::EvqUniform && !qualifier.hasBinding() &&
|
||||
name.compare(MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != 0) {
|
||||
m_uniformBlocksWithoutBinding->insert(name.c_str());
|
||||
}
|
||||
|
||||
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,16 +29,19 @@ namespace MobileGL {
|
||||
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
|
||||
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
|
||||
ExplicitVarSlotMap* opaqueUniformBindings,
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr)
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr,
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr)
|
||||
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
|
||||
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
|
||||
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding) {}
|
||||
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding),
|
||||
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding) {}
|
||||
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
|
||||
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
|
||||
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr)
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr,
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr)
|
||||
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
|
||||
opaqueUniformBindings, storageBlocksWithoutBinding) {}
|
||||
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding) {}
|
||||
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
||||
@@ -62,6 +65,13 @@ namespace MobileGL {
|
||||
// layout(binding = N). GL 4.3 core 7.8 gives such a block binding ZERO; see
|
||||
// ProgramLinkTask::SeedDefaultStorageBlockBindings for what is done with them.
|
||||
std::set<String>* m_storageBlocksWithoutBinding = nullptr;
|
||||
// The same capture for UNIFORM blocks. GL 4.6 core 7.6.2 gives an unqualified uniform
|
||||
// block binding ZERO, and glslang's auto-mapper does not: it packs uniform blocks into
|
||||
// the same slot space as samplers and images (spvVersion.openGl is 0 under
|
||||
// setEnvClient(EShClientVulkan), so TDefaultGlslIoResolver::resolveBinding keys every
|
||||
// resource kind on set 0), so an unbound block declared after an unbound image lands on
|
||||
// 1. See ProgramLinkTask's UBO reflection loop for what is done with them.
|
||||
std::set<String>* m_uniformBlocksWithoutBinding = nullptr;
|
||||
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
||||
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
||||
bool m_plainUniformLocationsAssigned = false;
|
||||
|
||||
@@ -134,7 +134,21 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
case GL_RGB16:
|
||||
case GL_RGB10:
|
||||
case GL_RGB12:
|
||||
return {GL_RGBA32F, GL_RGBA, GL_FLOAT};
|
||||
// Same reasoning as GL_RGB16_SNORM above, and the same shape: keep the
|
||||
// unsigned-normalized encoding whenever the driver has it, because
|
||||
// GL_RGBA16 is the SAME-WIDTH four-channel sibling and GL_RGBA32F is not.
|
||||
// That matters beyond storage size. ARB_texture_view puts all five 48-bit
|
||||
// formats in one view class, so a GL_RGB16 texture viewed as GL_RGB16UI has
|
||||
// to alias storage the ES driver also considers compatible; against a
|
||||
// GL_RGBA32F carrier the view is a different class and glTextureView is
|
||||
// refused outright (KHR-GL4x.texture_view.view_classes). Against GL_RGBA16
|
||||
// the whole class lands on ES's 64-bit class and every channel reinterprets
|
||||
// bit-exactly. EXT_texture_norm16 - the absence of which is what NoNorm16
|
||||
// means - is also what makes GL_RGBA16 colour-renderable, so the two
|
||||
// questions have one answer.
|
||||
return (options & PixelFormatNormalizeOptionBit::NoNorm16)
|
||||
? ThreeChannelWidening{GL_RGBA32F, GL_RGBA, GL_FLOAT}
|
||||
: ThreeChannelWidening{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT};
|
||||
// Floating point.
|
||||
case GL_RGB16F:
|
||||
return {GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT};
|
||||
|
||||
@@ -239,13 +239,34 @@ public final class PostActivity extends Activity {
|
||||
nativeLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the whole report to logcat. The report is the only machine-readable form of the
|
||||
* POST, and logcat drops everything past roughly 4000 bytes of a single entry - which is
|
||||
* less than one backend section, so a one-call log silently truncated the report to about
|
||||
* the first dozen rows. Each chunk is prefixed with its index so a reader can reassemble
|
||||
* them in order (concatenate the payloads after the "] " separator).
|
||||
*/
|
||||
private static void logReport(String json) {
|
||||
if (json == null) {
|
||||
Log.i(TAG, "<null report>");
|
||||
return;
|
||||
}
|
||||
final int chunkSize = 3000;
|
||||
final int chunks = (json.length() + chunkSize - 1) / chunkSize;
|
||||
for (int index = 0; index < chunks; ++index) {
|
||||
final int start = index * chunkSize;
|
||||
final int end = Math.min(start + chunkSize, json.length());
|
||||
Log.i(TAG, "[" + (index + 1) + "/" + chunks + "] " + json.substring(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
private static void runDriverPost() {
|
||||
String json = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
ensureNativeLoaded();
|
||||
json = nativeRunDriverPost();
|
||||
Log.i(TAG, json == null ? "<null report>" : json);
|
||||
logReport(json);
|
||||
} catch (Throwable error) {
|
||||
Log.e(TAG, "Driver POST failed", error);
|
||||
failure = error;
|
||||
@@ -385,9 +406,54 @@ public final class PostActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
renderKnownDriverBugs(backend.optJSONArray("knownDriverBugs"));
|
||||
renderFormatCapabilities(backend.optJSONObject("formatCapabilities"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Known Driver Bugs" section: core functionality this driver advertises, accepts,
|
||||
* and then does not perform. Separate from the capability checks above because it answers
|
||||
* a different question and uses its own vocabulary.
|
||||
*
|
||||
* Only bugs the device actually HAS are reported, so a clean driver renders no section at
|
||||
* all rather than a list of reassurances - which is why the verdicts are FIXED (a MobileGL
|
||||
* quirk makes application behaviour correct anyway) and UNFIXABLE (no substitute; the
|
||||
* one-liner says what MobileGL does defensively), never PASS/FAIL.
|
||||
*/
|
||||
private void renderKnownDriverBugs(JSONArray bugs) {
|
||||
if (bugs == null || bugs.length() == 0) {
|
||||
return;
|
||||
}
|
||||
addText("Known driver bugs", 14, COLOR_TEXT, true, dp(16));
|
||||
LinearLayout table = new LinearLayout(this);
|
||||
table.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams tableParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
tableParams.topMargin = dp(6);
|
||||
contentLayout.addView(table, tableParams);
|
||||
|
||||
int rowIndex = 0;
|
||||
for (int i = 0; i < bugs.length(); ++i) {
|
||||
JSONObject bug = bugs.optJSONObject(i);
|
||||
if (bug == null) {
|
||||
continue;
|
||||
}
|
||||
// addCheckRow renders name + chip + collapsible detail, which is exactly this
|
||||
// section's shape; the chip text is the verdict rather than a status.
|
||||
JSONObject row = new JSONObject();
|
||||
try {
|
||||
row.put("name", bug.optString("name", "unnamed bug"));
|
||||
row.put("status", bug.optString("verdict", "UNFIXABLE"));
|
||||
row.put("detail", bug.optString("detail", ""));
|
||||
} catch (JSONException ignored) {
|
||||
continue;
|
||||
}
|
||||
addCheckRow(table, row, rowIndex++);
|
||||
}
|
||||
}
|
||||
|
||||
/** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */
|
||||
private static String backendTypeForSection(String sectionName) {
|
||||
switch (sectionName.toLowerCase(Locale.ROOT)) {
|
||||
@@ -731,6 +797,13 @@ public final class PostActivity extends Activity {
|
||||
return COLOR_FAIL;
|
||||
case "INFO":
|
||||
return COLOR_INFO;
|
||||
// The "Known driver bugs" section's own vocabulary. Every row there is a defect
|
||||
// this device HAS, so neither verdict is reassuring: FIXED means MobileGL papers
|
||||
// over it and applications still behave correctly, UNFIXABLE means they do not.
|
||||
case "FIXED":
|
||||
return COLOR_WARN;
|
||||
case "UNFIXABLE":
|
||||
return COLOR_FAIL;
|
||||
default:
|
||||
return COLOR_TEXT;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user