From 6162603072c5190ce37a8b31ac950870b08c20ce Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 10:41:52 -0400 Subject: [PATCH] [Feature, Fix, Test] (GLState, GLImpl, DirectVulkan, DirectGLES): implement glTextureView over shared texture storage --- CMakeLists.txt | 1 + .../DirectGLES/BackendObject_DirectGLES.cpp | 20 +- .../DirectGLES/BackendObject_DirectGLES.h | 5 +- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 39 +- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 133 ++- MobileGL/MG_Backend/DirectGLES/Managers.h | 36 + .../BackendObject_DirectVulkan.cpp | 6 + .../DirectVulkan/Renderer/UniformManager.cpp | 42 +- .../DirectVulkan/Renderer/VkClearManager.cpp | 49 +- .../DirectVulkan/Renderer/VkClearManager.h | 3 +- .../Renderer/VkRenderPassManager.cpp | 16 +- .../Renderer/VkTextureManager.cpp | 301 ++++++- .../DirectVulkan/Renderer/VkTextureManager.h | 85 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 27 +- .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 2 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 297 ++++++- MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h | 2 + .../MG_Impl/GLImpl/Texture/Validators.cpp | 140 +++ MobileGL/MG_Impl/GLImpl/Texture/Validators.h | 29 + MobileGL/MG_IntegrationTest/CMakeLists.txt | 1 + .../Scenarios/TextureViewScenario.cpp | 799 ++++++++++++++++++ MobileGL/MG_State/GLState/Core.cpp | 7 + MobileGL/MG_State/GLState/Core.h | 5 + .../GLState/TextureState/TextureObject.cpp | 7 + .../GLState/TextureState/TextureObject.h | 41 + .../TextureState/TextureObjectView.cpp | 289 +++++++ .../GLState/TextureState/TextureObjectView.h | 108 +++ .../GLState/TextureState/TextureState.cpp | 11 + .../GLState/TextureState/TextureState.h | 7 + .../BackendLoader/BackendLoaderTest.cpp | 10 +- .../Program/ParallelShaderCompileTest.cpp | 4 +- MobileGL/MG_Test/Texture/CMakeLists.txt | 18 + MobileGL/MG_Test/Texture/TextureViewTest.cpp | 477 +++++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 27 + .../MG_Util/BackendLoaders/OpenGL/Loader.h | 16 + MobileGL/MG_Util/SelfTest/DriverPost.cpp | 3 +- 36 files changed, 2995 insertions(+), 68 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.cpp create mode 100644 MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp create mode 100644 MobileGL/MG_State/GLState/TextureState/TextureObjectView.h create mode 100644 MobileGL/MG_Test/Texture/TextureViewTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ae52f6bc..b0bf0a0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -392,6 +392,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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index abaf31d7..155b758b 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -753,7 +753,7 @@ namespace MobileGL::MG_Backend::DirectGLES { .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version // Baseline advertisement (no runtime capabilities yet); reconciled once // the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions. - .Extensions = BuildAdvertisedExtensions(false, false, false, false), + .Extensions = BuildAdvertisedExtensions(false, false, false, false, false), .IsCompatibilityProfile = false // Is Compatibility Profile }, .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability @@ -777,7 +777,8 @@ namespace MobileGL::MG_Backend::DirectGLES { MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions( AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy, capabilities.SupportsDrawIndirect, - capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance); + capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance, + capabilities.SupportsTextureView); } } // namespace @@ -990,7 +991,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported, Bool drawIndirectSupported, - Bool nonZeroIndirectBaseInstanceSupported) { + Bool nonZeroIndirectBaseInstanceSupported, + Bool textureViewSupported) { Vector extensions = { V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, 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, @@ -1065,6 +1067,18 @@ namespace MobileGL::MG_Backend::DirectGLES { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { extensions.push_back(E_GL_ARB_timer_query); } + // Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core + // texture views at any version and no honest emulation exists: a view is a SECOND NAME + // over the SAME storage, so that writes through either are visible through the other and + // the two carry independent per-texture parameters at the same time - which is exactly + // what applications use it for (Better Clouds samples one D24S8 through its own name with + // DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in + // a single shading pass). A copy-based fallback satisfies neither half, and fails + // silently; withholding the string and answering glTextureView with INVALID_OPERATION is + // the only behaviour that cannot be mistaken for success. + if (textureViewSupported) { + 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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index bb9a48be..07463120 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -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 BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported, Bool drawIndirectSupported, - Bool nonZeroIndirectBaseInstanceSupported); + Bool nonZeroIndirectBaseInstanceSupported, + Bool textureViewSupported); // Format: , OpenGL ES . — the exact string an // initialized backend returns from GetBackendAPIVersionString (and that ends up diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 4b2ee6c5..ae958394 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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(); + auto& backendSlot = backendTextureSlot ? *backendTextureSlot + : g_backendTextureObjects.GetOrCreate(textureObject); + if (!backendSlot) { + backendSlot = MakeShared(); } + + // 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 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 diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index f3cf9b05..5f1a2433 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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(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& 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& 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 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(baseSize.x()), + static_cast(baseSize.y()), + static_cast(baseSize.z()), + static_cast(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& 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()); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index a99609c0..13227ab8 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -792,6 +792,18 @@ namespace MobileGL::MG_Backend::DirectGLES { BackendTextureObject(const BackendTextureObject&) = delete; BackendTextureObject& operator=(const BackendTextureObject&) = delete; void SyncMipmapsToBackend(const SharedPtr& 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& stateTextureObject); + void StampViewSyncKeys(const SharedPtr& 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& stateTextureObject); void SyncTextureParamsToBackend(const SharedPtr& 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); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index a9b54e6a..8e8e0d57 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -533,6 +533,12 @@ 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, // 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. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 4e86572e..f1f40f51 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -47,7 +47,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { auto attachedTexture = attachment.GetTexture(); if (attachedTexture && attachedTexture.get() == &texture) { outAttachment = attachmentType; - outLevel = attachment.GetTextureLevel(); + outLevel = static_cast(ToStorageMipLevel(attachment.GetTexture().get(), + attachment.GetTextureLevel())); return true; } } @@ -416,16 +417,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { numericDomain == SamplerNumericDomain::UnsignedInteger; SamplerResolveMemo* viewFormatMemo = binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr; + // The format this GL texture presents to the shader. For a texture created by + // glTextureView that is the format the VIEW reinterpreted its storage as (GL 4.6 core + // 8.18), not the storage image's own - resolving the numeric domain against the latter + // would pick a sampled view for a format the shader never declared. The probe is behind + // IsTextureView() so nothing about the ordinary per-draw path changes. + const VkFormat sampledSourceFormat = + texture->IsTextureView() + ? m_textureManager->ResolveTextureViewWindow(*texture, *resource).format + : resource->format; VkFormat sampledViewFormat; if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid && - viewFormatMemo->viewFormatSource == resource->format && + viewFormatMemo->viewFormatSource == sampledSourceFormat && viewFormatMemo->viewFormatDomain == numericDomain) { sampledViewFormat = viewFormatMemo->viewFormat; } else { sampledViewFormat = - VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain); + VkTextureManager::ResolveSampledImageViewFormat(sampledSourceFormat, numericDomain); if (viewFormatMemo != nullptr) { - viewFormatMemo->viewFormatSource = resource->format; + viewFormatMemo->viewFormatSource = sampledSourceFormat; viewFormatMemo->viewFormatDomain = numericDomain; viewFormatMemo->viewFormat = sampledViewFormat; viewFormatMemo->viewFormatValid = true; @@ -440,9 +450,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } // No reinterpretation requested: bind the depth-or-color aspect view the sync above - // already produced instead of re-entering GetOrCreateSampledImageView's sync path. + // already produced instead of re-entering GetOrCreateSampledImageView's sync path. A GL + // texture view is excluded because resource->sampledView belongs to the texture it VIEWS + // - same image, but the storage texture's level range and depth/stencil aspect, which is + // exactly the state a view exists to differ on. const VkImageView sampledImageView = - sampledViewFormat == resource->format + (!texture->IsTextureView() && sampledViewFormat == resource->format) ? resource->sampledView : m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat); if (sampledImageView == VK_NULL_HANDLE) { @@ -547,7 +560,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource->sampledLevelCount), .imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ? samplerBindingOverride.imageView : - (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView), + // Same reason as in ResolveSamplerDescriptor: the resource's own views describe + // the storage texture, so a view has to be asked for its own. + (samplerBindingOverride.texture->IsTextureView() + ? m_textureManager->GetOrCreateSampledImageView(*samplerBindingOverride.texture, + VK_FORMAT_UNDEFINED) + : (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView)), .imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ? samplerBindingOverride.imageLayout : resource->layout, }; @@ -1029,8 +1047,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(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(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", diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp index a49c8c76..2f507cbb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp @@ -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(rawTexture->GetViewMinLevel()); + baseArrayLayer += static_cast(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& storageTexture = storageOwner ? storageOwner : texture; + const PendingClearKey key = MakePendingClearKey(storageTexture.get()); const std::lock_guard 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(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& storageTexture = storageOwner ? storageOwner : texture; const std::lock_guard 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(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 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 lock(m_mutex); SharedPtr liveTexture; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h index ea6d920d..0008d5c1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h @@ -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(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 9ef72b17..1d829cf6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -72,7 +72,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget(); if (!IsCubeMapFaceUploadTarget(uploadTarget)) { - return static_cast(std::max(attachment.GetTextureLayer(), 0)); + return ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); } return static_cast(uploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX); } @@ -633,11 +633,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(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(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 +1008,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; auto& att = fbo.GetAttachment(drawbuf); - const Uint32 attachmentMipLevel = static_cast(std::max(att.GetTextureLevel(), 0)); + const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel()); const auto textureTarget = texture->GetTarget(); const Uint32 attachmentIndex = static_cast(attachmentDescriptions.size()); attachmentDescriptions.emplace_back(); @@ -1144,7 +1146,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(); @@ -1254,7 +1257,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { } else if (selectedDepthStencilAttachment->IsTexture()) { auto& texture = *selectedDepthStencilAttachment->GetTexture(); const Uint32 attachmentMipLevel = - static_cast(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", diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index ce259330..ad14a451 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -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,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) { + m_viewRequestedImageFlags.erase(identity); auto resourceIt = m_textureResources.find(identity); if (resourceIt != m_textureResources.end()) { DeferResourceRelease(Move(resourceIt->second)); @@ -737,9 +754,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 +865,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 +905,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 +945,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 +988,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 +1031,77 @@ 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, + }; + 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(window.format), + static_cast(resource.format)); return VK_NULL_HANDLE; } + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); + const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); + const VkImageView view = + CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType, + window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount, + &sampledComponents); + 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(window.format), + static_cast(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,11 @@ 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, }; const auto existing = resource->alternateSampledViews.find(key); if (existing != resource->alternateSampledViews.end()) { @@ -1029,6 +1166,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 || @@ -1613,7 +1752,13 @@ 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. + const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture); + 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 +1952,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. @@ -2392,6 +2547,126 @@ 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()); + 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(texture.GetViewMinLevel()); + window.baseArrayLayer = static_cast(texture.GetViewMinLayer()); + window.layerCount = static_cast(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) { + 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; + } + const TextureTarget viewTarget = viewTexture.GetTarget(); + if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) { + required |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + } + if (required == 0) { + return; + } + VkImageCreateFlags& stored = m_viewRequestedImageFlags[MakeTextureIdentity(&storageTexture)]; + stored |= required; + } + + VkImageCreateFlags VkTextureManager::GetViewRequestedImageFlags( + const MG_State::GLState::ITextureObject& storageTexture) const { + const auto it = m_viewRequestedImageFlags.find( + MakeTextureIdentity(const_cast(&storageTexture))); + return it == m_viewRequestedImageFlags.end() ? 0 : it->second; + } + Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) { MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 423da9da..5b4b013b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -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(glLevel > 0 ? glLevel : 0); + return texture != nullptr ? level + static_cast(texture->GetViewMinLevel()) : level; +} + +inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) { + const Uint32 layer = static_cast(glLayer > 0 ? glLayer : 0); + return texture != nullptr ? layer + static_cast(texture->GetViewMinLayer()) : layer; +} + class VkTextureManager { public: // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass @@ -139,17 +160,28 @@ 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; 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; } }; @@ -157,10 +189,14 @@ public: SizeT operator()(const SampledImageViewKey& key) const { SizeT hash = std::hash{}(key.baseMipLevel); hash ^= std::hash{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(static_cast(key.viewType)) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(static_cast(key.format)) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(static_cast(key.aspect)) + + 0x9e3779b9u + (hash << 6) + (hash >> 2); return hash; } }; @@ -358,6 +394,45 @@ 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; + Bool isTextureView = false; + }; + 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; + // 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 +647,14 @@ private: std::unordered_map m_textureResources; // Textures that have been bound to a GL image unit (see MarkStorageImageTexture). std::unordered_set 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 m_viewRequestedImageFlags; // Supported multisample counts per format, so repeat texture syncs do not // re-query vkGetPhysicalDeviceImageFormatProperties. std::unordered_map m_multisampleCountsByFormat; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 93227887..03f7ca79 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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(uploadTarget) - static_cast(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(uploadTarget) - + static_cast(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(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(std::max(attachment.GetTextureLevel(), 0)); + outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); outBinding.mipLevelCount = resource->mipLevels; if (AttachmentIsDepthSlice(attachment)) { - outBinding.depthOffset = static_cast(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(std::max(attachment.GetTextureLevel(), 0)); + outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); outBinding.mipLevelCount = resource->mipLevels; if (AttachmentIsDepthSlice(attachment)) { - outBinding.depthOffset = static_cast(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(std::max(attachment.GetTextureLevel(), 0)); + outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); outBinding.mipLevelCount = 1; if (AttachmentIsDepthSlice(attachment)) { - outBinding.depthOffset = static_cast(std::max(attachment.GetTextureLayer(), 0)); + outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); outBinding.baseArrayLayer = 0; } else { outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment); @@ -9828,8 +9833,8 @@ void main() { vkFormat = resource->format; trackedLayout = &resource->layout; imageAspect = resource->aspect; - mipLevel = static_cast(std::max(attachment.GetTextureLevel(), 0)); - baseArrayLayer = static_cast(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); diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index c23a9f3e..18bd9118 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -996,7 +996,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) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index f0077c71..cfe9f77f 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -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(std::max(baseSize.y(), 1)); + case TextureTarget::Texture2DArray: + case TextureTarget::TextureCubeMapArray: + case TextureTarget::Texture2DMultisampleArray: + return static_cast(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 , + // TEXTURE_VIEW_MIN_LAYER 0, TEXTURE_VIEW_NUM_LAYERS the layer count. That is not just a + // query detail: glTextureView COMPOSES onto these (" and the value of + // TEXTURE_VIEW_NUM_LEVELS from the original texture minus ", 8.18), so leaving + // them at the mutable-texture default of 0 would clamp every view to zero levels. + void SeedImmutableViewState(const SharedPtr& textureObject, Uint levels) { + if (!textureObject) return; + textureObject->SetViewLevelLayerRange( + 0, levels, 0, + LayerCountOfImmutableTexture(textureObject->GetTarget(), textureObject->GetBaseSize())); + } + Bool ValidateTextureMutable(const SharedPtr& 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(textureObject->GetViewMinLevel()); + break; case GL_TEXTURE_VIEW_MIN_LAYER: - *params = 0; + *params = static_cast(textureObject->GetViewMinLayer()); break; case GL_TEXTURE_VIEW_NUM_LEVELS: + *params = static_cast(textureObject->GetViewNumLevels()); + break; case GL_TEXTURE_VIEW_NUM_LAYERS: - *params = 0; + *params = static_cast(textureObject->GetViewNumLayers()); break; default: MG_State::pGLContext->RecordError( @@ -2845,6 +2881,28 @@ namespace MobileGL::MG_Impl::GLImpl { *params = static_cast(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(textureObject->GetViewMinLevel()); + } + break; + case GL_TEXTURE_VIEW_NUM_LEVELS: + if (params) { + *params = static_cast(textureObject->GetViewNumLevels()); + } + break; + case GL_TEXTURE_VIEW_MIN_LAYER: + if (params) { + *params = static_cast(textureObject->GetViewMinLayer()); + } + break; + case GL_TEXTURE_VIEW_NUM_LAYERS: + if (params) { + *params = static_cast(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(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(textureObject->GetViewMinLevel()); + } + break; + case GL_TEXTURE_VIEW_NUM_LEVELS: + if (params) { + *params = static_cast(textureObject->GetViewNumLevels()); + } + break; + case GL_TEXTURE_VIEW_MIN_LAYER: + if (params) { + *params = static_cast(textureObject->GetViewMinLayer()); + } + break; + case GL_TEXTURE_VIEW_NUM_LAYERS: + if (params) { + *params = static_cast(textureObject->GetViewNumLayers()); + } + break; case GL_TEXTURE_BORDER_COLOR: if (params) { const auto& borderColor = textureObject->GetBorderColor(); @@ -4779,6 +4859,7 @@ namespace MobileGL::MG_Impl::GLImpl { // longer pre-existing chain has to be dropped explicitly. textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); textureObject->SetImmutableLevels(static_cast(levels)); + SeedImmutableViewState(textureObject, static_cast(levels)); } void TextureStorage2D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) { @@ -4853,6 +4934,7 @@ namespace MobileGL::MG_Impl::GLImpl { textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast(levels)); } textureObject->SetImmutableLevels(static_cast(levels)); + SeedImmutableViewState(textureObject, static_cast(levels)); } void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, @@ -4927,6 +5009,7 @@ namespace MobileGL::MG_Impl::GLImpl { // See TextureStorage1D. textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); textureObject->SetImmutableLevels(static_cast(levels)); + SeedImmutableViewState(textureObject, static_cast(levels)); } // Shared front half of glTextureStorage2DMultisample/3DMultisample. The target forms are reached @@ -5007,6 +5090,211 @@ namespace MobileGL::MG_Impl::GLImpl { }); } + namespace { + void RecordTextureViewError(ErrorCode code, const String& message) { + MG_State::pGLContext->RecordError(code, + MakeUnique("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& 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) is 0. + if (texture == 0) { + RecordTextureViewError(ErrorCode::InvalidValue, "texture must not be zero."); + return; + } + // b) 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) 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) 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) 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 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 = + origtexture's TEXTURE_VIEW_MIN_LEVEL + // TEXTURE_VIEW_NUM_LEVELS = min(, origtexture's TEXTURE_VIEW_NUM_LEVELS - ) + // TEXTURE_VIEW_MIN_LAYER = + origtexture's TEXTURE_VIEW_MIN_LAYER + // TEXTURE_VIEW_NUM_LAYERS = min(, origtexture's TEXTURE_VIEW_NUM_LAYERS - ) + // 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 +5400,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, diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h index 604f881b..5ccd0190 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h @@ -60,6 +60,8 @@ namespace MobileGL::MG_Impl::GLImpl { void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params); void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params); void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params); + void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, + GLuint numlevels, GLuint minlayer, GLuint numlayers); void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width); void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index ec423efb..cc85f197 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -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 diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 6ccb7782..033f8fea 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -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 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 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 diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 6e4fcabf..77e0a709 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.cpp new file mode 100644 index 00000000..1f6d6373 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.cpp @@ -0,0 +1,799 @@ +// 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 +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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 m_textures; + std::vector m_fbos; + std::vector m_rbos; + std::vector 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(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(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(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(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(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(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(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(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(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 texels(static_cast(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(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(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(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 words(static_cast(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(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(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(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(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 . + 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(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(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(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(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(GL_FRAMEBUFFER_COMPLETE)); + const Image sliced = ReadFbo(viewFbo, kSize, kSize); + const Rgba8 expected{static_cast(kChosenLayer * 255 / 8), + static_cast(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"); + } + + // ------------------------------------------------------------------------------------ + // 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(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(GL_NO_ERROR)); + const GLuint secondView = MakeTexture(); + glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1); + ASSERT_EQ(glGetError(), static_cast(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(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(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(GL_FALSE)); + ASSERT_EQ(glGetError(), static_cast(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(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 diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 87ac75e6..a13e351d 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -267,6 +267,13 @@ namespace MobileGL::MG_State { return m_textureState.CreateTextureObject(index, target); } + const SharedPtr& GLContext::CreateTextureViewObject( + Uint index, TextureTarget target, const SharedPtr& 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 diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 70c2a5e9..2e6cfada 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -111,6 +111,11 @@ namespace MobileGL { // Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject. const SharedPtr& GetDefaultTextureObject(TextureTarget target) const; const SharedPtr& CreateTextureObject(Uint index, TextureTarget target); + // See TextureState::CreateTextureViewObject (glTextureView, GL 4.6 core 8.18). + const SharedPtr& CreateTextureViewObject(Uint index, TextureTarget target, + const SharedPtr& storageOwner, + Uint minLevel, Uint numLevels, Uint minLayer, + Uint numLayers); void MarkTextureObjectForDeletion(Uint index); TextureUnit& GetTextureUnitObject(Int unit); ImageTextureBinding& GetImageTextureBinding(Int unit); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 1d326d3c..7da4e400 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -291,6 +291,13 @@ namespace MobileGL { return m_lifetimeId; } + const SharedPtr& 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 noStorageOwner = nullptr; + return noStorageOwner; + } + Uint TextureObjectWithOneMipmap::GetMipmapLevelCount() const { return m_textureStorage.GetLevelCount(); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 1f214450..5954c682 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -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 " 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& 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& 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; }; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp new file mode 100644 index 00000000..ae2ed2e9 --- /dev/null +++ b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp @@ -0,0 +1,289 @@ +// 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 + +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 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 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(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(m_viewNumLayers); + break; + case LayerAxis::Z: + size.z() = static_cast(m_viewNumLayers); + break; + case LayerAxis::None: + break; + } + return size; + } + + 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(std::max(ownerSize.x(), 0)) * + static_cast(std::max(ownerSize.y(), 0)) * + static_cast(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(std::max(viewSize.x(), 0)) * + static_cast(std::max(viewSize.y(), 0)) * + static_cast(std::max(viewSize.z(), 1)); + return (ownerBytes / ownerTexels) * viewTexels; + } + + 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; + m_ownerMipmap->UpdateMipmapSubData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), input); + } + + void* TextureObjectView::MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) { + if (m_ownerMipmap == nullptr) return nullptr; + return m_ownerMipmap->MapMipmapData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(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), 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(m_uploadTargets.size()); ++i) { + if (m_uploadTargets[i] == target) return i; + } + return 0; + } +} // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h new file mode 100644 index 00000000..0c71a2c9 --- /dev/null +++ b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h @@ -0,0 +1,108 @@ +// 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 + // " 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. + // + // LAYER offsets are deliberately NOT applied here. The TextureObjectMipmap interface + // addresses storage as (upload target, level) and a layer lives INSIDE a level's blob, so a + // layer offset is not expressible at this boundary. The entry points that move texels for a + // view (glTexSubImage*, glGetTexImage) therefore redirect to the owner themselves and add + // GetViewMinLayer() to the z coordinate there, where it can be said. What this class does + // apply is the view's layer COUNT, because the level extents it reports are what both + // backends size their images and views from. + class TextureObjectView : public TextureObjectMipmap { + public: + TextureObjectView(Uint externalIndex, TextureTarget target, SharedPtr storageOwner, + Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers); + + const SharedPtr& GetViewStorageOwner() const override { return m_storageOwner; } + const Vector& GetUploadTargets() const override { return m_uploadTargets; } + + // GL 4.6 core 8.18: "TEXTURE_IMMUTABLE_LEVELS is set to the value of + // TEXTURE_IMMUTABLE_LEVELS from the original texture" - NOT to . 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; + + SharedPtr 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 m_uploadTargets; + }; +} // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index f65269c1..60d249d5 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -18,6 +18,7 @@ #include "TextureObject2DCube.h" #include "TextureObjectBuffer.h" #include "TextureObjectStubs.h" +#include "TextureObjectView.h" namespace MobileGL::MG_State::GLState { static std::atomic s_nextTextureStateContextId = 1; @@ -104,6 +105,16 @@ namespace MobileGL::MG_State::GLState { return textureObject; } + const SharedPtr& TextureState::CreateTextureViewObject( + Uint index, TextureTarget target, const SharedPtr& 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(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); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index c2d1c9f8..c9d9d291 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -48,6 +48,13 @@ namespace MobileGL::MG_State::GLState { TextureState(); void GenerateNames(Uint number, Vector& textures); const SharedPtr& 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& CreateTextureViewObject(Uint index, TextureTarget target, + const SharedPtr& storageOwner, + Uint minLevel, Uint numLevels, Uint minLayer, + Uint numLayers); const SharedPtr& 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 diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 4b35266d..7b5706a9 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -1087,11 +1087,11 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end(); }; - const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false); + const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false); EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic)); EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic)); - const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false); + const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false); EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic)); EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic)); @@ -1113,17 +1113,17 @@ TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) { }; const auto esWithoutIndirect = - MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false); + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false); EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance)); const auto esWithoutBaseInstance = - MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false); + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false); EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance)); const auto esWithBoth = - MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true); + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false); EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance)); diff --git a/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp b/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp index 94a7d0e8..098d9ed4 100644 --- a/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp +++ b/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp @@ -516,14 +516,14 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) { { const AsyncModeScope async(true); - EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false), + EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false), E_GL_KHR_parallel_shader_compile)); EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false), E_GL_KHR_parallel_shader_compile)); } { const AsyncModeScope async(false); - EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false), + EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false), E_GL_KHR_parallel_shader_compile)) << "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading"; EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false), diff --git a/MobileGL/MG_Test/Texture/CMakeLists.txt b/MobileGL/MG_Test/Texture/CMakeLists.txt index 8c46131e..dd7fdf3b 100644 --- a/MobileGL/MG_Test/Texture/CMakeLists.txt +++ b/MobileGL/MG_Test/Texture/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Test/Texture/TextureViewTest.cpp b/MobileGL/MG_Test/Texture/TextureViewTest.cpp new file mode 100644 index 00000000..ee42e927 --- /dev/null +++ b/MobileGL/MG_Test/Texture/TextureViewTest.cpp @@ -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 + +#include + +#include "Includes.h" +#include "Init.h" +#include +#include +#include +#include +#include +#include + +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(advertiseTextureView); + } + ~ScopedBackendOverride() { MG_Backend::pActiveBackendObject = Move(m_previous); } + + private: + UniquePtr 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(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 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 . + 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 and TEXTURE_VIEW_NUM_LEVELS from the + // original minus ", 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 " 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(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 diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index e4d3a4b7..5ae9ba31 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -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 || diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index e5c806a1..b3eec874 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -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 diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 3dc44e16..2f37df51 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -1183,7 +1183,8 @@ namespace MobileGL::MG_Util::SelfTest { advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions( summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy, summary.caps.SupportsDrawIndirect, - summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance)); + summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance, + summary.caps.SupportsTextureView)); } AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString, advertisedExtensions);