Compare commits

...
27 Commits
Author SHA1 Message Date
swung0x48 4446c861be [Fix, Test] (DirectVulkan): keep the draw when a program declares a storage block the application left unbound 2026-08-24 04:57:05 -04:00
swung0x48 602da1d131 [Merge] (GLImpl, DirectGLES, DirectVulkan): complete direct state access and the compressed-texture family 2026-08-22 22:47:08 -04:00
swung0x48 06bbaf32b1 [Merge] (GLImpl, DirectGLES, DirectVulkan): take the extension advertisements under the DSA completion 2026-08-22 22:42:47 -04:00
swung0x48 9bcf0a15a0 [Docs] (DirectGLES, DirectVulkan): correct what advertising cube map arrays actually unlocks 2026-08-22 22:03:26 -04:00
swung0x48 26e5a946ac [Test] (GLImpl): pin the DSA name rule on the compressed 3D by-name entry point 2026-08-22 21:56:51 -04:00
swung0x48 dc543fa905 [Feat, Test] (DirectGLES, DirectVulkan, SelfTest): advertise the implemented extensions that were never named 2026-08-22 21:54:17 -04:00
swung0x48 f559d68728 [Feat, Test] (GLImpl): store and patch compressed 3D texture images, and wire the 1D/3D named entry points 2026-08-22 21:49:33 -04:00
swung0x48 48968a663f [Fix, Test] (GLImpl, Util): let a buffer clear take a GL_INT pattern into a normalized format 2026-08-22 21:49:26 -04:00
swung0x48 0fdcb5d6c4 [Feat] (DirectGLES, DirectVulkan): advertise GL_ARB_sync and GL_ARB_shader_atomic_counters 2026-08-22 21:45:13 -04:00
swung0x48 6b25e7a7e3 [Fix] (GLState): report the storage flags glBufferData implies 2026-08-22 21:45:13 -04:00
swung0x48 50d260c840 [Fix] (GLImpl): reject memory-barrier bits the spec does not define 2026-08-22 21:45:13 -04:00
swung0x48 6a8bf4c03c [Fix, Test] (DirectVulkan): resolve a lowered atomic-counter block from the atomic-counter binding points 2026-08-22 21:45:13 -04:00
swung0x48 01098e9dd7 [Fix] (GLImpl): raise the errors ARB_sync specifies for ClientWaitSync, WaitSync and GetSynciv 2026-08-22 21:45:12 -04:00
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
66 changed files with 6015 additions and 183 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, 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, capabilities.SupportsTextureCubeMapArray);
}
} // 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, Bool cubeMapArraySupported) {
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,77 @@ 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,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
// buffer the application bound. DirectVulkan reaches the same place through its own
// descriptor resolution, so the string is symmetric.
E_GL_ARB_shader_atomic_counters,
// 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,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
// driver's identical parameters.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
E_GL_ARB_viewport_array,
// 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 +1143,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
// reports. So the string follows the host capability rather than the version.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
// 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, Bool cubeMapArraySupported);
// 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,11 +500,11 @@ 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.
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
@@ -512,9 +512,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported) {
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
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 +539,79 @@ 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,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
// now reaches the shader on BOTH backends - Magma resolves the lowered
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
// landed, because the counter silently read whatever was bound as SSBO N instead.
E_GL_ARB_shader_atomic_counters,
// 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,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it. Kept identical to the DirectGLES block so the two
// backends do not disagree about what MobileGL is.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports.
E_GL_ARB_viewport_array,
// 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.
@@ -585,6 +664,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
// view type cannot be created without the imageCubeArray device feature - so the string
// follows the feature, not the version, exactly as the per-layer attachment bit does.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
return extensions;
}
@@ -715,7 +806,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
m_vulkanCaps.SupportsImageCubeArray);
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -75,7 +75,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported);
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -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:
@@ -17,6 +17,7 @@
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
#include <algorithm>
#include <cstdio>
@@ -47,7 +48,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 +418,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 +451,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 +561,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,
};
@@ -905,23 +924,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
binding);
// An atomic counter is not an SSBO the application ever declared: glslang lowers every
// atomic_uint onto a synthesized gl_AtomicCounterBlock_<N> storage block, where N is the
// GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary
// storage-block slot, so resolving it the SSBO way looked up GL_SHADER_STORAGE_BUFFER
// point N' - which is never where glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, N, ...) put
// the buffer. The counter therefore never reached the shader (KHR-GL43
// shader_atomic_counters.advanced-usage-*), and when the application also bound an SSBO at
// the colliding slot the descriptor silently aliased it, so the dispatch wrote over the
// application's own buffer. DirectGLES has always taken this branch explicitly
// (SyncAtomicCounterBuffers); this is the same rule in Magma's descriptor resolution.
//
// Only the SOURCE of the handle differs. The per-counter layout(offset=) is already folded
// into the block's SPIR-V member offsets on this path (FlattenAtomicCounterBlockPass is
// DirectGLES-only), so everything below - residency, the glBindBufferRange window, the
// descriptor fill - is target-agnostic and stays exactly as it was.
const String& blockName = programObj.storageBlockNameByBinding[binding];
const Int atomicCounterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(blockName);
const Bool isAtomicCounterBlock = atomicCounterBinding >= 0;
const BufferTarget bufferTarget =
isAtomicCounterBlock ? BufferTarget::AtomicCounter : BufferTarget::ShaderStorage;
// A block instance array declares one block whose elements take consecutive GL binding
// points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole
// array to that one block - so the element index IS the offset from its binding.
// array to that one block - so the element index IS the offset from its binding. glslang
// synthesizes one counter block per GL binding, so a counter block is never an instance
// array and `element` is always 0 there; the +element rule stays with the SSBO case.
const GLuint frontendBinding =
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
isAtomicCounterBlock
? static_cast<GLuint>(atomicCounterBinding)
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
const Uint32 bindingPointCount =
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
"ResolveStorageBufferDescriptor: frontend SSBO binding %u out of range for block '%s'",
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
frontendBinding, blockName.c_str());
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
return false;
// NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a
// program declare a shader storage block the application never binds a buffer to:
// the block simply has no store, so a read is undefined and a write goes nowhere.
// Refusing here used to take the whole draw or dispatch with it (SetupDraw and
// DispatchCompute both skip on a false return), which is how AcceleratedRendering's
// GUI batch lost its backgrounds: its vertex-transform compute shader declares a
// `Meshes` block it only reads when a vertex comes from a cached server mesh, and a
// batch of plain GUI blits has no meshes and so binds nothing there. The dispatch
// never ran, the transformed vertex buffer stayed as it was, and every hotbar and
// container-screen background quad came out degenerate. A shared zero-filled
// placeholder puts something legal in the descriptor and lets the draw proceed.
const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor();
if (!placeholder.IsValid()) {
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no buffer bound at frontend binding %u for block "
"'%s', and the placeholder descriptor could not be created",
frontendBinding, blockName.c_str());
return false;
}
MGLOG_D("ResolveStorageBufferDescriptor: frontend binding %u ('%s') is unbound; using the placeholder "
"descriptor",
frontendBinding, blockName.c_str());
outBufferInfo.buffer = placeholder.buffer;
outBufferInfo.offset = placeholder.offset;
outBufferInfo.range = placeholder.size;
return true;
}
// The shader may write this buffer, and those writes land in GPU memory behind the
@@ -1020,8 +1085,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 +1101,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",
@@ -16,6 +16,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
constexpr SizeT kLiveResourcePruneThreshold = 256;
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
// every minStorageBufferOffsetAlignment in the wild, and free.
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
// A zero-copy persistent buffer is created once and never recreated (the app holds
// its mapped pointer), and may be bound to any role, so it carries every usage.
// TRANSFER_DST is added by CreateResidentStorage.
@@ -130,6 +134,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
m_transientUploadArena.Shutdown();
m_unboundStorageBuffer.Destroy();
DestroyAllDeferredReleases();
ReleaseAllLiveResources();
m_copyProvider = nullptr;
@@ -706,6 +711,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredResourceReleases[frameIndex].clear();
}
BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
if (!m_unboundStorageBuffer.IsValid()) {
if (m_initInfo.allocator == nullptr) {
return {};
}
// Host-visible so the zero fill needs no command buffer: this can be reached from
// descriptor resolution, which runs inside an already-open recording and must not
// start a copy of its own. The size is a whole minStorageBufferOffsetAlignment-safe
// block rather than 4 bytes so that a shader which does read the block gets a
// plausible unsized-array length instead of one that rounds to zero.
const Bool created = m_unboundStorageBuffer.Create({
.allocator = m_initInfo.allocator,
.size = kUnboundStorageDescriptorBytes,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
});
if (!created) {
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundStorageDescriptor: placeholder creation failed");
m_unboundStorageBuffer.Destroy();
return {};
}
if (void* mapped = m_unboundStorageBuffer.GetMappedData()) {
Memset(mapped, 0, static_cast<SizeT>(kUnboundStorageDescriptorBytes));
}
}
return m_unboundStorageBuffer.GetSlice();
}
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
switch (kind) {
case BufferKind::Vertex:
@@ -120,6 +120,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
VkDeviceSize alignment, BufferSlice& outSlice);
// The descriptor a shader storage block gets when the program declares it and the
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
// legal state - the block simply has no store, so reads are undefined and writes go
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
// buffer, created once and shared by every unbound binding: bindings that are only
// declared (the case this exists for) never touch it, and one that is actually read
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
// that indexes past it.
BufferSlice AcquireUnboundStorageDescriptor();
// Draw-time acquire for resident (device-storage) buffers: ensures the
// resource exists and is fully uploaded, marks it used this frame.
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
@@ -180,6 +191,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena;
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
// with the manager.
VkBufferObject m_unboundStorageBuffer;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
@@ -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
@@ -688,7 +713,34 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
namespace {
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
// GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole.
// Forwarding an undefined bit to the host driver let a caller that had computed its mask
// wrongly (or reused an ES-only bit) get silence instead of the error the spec promises.
constexpr GLbitfield kAllDefinedBarrierBits =
GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT |
GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT |
GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT;
Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) {
if (barriers == GL_ALL_BARRIER_BITS) return true;
if ((barriers & ~kAllDefinedBarrierBits) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function,
"barriers contains bits that are not defined barrier bits."));
return false;
}
return true;
}
} // namespace
void MemoryBarrier(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
MG_State::pGLContext->RecordError(
@@ -700,6 +752,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void MemoryBarrierByRegion(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
if (!memoryBarrierByRegion) {
MG_State::pGLContext->RecordError(
@@ -836,6 +889,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 +916,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 +973,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 +1013,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)
@@ -1060,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
@@ -1848,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits)
+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;
+43 -1
View File
@@ -69,8 +69,25 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and
// any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller
// that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a
// flush it never got.
if ((flags & ~static_cast<GLbitfield>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT."));
return GL_WAIT_FAILED;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
// The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning
// the enum alone left glGetError() clean and the failure indistinguishable from a
// genuine wait failure on a live sync.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return GL_WAIT_FAILED;
}
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
@@ -95,6 +112,9 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return;
}
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
@@ -125,8 +145,22 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
// GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE
// and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean
// and write a plausible-looking zero, which is the one failure mode a caller cannot tell
// apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118),
// but a mistyped pname answered a bare 0 that no query ever returns.
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must not be negative."));
return;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
if (length) {
*length = 0;
}
@@ -152,7 +186,15 @@ namespace MobileGL::MG_Impl::GLImpl {
value = static_cast<GLint>(syncObject->flags);
break;
default:
break;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or "
"GL_SYNC_FLAGS."));
if (length) {
*length = 0;
}
return;
}
if (length) {
+551 -10
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();
@@ -4019,11 +4099,171 @@ namespace MobileGL::MG_Impl::GLImpl {
"1D textures are not supported by this implementation"));
}
// The three-dimensional twin of CompressedTexSubImage2D_State: a block-aligned box of the
// compressed image the level shadows is replaced, slice by slice. Same deviation as the 2D form
// - the uncompressed texel shadow beside it is NOT touched, so what changes is the image
// glGetCompressedTexImage hands back, not what the level samples as.
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data) {
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
// ======================= Converting ================================
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// Zero block width doubles as "format is not a specific compressed format", the
// INVALID_ENUM case - one lookup answers both questions.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
// A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
// accepts one.
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"A proxy target has no texture image to modify."));
return;
}
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
if (width < 0 || height < 0 || depth < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"width, height and depth must be non-negative."));
return;
}
if (compressedInfo.blockWidth == 0) {
RecordUnsupportedCompressedFormat(__func__);
return;
}
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (textureMipmapObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
return;
}
// GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
// exactly this compressed format. That is also what makes the block arithmetic below
// sound - the level's grid is measured with THIS format's block size.
const GLenum levelFormat =
textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
if (levelFormat != format) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"format does not match the internal format of the texture image."));
return;
}
const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
const Int levelDepth = std::max(levelSize.z(), 1);
// Subtractions rather than sums for the reason CompressedTexSubImage2D_State spells out:
// offset + extent are both application-supplied GLints and a signed overflow is undefined.
if (xoffset < 0 || yoffset < 0 || zoffset < 0 || width > levelSize.x() - xoffset ||
height > levelSize.y() - yoffset || depth > levelDepth - zoffset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The replaced region does not lie within the texture image."));
return;
}
// GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
// and must either be a whole number of blocks wide/high or run to the image's edge. Every
// format that reaches here is 4x4x1, so the depth axis carries no block alignment rule -
// each slice is its own block grid.
const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
const Bool alignedX = (xoffset % blockWidth == 0) &&
(width % blockWidth == 0 || xoffset + width == levelSize.x());
const Bool alignedY = (yoffset % blockHeight == 0) &&
(height % blockHeight == 0 || yoffset + height == levelSize.y());
if (!alignedX || !alignedY) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The replaced region is not aligned to the format's compressed blocks."));
return;
}
// Exactly the size the format and dimensions imply, which is also what keeps the copy
// below in bounds.
const SizeT expectedImageSize =
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth});
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"imageSize does not match the compressed image size."));
return;
}
// ======================= Processing ================================
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
const void* compressedBytes = CompressedUnpackSource(data);
if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
if (compressedBytes == nullptr) {
// No unpack buffer and a null client pointer: there is nothing to read. GL leaves
// this undefined rather than erroring, and dereferencing it is the one answer that
// is never acceptable.
MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
return;
}
static std::atomic<Bool> announcedNoCodec3D{false};
if (!announcedNoCodec3D.exchange(true)) {
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
"reach the texels this level SAMPLES as. Upload through glTexSubImage3D for "
"that.",
__func__);
}
const SizeT blobSize =
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
const void* existing =
textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
if (blobSize == 0 || existing == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The texture level holds no compressed image to modify."));
return;
}
Vector<Uint8> blob(blobSize);
Memcpy(blob.data(), existing, blobSize);
const SizeT blockByteSize = compressedInfo.blockByteSize;
const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
compressedInfo.blockWidth;
const SizeT levelBlocksY = (static_cast<SizeT>(levelSize.y()) + compressedInfo.blockHeight - 1) /
compressedInfo.blockHeight;
const SizeT levelRowBytes = levelBlocksX * blockByteSize;
const SizeT levelSliceBytes = levelRowBytes * levelBlocksY;
const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
compressedInfo.blockWidth;
const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
compressedInfo.blockHeight;
const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
const SizeT regionRowBytes = regionBlocksX * blockByteSize;
const SizeT regionSliceBytes = regionRowBytes * regionBlocksY;
const auto* source = static_cast<const Uint8*>(compressedBytes);
for (SizeT slice = 0; slice < static_cast<SizeT>(depth); ++slice) {
const SizeT destSliceBase = (static_cast<SizeT>(zoffset) + slice) * levelSliceBytes;
for (SizeT row = 0; row < regionBlocksY; ++row) {
const SizeT destOffset =
destSliceBase + (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
Memcpy(blob.data() + destOffset, source + slice * regionSliceBytes + row * regionRowBytes,
regionRowBytes);
}
}
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
blob.data(), blobSize);
}
// Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a
@@ -4198,15 +4438,83 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordUnsupportedCompressedFormat(__func__);
}
// The three-dimensional twin of CompressedTexImage2D_State, and the same deviation applies: the
// blocks are shadowed verbatim for glGetCompressedTexImage while the texels this level SAMPLES
// as stay zero, because there is no BC/ETC decoder here. A 3D compressed image is a stack of
// `depth` two-dimensional block grids - every format that reaches here has a 4x4x1 block - so
// the blob layout is slice-major and CalculateCompressedTextureImageSize already multiplies by
// depth.
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth, GLint border, GLsizei imageSize, const void* data) {
// ======================= Converting ================================
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
// Zero block width doubles as "internalformat is not a specific compressed format", which is
// the INVALID_ENUM case - one lookup answers both questions.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
if (compressedInfo.blockWidth == 0) {
RecordUnsupportedCompressedFormat(__func__);
return;
}
// GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply,
// otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds.
const SizeT expectedImageSize =
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth});
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"imageSize does not match the compressed image size."));
return;
}
// Object resolution copied from TexImage3D_State rather than routed through
// GetTextureObjectByTarget, for the reason CompressedTexImage2D_State gives: a proxy target
// is legal here and only CreateOrReplaceProxyTextureObject gives it an object to answer the
// level queries from.
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
// ======================= Processing ================================
const TextureInternalFormat textureInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
textureObject->SetInternalFormat(textureInternalFormat);
// A proxy records the format and nothing else - it must never take storage, and it must never
// be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being
// INVALID_OPERATION.
if (isProxy) return;
const SizeT internalBpp =
MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte);
const SizeT internalBytes =
static_cast<SizeT>(width) * static_cast<SizeT>(height) * static_cast<SizeT>(depth) * internalBpp;
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
// AllocateStorage clears any compressed image the level used to hold, so this must run before
// SetMipmapCompressedImage re-arms it.
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
const void* compressedBytes = CompressedUnpackSource(data);
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
expectedImageSize);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
}
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
@@ -4779,6 +5087,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 +5139,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 +5168,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 +5243,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 +5324,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 +5634,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,
@@ -5226,6 +5749,14 @@ namespace MobileGL::MG_Impl::GLImpl {
free(processedPixels);
}
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CompressedTexSubImage1D_State(target, level, xoffset, width, format, imageSize, data);
});
}
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -5234,6 +5765,16 @@ namespace MobileGL::MG_Impl::GLImpl {
});
}
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CompressedTexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format,
imageSize, data);
});
}
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -37,8 +37,13 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum format, GLenum type, const void* pixels);
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data);
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data);
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
@@ -60,6 +65,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
@@ -105,6 +106,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp
Scenarios/RelinkStageSetScenario.cpp
Scenarios/GuiBatchScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -87,11 +87,6 @@ void main() {
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
<< "; this needs 3 and 2";
}
if (!AtomicCountersAreWired()) {
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
<< "is still resolved from the shader-storage binding points";
}
m_program = CompileComputeProgram(kCounterComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
}
@@ -105,12 +100,6 @@ void main() {
m_program = 0;
}
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
// frontend half (limits, reflection queries, the link-time offset rules) is
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
@@ -0,0 +1,794 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GuiBatchScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A GUI QUAD DRAWN THE WAY AcceleratedRendering DRAWS ONE.
//
// The mod replaces every GUI blit with a compute pass: the application writes unit-space
// vertices into a persistently mapped SSBO, a compute shader multiplies them by a shared
// transform into a second buffer, and that second buffer is then bound as GL_ARRAY_BUFFER of a
// DSA vertex array and drawn with glDrawElementsBaseVertex through vanilla's position_tex_color
// program. Every element of that is replayed here, one axis per test, so a failure names the
// element that killed the quad rather than "the GUI is broken".
//
// The axis that mattered is MeshesBlockLeftUnbound. The mod's vertex-transform compute shader
// declares SIX storage blocks, and the last of them - `Meshes`, the cache of pre-uploaded model
// geometry - is read only when a vertex says it comes from a cached mesh. A batch of plain GUI
// blits has no cached meshes, so the mod binds nothing at that point and the shader never reads
// it. GL 4.6 core 7.8 is explicit that this is legal: a storage block with no buffer at its
// binding point simply has no store.
//
// DirectVulkan used to refuse the whole descriptor set over it, and both SetupDraw and
// DispatchCompute skip their work on that refusal - so the transform dispatch never ran, the
// output vertex buffer kept whatever was in it, and every hotbar and container-screen background
// quad came out degenerate. Items were unaffected because item geometry DOES come from cached
// meshes, which is what made the bug look like "only the backgrounds disappear".
//
// The assertions are whole-region, not centre-pixel: a quad that survives with three stale
// vertices still paints its centre.
//
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound binding to the GLES driver,
// which does what GL says, so it is the control - every test here must stay green on both.
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kTransformComputeSource = R"(#version 460 core
struct Vertex {
float x;
float y;
float z;
float u0;
float v0;
uint color;
};
struct VaryingData {
int offset;
int sharing;
int mesh;
int shouldCull;
};
struct SharingData {
mat4 transform;
mat3 normal;
};
layout(local_size_x = 128) in;
layout(binding=0, std430) restrict readonly buffer VerticesIn {
Vertex verticesIn[];
};
layout(binding=1, std430) restrict writeonly buffer VerticesOut {
Vertex verticesOut[];
};
layout(binding=2, std430) restrict readonly buffer Sharings {
SharingData sharings[];
};
layout(binding=3, std430) restrict readonly buffer VaryingsIn {
VaryingData varyingsIn[];
};
layout(binding=4, std430) restrict writeonly buffer VaryingsOut {
VaryingData varyingsOut[];
};
layout(binding=5, std430) restrict readonly buffer Meshes {
Vertex meshVertices[];
};
layout(location=0) uniform uint vertexCount;
layout(location=1) uniform uint vertexOffset;
layout(location=2) uniform uint varyingOffset;
void main() {
uint indexIn = gl_GlobalInvocationID.x;
uint vertexOut = indexIn + vertexOffset;
uint varyingOut = indexIn + varyingOffset;
if (indexIn >= vertexCount) {
return;
}
int offset = varyingsIn[indexIn] .offset;
uint reference = indexIn - offset;
int sharing = varyingsIn[reference] .sharing;
int mesh = varyingsIn[reference] .mesh;
mat4 transformMatrix;
if (sharing != -1) {
transformMatrix = sharings[sharing].transform;
} else {
transformMatrix = mat4(1.0);
}
Vertex vertexIn;
vec4 colorMesh;
if (mesh != -1) {
vertexIn = meshVertices[mesh + offset];
colorMesh = unpackUnorm4x8 (vertexIn.color);
} else {
vertexIn = verticesIn[indexIn];
colorMesh = vec4 (1.0);
}
vec4 colorIn = unpackUnorm4x8 (verticesIn[reference].color);
vec4 posOut = transformMatrix * vec4 (vertexIn.x, vertexIn.y, vertexIn.z, 1.0);
vec4 colorOut = colorMesh * colorIn;
verticesOut[vertexOut].x = posOut.x;
verticesOut[vertexOut].y = posOut.y;
verticesOut[vertexOut].z = posOut.z;
verticesOut[vertexOut].u0 = vertexIn.u0;
verticesOut[vertexOut].v0 = vertexIn.v0;
verticesOut[vertexOut].color = packUnorm4x8 (colorOut);
varyingsOut[varyingOut].offset = offset;
varyingsOut[varyingOut].shouldCull = varyingsIn[reference].shouldCull;
}
)";
// Vanilla position_tex_color, spelled the way MC ships it: #version 150, no explicit
// attribute locations (they come from glBindAttribLocation in format order) and the
// ProjMat/ModelViewMat pair the mod re-uploads through setDefaultUniforms.
constexpr const char* kBlitVertexSource = R"(#version 150
in vec3 Position;
in vec2 UV0;
in vec4 Color;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
out vec2 texCoord0;
out vec4 vertexColor;
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
texCoord0 = UV0;
vertexColor = Color;
}
)";
constexpr const char* kBlitFragmentSource = R"(#version 150
uniform sampler2D Sampler0;
in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;
void main() {
fragColor = texture(Sampler0, texCoord0) * vertexColor;
}
)";
constexpr int kFboSize = 64;
constexpr int kShaderStorageRestoreRange = 9;
constexpr int kAtomicCounterRestoreRange = 1;
constexpr int kBuilders = 2;
struct GuiVertex {
float x, y, z;
float u, v;
std::uint32_t color;
};
static_assert(sizeof(GuiVertex) == 24, "POSITION_TEX_COLOR is 24 bytes");
struct VaryingData {
std::int32_t offset;
std::int32_t sharing;
std::int32_t mesh;
std::int32_t shouldCull;
};
struct IndexedBinding {
GLint buffer = 0;
GLint start = 0;
GLint size = 0;
};
// Which parts of the mod's real frame this replay reproduces. Each test flips exactly
// one on top of the baseline so a failure names the element that killed the quad.
struct Fidelity {
bool shortIndices = false; // MC's AutoStorageIndexBuffer is USHORT at small counts
bool baseVertex = false; // ...and the second builder draws at a base vertex
bool twoBuilders = false; // two render types share one output buffer
bool blendAndDepth = false; // TRANSLUCENT_TRANSPARENCY + LEQUAL_DEPTH_TEST
bool regrow = false; // MutableBuffer.doExpand replaces the GL name
bool rewriteMapEachFrame = false;
bool skipRelayout = false; // bindDrawBuffers() only re-lays-out when resized
bool leaveMeshesUnbound = false; // a batch with no server meshes never binds binding 5
};
class GuiBatchScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_transform = CompileComputeProgram(kTransformComputeSource);
ASSERT_NE(m_transform, 0u) << m_buildLog;
m_blit = CompileBlitProgram();
ASSERT_NE(m_blit, 0u) << m_buildLog;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u);
MakeTexture();
MakeIndexBuffers();
MakeAcceleratedBuffers();
m_laidOut = false;
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
DestroyColorFbo(m_target);
if (m_transform) glDeleteProgram(m_transform);
if (m_blit) glDeleteProgram(m_blit);
if (m_texture) glDeleteTextures(1, &m_texture);
if (!m_buffers.empty()) glDeleteBuffers((GLsizei)m_buffers.size(), m_buffers.data());
if (!m_vaos.empty()) glDeleteVertexArrays((GLsizei)m_vaos.size(), m_vaos.data());
m_buffers.clear();
m_vaos.clear();
}
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
GLuint CompileOne(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
return shader;
}
// glBindAttribLocation in format order, exactly as ShaderInstance does it.
unsigned int CompileBlitProgram() {
const GLuint vs = CompileOne(GL_VERTEX_SHADER, kBlitVertexSource);
if (!vs) return 0;
const GLuint fs = CompileOne(GL_FRAGMENT_SHADER, kBlitFragmentSource);
if (!fs) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glBindAttribLocation(program, 0, "Position");
glBindAttribLocation(program, 1, "UV0");
glBindAttribLocation(program, 2, "Color");
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("blit program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
void MakeTexture() {
std::vector<std::uint8_t> pixels(4 * 4 * 4, 0);
for (int i = 0; i < 16; ++i) {
pixels[i * 4 + 2] = 255;
pixels[i * 4 + 3] = 255;
}
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
}
GLuint NewBuffer() {
GLuint b = 0;
glCreateBuffers(1, &b);
m_buffers.push_back(b);
return b;
}
GLuint NewVao() {
GLuint v = 0;
glCreateVertexArrays(1, &v);
m_vaos.push_back(v);
return v;
}
void MakeIndexBuffers() {
std::uint32_t wide[12];
std::uint16_t narrow[12];
for (int quad = 0; quad < 2; ++quad) {
const std::uint32_t base = (std::uint32_t)(quad * 4);
const std::uint32_t pattern[6] = {base, base + 1, base + 2, base + 2, base + 3, base};
for (int i = 0; i < 6; ++i) {
wide[quad * 6 + i] = pattern[i];
narrow[quad * 6 + i] = (std::uint16_t)pattern[i];
}
}
m_wideIndices = NewBuffer();
glNamedBufferStorage(m_wideIndices, sizeof(wide), wide, 0);
m_narrowIndices = NewBuffer();
glNamedBufferStorage(m_narrowIndices, sizeof(narrow), narrow, 0);
}
static void SetupAttributes() {
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (const void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 24, (const void*)12);
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 24, (const void*)20);
glEnableVertexAttribArray(2);
}
void WriteInputs() {
const GuiVertex unit[4] = {
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0xFFFFFFFFu},
{0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0xFFFFFFFFu},
{1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0xFFFFFFFFu},
{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0xFFFFFFFFu},
};
for (int b = 0; b < kBuilders; ++b) {
std::memcpy(m_inVertexMap[b], unit, sizeof(unit));
VaryingData varyings[4];
for (int i = 0; i < 4; ++i) {
varyings[i].offset = i;
varyings[i].sharing = b;
varyings[i].mesh = -1;
varyings[i].shouldCull = 0;
}
std::memcpy(m_inVaryingMap[b], varyings, sizeof(varyings));
}
}
void MakeAcceleratedBuffers() {
const GLbitfield persistent = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
for (int b = 0; b < kBuilders; ++b) {
m_inVertices[b] = NewBuffer();
glNamedBufferStorage(m_inVertices[b], 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, persistent);
m_inVertexMap[b] =
glMapNamedBufferRange(m_inVertices[b], 0, 4 * (GLsizeiptr)sizeof(GuiVertex), persistent);
m_inVaryings[b] = NewBuffer();
glNamedBufferStorage(m_inVaryings[b], 4 * (GLsizeiptr)sizeof(VaryingData), nullptr, persistent);
m_inVaryingMap[b] =
glMapNamedBufferRange(m_inVaryings[b], 0, 4 * (GLsizeiptr)sizeof(VaryingData), persistent);
}
// Two SharingData entries: builder 0 lands left of centre, builder 1 right.
float sharing[56] = {};
const float tx[2] = {-0.9f, 0.1f};
for (int b = 0; b < 2; ++b) {
float* m = sharing + b * 28;
m[0] = 0.8f;
m[5] = 1.0f;
m[10] = 1.0f;
m[15] = 1.0f;
m[12] = tx[b];
m[13] = -0.5f;
m[16] = 1.0f;
m[20] = 1.0f;
m[24] = 1.0f;
}
m_sharings = NewBuffer();
glNamedBufferStorage(m_sharings, sizeof(sharing), nullptr, persistent);
void* r = glMapNamedBufferRange(m_sharings, 0, sizeof(sharing), persistent);
std::memcpy(r, sharing, sizeof(sharing));
m_outSize = 8 * (GLsizeiptr)sizeof(GuiVertex);
m_outVertices = NewBuffer();
glNamedBufferStorage(m_outVertices, m_outSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
m_outVaryings = NewBuffer();
glNamedBufferStorage(m_outVaryings, 8 * (GLsizeiptr)sizeof(VaryingData), nullptr,
GL_DYNAMIC_STORAGE_BIT);
m_meshes = NewBuffer();
glNamedBufferStorage(m_meshes, 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, GL_DYNAMIC_STORAGE_BIT);
m_vao = NewVao();
WriteInputs();
}
std::vector<IndexedBinding> Record(GLenum b, GLenum s, GLenum z, int range) {
std::vector<IndexedBinding> saved((std::size_t)range);
for (int i = 0; i < range; ++i) {
glGetIntegeri_v(b, (GLuint)i, &saved[(std::size_t)i].buffer);
glGetIntegeri_v(s, (GLuint)i, &saved[(std::size_t)i].start);
glGetIntegeri_v(z, (GLuint)i, &saved[(std::size_t)i].size);
}
return saved;
}
void Restore(GLenum target, const std::vector<IndexedBinding>& saved) {
for (std::size_t i = 0; i < saved.size(); ++i) {
if (saved[i].start == 0 && saved[i].size == 0) {
glBindBufferBase(target, (GLuint)i, (GLuint)saved[i].buffer);
} else {
glBindBufferRange(target, (GLuint)i, (GLuint)saved[i].buffer, saved[i].start,
saved[i].size);
}
}
}
// MutableBuffer.doExpand: a NEW immutable store, copied from the old one, old one gone.
void RegrowOutputBuffer() {
const GLsizeiptr newSize = m_outSize * 2;
GLuint grown = 0;
glCreateBuffers(1, &grown);
glNamedBufferStorage(grown, newSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
glCopyNamedBufferSubData(m_outVertices, grown, 0, 0, m_outSize);
glDeleteBuffers(1, &m_outVertices);
for (auto& b : m_buffers) {
if (b == m_outVertices) b = grown;
}
m_outVertices = grown;
m_outSize = newSize;
}
void Frame(const Fidelity& f, int builders) {
if (f.rewriteMapEachFrame) WriteInputs();
// --- prepareBuffers() -------------------------------------------------
const std::vector<IndexedBinding> ssbo =
Record(GL_SHADER_STORAGE_BUFFER_BINDING, GL_SHADER_STORAGE_BUFFER_START,
GL_SHADER_STORAGE_BUFFER_SIZE, kShaderStorageRestoreRange);
const std::vector<IndexedBinding> counters =
Record(GL_ATOMIC_COUNTER_BUFFER_BINDING, GL_ATOMIC_COUNTER_BUFFER_START,
GL_ATOMIC_COUNTER_BUFFER_SIZE, kAtomicCounterRestoreRange);
GLint currentProgram = 0;
glGetIntegerv(GL_CURRENT_PROGRAM, &currentProgram);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outVertices);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_sharings);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_outVaryings);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, f.leaveMeshesUnbound ? 0 : m_meshes);
glUseProgram(m_transform);
for (int b = 0; b < builders; ++b) {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inVertices[b]);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_inVaryings[b]);
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexCount"), 4u);
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexOffset"),
(GLuint)(4 * b));
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "varyingOffset"),
(GLuint)(4 * b));
glDispatchCompute(1, 1, 1);
}
glUseProgram(0);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glUseProgram((GLuint)currentProgram);
Restore(GL_SHADER_STORAGE_BUFFER, ssbo);
Restore(GL_ATOMIC_COUNTER_BUFFER, counters);
if (f.regrow) {
RegrowOutputBuffer();
m_laidOut = false; // isResized() forces the relayout
}
// --- drawBuffers() ----------------------------------------------------
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT |
GL_COMMAND_BARRIER_BIT);
glBindVertexArray(m_vao);
if (!m_laidOut || !f.skipRelayout) {
glBindBuffer(GL_ARRAY_BUFFER, m_outVertices);
SetupAttributes();
m_laidOut = true;
}
if (f.blendAndDepth) {
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
}
const GLenum indexType = f.shortIndices ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT;
const GLuint indexBuffer = f.shortIndices ? m_narrowIndices : m_wideIndices;
const GLsizei indexStride = f.shortIndices ? 2 : 4;
for (int b = 0; b < builders; ++b) {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); // AutoStorageIndexBuffer.bind
glUseProgram(m_blit);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
glUniform1i(glGetUniformLocation(m_blit, "Sampler0"), 0);
UploadIdentityMatrices();
if (f.baseVertex) {
// The mod's BASEVERTEX path: every builder reads the SAME first six
// indices and offsets the vertices with a base vertex.
glDrawElementsBaseVertex(GL_TRIANGLES, 6, indexType, (const void*)0, 4 * b);
} else {
glDrawElements(GL_TRIANGLES, 6, indexType, (const void*)(intptr_t)(b * 6 * indexStride));
}
glUseProgram(0);
}
if (f.blendAndDepth) {
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
}
glBindVertexArray(0);
}
void UploadIdentityMatrices() {
static const float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ModelViewMat"), 1, GL_FALSE, identity);
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ProjMat"), 1, GL_FALSE, identity);
}
void ExpectQuads(const Image& image, int builders, const char* when) {
EXPECT_TRUE(RegionIsMostly(image, 6, 26, 19, 45, "blue", 0.0,
std::string("left accelerated quad, ") + when));
if (builders > 1) {
EXPECT_TRUE(RegionIsMostly(image, 38, 58, 19, 45, "blue", 0.0,
std::string("right accelerated quad, ") + when));
}
}
void RunFrames(const Fidelity& f, int builders, int frames, const char* when) {
for (int i = 0; i < frames; ++i) {
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
Frame(f, builders);
ASSERT_EQ(FirstGLError(), 0u) << "frame " << i << " raised a GL error (" << when << ")";
const Image image = ReadPixels(kFboSize, kFboSize);
ExpectQuads(image, builders, (std::string(when) + ", frame " + std::to_string(i)).c_str());
Gl().EndFrame();
}
}
unsigned int m_transform = 0;
unsigned int m_blit = 0;
ColorFbo m_target{};
GLuint m_texture = 0;
GLuint m_wideIndices = 0;
GLuint m_narrowIndices = 0;
GLuint m_vao = 0;
GLuint m_inVertices[kBuilders] = {};
GLuint m_inVaryings[kBuilders] = {};
void* m_inVertexMap[kBuilders] = {};
void* m_inVaryingMap[kBuilders] = {};
GLuint m_sharings = 0;
GLuint m_outVertices = 0;
GLuint m_outVaryings = 0;
GLuint m_meshes = 0;
GLsizeiptr m_outSize = 0;
bool m_laidOut = false;
std::vector<GLuint> m_buffers;
std::vector<GLuint> m_vaos;
std::string m_buildLog;
};
} // namespace
TEST_F(GuiBatchScenario, Baseline) {
if (!Ready() || IsSkipped()) return;
RunFrames(Fidelity{}, 1, 1, "baseline");
}
TEST_F(GuiBatchScenario, ShortIndices) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.shortIndices = true;
RunFrames(f, 1, 1, "short indices");
}
TEST_F(GuiBatchScenario, TwoBuildersWideIndices) {
if (!Ready() || IsSkipped()) return;
RunFrames(Fidelity{}, 2, 1, "two builders, wide indices");
}
TEST_F(GuiBatchScenario, TwoBuildersBaseVertex) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
RunFrames(f, 2, 1, "two builders, base vertex");
}
TEST_F(GuiBatchScenario, TwoBuildersShortIndicesBaseVertex) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
RunFrames(f, 2, 1, "two builders, short indices, base vertex");
}
TEST_F(GuiBatchScenario, BlendAndDepth) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
RunFrames(f, 2, 1, "blend and depth");
}
TEST_F(GuiBatchScenario, ThreeFramesWithoutRelayout) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
f.skipRelayout = true;
f.rewriteMapEachFrame = true;
RunFrames(f, 2, 3, "three frames without relayout");
}
TEST_F(GuiBatchScenario, MeshesBlockLeftUnbound) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
f.leaveMeshesUnbound = true;
RunFrames(f, 2, 1, "Meshes block left unbound");
}
TEST_F(GuiBatchScenario, FullFidelityWithRegrowth) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
f.skipRelayout = true;
f.rewriteMapEachFrame = true;
RunFrames(f, 2, 1, "full fidelity, pre-growth");
f.regrow = true;
RunFrames(f, 2, 1, "full fidelity, growth frame");
f.regrow = false;
RunFrames(f, 2, 2, "full fidelity, post-growth");
}
namespace {
// Atomic counter blocks resolve through the SAME descriptor path as shader storage
// blocks (glslang rewrites every atomic_uint into a synthesized storage block), so an
// unbound GL_ATOMIC_COUNTER_BUFFER point loses the dispatch for exactly the same reason
// an unbound SSBO did. The mod reaches this with its INDIRECT draw method, whose culling
// shaders carry a counter the BASEVERTEX default never binds.
//
// The counter is INCREMENTED, not merely declared: an unreferenced one is optimised out
// before it ever reaches a descriptor, so a shader that only declares it proves nothing.
constexpr const char* kCounterComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 0, offset = 0) uniform atomic_uint g_unbound;
layout(std430, binding = 0) buffer Output { uint g_data[]; };
void main() {
atomicCounterIncrement(g_unbound);
g_data[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x + 1u;
}
)";
class UnboundCounterBlockScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint counters = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
if (counters < 1) {
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters;
}
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &kCounterComputeSource, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
ASSERT_NE(compiled, GL_FALSE);
m_program = glCreateProgram();
glAttachShader(m_program, shader);
glLinkProgram(m_program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(m_program, GL_LINK_STATUS, &linked);
ASSERT_NE(linked, GL_FALSE);
glGenBuffers(1, &m_buffer);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
if (m_buffer) glDeleteBuffers(1, &m_buffer);
if (m_program) glDeleteProgram(m_program);
}
unsigned int m_program = 0;
GLuint m_buffer = 0;
};
} // namespace
TEST_F(UnboundCounterBlockScenario, ADeclaredButUnboundCounterDoesNotLoseTheDispatch) {
if (!Ready() || IsSkipped()) return;
constexpr int kElements = 4;
const std::vector<unsigned int> zeros((std::size_t)kElements, 0u);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)(zeros.size() * sizeof(unsigned int)), zeros.data(),
GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
// Nothing is bound at GL_ATOMIC_COUNTER_BUFFER point 0 on purpose.
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(m_program);
glDispatchCompute(kElements, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
std::vector<unsigned int> values((std::size_t)kElements, 0xDEADBEEFu);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(values.size() * sizeof(unsigned int)),
values.data());
for (int i = 0; i < kElements; ++i) {
EXPECT_EQ(values[(std::size_t)i], (unsigned int)(i + 1))
<< "element " << i << " came back as " << values[(std::size_t)i]
<< "; zero everywhere means the whole dispatch was dropped over the unbound counter block";
}
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
}
} // namespace MGITest
@@ -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
@@ -118,7 +118,13 @@ namespace MobileGL::MG_State::GLState {
// record that so backends skip uploading the stale shadow bytes.
m_hasDefinedContent = (data != nullptr) || size == 0;
m_isImmutableStorage = false;
m_storageFlags = 0;
// GL 4.6 core 6.2 defines glBufferData as glBufferStorage with
// DYNAMIC_STORAGE_BIT | MAP_READ_BIT | MAP_WRITE_BIT, so GL_BUFFER_STORAGE_FLAGS has to
// report those three afterwards. Reporting 0 - the value that belongs to a buffer whose
// store has never been specified - told an application that a perfectly writable
// glBufferData buffer accepted neither glBufferSubData nor a map. Only the IMMUTABLE flag
// distinguishes the two cases, and it is cleared just above.
m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
NotifyRespecify();
}
+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,22 +1087,76 @@ 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, 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, false);
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false, false);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
}
// Cube map arrays are core at the version MobileGL claims, but there is nothing underneath on a
// pre-ES-3.2 driver without EXT/OES_texture_cube_map_array, and no VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
// without the imageCubeArray feature. The string has to follow the capability on both backends -
// and it has to BE there when the capability is, because KHR-GL4*.texture_gather.*-cube-array
// gates on the string with no core-version fallback.
TEST(CubeMapArrayAdvertisement, FollowsTheHostCapabilityOnBothBackends) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto esWithout =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false);
EXPECT_FALSE(contains(esWithout, MobileGL::E_GL_ARB_texture_cube_map_array));
const auto esWith =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, true);
EXPECT_TRUE(contains(esWith, MobileGL::E_GL_ARB_texture_cube_map_array));
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false,
false);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_ARB_texture_cube_map_array));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, true);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_cube_map_array));
}
// The core-plumbing strings carry no capability gate: they name entry points that have been real
// on both backends for as long as the backends have existed, and an application that gates its
// entry-point resolution on the string (LWJGL does) would otherwise call through null. Pinned
// together so a future edit cannot quietly drop one, and pinned on BOTH backends so the two
// cannot disagree about what MobileGL is.
TEST(CorePlumbingAdvertisement, IsUnconditionalAndIdenticalOnBothBackends) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const MobileGL::GLExtension expected[] = {
MobileGL::E_GL_ARB_sync, MobileGL::E_GL_ARB_shader_atomic_counters,
MobileGL::E_GL_ARB_vertex_array_object, MobileGL::E_GL_ARB_sampler_objects,
MobileGL::E_GL_ARB_map_buffer_range, MobileGL::E_GL_ARB_copy_buffer,
MobileGL::E_GL_ARB_copy_image, MobileGL::E_GL_ARB_texture_swizzle,
MobileGL::E_GL_ARB_vertex_type_2_10_10_10_rev, MobileGL::E_GL_ARB_texture_rg,
MobileGL::E_GL_ARB_depth_buffer_float, MobileGL::E_GL_ARB_texture_float,
MobileGL::E_GL_ARB_viewport_array};
// Every gate off: none of these may depend on one.
const auto es = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false,
false);
const auto vk = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false);
for (const auto extension : expected) {
EXPECT_TRUE(contains(es, extension)) << "DirectGLES stopped advertising extension " << extension;
EXPECT_TRUE(contains(vk, extension)) << "DirectVulkan stopped advertising extension " << extension;
}
}
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
@@ -1113,27 +1167,27 @@ 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, 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, 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, false);
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
const auto vkWithoutBaseInstance =
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false);
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
const auto vkWithBoth =
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true, false);
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
}
+56
View File
@@ -711,6 +711,62 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) {
EXPECT_EQ(actual, (Vector<Uint32>{0, pattern, pattern, pattern, 0}));
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core table 8.2 pairs GL_INT with the non-integer base formats as a signed-normalized
// source, so a GL_R8 clear whose pattern arrives as (GL_RED, GL_INT) is legal. The pair used to be
// rejected with INVALID_VALUE, which is the first call
// KHR-GL45.direct_state_access.buffers_functional makes.
TEST_F(BufferTest, ClearNamedBufferSubDataAcceptsSignedNormalizedIntPattern) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::CreateBuffers(1, &buffer);
const Vector<Uint8> initial(24, 0x7F);
MobileGL::MG_Impl::GLImpl::NamedBufferStorage(
buffer, initial.size(), initial.data(),
GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT | GL_MAP_PERSISTENT_BIT);
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const GLint zero = 0;
MobileGL::MG_Impl::GLImpl::ClearNamedBufferSubData(buffer, GL_R8, 0, sizeof(GLint), GL_RED, GL_INT, &zero);
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Vector<Uint8> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
Vector<Uint8> expected(initial);
for (SizeT i = 0; i < sizeof(GLint); ++i) expected[i] = 0;
EXPECT_EQ(actual, expected);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
// The same pair on the bound-target entry point: the DSA and the bound call share
// ClearBufferRange_State, and a regression in either direction has to show up here too.
TEST_F(BufferTest, ClearBufferSubDataAcceptsSignedNormalizedIntPattern) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
const Vector<Uint8> initial(8, 0x7F);
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
// GL_INT is signed-normalized against 2^31-1, so the maximum maps to a saturated GL_R8 texel.
const GLint one = 2147483647;
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(GL_ARRAY_BUFFER, GL_R8, 0, 4, GL_RED, GL_INT, &one);
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Vector<Uint8> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
EXPECT_EQ(actual, (Vector<Uint8>{0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x7F, 0x7F, 0x7F}));
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
TEST_F(BufferTest, ClearBufferSubDataInitializesIrisStaticSsboRange) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
@@ -516,17 +516,17 @@ 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, false),
E_GL_KHR_parallel_shader_compile));
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, 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, 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),
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::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";
}
+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
+213 -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,
@@ -2195,6 +2227,175 @@ TEST_F(TextureTest, CompressedTextureSubImage2DModifiesTheNamedTextureOnly) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
namespace {
// 8x8x8 RGTC1: 2x2 blocks of 8 bytes per slice, so a slice is 32 bytes and the stack is 256.
constexpr GLsizei kRgtc1Size8x8x8 = 256;
constexpr GLsizei kRgtc1Slice8x8 = 32;
GLuint MakeCompressedRgtc1Texture3D() {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture);
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Size8x8x8,
nullptr);
return texture;
}
} // namespace
// glCompressedTexImage3D used to answer GL_INVALID_ENUM to every call, which is what threw
// KHR-GL45.direct_state_access.textures_compressed_subimage out with an InternalError: the CTS
// asserts no error on it. A 3D compressed image is a stack of per-slice block grids, and the whole
// stack has to come back byte for byte.
TEST_F(TextureTest, CompressedTexImage3DShadowsTheWholeStackForReadback) {
Uint8 whole[kRgtc1Size8x8x8];
for (Int i = 0; i < kRgtc1Size8x8x8; ++i) whole[i] = static_cast<Uint8>(i);
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture);
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Size8x8x8,
whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, whole, sizeof(whole)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// An imageSize that is not the one the format and the three dimensions imply - the depth axis
// is the term a 2D-shaped size calculation would drop.
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Slice8x8,
whole);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// Where the incoming blocks land. The box below is one block wide, one block high and two slices
// deep, starting at block (1,1) of slice 3: an implementation that dropped the slice stride, the
// block-row term or the block-column term puts them somewhere else, and a full-image write would
// hide all three.
TEST_F(TextureTest, CompressedTexSubImage3DPlacesBlocksSliceBySlice) {
const GLuint texture = MakeCompressedRgtc1Texture3D();
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 zeros[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, zeros);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const Uint8 box[16] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7};
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 4, 4, 3, 4, 4, 2, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(box)), box);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kRgtc1Size8x8x8] = {};
// slice 3, block row 1, block column 1 -> 3*32 + 1*16 + 1*8, and the same place one slice on.
std::memcpy(expected + 3 * kRgtc1Slice8x8 + 16 + 8, box, 8);
std::memcpy(expected + 4 * kRgtc1Slice8x8 + 16 + 8, box + 8, 8);
Uint8 stored[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// glCompressedTextureSubImage3D was an exported no-op that raised no error at all. It must reach the
// NAMED texture and leave the binding it borrowed exactly as it found it.
TEST_F(TextureTest, CompressedTextureSubImage3DModifiesTheNamedTextureOnly) {
const GLuint bound = MakeCompressedRgtc1Texture3D();
Uint8 boundImage[kRgtc1Size8x8x8];
std::memset(boundImage, 0x11, sizeof(boundImage));
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, boundImage);
const GLuint named = MakeCompressedRgtc1Texture3D();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, bound); // `named` is NOT the bound texture
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 namedImage[kRgtc1Size8x8x8];
std::memset(namedImage, 0x22, sizeof(namedImage));
MG_Impl::GLImpl::CompressedTextureSubImage3D(named, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, namedImage);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, boundImage, sizeof(stored)), 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, named);
std::memset(stored, 0, sizeof(stored));
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, namedImage, sizeof(stored)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, CompressedTexSubImage3DRejectsTheRegionsGLForbids) {
const GLuint texture = MakeCompressedRgtc1Texture3D();
Uint8 blocks[kRgtc1Size8x8x8] = {};
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A format that is not the one the image is stored in.
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RG_RGTC2, 512, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A start that is not on a block boundary.
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 2, 0, 0, 4, 8, 8, GL_COMPRESSED_RED_RGTC1, 128, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A box that runs past the last slice - the depth bound a 2D-shaped range check never applies.
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 6, 8, 8, 4, GL_COMPRESSED_RED_RGTC1, 128, blocks);
ExpectSingleGlError(GL_INVALID_VALUE);
(void)texture;
}
// The DSA name rule the CTS's textures_creation pair does not reach for these two entry points: a
// name handed out by glGenTextures has no object until it is first bound, so a by-name call on it is
// INVALID_OPERATION - and, unlike the stub these replaced, it has to SAY so rather than return
// quietly. A glCreateTextures name is a created object and gets past the name check.
TEST_F(TextureTest, CompressedTextureSubImage3DRejectsAGeneratedButNeverBoundName) {
GLuint generated = 0;
MG_Impl::GLImpl::GenTextures(1, &generated);
ASSERT_NE(generated, 0u);
DrainPendingGlErrors();
Uint8 blocks[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::CompressedTextureSubImage3D(generated, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A created name is past the name check, so whatever it answers is about the IMAGE (this one
// holds none yet), never about the name.
GLuint created = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &created);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CompressedTextureSubImage3D(created, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, blocks);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION)
<< "a created 3D texture with no compressed image is an image error, not a name error";
DrainPendingGlErrors();
}
// Core GL defines no compressed format for a 1D target, so both the bound and the by-name entry
// point have to REFUSE the call. The by-name one used to be an exported no-op that raised nothing,
// which is the one answer an application cannot act on.
TEST_F(TextureTest, CompressedTextureSubImage1DRefusesLikeTheBoundCall) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, texture);
MG_Impl::GLImpl::TexImage1D(GL_TEXTURE_1D, 0, GL_R8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
Uint8 blocks[16] = {};
MG_Impl::GLImpl::CompressedTexSubImage1D(GL_TEXTURE_1D, 0, 0, 8, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(blocks)), blocks);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::CompressedTextureSubImage1D(texture, 0, 0, 8, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(blocks)), blocks);
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(TextureTest, CompressedTexSubImage2DRejectsTheRegionsGLForbids) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
Uint8 blocks[kRgtc1Size8x8] = {};
@@ -4033,6 +4234,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 +4250,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
+6 -2
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, summary.caps.SupportsTextureCubeMapArray));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -2008,6 +2009,7 @@ namespace MobileGL::MG_Util::SelfTest {
Bool samplerAnisotropySupported = false;
Bool drawIndirectFirstInstanceSupported = false;
Bool shaderDrawParametersSupported = false;
Bool imageCubeArraySupported = false;
};
} // namespace
@@ -2299,6 +2301,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
summary.imageCubeArraySupported = features.imageCubeArray == VK_TRUE;
summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE;
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
@@ -2720,7 +2723,8 @@ namespace MobileGL::MG_Util::SelfTest {
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported,
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported));
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported,
summary.imageCubeArraySupported));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::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());
+23
View File
@@ -20,6 +20,28 @@ namespace MobileGL {
// buffer, and the trailing number is the only place the GL binding survives.
inline constexpr const char* ATOMIC_COUNTER_BLOCK_PREFIX = "gl_AtomicCounterBlock";
// "gl_AtomicCounterBlock_5" -> 5; -1 for any name that is not one of these blocks.
// Recovering N from the NAME is not a shortcut, it is the only way: the block reaches
// a backend auto-mapped to whatever storage-block slot the IO mapper had free, and
// that number has no relation to the GL atomic-counter binding the application asked
// for (see TMglGlslIoResolver). A backend that resolves the block from the
// shader-storage binding points therefore binds the wrong buffer - or, worse, the
// application's own SSBO at the same slot.
inline Int AtomicCounterBlockGlBinding(StringView name) {
const SizeT prefixLength = StringView(ATOMIC_COUNTER_BLOCK_PREFIX).size();
// Needs the prefix, the '_' and at least one digit.
if (name.size() <= prefixLength + 1) return -1;
if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1;
if (name[prefixLength] != '_') return -1;
Int binding = 0;
for (SizeT i = prefixLength + 1; i < name.size(); ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
binding = binding * 10 + (name[i] - '0');
if (binding > 0x0FFFFFFF) return -1; // absurd suffix; treat as not-a-counter
}
return binding;
}
// Atomic-counter limits, in ONE place because GL 4.6 requires glGetIntegerv and the
// shading language's gl_MaxAtomicCounter* constants to report the same numbers
// (KHR-GL43.shader_atomic_counters.basic-glsl-built-in compares them directly).
@@ -76,6 +98,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;
@@ -442,14 +442,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return packed.fieldCount == mapping.channelCount;
}
// GL 4.6 core table 8.2: every unpacked component type pairs with every base format,
// with only two exclusions - an integer format takes integer types only, and the two
// floating types need a non-integer format. This used to be derived from
// GetDirectShadowComponentForType, which answers a different question (is the client
// layout byte-identical to some shadow layout) and has no SNorm32 to hand back for
// (non-integer format, GL_INT). That legal pair was therefore rejected outright, even
// though ConvertUnpackRow decodes it through DecodeComponentToFloat like every other
// normalized type - which is what glClearBufferData(GL_R8, GL_RED, GL_INT) needs.
switch (type) {
case TexturePixelDataType::UnsignedInt5999Rev:
case TexturePixelDataType::UnsignedInt101111Rev:
return !mapping.isInteger && mapping.channelCount == 3;
default: {
ShadowComponent component{};
return GetDirectShadowComponentForType(type, mapping.isInteger, component);
}
case TexturePixelDataType::UnsignedByte:
case TexturePixelDataType::Byte:
case TexturePixelDataType::UnsignedShort:
case TexturePixelDataType::Short:
case TexturePixelDataType::UnsignedInt:
case TexturePixelDataType::Int:
return true;
case TexturePixelDataType::HalfFloat:
case TexturePixelDataType::Float:
return !mapping.isInteger;
default:
return 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};