diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 282f6d04..e4348e64 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1666,7 +1666,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram( const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) { - const HashType hash = ComputeHash(program, flags); + // Hashing the full SPIR-V of every stage is far too expensive to repeat per draw; + // reuse the program's memoized hash while its backend state version is unchanged. + HashType hash = 0; + if (!program.GetBackendHashMemo(flags.GetRaw(), hash)) { + hash = ComputeHash(program, flags); + program.SetBackendHashMemo(flags.GetRaw(), hash); + } auto it = m_cache.find(hash); if (it != m_cache.end()) { return it->second; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp index c39ace69..5da2a293 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp @@ -31,8 +31,19 @@ static const char* string_VkColorSpaceKHR(VkColorSpaceKHR) { return "VkColorSpaceKHR(unknown)"; } -static const char* string_VkPresentModeKHR(VkPresentModeKHR) { - return "VkPresentModeKHR(unknown)"; +static const char* string_VkPresentModeKHR(VkPresentModeKHR presentMode) { + switch (presentMode) { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "VK_PRESENT_MODE_IMMEDIATE_KHR"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "VK_PRESENT_MODE_MAILBOX_KHR"; + case VK_PRESENT_MODE_FIFO_KHR: + return "VK_PRESENT_MODE_FIFO_KHR"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "VK_PRESENT_MODE_FIFO_RELAXED_KHR"; + default: + return "VkPresentModeKHR(unknown)"; + } } static const char* string_VkSurfaceTransformFlagBitsKHR(VkSurfaceTransformFlagBitsKHR) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index af92fc65..00294d2b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -822,12 +822,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(m_samplerManager != nullptr, "BindProgramUniformBuffers: sampler manager is null"); MOBILEGL_ASSERT(m_bufferManager != nullptr, "BindProgramUniformBuffers: buffer manager is null"); - Vector writes; + auto& writes = m_writesScratch; + auto& bufferInfos = m_bufferInfosScratch; + auto& imageInfos = m_imageInfosScratch; + auto& texelBufferViews = m_texelBufferViewsScratch; + auto& dynamicOffsets = m_dynamicOffsetsScratch; + writes.clear(); + bufferInfos.clear(); + imageInfos.clear(); + texelBufferViews.clear(); + dynamicOffsets.clear(); writes.reserve(m_maxBindings); - Vector bufferInfos; - Vector imageInfos; - Vector texelBufferViews; - Vector dynamicOffsets; bufferInfos.reserve(m_maxBindings); imageInfos.reserve(m_maxBindings); texelBufferViews.reserve(m_maxBindings); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index e38ff013..d1699968 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -113,6 +113,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkTextureManager* m_textureManager = nullptr; VkSamplerManager* m_samplerManager = nullptr; mutable SharedPtr m_fallbackTexture2D; + + // Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps + // capacity) so the descriptor-write path stops allocating on every draw. + Vector m_writesScratch; + Vector m_bufferInfosScratch; + Vector m_imageInfosScratch; + Vector m_texelBufferViewsScratch; + Vector m_dynamicOffsetsScratch; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 5c2b398b..7e83a95e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -38,10 +38,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { return XXH64_digest(m_hashState); } + VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash( + const MG_State::GLState::VertexArrayObject& vao) const { + HashType hash = 0; + if (!vao.GetBackendHashMemo(hash)) { + hash = ComputeHash(vao); + vao.SetBackendHashMemo(hash); + } + return hash; + } + const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const MG_State::GLState::VertexArrayObject& vao) { - const HashType hash = ComputeHash(vao); - return GetOrCreateVertexInputState(vao, hash); + return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao)); } const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index 54bceb92..f1c108df 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -38,6 +38,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VertexInputStateFactory(const VertexInputStateFactory&) = delete; HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const; + // Memoized ComputeHash: reuses the VAO's cached hash while its config version + // is unchanged. Use this on per-draw paths. + HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const; const BackendVertexInputState& GetOrCreateVertexInputState( const MG_State::GLState::VertexArrayObject& vao, HashType hash); const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 51bc4b7d..6e75e4b3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -675,7 +675,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Vector staleAliases; - staleAliases.reserve(m_aliveObjects.size()); for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) { if (it->first.texture != texture) { continue; @@ -698,12 +697,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { auto aliveIt = m_aliveObjects.find(identity); if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) { EraseTrackedTexture(aliveIt->first); + aliveIt = m_aliveObjects.end(); } - const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex()); - if (liveTexture && liveTexture.get() == &texture) { - m_aliveObjects[identity] = WeakPtr(liveTexture); - PruneStaleTextureAliases(&texture); + // Only (re)register and prune when this (texture, lifetime) pair is new: stale + // aliases can only come into existence through an address reuse, which by + // construction introduces a new identity. Doing this unconditionally made every + // sampled-texture sync scan the entire alive-texture map per draw. + if (aliveIt == m_aliveObjects.end()) { + const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex()); + if (liveTexture && liveTexture.get() == &texture) { + m_aliveObjects[identity] = WeakPtr(liveTexture); + PruneStaleTextureAliases(&texture); + } } auto it = m_textureResources.find(identity); @@ -1033,7 +1039,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } - auto* mipTexture = dynamic_cast(&texture); + auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); if (!mipTexture) { MGLOG_D("%s: not TextureObjectMipmap", __func__); return false; @@ -1579,7 +1585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { IntVec3& outTexelSize, SizeT& outByteSize, Uint32& outMipLevelCount) { - const auto* mipTexture = dynamic_cast(&texture); + const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); if (!mipTexture) { MGLOG_D("%s: not TextureObjectMipmap", __func__); return false; @@ -1639,7 +1645,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(mipLevels > 0, "ResolveViewMipRange: mipLevels must be > 0"); Uint32 definedMipLevels = mipLevels; - if (const auto* mipTexture = dynamic_cast(&texture)) { + if (const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture)) { const auto& targets = texture.GetUploadTargets(); for (const auto target : targets) { const Uint32 uploadMipLevels = GetUploadMipLevelCount(*mipTexture, target); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 8530f421..ee9acc1e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2077,8 +2077,10 @@ void main() { const auto bindingCount = vertexInputState.bindings.size() + static_cast(std::popcount(missingAttribMask)); - Vector vkBuffers(bindingCount, VK_NULL_HANDLE); - Vector vkOffsets(bindingCount, 0); + auto& vkBuffers = m_vertexBuffersScratch; + auto& vkOffsets = m_vertexOffsetsScratch; + vkBuffers.assign(bindingCount, VK_NULL_HANDLE); + vkOffsets.assign(bindingCount, 0); auto findBufferByKey = [&](SizeT bufferKey) -> const MG_State::GLState::BufferObject* { const auto& attrs = vao.GetAllAttributes(); @@ -2690,6 +2692,7 @@ void main() { MOBILEGL_ASSERT(offset + size <= m_depthMipmapResources.program->GetUBOSize(), "GenerateDepthMipmapWithShader: uniform write out of bounds"); memcpy(depthProgramData + offset, data, size); + m_depthMipmapResources.program->MarkUBOContentDirty(); }; for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) { @@ -2843,12 +2846,13 @@ void main() { } #endif - auto vertexInputHash = m_vertexInputStateFactory->ComputeHash(vao); + auto vertexInputHash = m_vertexInputStateFactory->GetOrComputeHash(vao); auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao, vertexInputHash); const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vis.attributes); const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask; - Vector patchedAttributes = vis.attributes; + auto& patchedAttributes = m_patchedAttributesScratch; + patchedAttributes.assign(vis.attributes.begin(), vis.attributes.end()); Bool hasPatchedVertexAttributes = false; for (auto& attribute : patchedAttributes) { if (attribute.location >= 32 || (activeAttribMask & (1u << attribute.location)) == 0) { @@ -3188,7 +3192,7 @@ void main() { // which probably indicates it's been gone through codepath like `fbo attach` -> `clear` -> `fbo detach`, and // without draws in between to give it a chance to materialize such clear. // Deal with this situation here. - Vector sampledTextures; + auto& sampledTextures = m_sampledTexturesScratch; // cleared by CollectSampledTextures Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures); MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__); MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s", @@ -3848,6 +3852,7 @@ void main() { writeUniform(m_blitResources.dstRectLocation, blitUniformData.dstRect, sizeof(blitUniformData.dstRect)); writeUniform(m_blitResources.surfaceTransformLocation, &blitUniformData.surfaceTransform, sizeof(blitUniformData.surfaceTransform)); + m_blitResources.program->MarkUBOContentDirty(); const auto samplerBindingOverride = UniformManager::SamplerBindingOverride{ .binding = m_blitResources.samplerBinding, @@ -4875,7 +4880,7 @@ void main() { MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture."); MOBILEGL_ASSERT(texture->IsComplete(), "GenerateMipmap requires a complete texture."); - auto* mipmapTexture = dynamic_cast(texture.get()); + auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get()); MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires a mipmapped texture object."); const Uint32 currentMipLevelCount = static_cast(mipmapTexture->GetMipmapLevelCount()); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 61c2fa26..96d2183d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -272,6 +272,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { DepthMipmapResources m_depthMipmapResources; Vector m_deferredDepthMipmapCleanup; + // Per-draw scratch buffers (clear keeps capacity) — these paths run for every + // draw call and must not allocate. + Vector m_sampledTexturesScratch; + Vector m_vertexBuffersScratch; + Vector m_vertexOffsetsScratch; + Vector m_patchedAttributesScratch; + void CreateInstance(); VkResult SetupDebugMessenger(); VkResult DestroyDebugMessenger(); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index ed89d523..56abafb1 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -105,6 +105,14 @@ namespace { namespace MobileGL::MG_State::GLState { void ProgramObject::ResetLinkArtifacts() { + // Relinking regenerates the SPIR-V, so any backend-cached state keyed on + // m_backendStateVersion (e.g. the content-hash memo) must be invalidated, + // along with every link-derived backend cache (m_linkVersion) and the + // last-uploaded-UBO gate (a relink resets uniforms to their initial values, + // and that reset must reach the GPU). + ++m_backendStateVersion; + ++m_linkVersion; + MarkUBOContentDirty(); m_program.reset(); m_generatedSpirv.clear(); m_uniformLocations.clear(); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index fc703b0a..b53b6046 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -170,7 +170,33 @@ namespace MobileGL::MG_State::GLState { void* MapUBO() { return m_globalUboScratch.data(); } const void* GetUBOData() const { return m_globalUboScratch.data(); } Uint GetUBOSize() const { return static_cast(m_globalUboScratch.size()); } + // Content version of the CPU-side global-UBO shadow: writers bump it so backends + // can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the + // backends' "never uploaded" sentinel, so skip over it on wrap. + Uint32 GetUBOContentVersion() const { return m_uboContentVersion; } + void MarkUBOContentDirty() { + if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0; + } Uint32 GetBackendStateVersion() const { return m_backendStateVersion; } + // Bumped only by (re)linking — lets backends detect that every piece of + // link-derived reflection (locations, block order, UBO layout) is stale. + Uint32 GetLinkVersion() const { return m_linkVersion; } + + // Content-hash memo for backends: avoids re-hashing the generated SPIR-V on every + // draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and + // the binding setters below invalidate it by bumping m_backendStateVersion. + Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const { + if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) { + return false; + } + outHash = m_backendHashMemo; + return true; + } + void SetBackendHashMemo(Uint flags, Uint64 hash) const { + m_backendHashMemo = hash; + m_backendHashMemoVersion = m_backendStateVersion; + m_backendHashMemoFlags = flags; + } void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) { if (location >= m_uniformSamplerOrImageUnitIndex.size() || @@ -308,5 +334,13 @@ namespace MobileGL::MG_State::GLState { Bool m_linkStatus = false; Bool m_validateStatus = true; Uint32 m_backendStateVersion = 0; + + // Backend-owned content-hash memo (see GetBackendHashMemo): valid only while + // m_backendStateVersion and the compile flags match the recorded values. + mutable Uint64 m_backendHashMemo = 0; + mutable Uint32 m_backendHashMemoVersion = ~0u; + mutable Uint m_backendHashMemoFlags = 0; + Uint32 m_uboContentVersion = 0; + Uint32 m_linkVersion = 0; }; } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index e571d1e2..4b753cd9 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -128,6 +128,20 @@ namespace MobileGL::MG_State::GLState { virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; }; + // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the + // only hierarchy branch whose storage type reports Mipmap, so the tag check makes + // the static_cast safe. + inline TextureObjectMipmap* AsMipmapTexture(ITextureObject* texture) { + return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) + ? static_cast(texture) + : nullptr; + } + inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) { + return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) + ? static_cast(texture) + : nullptr; + } + class TextureObjectWithOneMipmap : public TextureObjectMipmap { public: TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex) diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp index 3fb96478..7e8e4795 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp @@ -207,16 +207,19 @@ namespace MobileGL::MG_State::GLState { void VertexArrayObject::BumpAttributeFormatVersion(Uint index) { if (index >= MAX_VERTEX_ATTRIBS) return; ++m_attributeVersions[index].FormatVersion; + ++m_configVersion; } void VertexArrayObject::BumpAttributeBufferVersion(Uint index) { if (index >= MAX_VERTEX_ATTRIBS) return; ++m_attributeVersions[index].BufferVersion; + ++m_configVersion; } void VertexArrayObject::BumpAttributeSwitchVersion(Uint index) { if (index >= MAX_VERTEX_ATTRIBS) return; ++m_attributeVersions[index].SwitchVersion; + ++m_configVersion; } const VertexAttributeVersion& VertexArrayObject::GetAttributeVersion(Uint index) const { diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h index 102586ca..9497dd76 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h @@ -81,6 +81,23 @@ namespace MobileGL { const VertexAttributeVersion& GetAttributeVersion(Uint index) const; const Array& GetAllAttributeVersions() const; + // Aggregate of every per-attribute version bump; lets backends detect + // "any vertex-input state changed" with one compare. + Uint32 GetConfigVersion() const { return m_configVersion; } + + // Backend-owned content-hash memo, valid while the config version matches + // (same idea as ProgramObject's hash memo — avoids re-hashing all + // attributes on every draw). + Bool GetBackendHashMemo(Uint64& outHash) const { + if (m_backendHashMemoVersion != m_configVersion) return false; + outHash = m_backendHashMemo; + return true; + } + void SetBackendHashMemo(Uint64 hash) const { + m_backendHashMemo = hash; + m_backendHashMemoVersion = m_configVersion; + } + private: void BumpAttributeFormatVersion(Uint index); void BumpAttributeBufferVersion(Uint index); @@ -100,6 +117,10 @@ namespace MobileGL { // ARB_vertex_attrib_binding API; only such attributes are re-resolved, so the // classic glVertexAttribPointer path keeps its exact historical behavior. Array m_attributeUsesBindingModel = {}; + + Uint32 m_configVersion = 0; + mutable Uint64 m_backendHashMemo = 0; + mutable Uint32 m_backendHashMemoVersion = ~0u; }; } // namespace GLState } // namespace MG_State diff --git a/MobileGL/MG_Util/Types.h b/MobileGL/MG_Util/Types.h index 040dfe14..42b6e312 100644 --- a/MobileGL/MG_Util/Types.h +++ b/MobileGL/MG_Util/Types.h @@ -336,6 +336,8 @@ namespace MobileGL { operator Bool() const { return Any(); } + typename Underlying::type GetRaw() const { return flags; } + private: Bool Any() const { return static_cast(flags) != 0; }