Compare commits

...
14 Commits
Author SHA1 Message Date
swung0x48 82244e9048 [Merge] (GLState, GLImpl, DirectGLES, DirectVulkan, ShaderTranspiler): land texture views, the 4.3 version claim and the extension unlocks 2026-08-22 14:28:37 -04:00
swung0x48 7bd2f08313 [Fix] (DirectGLES): stop advertising GL_ARB_texture_view, whose functional half fails on a host driver that has EXT_texture_view 2026-08-22 13:40:35 -04:00
swung0x48 121b99f8c3 [Fix, Test] (DirectVulkan, GLImpl): size a 1D-array texture's mip chain and layered attachment by the axis that carries its layers 2026-08-22 12:52:10 -04:00
swung0x48 e8d79344d6 [Fix] (DirectVulkan): resolve a cube-map-array sampler to the cube-map-array texture target 2026-08-22 12:47:13 -04:00
swung0x48 88be35c9ba [Fix, Test] (Util): widen a three-channel 16-bit texture to its same-width four-channel sibling 2026-08-22 12:41:18 -04:00
swung0x48 60808b6cf2 [Feat] (DirectGLES, DirectVulkan): advertise base-vertex draws, instanced arrays, KHR_debug and the 4.1-4.3 version tokens 2026-08-22 12:41:18 -04:00
swung0x48 eb2f14e55a [Fix, Test] (GLImpl): validate count, instancecount, drawcount, type and range on the base-vertex draw family 2026-08-22 12:41:18 -04:00
swung0x48 0eb5d54bb8 [Fix, Test] (GLState, ShaderTranspiler): report GL's default binding of zero for an unqualified uniform block 2026-08-22 12:41:18 -04:00
swung0x48 6a2e9dc791 Merge branch 'target-gl43' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL into gl43-claim 2026-08-22 11:42:58 -04:00
swung0x48 d5aceebd7b Merge branch 'dev' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL into gl43-claim 2026-08-22 11:42:50 -04:00
swung0x48 473d9951b7 [Fix, Test] (DirectVulkan, GLState): apply a texture view level and layer window at every subresource boundary 2026-08-22 11:17:09 -04:00
swung0x48 13783e3aec [Feat] (DirectGLES, DirectVulkan): raise the advertised OpenGL target version to 4.3 2026-08-22 10:53:45 -04:00
swung0x48 6bb844b1c1 [Feature, Test] (GLImpl): give KHR_debug a real group stack and object labels 2026-08-22 10:42:49 -04:00
swung0x48 6162603072 [Feature, Fix, Test] (GLState, GLImpl, DirectVulkan, DirectGLES): implement glTextureView over shared texture storage 2026-08-22 10:41:52 -04:00
57 changed files with 4301 additions and 137 deletions
+2
View File
@@ -335,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
@@ -392,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
+7
View File
@@ -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;
+1
View File
@@ -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
+34 -5
View File
@@ -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
: g_backendTextureObjects.GetOrCreate(textureObject);
if (!backendObj) {
backendObj = MakeShared<BackendTextureObject>();
auto& backendSlot = backendTextureSlot ? *backendTextureSlot
: g_backendTextureObjects.GetOrCreate(textureObject);
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
+132 -1
View File
@@ -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());
+36
View File
@@ -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,9 +1222,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
depthAttachmentDescription.format = depthTextureResource->format;
depthAttachmentSampleCount = depthTextureResource->sampleCount;
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
attachmentExtent =
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(),
swapchainExtent);
attachmentExtent = ResolveRenderPassFramebufferExtent(
isDefaultFbo,
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
swapchainExtent);
} else {
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
@@ -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)) {
+271
View File
@@ -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
+42
View File
@@ -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)
+7 -4
View File
@@ -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;
+300 -5
View File
@@ -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
+7
View File
@@ -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
+5
View File
@@ -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();
}
@@ -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),
+2 -2
View File
@@ -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),
@@ -587,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),
@@ -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
+19
View File
@@ -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
+253
View File
@@ -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
+18
View File
@@ -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
+44 -4
View File
@@ -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
+2 -1
View File
@@ -1268,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);
@@ -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());
@@ -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};