diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 7c8035b3..18868b8e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2169,6 +2169,16 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); m_cacheSamplerParameters.maxLod = samplerParams.maxLod; } + if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { + if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { + g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, + samplerParams.maxAnisotropy); + } + // Unsupported GLES backends intentionally treat anisotropy as a + // frontend-only no-op; remember the observed value so the cache + // remains coherent without issuing an illegal enum every sync. + m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; + } DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); @@ -3060,6 +3070,13 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); m_cacheSamplerParameters.maxLod = samplerParams.maxLod; } + if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { + if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { + g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT, + samplerParams.maxAnisotropy); + } + m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; + } #undef SYNC_SAMPLER_PARAM_IF_CHANGED m_isInitialized = true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 98e3d2a6..be105242 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -197,16 +197,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { return static_cast(bufferObject.GetBackendResource().get()); } - SharedPtr VkBufferManager::GetOrCreateResource( + VkBufferResource* VkBufferManager::GetOrCreateResource( const SharedPtr& bufferObject) { - auto existing = std::static_pointer_cast(bufferObject->GetBackendResource()); + // Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's + // backend-resource SharedPtr (already set, or set below), so callers that only dereference + // it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind. + const auto& existing = bufferObject->GetBackendResource(); if (existing) { - return existing; + return static_cast(existing.get()); } auto resource = MakeShared(); + VkBufferResource* raw = resource.get(); bufferObject->SetBackendResource(resource); TrackLiveResource(resource); - return resource; + return raw; } void VkBufferManager::TrackLiveResource(const SharedPtr& resource) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 0e346080..77b2f855 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -124,7 +124,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { private: Bool InitializeTransientArenas(); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); - SharedPtr GetOrCreateResource(const SharedPtr& bufferObject); + VkBufferResource* GetOrCreateResource(const SharedPtr& bufferObject); static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject); Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags = 0); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 54cdc7db..9a6ca4ce 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -718,6 +718,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (hasClear) { pendingClearAttachments.emplace_back(PendingClearAttachmentInfo { .attachmentIndex = attachmentIndex, + .colorAttachmentSlot = i, .key = VkClearManager::MakePendingClearKey(att) }); } @@ -1015,7 +1016,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { hasDepthStencilAttachment, renderPassSampleCount, extent, - static_cast(framebufferLayers) }; + framebufferLayers }; MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d", static_cast(hash), static_cast(compatibilityHash), diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index 9fe0a299..7fe370a6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -27,7 +27,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; struct PendingClearAttachmentInfo { + // Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space). Uint32 attachmentIndex = 0; + // Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL + // draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete. + // Only meaningful for color clears. + Uint32 colorAttachmentSlot = 0; PendingClearKey key{}; MG_State::GLState::RenderbufferObject* renderbuffer = nullptr; Bool hasInlinePayload = false; @@ -68,7 +73,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool hasDepthStencilAttachment = false; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; IntVec2 extent = {0, 0}; - Uint32 subpass = 0; + // VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments). + Uint32 layers = 1; RenderPassEntry() = default; RenderPassEntry(const RenderPassEntry&) = delete; @@ -84,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment); std::swap(sampleCount, that.sampleCount); std::swap(extent, that.extent); - std::swap(subpass, that.subpass); + std::swap(layers, that.layers); } RenderPassEntry( Uint64 hash, @@ -97,7 +103,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 colorAttachmentCount, Bool hasDepthStencilAttachment, VkSampleCountFlagBits sampleCount, - IntVec2 extent, int subpass): + IntVec2 extent, Uint32 layers): hash(hash), renderPass(renderpass), framebuffer(framebuffer), @@ -109,7 +115,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { hasDepthStencilAttachment(hasDepthStencilAttachment), sampleCount(sampleCount), extent(extent), - subpass(subpass) + layers(layers) {} ~RenderPassEntry() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index 495e75af..2d837a92 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -99,6 +99,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod))); const auto lodBias = sampler.GetLodBias(); XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias))); + // Anisotropy is currently an accepted frontend-only state on DirectVulkan. + // Keep it out of the key so changing this no-op does not manufacture duplicate + // VkSamplers while sampler versioning still exposes the new frontend value. const auto compareMode = sampler.GetCompareMode(); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); const auto compareFunc = ResolveCompareFunc(sampler, texture); @@ -125,6 +128,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR()); samplerInfo.mipLodBias = sampler.GetLodBias(); + // DirectVulkan does not yet plumb samplerAnisotropy feature/limit discovery; + // preserve the accepted frontend state without requesting an unsupported feature. samplerInfo.anisotropyEnable = VK_FALSE; samplerInfo.maxAnisotropy = 1.0f; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 67ff8eed..84af9d4d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -21,6 +21,7 @@ #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" #include +#include #include #include #include @@ -2150,8 +2151,8 @@ void main() { if (!supported) { // SetupDraw's pre-flight should have rejected this already; never upload a null payload. MGLOG_E("UploadAndBindVertexStreams skipped: unsupported current generic vertex attribute type: " - "location=%u type=0x%x", - location, glType); + "programHash=%llu location=%u type=0x%x", + static_cast(programObj.hash), location, glType); return false; } @@ -3584,8 +3585,73 @@ void main() { 1, &memoryBarrier, 0, nullptr, 0, nullptr); } + VulkanRenderer::ScissoredClearPrep VulkanRenderer::PrepareScissoredClear( + const MG_State::GLState::FramebufferObject& framebuffer, VkClearRect& outClearRect) { + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + + auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); + auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired); + if (renderPassEntry->attachmentCount == 0 || + renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { + return ScissoredClearPrep::NoOp; + } + + VkClearRect clearRect{}; + clearRect.rect = framebuffer.IsDefaultFramebuffer() + ? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(), + renderPassEntry->extent, + m_swapchainObject.GetPreTransform()) + : MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent); + clearRect.baseArrayLayer = 0; + // GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer. + clearRect.layerCount = renderPassEntry->layers; + if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { + return ScissoredClearPrep::NoOp; + } + // A scissor that covers the whole target is a whole-surface clear; the deferred loadOp + // path is equivalent and cheaper (no render pass churn, loadOp=CLEAR on tilers). + if (clearRect.rect.offset.x == 0 && clearRect.rect.offset.y == 0 && + clearRect.rect.extent.width == static_cast(renderPassEntry->extent.x()) && + clearRect.rect.extent.height == static_cast(renderPassEntry->extent.y())) { + return ScissoredClearPrep::NotNeeded; + } + + if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + activeRenderPass = nullptr; + // Re-resolve: ending the pass updates tracked attachment layouts, which feed the + // entry's load ops and initial layouts. + renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired); + } + // A still-active pass is necessarily compatible here: the block above ended any + // incompatible one and nothing since can change the active pass. + if (activeRenderPass) { + // Materialize any older whole-attachment clear before applying this + // ordered, scissored clear. + ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); + } else { + const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); + MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__); + if (!began) { + return ScissoredClearPrep::NoOp; + } + } + outClearRect = clearRect; + return ScissoredClearPrep::Ready; + } + void VulkanRenderer::Clear(GLbitfield mask) { m_clearManager->CollectGarbage(); + if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { + return; + } + // GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { + return; + } auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)"); if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) { @@ -3604,109 +3670,89 @@ void main() { // GuiItemAtlas: animated items clear only their atlas slot before being // redrawn. Queueing that clear as a loadOp erases every cached static item. if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { - auto& frame = m_frameContext.GetCurrent(); - if (!frame.isCommandRecording) { - m_frameContext.BeginCommandRecording(); - } - - auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); - auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*fbo, m_imageIndexAcquired); - if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { - VkRenderPassManager::EndRenderPass(frame.commandBuffer); - activeRenderPass = nullptr; - renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*fbo, m_imageIndexAcquired); - } - if (renderPassEntry->attachmentCount == 0 || - renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { - return; - } - - if (activeRenderPass && activeRenderPass->CompatibleWith(*renderPassEntry)) { - // Materialize any older whole-attachment clear before applying this - // ordered, scissored clear. - ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); - } else { - const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); - MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__); - } - VkClearRect clearRect{}; - clearRect.rect = fbo->IsDefaultFramebuffer() - ? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(), - renderPassEntry->extent, - m_swapchainObject.GetPreTransform()) - : MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent); - clearRect.baseArrayLayer = 0; - clearRect.layerCount = 1; - if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { + switch (PrepareScissoredClear(*fbo, clearRect)) { + case ScissoredClearPrep::NoOp: + return; + case ScissoredClearPrep::NotNeeded: + break; // full-coverage scissor: the deferred whole-surface path below is equivalent + case ScissoredClearPrep::Ready: { + VkClearAttachment clearAttachments[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS + 1]; + Uint32 clearAttachmentCount = 0; + + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + const auto& drawBuffers = fbo->GetDrawBuffers(); + for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { + const auto attachmentType = drawBuffers[drawBufferIndex]; + if (attachmentType == FramebufferAttachmentType::None) { + continue; + } + const auto& attachment = fbo->GetAttachment(attachmentType); + if (!attachment.IsComplete()) { + continue; + } + + const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex); + if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { + continue; + } + if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { + MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported"); + continue; + } + + MG_State::GLState::ITextureObject* colorTexture = nullptr; + if (attachment.IsTexture()) { + colorTexture = attachment.GetTexture().get(); + } + VkClearAttachment clearAttachment{}; + clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clearAttachment.colorAttachment = drawBufferIndex; + clearAttachment.clearValue.color = { + payload.color.x(), payload.color.y(), payload.color.z(), + ResolveColorClearAlpha(colorTexture, payload.color.w()) + }; + clearAttachments[clearAttachmentCount++] = clearAttachment; + } + } + + VkImageAspectFlags depthStencilAspects = 0; + if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) { + const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth); + if (depthAttachment.IsComplete()) { + depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + } + if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { + const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); + if (stencilAttachment.IsComplete()) { + // GL 3.3 §4.2.3: the clear is masked by the front stencil write mask. + // vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or + // zero mask can be expressed; treat a partial mask like a partial color mask. + const Uint32 stencilWriteMask = + MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask; + if ((stencilWriteMask & 0xFFu) == 0xFFu) { + depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } else if (stencilWriteMask != 0) { + MGLOG_W("DirectVulkan: scissored glClear with a partial stencil write mask is not supported"); + } + } + } + if (depthStencilAspects != 0) { + VkClearAttachment clearAttachment{}; + clearAttachment.aspectMask = depthStencilAspects; + clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil}; + clearAttachments[clearAttachmentCount++] = clearAttachment; + } + + if (clearAttachmentCount != 0) { + vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, + clearAttachmentCount, clearAttachments, + 1, &clearRect); + } return; } - - Vector clearAttachments; - clearAttachments.reserve(fbo->GetDrawBuffers().size() + 1); - - if ((mask & GL_COLOR_BUFFER_BIT) != 0) { - const auto& drawBuffers = fbo->GetDrawBuffers(); - for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { - const auto attachmentType = drawBuffers[drawBufferIndex]; - if (attachmentType == FramebufferAttachmentType::None) { - continue; - } - const auto& attachment = fbo->GetAttachment(attachmentType); - if (!attachment.IsComplete()) { - continue; - } - - const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex); - if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { - continue; - } - if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { - MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported"); - continue; - } - - MG_State::GLState::ITextureObject* colorTexture = nullptr; - if (attachment.IsTexture()) { - colorTexture = attachment.GetTexture().get(); - } - VkClearAttachment clearAttachment{}; - clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - clearAttachment.colorAttachment = drawBufferIndex; - clearAttachment.clearValue.color = { - payload.color.x(), payload.color.y(), payload.color.z(), - ResolveColorClearAlpha(colorTexture, payload.color.w()) - }; - clearAttachments.push_back(clearAttachment); - } } - - VkImageAspectFlags depthStencilAspects = 0; - if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) { - const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth); - if (depthAttachment.IsComplete()) { - depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; - } - } - if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { - const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); - if (stencilAttachment.IsComplete()) { - depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; - } - } - if (depthStencilAspects != 0) { - VkClearAttachment clearAttachment{}; - clearAttachment.aspectMask = depthStencilAspects; - clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil}; - clearAttachments.push_back(clearAttachment); - } - - if (!clearAttachments.empty()) { - vkCmdClearAttachments(frame.commandBuffer, - static_cast(clearAttachments.size()), clearAttachments.data(), - 1, &clearRect); - } - return; } m_clearManager->QueueClear(mask, payload, *fbo); @@ -3717,11 +3763,62 @@ void main() { const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { m_clearManager->CollectGarbage(); + // GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { + return; + } if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) { RecordUnsupportedFramebufferError(__func__); return; } + // Validate (buffer, drawbuffer) up front so GL errors fire regardless of which clear + // path is taken below. + switch (buffer) { + case GL_COLOR: + if (drawbuffer < 0 || + drawbuffer >= static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range"); + return; + } + break; + case GL_DEPTH: + if (drawbuffer != 0) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0"); + return; + } + break; + case GL_STENCIL: + if (drawbuffer != 0) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0"); + return; + } + break; + case GL_DEPTH_STENCIL: + if (drawbuffer != 0) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0"); + return; + } + break; + default: + RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target"); + return; + } + + // GL 3.3 §4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + VkClearRect clearRect{}; + switch (PrepareScissoredClear(framebuffer, clearRect)) { + case ScissoredClearPrep::NoOp: + return; + case ScissoredClearPrep::NotNeeded: + break; // full-coverage scissor: the deferred whole-surface path below is equivalent + case ScissoredClearPrep::Ready: + RecordScissoredClearBuffer(framebuffer, buffer, drawbuffer, clearPayload, clearRect); + return; + } + } + auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType) { if (attachmentType == FramebufferAttachmentType::None) { return; @@ -3738,43 +3835,84 @@ void main() { }; switch (buffer) { - case GL_COLOR: { - if (drawbuffer < 0 || - drawbuffer >= static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range"); - return; - } + case GL_COLOR: queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer]); return; - } case GL_DEPTH: - if (drawbuffer != 0) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0"); - return; - } queueAttachmentClear(FramebufferAttachmentType::Depth); return; case GL_STENCIL: - if (drawbuffer != 0) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0"); - return; - } queueAttachmentClear(FramebufferAttachmentType::Stencil); return; case GL_DEPTH_STENCIL: - if (drawbuffer != 0) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0"); - return; - } queueAttachmentClear(FramebufferAttachmentType::Depth); queueAttachmentClear(FramebufferAttachmentType::Stencil); return; default: - RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target"); return; } } + void VulkanRenderer::RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer, + GLenum buffer, GLint drawbuffer, + const ClearAttachmentPayload& clearPayload, + const VkClearRect& clearRect) { + VkClearAttachment clearAttachment{}; + + if (buffer == GL_COLOR) { + const auto attachmentType = framebuffer.GetDrawBuffers()[drawbuffer]; + if (attachmentType == FramebufferAttachmentType::None) { + return; + } + const auto& attachment = framebuffer.GetAttachment(attachmentType); + if (!attachment.IsComplete()) { + return; + } + const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast(drawbuffer)); + if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { + return; + } + if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { + MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial color mask is not supported"); + return; + } + MG_State::GLState::ITextureObject* colorTexture = nullptr; + if (attachment.IsTexture()) { + colorTexture = attachment.GetTexture().get(); + } + clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clearAttachment.colorAttachment = static_cast(drawbuffer); + clearAttachment.clearValue.color = { + clearPayload.color.x(), clearPayload.color.y(), clearPayload.color.z(), + ResolveColorClearAlpha(colorTexture, clearPayload.color.w()) + }; + } else { + VkImageAspectFlags aspects = 0; + if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() && + framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) { + aspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0 && + framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) { + // GL 3.3 §4.2.3: the clear is masked by the front stencil write mask (see Clear). + const Uint32 stencilWriteMask = + MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask; + if ((stencilWriteMask & 0xFFu) == 0xFFu) { + aspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } else if (stencilWriteMask != 0) { + MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial stencil write mask is not supported"); + } + } + if (aspects == 0) { + return; + } + clearAttachment.aspectMask = aspects; + clearAttachment.clearValue.depthStencil = {clearPayload.depth, clearPayload.stencil}; + } + + vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, 1, &clearAttachment, 1, &clearRect); + } + void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); @@ -3787,7 +3925,8 @@ void main() { void VulkanRenderer::ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; - payload.depth = depth; + // Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022). + payload.depth = std::clamp(depth, 0.0f, 1.0f); payload.stencil = static_cast(stencil); QueueClearBufferPayload(buffer, drawbuffer, payload); } @@ -3804,7 +3943,7 @@ void main() { break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; - payload.depth = value[0]; + payload.depth = std::clamp(value[0], 0.0f, 1.0f); break; default: break; @@ -3826,7 +3965,7 @@ void main() { break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; - payload.depth = value[0]; + payload.depth = std::clamp(value[0], 0.0f, 1.0f); break; default: break; @@ -3842,7 +3981,8 @@ void main() { } ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; - payload.depth = depth; + // Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022). + payload.depth = std::clamp(depth, 0.0f, 1.0f); payload.stencil = static_cast(stencil); QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload); } @@ -6863,7 +7003,8 @@ void main() { static_cast(activeRenderPass->extent.y()) }; clearRect.baseArrayLayer = 0; - clearRect.layerCount = 1; + // Compatible entries share the framebuffer layer count; layered attachments clear every layer. + clearRect.layerCount = compatibleRenderPassEntry.layers; for (const auto& pending : compatibleRenderPassEntry.pendingClearAttachments) { if (!pending.hasInlinePayload && pending.key.texture == nullptr) { @@ -6884,7 +7025,9 @@ void main() { clearAttachment.clearValue.depthStencil = {1.0f, 0}; if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) { clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - clearAttachment.colorAttachment = pending.attachmentIndex; + // VkClearAttachment::colorAttachment indexes the subpass pColorAttachments (draw-buffer + // slot space, with UNUSED holes), not the compacted attachment descriptions. + clearAttachment.colorAttachment = pending.colorAttachmentSlot; clearAttachment.clearValue.color = { clearPayload.color.x(), clearPayload.color.y(), diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 0e657099..b64de3b2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -131,6 +131,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, const RenderPassEntry& compatibleRenderPassEntry); + enum class ScissoredClearPrep { + NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead + NoOp, // nothing to clear (degenerate target or empty scissor rect) + Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect + }; + ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer, + VkClearRect& outClearRect); + void Clear(GLbitfield mask); void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value); @@ -275,6 +283,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload); + void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer, + GLenum buffer, GLint drawbuffer, + const ClearAttachmentPayload& clearPayload, + const VkClearRect& clearRect); // ---- Submission fence tracking (GL sync objects) ---- // One record per vkQueueSubmit still in flight, in ascending submit diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp index b27a0665..b47b492e 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp @@ -441,7 +441,10 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint bufferName = buffers[i]; if (bufferName == 0) continue; - if (!BufferImpl::ValidateBufferName(bufferName, true)) continue; + // GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently + // ignored here, so probe with the non-recording query - the shared validator would + // record INVALID_OPERATION, which is only correct on the bind path. + if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue; MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName); } } diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 8e00e400..a04beafd 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -1364,7 +1364,9 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint bufferName = renderbuffers[i]; if (bufferName == 0) continue; - if (!FramebufferImpl::ValidateRenderbufferName(bufferName)) continue; + // GL 3.3 core 4.4.2: unknown names are silently ignored on delete; the shared bind-path + // validator would record INVALID_OPERATION instead. + if (!MG_State::pGLContext->ValidateRenderbufferName(bufferName)) continue; MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName); } } @@ -1387,7 +1389,9 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint bufferName = framebuffers[i]; if (bufferName == 0) continue; - if (!FramebufferImpl::ValidateFramebufferName(bufferName)) continue; + // GL 3.3 core 4.4.1: unknown names are silently ignored on delete; the shared bind-path + // validator would record INVALID_OPERATION instead. + if (!MG_State::pGLContext->ValidateFramebufferName(bufferName)) continue; MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName); } } @@ -1877,6 +1881,13 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + // Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must + // raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators; + // runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM. + if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) { + return false; + } + // Check PBO state const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); diff --git a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp index 54d0557b..aa1fe5a1 100644 --- a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp +++ b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp @@ -649,7 +649,9 @@ namespace MobileGL::MG_Impl::GLImpl { } void ClearDepth_State(GLclampd depth) { - MG_State::pGLContext->SetClearDepth(static_cast(depth)); + // GL 3.3 §4.2.3: the clear depth is clamped to [0,1] at specification time (Vulkan clear + // values additionally require it: VUID-VkClearDepthStencilValue-depth-00022). + MG_State::pGLContext->SetClearDepth(ClampUnitFloat(static_cast(depth))); } void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp index ceec7248..2c78160c 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp @@ -14,7 +14,13 @@ namespace MobileGL::MG_Impl::GLImpl { namespace { - Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isInteger) { + Float ReadSamplerScalar(const void* param, Bool isFloat, Bool isUnsignedInteger) { + if (isFloat) return *(const GLfloat*)param; + if (isUnsignedInteger) return static_cast(*(const GLuint*)param); + return static_cast(*(const GLint*)param); + } + + Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) { if (param == nullptr) return false; switch (pname) { @@ -22,6 +28,13 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_MAX_LOD: case GL_TEXTURE_LOD_BIAS: return true; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", "SetSamplerParam_State", + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0.")); + return false; default: break; } @@ -29,14 +42,15 @@ namespace MobileGL::MG_Impl::GLImpl { if (isFloat) { return SamplerImpl::ValidateSamplerFloatParam(pname, *(const GLfloat*)param); } - if (isInteger) { + if (isUnsignedInteger) { return SamplerImpl::ValidateSamplerIntParam(pname, static_cast(*(const GLuint*)param)); } return SamplerImpl::ValidateSamplerIntParam(pname, *(const GLint*)param); } } // namespace - void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, bool isInteger) { + void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, + bool isUnsignedInteger) { if (param == nullptr) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return; @@ -47,7 +61,7 @@ namespace MobileGL::MG_Impl::GLImpl { } auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler); if (!SamplerImpl::ValidateSamplerObject(sampler)) return; - if (!ValidateSamplerParameterValue(pname, param, isFloat, isInteger)) return; + if (!ValidateSamplerParameterValue(pname, param, isFloat, isUnsignedInteger)) return; using namespace MG_Util; switch (pname) { @@ -76,6 +90,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: samplerObj->SetLodBias(*(const GLfloat*)param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + samplerObj->SetMaxAnisotropy(ReadSamplerScalar(param, isFloat, isUnsignedInteger)); + break; case GL_TEXTURE_COMPARE_MODE: samplerObj->SetCompareMode(MG_Util::ConvertGLEnumToSamplerCompareMode(*(const GLint*)param)); break; @@ -89,7 +106,8 @@ namespace MobileGL::MG_Impl::GLImpl { } } - void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, bool isInteger) { + void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, + bool isUnsignedInteger) { if (params == nullptr) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return; @@ -129,6 +147,15 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: *(GLfloat*)params = samplerObj->GetLodBias(); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (isFloat) { + *(GLfloat*)params = samplerObj->GetMaxAnisotropy(); + } else if (isUnsignedInteger) { + *(GLuint*)params = static_cast(samplerObj->GetMaxAnisotropy()); + } else { + *(GLint*)params = static_cast(samplerObj->GetMaxAnisotropy()); + } + break; case GL_TEXTURE_COMPARE_MODE: *(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()); break; @@ -206,7 +233,16 @@ namespace MobileGL::MG_Impl::GLImpl { if (sampler == 0) { textureUnit.SetSamplerObject(nullptr); } else { - if (!SamplerImpl::ValidateSamplerName(sampler)) return; + // GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already + // deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same + // name, which is why this cannot go through the shared SamplerImpl validator. + if (!MG_State::pGLContext->ValidateSamplerName(sampler)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "BindSampler_State", + std::format("Invalid sampler name {}", sampler))); + return; + } Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler); if (!doesSamplerObjectCreated) { MG_State::pGLContext->CreateSamplerObject(sampler); @@ -240,7 +276,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) { - SetSamplerParam_State(sampler, pname, param, false, true); + SetSamplerParam_State(sampler, pname, param, false, false); } void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) { @@ -268,7 +304,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) { - GetSamplerParam_State(sampler, pname, params, false, true); + GetSamplerParam_State(sampler, pname, params, false, false); } void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) { diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp index 83934ce3..9a028d31 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp @@ -99,6 +99,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { case GL_TEXTURE_LOD_BIAS: return true; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (!(param >= 1.0f)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", "ValidateSamplerFloatParam", + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0.")); + return false; + } + return true; + case GL_TEXTURE_BORDER_COLOR: if (param < 0.0f || param > 1.0f) { MG_State::pGLContext->RecordError( @@ -125,6 +135,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { } return true; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (param < 1) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", "ValidateSamplerIntParam", + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.")); + return false; + } + return true; + default: return ValidateSamplerParam(pname, static_cast(param)); } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 8d74332b..d1941da7 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -74,6 +74,16 @@ namespace MobileGL::MG_Impl::GLImpl { return true; } + Bool ValidateMaxAnisotropy(Float maxAnisotropy, const char* caller) { + if (maxAnisotropy >= 1.0f) return true; + + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", caller, + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0.")); + return false; + } + template void WithTemporarilyBoundNamedTexture(const SharedPtr& textureObject, Fn&& fn) { @@ -284,10 +294,16 @@ namespace MobileGL::MG_Impl::GLImpl { MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS)); } - Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) { + // Array targets store their layer count in z; layers never participate in mip + // reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level. + Bool DepthParticipatesInMipmapping(TextureTarget target) { + return target == TextureTarget::Texture3D; + } + + Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) { Int maxDimension = std::max( baseTexelSize.x(), - std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); + std::max(baseTexelSize.y(), depthMips ? std::max(baseTexelSize.z(), 1) : 1)); Uint mipLevelCount = 1; while (maxDimension > 1) { maxDimension = std::max(maxDimension / 2, 1); @@ -296,11 +312,12 @@ namespace MobileGL::MG_Impl::GLImpl { return mipLevelCount; } - IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) { + IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) { return { std::max(baseTexelSize.x() >> static_cast(relativeLevel), 1), std::max(baseTexelSize.y() >> static_cast(relativeLevel), 1), - std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1), + depthMips ? std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1) + : std::max(baseTexelSize.z(), 1), }; } @@ -323,9 +340,10 @@ namespace MobileGL::MG_Impl::GLImpl { } const SizeT bytesPerTexel = baseByteSize / baseTexelCount; - const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize); + const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget()); + const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips); for (Uint level = 1; level < requiredLevelCount; ++level) { - const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level); + const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips); const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * static_cast(levelTexelSize.y()) * static_cast(levelTexelSize.z()); @@ -453,6 +471,9 @@ namespace MobileGL::MG_Impl::GLImpl { Bool ValidateTextureParameterForTarget(const SharedPtr& textureObject, GLenum pname, GLint param, const char* caller) { const auto target = textureObject->GetTarget(); + if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) { + return false; + } if ((pname == GL_TEXTURE_BASE_LEVEL || pname == GL_TEXTURE_MAX_LEVEL) && param < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, @@ -482,7 +503,8 @@ namespace MobileGL::MG_Impl::GLImpl { (pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T || pname == GL_TEXTURE_WRAP_R || pname == GL_TEXTURE_MIN_FILTER || pname == GL_TEXTURE_MAG_FILTER || pname == GL_TEXTURE_MIN_LOD || pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE || - pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR)) { + pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR || + pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", caller, @@ -581,6 +603,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: textureObject->GetSamplerObject()->SetLodBias((GLfloat)param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + textureObject->GetSamplerObject()->SetMaxAnisotropy(static_cast(param)); + break; case GL_GENERATE_MIPMAP: g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != GL_FALSE); break; @@ -597,7 +622,10 @@ namespace MobileGL::MG_Impl::GLImpl { void TextureParameterObjectf_State(const SharedPtr& textureObject, GLenum pname, GLfloat param, const char* caller) { if (!textureObject) return; - if (!ValidateTextureParameterForTarget(textureObject, pname, static_cast(param), caller)) return; + if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) return; + const GLint validationParam = + pname == GL_TEXTURE_MAX_ANISOTROPY_EXT ? 1 : static_cast(param); + if (!ValidateTextureParameterForTarget(textureObject, pname, validationParam, caller)) return; switch (pname) { case GL_TEXTURE_MAG_FILTER: @@ -643,6 +671,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: textureObject->GetSamplerObject()->SetLodBias(param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + textureObject->GetSamplerObject()->SetMaxAnisotropy(param); + break; case GL_GENERATE_MIPMAP: g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f); break; @@ -712,6 +743,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = (GLint)MG_Util::ConvertSamplerCompareFuncToGLEnum( textureObject->GetSamplerObject()->GetSamplerCompareFunc()); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + *params = static_cast(textureObject->GetSamplerObject()->GetMaxAnisotropy()); + break; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -898,11 +932,11 @@ namespace MobileGL::MG_Impl::GLImpl { auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& textureObject = bindingSlot.GetBoundObject(); + if (!TextureImpl::ValidateTextureObject(textureObject)) return; TextureInternalFormat textureInternalFormat = textureObject->GetFormat(); MGLOG_D("%s: working on texture %d", __func__, textureObject->GetExternalIndex()); // ===================== Error Checking ============================== - if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height)) return; if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat, texturePixelDataType)) @@ -1122,6 +1156,10 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: textureObject->GetSamplerObject()->SetLodBias(param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (!ValidateMaxAnisotropy(param, __func__)) return; + textureObject->GetSamplerObject()->SetMaxAnisotropy(param); + break; case GL_GENERATE_MIPMAP: g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f); break; @@ -1762,7 +1800,9 @@ namespace MobileGL::MG_Impl::GLImpl { GLboolean IsTexture_State(GLuint texture) { // ======================= Processing ================================ - if (!TextureImpl::ValidateTextureName(texture, true)) return GL_FALSE; + // GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved + // name is just GL_FALSE. Probing with the recording validator (as every other Is* entry + // point already avoids doing) would leave a spurious INVALID_VALUE behind. return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE; } @@ -1951,6 +1991,11 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->GetSamplerObject()->GetSamplerCompareFunc()); } break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (params) { + *params = static_cast(textureObject->GetSamplerObject()->GetMaxAnisotropy()); + } + break; case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE: if (params) { *params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE; @@ -2097,6 +2142,11 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->GetSamplerObject()->GetSamplerCompareFunc()); } break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (params) { + *params = textureObject->GetSamplerObject()->GetMaxAnisotropy(); + } + break; default: MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "GetTexParameterfv_State", @@ -2368,7 +2418,7 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint textureName = textures[i]; if (textureName == 0) continue; - if (!TextureImpl::ValidateTextureName(textureName, true)) continue; + if (!MG_State::pGLContext->ValidateTextureName(textureName)) continue; MG_State::pGLContext->MarkTextureObjectForDeletion(textureName); } } @@ -2591,6 +2641,9 @@ namespace MobileGL::MG_Impl::GLImpl { return; } + // GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted - + // is not a legal bind target in the core profile (no application-generated names), and the error + // is INVALID_OPERATION, not INVALID_VALUE. if (!MG_State::pGLContext->ValidateTextureName(texture)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -2598,8 +2651,6 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - if (!TextureImpl::ValidateTextureName(texture, true)) return; - // ======================= Processing ================================ Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture); if (!doesTextureExist) { @@ -2994,10 +3045,13 @@ namespace MobileGL::MG_Impl::GLImpl { auto* textureMipmapObject = static_cast(textureObject.get()); textureObject->SetInternalFormat(textureInternalFormat); + // Array targets keep their layer count constant across levels; only true 3D + // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D). + const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget()); for (GLsizei level = 0; level < levels; ++level) { const GLsizei levelWidth = std::max(1, width >> level); const GLsizei levelHeight = std::max(1, height >> level); - const GLsizei levelDepth = std::max(1, depth >> level); + const GLsizei levelDepth = depthMips ? std::max(1, depth >> level) : depth; const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight, levelDepth); textureMipmapObject->AllocateStorage(textureUploadTarget, level, diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index c72e7442..3de811a1 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -283,7 +283,9 @@ namespace MobileGL::MG_Impl::GLImpl { GLuint vao = arrays[i]; if (vao == 0) continue; - if (!VertexArrayImpl::ValidateVertexArrayName(vao)) continue; + // GL 3.3 core 2.10: unknown names are silently ignored on delete; the shared bind-path + // validator would record INVALID_OPERATION instead. + if (!MG_State::pGLContext->ValidateVertexArrayName(vao)) continue; if (MG_State::pGLContext->GetBoundVertexArray() && MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) { diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp index 68569872..fad9abb7 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp @@ -78,6 +78,13 @@ namespace MobileGL { ++m_version; } + void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) { + if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return; + + m_samplerParameters.maxAnisotropy = maxAnisotropy; + ++m_version; + } + void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) { if (func == m_samplerParameters.compareFunc) return; @@ -128,6 +135,10 @@ namespace MobileGL { return m_samplerParameters.lodBias; } + Float SamplerObject::GetMaxAnisotropy() const { + return m_samplerParameters.maxAnisotropy; + } + SamplerCompareMode SamplerObject::GetCompareMode() const { return m_samplerParameters.compareMode; } diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h index 16fda9b0..54352396 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h @@ -65,6 +65,7 @@ namespace MobileGL { Float minLod = -1000.0f; Float maxLod = 1000.0f; Float lodBias = 0.0f; + Float maxAnisotropy = 1.0f; SamplerCompareFunc compareFunc = SamplerCompareFunc::Always; SamplerCompareMode compareMode = SamplerCompareMode::None; }; @@ -83,6 +84,7 @@ namespace MobileGL { void SetMipmapMode(SamplerMipmapMode mode); void SetLodRange(Float minLod, Float maxLod); void SetLodBias(Float bias); + void SetMaxAnisotropy(Float maxAnisotropy); void SetSamplerCompareFunc(SamplerCompareFunc func); void SetCompareMode(SamplerCompareMode mode); @@ -95,6 +97,7 @@ namespace MobileGL { Float GetMinLod() const; Float GetMaxLod() const; Float GetLodBias() const; + Float GetMaxAnisotropy() const; SamplerCompareMode GetCompareMode() const; SamplerCompareFunc GetSamplerCompareFunc() const; Uint GetExternalIndex() const; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index 31617504..b2bfc78f 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -110,6 +110,9 @@ namespace MobileGL::MG_State::GLState { BumpTextureBindGeneration(); m_textureObjects.erase(index); } + // Release the name itself even when GenTextures only reserved it and no bind ever + // instantiated an object: GL 3.3 core 3.8.1 makes a deleted name unused again (so a + // later bind of it must fail), and the reservation has to return to the free list. m_indexGenerator.Delete(index); } } diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index ff5adb4a..8ee37032 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,7 @@ namespace { GLenum errorRaisedByDraw = GL_NO_ERROR; GLenum pendingError = GL_NO_ERROR; + std::vector extensions; GLuint nextBufferId = 1; GLuint nextShaderId = 1; @@ -86,7 +88,7 @@ namespace { *data = 1; break; case GL_NUM_EXTENSIONS: - *data = 0; + *data = static_cast(g_fake.extensions.size()); break; default: // Leave the caller's defaults for every other capability query. @@ -114,9 +116,10 @@ namespace { return reinterpret_cast(""); } }; - // GL_NUM_EXTENSIONS reports 0 above, so this is never reached; it exists so the - // table stays complete if the extension loop ever runs. - funcs.glGetStringi = [](GLenum, GLuint) -> const GLubyte* { return nullptr; }; + funcs.glGetStringi = [](GLenum name, GLuint index) -> const GLubyte* { + if (name != GL_EXTENSIONS || index >= g_fake.extensions.size()) return nullptr; + return reinterpret_cast(g_fake.extensions[index].c_str()); + }; funcs.glGetFloatv = [](GLenum pname, GLfloat* data) { switch (pname) { // Two-component range queries. @@ -400,3 +403,20 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) { EXPECT_FALSE(conformingCaps.IndirectDrawInstanceIdIncludesBaseInstance); ExpectProbeReleasedAllObjects(); } + +TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities absentCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs)); + EXPECT_FALSE(absentCaps.SupportsTextureFilterAnisotropy); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic"); + MobileGL::MG_External::GLESCapabilities presentCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs)); + EXPECT_TRUE(presentCaps.SupportsTextureFilterAnisotropy); +} diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 2220a19d..fcd72d04 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" #include @@ -20,9 +22,31 @@ using namespace MobileGL; class BufferTest : public ::testing::Test { protected: - void SetUp() override { MobileGL::Initialize(); } + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so drain whatever an earlier test left pending - otherwise an error-code assertion + // here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite. + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } - void TearDown() override {} + // 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(); + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } }; TEST_F(BufferTest, Binding) { @@ -105,6 +129,53 @@ TEST_F(BufferTest, GenerateManyNames_NoPrematureCreation) { } } +// GL 3.3 core 2.9 name lifecycle. The same three rules are asserted per object family (see the +// texture/vertex-array/framebuffer/renderbuffer suites): a deleted or never-generated name is +// INVALID_OPERATION to bind, deleting one is silent, and a generated-but-never-bound reservation +// is still released so the name gets recycled. +TEST_F(BufferTest, DeleteOfUnknownOrAlreadyDeletedBufferNameIsSilent) { + GLuint buffer = 0; + MG_Impl::GLImpl::GenBuffers(1, &buffer); + ASSERT_NE(buffer, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Double delete, name 0 and a never-generated name must all be ignored without an error. + MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteBuffers(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(BufferTest, DeleteGeneratedButUnboundBufferNameReleasesReservationAndBindFails) { + GLuint buffer = 0; + MG_Impl::GLImpl::GenBuffers(1, &buffer); + ASSERT_NE(buffer, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer)); + + MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(buffer)); + + MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenBuffers(1, &recycled); + EXPECT_EQ(recycled, buffer); +} + +TEST_F(BufferTest, BindNeverGeneratedBufferNameIsInvalidOperation) { + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, std::numeric_limits::max()); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + TEST_F(BufferTest, AcquireMemory) { auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform); Vector bufferNames; diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index c5d2c062..d2642d6b 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" #include @@ -78,8 +80,30 @@ namespace { class FramebufferTest : public ::testing::Test { protected: + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so drain whatever an earlier test left pending - otherwise an error-code assertion + // here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite. + 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 TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } + void SetUp() override { MobileGL::Initialize(); + DrainPendingGlErrors(); const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0); ASSERT_NE(defaultFramebuffer, nullptr); MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(defaultFramebuffer); @@ -122,6 +146,83 @@ TEST_F(FramebufferTest, CreateFramebuffersCreatesObjectsImmediately) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL 3.3 core 4.4.1/4.4.2 name lifecycle - mirrors the rules asserted for the other object +// families: deleting an unknown name is silent, a released reservation is recycled, and binding +// a dead name is INVALID_OPERATION. +TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedFramebufferNameIsSilent) { + GLuint framebuffer = 0; + MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer); + ASSERT_NE(framebuffer, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteFramebuffers(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(FramebufferTest, DeleteGeneratedButUnboundFramebufferNameReleasesReservationAndBindFails) { + GLuint framebuffer = 0; + MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer); + ASSERT_NE(framebuffer, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateFramebufferName(framebuffer)); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateFramebufferName(framebuffer)); + + MG_Impl::GLImpl::BindFramebuffer(GL_FRAMEBUFFER, framebuffer); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenFramebuffers(1, &recycled); + EXPECT_EQ(recycled, framebuffer); +} + +TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedRenderbufferNameIsSilent) { + GLuint renderbuffer = 0; + MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer); + ASSERT_NE(renderbuffer, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteRenderbuffers(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(FramebufferTest, DeleteGeneratedButUnboundRenderbufferNameReleasesReservationAndBindFails) { + GLuint renderbuffer = 0; + MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer); + ASSERT_NE(renderbuffer, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer)); + + MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer)); + + MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenRenderbuffers(1, &recycled); + EXPECT_EQ(recycled, renderbuffer); +} + TEST_F(FramebufferTest, DefaultFramebufferIdentityTracksFramebufferNameZero) { const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0); ASSERT_NE(defaultFramebuffer, nullptr); diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 40cbd817..1b272dd3 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -107,7 +107,7 @@ void main() { PreprocessShaderSource(ShaderStage::Vertex, source); - EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("#version 330 core\n"), 0); EXPECT_NE(source.find("in vec3 position;"), String::npos); EXPECT_NE(source.find("out vec2 uv;"), String::npos); EXPECT_EQ(source.find("attribute"), String::npos); @@ -136,7 +136,7 @@ void main() { PreprocessShaderSource(ShaderStage::Fragment, source); - EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("#version 330 core\n"), 0); EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos); EXPECT_NE(source.find("in vec2 uv;"), String::npos); EXPECT_NE(source.find("texture(texture0, uv)"), String::npos); @@ -153,6 +153,243 @@ void main() { } } +TEST_F(ProgramUtilTest, PreprocessMinecraft112BlurShaderKeepsLegacySampleIdentifier) { + using namespace MG_Util::ShaderTranspiler; + + // assets/minecraft/shaders/program/blur.fsh from the unmodified Minecraft 1.12 client jar. + String source = R"(#version 120 + +uniform sampler2D DiffuseSampler; + +varying vec2 texCoord; +varying vec2 oneTexel; + +uniform vec2 InSize; + +uniform vec2 BlurDir; +uniform float Radius; + +void main() { + vec4 blurred = vec4(0.0); + float totalStrength = 0.0; + float totalAlpha = 0.0; + float totalSamples = 0.0; + for(float r = -Radius; r <= Radius; r += 1.0) { + vec4 sample = texture2D(DiffuseSampler, texCoord + oneTexel * r * BlurDir); + + // Accumulate average alpha + totalAlpha = totalAlpha + sample.a; + totalSamples = totalSamples + 1.0; + + // Accumulate smoothed blur + float strength = 1.0 - abs(r / Radius); + totalStrength = totalStrength + strength; + blurred = blurred + sample; + } + gl_FragColor = vec4(blurred.rgb / (Radius * 2.0 + 1.0), totalAlpha); +} +)"; + + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 330 core\n"), 0); + EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos); + EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos); + EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos); + EXPECT_NE(source.find("totalSamples = totalSamples + 1.0;"), String::npos); + EXPECT_NE(source.find("blurred = blurred + sample;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessLegacySampleInterfaceIdentifiersKeepNames) { + using namespace MG_Util::ShaderTranspiler; + + String vertexSource = R"(#version 150 +attribute vec3 sample; + +void main() { + gl_Position = vec4(sample, 1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Vertex, vertexSource); + + EXPECT_EQ(vertexSource.find("#version 330 core\n"), 0); + EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos); + + ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource}; + auto vertexResult = ShaderCompiler::CompileShader(vertexAttrib); + if (!vertexResult) { + FAIL() << "errc: " << vertexResult.error().errc << "\nlog: " << vertexResult.error().log + << "\nsource:\n" << vertexSource; + } + + String fragmentSource = R"(#version 150 +uniform sampler2D sample; +varying vec2 texCoord; + +void main() { + gl_FragColor = texture2D(sample, texCoord); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, fragmentSource); + + EXPECT_EQ(fragmentSource.find("#version 330 core\n"), 0); + EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos); + EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos); + + ShaderAttrib fragmentAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource}; + auto fragmentResult = ShaderCompiler::CompileShader(fragmentAttrib); + if (!fragmentResult) { + FAIL() << "errc: " << fragmentResult.error().errc << "\nlog: " << fragmentResult.error().log + << "\nsource:\n" << fragmentSource; + } +} + +TEST_F(ProgramUtilTest, PreprocessEsslVersionsRemainVulkanCompatible) { + using namespace MG_Util::ShaderTranspiler; + + const auto verifyVersion = [](const char* inputVersion, const char* expectedVersion) { + SCOPED_TRACE(inputVersion); + String source = inputVersion; + source += R"( +precision mediump float; +out vec4 fragColor; + +void main() { + fragColor = vec4(1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find(expectedVersion), 0); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + }; + + // Preserve the pre-existing desktop-core route: the current resource table cannot parse ESSL built-ins. + verifyVersion("#version 300 es", "#version 460 core\n"); + verifyVersion("#version 310 es", "#version 460 core\n"); +} + +TEST_F(ProgramUtilTest, PreprocessModernDesktopVersionsRecognizesUtf8Bom) { + using namespace MG_Util::ShaderTranspiler; + + const auto verifyVersion = [](const char* inputVersion) { + SCOPED_TRACE(inputVersion); + String source = "\xef\xbb\xbf"; + source += inputVersion; + source += R"( +out vec4 fragColor; + +void main() { + fragColor = vec4(1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + }; + + verifyVersion("#version 400 core"); + verifyVersion("#version 460 core"); +} + +TEST_F(ProgramUtilTest, PreprocessUsesRealSpacedVersionDirectiveForInjectedOutput) { + using namespace MG_Util::ShaderTranspiler; + + String source = R"(// #version 460 core +/* "#version 400 core" */ +#line 7 "#version 460 core" +# version 120 +varying vec2 uv; + +void main() { + gl_FragColor = vec4(uv, 0.0, 1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + const SizeT versionPos = source.find("#version 330 core\n"); + const SizeT outputPos = source.find("out vec4 mg_FragColor;\n"); + EXPECT_NE(versionPos, String::npos); + EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n")); + EXPECT_NE(source.find("// #version 460 core"), String::npos); + EXPECT_EQ(source.find("#line"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) { + using namespace MG_Util::ShaderTranspiler; + + String source = R"(#version 400 core +sample in vec4 interpolatedColor; +out vec4 fragColor; + +void main() { + fragColor = interpolatedColor; +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessGpuShader5SampleQualifierUsesVersion460) { + using namespace MG_Util::ShaderTranspiler; + + for (const char* extension : {"GL_ARB_gpu_shader5", "GL_NV_gpu_shader5"}) { + SCOPED_TRACE(extension); + String source = "#version 150\n#extension "; + source += extension; + source += R"( : enable +sample in vec4 interpolatedColor; +out vec4 fragColor; + +void main() { + fragColor = interpolatedColor; +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + } +} + TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesFragData) { using namespace MG_Util::ShaderTranspiler; @@ -164,7 +401,7 @@ void main() { PreprocessShaderSource(ShaderStage::Fragment, source); - EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("#version 330 core\n"), 0); EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos); EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos); EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos); @@ -182,7 +419,7 @@ TEST_F(ProgramUtilTest, PreprocessKeepsDefaultPrecisionStatements) { // Mirrors the GL CTS helper shaders (e.g. glcPixelStorageModesTests): the old qualifier strip // turned "precision highp float;" into invalid "precision float;". Precision qualifiers are - // legal (and ignored) in the forced 460 core profile, so they now pass through untouched. + // legal (and ignored) in the normalized desktop core profile, so they now pass through untouched. String source = R"(#version 330 precision highp float; precision mediump int; @@ -212,7 +449,7 @@ TEST_F(ProgramUtilTest, PreprocessKeepsPrecisionInLegacyShaderForGlslang) { using namespace MG_Util::ShaderTranspiler; // Legacy ES-style shader: precision statements and qualifier macros are left for glslang - // (its preprocessor expands the #define; the 460 core parse ignores the qualifiers). + // (its preprocessor expands the #define; the normalized 330 core parse ignores the qualifiers). String source = R"(#define HIGHP_OR_DEFAULT highp precision HIGHP_OR_DEFAULT float; precision mediump int; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 3cdb734c..d19d098b 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -8,16 +8,21 @@ #include +#include + #include "Includes.h" #include "Init.h" #include +#include #include #include +#include #include #include #include -#include #include +#include +#include #include #include #include @@ -27,7 +32,32 @@ using namespace MobileGL; class TextureTest : public ::testing::Test { protected: - void SetUp() override { MobileGL::Initialize(); } + // 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; a runaway would otherwise hang the suite. + 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 (e.g. a shared validator firing before the + // specific check), which GetError() would hand out at unrelated call sites 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(); + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } }; namespace { @@ -135,6 +165,295 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(TextureTest, TextureMaxAnisotropyDefaultsToOneAndRoundTripsWithoutRedundantVersionBumps) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + const auto& samplerObject = textureObject->GetSamplerObject(); + ASSERT_NE(samplerObject, nullptr); + + GLfloat floatValue = 0.0f; + MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 1.0f); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 1.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint16 initialVersion = samplerObject->GetVersion(); + MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 4.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 4.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(initialVersion + 1)); + + GLint integerValue = 0; + MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &integerValue); + EXPECT_EQ(integerValue, 4); + MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 4.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint16 setVersion = samplerObject->GetVersion(); + MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 4.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 8.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(setVersion + 1)); +} + +TEST_F(TextureTest, TextureMaxAnisotropyBelowOneIsInvalidValueAndPreservesState) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + const auto& samplerObject = textureObject->GetSamplerObject(); + ASSERT_NE(samplerObject, nullptr); + const Uint16 initialVersion = samplerObject->GetVersion(); + + MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0.5f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 1.0f); + EXPECT_EQ(samplerObject->GetVersion(), initialVersion); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 1.0f); + EXPECT_EQ(samplerObject->GetVersion(), initialVersion); +} + +TEST_F(TextureTest, SamplerMaxAnisotropyUsesTheSameStateAndValidationSemantics) { + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + + GLfloat floatValue = 0.0f; + MG_Impl::GLImpl::GetSamplerParameterfv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(floatValue, 1.0f); + + const auto& samplerObject = MG_State::pGLContext->GetSamplerObject(sampler); + ASSERT_NE(samplerObject, nullptr); + const Uint16 initialVersion = samplerObject->GetVersion(); + + MG_Impl::GLImpl::SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 6.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(initialVersion + 1)); + + GLint integerValue = 0; + MG_Impl::GLImpl::GetSamplerParameteriv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &integerValue); + EXPECT_EQ(integerValue, 6); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint16 setVersion = samplerObject->GetVersion(); + MG_Impl::GLImpl::SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 6.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + MG_Impl::GLImpl::SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0.25f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + const GLint signedInvalidValue = -1; + MG_Impl::GLImpl::SamplerParameterIiv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &signedInvalidValue); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + const GLuint unsignedValue = 10; + MG_Impl::GLImpl::SamplerParameterIuiv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &unsignedValue); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 10.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(setVersion + 1)); + + GLuint queriedUnsignedValue = 0; + MG_Impl::GLImpl::GetSamplerParameterIuiv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &queriedUnsignedValue); + EXPECT_EQ(queriedUnsignedValue, unsignedValue); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// GL 3.3 core 3.8.2: BindSampler rejects a never-generated or already-deleted name with +// INVALID_OPERATION, while SamplerParameter* on the same name is INVALID_VALUE - the two paths +// must not share one validator. Delete of an unknown name stays silent. +TEST_F(TextureTest, BindSamplerRejectsUnknownNameWithInvalidOperationUnlikeSamplerParameter) { + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + MG_Impl::GLImpl::BindSampler(0, sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Deleting is silent, twice over, and the name is dead afterwards. + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindSampler(0, sampler); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // Same dead name through SamplerParameter*: INVALID_VALUE, so the two paths cannot share one + // validator - and neither may queue the other's code alongside its own. + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +TEST_F(TextureTest, GenThenBindCreatesObjectForUnsizedPackedBgraSubImageUpload) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureName(texture)); + // GenTextures only reserves the name; the object appears on first bind. + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(texture)); + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_TRUE); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 1, 0, GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); + const Uint8 pixels[] = { + 10, 20, 30, 40, + 50, 60, 70, 80, + }; + MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, pixels); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 expected[] = { + 30, 20, 10, 40, + 70, 60, 50, 80, + }; + EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// GL 3.3 core 3.8.1: DeleteTextures makes the name unused again whether or not a bind ever +// instantiated an object, so the reservation must go back to the generator's free list rather +// than leaking, and binding the dead name afterwards must fail. +TEST_F(TextureTest, DeleteGeneratedButUnboundNameReleasesReservationAndBindFails) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureName(texture)); + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(texture)); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + // IsTexture answers about a dead name without raising anything (GL 3.3 core 6.1.4). + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + + // The freed reservation is recycled (the generator's free list is LIFO, so the very same + // name comes back) - a delete that skipped the release would hand out a fresh name here. + GLuint recycled = 0; + MG_Impl::GLImpl::GenTextures(1, &recycled); + EXPECT_EQ(recycled, texture); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(recycled)); +} + +TEST_F(TextureTest, DeleteInstantiatedTextureInvalidatesNameUntilRegenerated) { + GLuint textures[2] = {}; + MG_Impl::GLImpl::GenTextures(2, textures); + ASSERT_NE(textures[0], 0u); + ASSERT_NE(textures[1], 0u); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureObject(textures[0])); + MG_Impl::GLImpl::DeleteTextures(1, &textures[0]); + + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(textures[0])); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(textures[0])); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[1]); + const auto fallbackObject = MG_State::pGLContext->GetTextureObject(textures[1]); + ASSERT_NE(fallbackObject, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]); + ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + fallbackObject); +} + +TEST_F(TextureTest, DeleteUnknownNamesIsSilentButBindUnknownNameIsInvalid) { + GLuint validTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &validTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, validTexture); + const auto boundObject = MG_State::pGLContext->GetTextureObject(validTexture); + ASSERT_NE(boundObject, nullptr); + + constexpr GLuint unknownNames[] = {0, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteTextures(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, unknownNames[1]); + ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + boundObject); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(unknownNames[1])); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(unknownNames[1])); +} + +TEST_F(TextureTest, BindTextureUnitEnumAsNameIsSilentNoOp) { + GLuint validTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &validTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, validTexture); + const auto boundObject = MG_State::pGLContext->GetTextureObject(validTexture); + ASSERT_NE(boundObject, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + constexpr GLuint textureUnitEnum = GL_TEXTURE7; + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureName(textureUnitEnum)); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textureUnitEnum); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + boundObject); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(textureUnitEnum)); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(textureUnitEnum)); +} + +TEST_F(TextureTest, TexSubImage2DWithoutBoundTextureReportsErrorInsteadOfDereferencingNull) { + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint8 pixel[] = {1, 2, 3, 4}; + MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + TEST_F(TextureTest, TextureStorageAndSubImageModifyNamedObjectOnly) { GLuint namedTexture = 0; GLuint boundTexture = 0; @@ -939,6 +1258,146 @@ TEST_F(TextureTest, BoundTexSubImage3DRejectsOutOfRangeLevel) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); } +// The GL CTS KHR-GL33.pixelstoragemodes.teximage3d cases upload GL_TEXTURE_2D_ARRAY +// textures through glTexImage3D with UNPACK_ROW_LENGTH / IMAGE_HEIGHT / SKIP_* set to +// extract a sub-cuboid; this mirrors that shape (scaled down) on the 2D-array target. +TEST_F(TextureTest, BoundTexImage3DOn2DArrayHonorsUnpackSubcuboidSelection) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture); + + // Source cuboid: 3x3 RGBA texels per image, 3 images; skip 1 image, 1 row, 1 pixel; + // upload the 2x2x2 sub-cuboid. Each source byte equals its own offset, so the stored + // shadow bytes must equal the offsets of the selected texels. + Uint8 pixels[3 * 3 * 3 * 4]; + for (SizeT i = 0; i < sizeof(pixels); ++i) { + pixels[i] = static_cast(i); + } + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 1); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetTarget(), TextureTarget::Texture2DArray); + auto* mipmapObject = static_cast(textureObject.get()); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(2, 2, 2)); + + const auto* stored = + static_cast(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2DArray, 0)); + ASSERT_NE(stored, nullptr); + SizeT storedIndex = 0; + for (SizeT image = 1; image <= 2; ++image) { // SKIP_IMAGES = 1 + for (SizeT row = 1; row <= 2; ++row) { // SKIP_ROWS = 1 + for (SizeT column = 1; column <= 2; ++column) { // SKIP_PIXELS = 1 + const SizeT srcOffset = image * 36 + row * 12 + column * 4; + for (SizeT b = 0; b < 4; ++b, ++storedIndex) { + EXPECT_EQ(stored[storedIndex], static_cast(srcOffset + b)) << "byte " << storedIndex; + } + } + } + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The shadow mip for packed sized formats keeps the client's packed bytes, so the +// canonical transfer triple must name the packed word type; the old default fallback +// (GL_UNSIGNED_BYTE) made backends read 4 bytes per texel from a 2-byte-per-texel +// shadow (KHR-GL33.pixelstoragemodes rgba4/rgb565 uploads), and GL_RGB10_A2UI got a +// non-integer GL_RGB transfer format the driver rejects outright. +TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFormats) { + using MG_Util::TextureFormatProcessor::NormalizePixelFormat; + struct { + GLenum internalFormat; + GLenum expectedFormat; + GLenum expectedType; + } cases[] = { + // RGBA4/RGB565/RGB5_A1 store canonical UNorm8 component shadows (PixelStoreProcessor + // GetInternalShadowLayout), so their transfer type is GL_UNSIGNED_BYTE; the 32-bit packed + // formats keep the packed word the shadow holds verbatim. + {GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE}, + {GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV}, + {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE}, + {GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV}, + }; + for (const auto& c : cases) { + GLenum outInternal = 0, outFormat = 0, outType = 0; + NormalizePixelFormat(c.internalFormat, PixelFormatNormalizeOptionBit::None, &outInternal, &outFormat, + &outType); + EXPECT_EQ(outInternal, c.internalFormat) << "internalformat 0x" << std::hex << c.internalFormat; + EXPECT_EQ(outFormat, c.expectedFormat) << "internalformat 0x" << std::hex << c.internalFormat; + EXPECT_EQ(outType, c.expectedType) << "internalformat 0x" << std::hex << c.internalFormat; + } +} + +// GL_RGB565 (ARB_ES2_compatibility / GL 4.1, used directly by the GL CTS) must round-trip +// through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with +// GL_RGB565 was rejected as an unknown internal format. +TEST_F(TextureTest, Rgb565InternalFormatRoundTripsThroughEnumConverters) { + EXPECT_EQ(MG_Util::ConvertGLEnumToTextureInternalFormat(GL_RGB565), TextureInternalFormat::RGB5); + EXPECT_EQ(MG_Util::ConvertGLEnumToTextureInternalFormat(GL_RGB5), TextureInternalFormat::RGB5); + // The ES-facing rendition of RGB5 is GL_RGB565 (desktop GL_RGB5 is not a legal sized + // internalformat on OpenGL ES backends). + EXPECT_EQ(MG_Util::ConvertTextureInternalFormatToGLEnum(TextureInternalFormat::RGB5), + static_cast(GL_RGB565)); +} + +// Regression guard: the DirectGLES backend must treat GL_TEXTURE_2D_ARRAY as a +// syncable target — it used to be skipped entirely, so 2D-array textures were never +// uploaded or bound (KHR-GL33.pixelstoragemodes.teximage3d.* failed wholesale). +TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) { + using MobileGL::MG_Backend::DirectGLES::TextureImpl::IsSupportedTextureTarget; + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D)); + // 1D and 1D-array are emulated as 2D / 2D-array (MapToBackendTextureTarget), matching + // SPIRV-Cross's ES 1D-as-2D shader emission; only rectangle textures stay unsupported. + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::TextureRectangle)); +} + +// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9); +// only true 3D textures halve depth per level. +TEST_F(TextureTest, TexStorage3DOn2DArrayKeepsLayerCountAcrossLevels) { + GLuint arrayTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &arrayTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, arrayTexture); + MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, 3, GL_RGBA8, 8, 8, 4); + + const auto arrayObject = MG_State::pGLContext->GetTextureObject(arrayTexture); + ASSERT_NE(arrayObject, nullptr); + auto* arrayMipmapObject = static_cast(arrayObject.get()); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(8, 8, 4)); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 1), IntVec3(4, 4, 4)); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 2), IntVec3(2, 2, 4)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Control: a real 3D texture still halves its depth per level. + GLuint volumeTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &volumeTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, volumeTexture); + MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_3D, 3, GL_RGBA8, 8, 8, 4); + + const auto volumeObject = MG_State::pGLContext->GetTextureObject(volumeTexture); + ASSERT_NE(volumeObject, nullptr); + auto* volumeMipmapObject = static_cast(volumeObject.get()); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 0), IntVec3(8, 8, 4)); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 1), IntVec3(4, 4, 2)); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 2), IntVec3(2, 2, 1)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, NamedTextureVectorParametersAndGettersWorkWithoutBinding) { GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); diff --git a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp index dc9736a8..8ae40a30 100644 --- a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp +++ b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" @@ -35,9 +37,31 @@ protected: return vbo; } - void SetUp() override { MobileGL::Initialize(); } + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so drain whatever an earlier test left pending - otherwise an error-code assertion + // here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite. + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } - void TearDown() override {} + // 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(); + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } }; TEST_F(VertexArrayTest, GenerateAndBindVAO) { @@ -57,6 +81,46 @@ TEST_F(VertexArrayTest, GenerateAndBindVAO) { // Do not detect if it supports default VAO } +// GL 3.3 core 2.10 name lifecycle - the same three rules the other object families assert: +// deleting an unknown name is silent, a released reservation is recycled, and binding a dead +// name is INVALID_OPERATION. +TEST_F(VertexArrayTest, DeleteOfUnknownOrAlreadyDeletedVertexArrayNameIsSilent) { + GLuint vao = 0; + MG_Impl::GLImpl::GenVertexArrays(1, &vao); + ASSERT_NE(vao, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteVertexArrays(1, &vao); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteVertexArrays(1, &vao); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteVertexArrays(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(VertexArrayTest, DeleteGeneratedButUnboundVertexArrayNameReleasesReservationAndBindFails) { + GLuint vao = 0; + MG_Impl::GLImpl::GenVertexArrays(1, &vao); + ASSERT_NE(vao, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateVertexArrayName(vao)); + + MG_Impl::GLImpl::DeleteVertexArrays(1, &vao); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateVertexArrayName(vao)); + + MG_Impl::GLImpl::BindVertexArray(vao); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenVertexArrays(1, &recycled); + EXPECT_EQ(recycled, vao); +} + TEST_F(VertexArrayTest, VertexAttributeSetup) { Vector vaoNames; MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames); @@ -264,7 +328,9 @@ TEST_F(VertexArrayTest, VertexBindingIndexIsBoundedByTheAdvertisedAttribLimit) { const GLuint outOfRange = MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs(); MG_Impl::GLImpl::VertexAttribBinding(0, outOfRange); - EXPECT_TRUE(MG_State::pGLContext->HasGLError()); + // Asserting the exact code (rather than just "some error") also consumes it, so the next test + // does not inherit it - GL error flags are sticky and this context is shared. + ExpectSingleGlError(GL_INVALID_VALUE); } // The default attribute -> binding-point mapping is the identity. It used to be a 16-element literal diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 5e6df140..9b9aa97c 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -799,6 +799,9 @@ namespace MobileGL::MG_Util::BackendLoader { if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) { caps.SupportsNorm16Texture = true; } + if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) { + caps.SupportsTextureFilterAnisotropy = true; + } if (std::strcmp(extension, "GL_EXT_base_instance") == 0) { caps.SupportsBaseInstance = true; } diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index ef0283b7..f07f4b00 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1031,6 +1031,9 @@ namespace MobileGL { String GLESShadingLanguageVersionString; Bool SupportsPersistentMapping = false; Bool SupportsNorm16Texture = false; + // GL_EXT_texture_filter_anisotropic is present, so sampler/texture + // anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES. + Bool SupportsTextureFilterAnisotropy = false; Bool SupportsBaseInstance = false; // GL_EXT_disjoint_timer_query is present in the extension string. Bool SupportsDisjointTimerQuery = false; diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 1b1dc8f5..6d3e1d68 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -131,6 +131,9 @@ namespace MobileGL { case GL_RGB4: return TextureInternalFormat::RGB4; case GL_RGB5: + // GL_RGB565 (GL 4.1 / ARB_ES2_compatibility, used directly by the GL CTS) is the + // ES-facing rendition of the legacy RGB5 resolution. + case GL_RGB565: return TextureInternalFormat::RGB5; case GL_RGB8: return TextureInternalFormat::RGB8; diff --git a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp index f97b37bc..bee9ac88 100644 --- a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp @@ -113,7 +113,10 @@ namespace MobileGL { case TextureInternalFormat::RGB4: return GL_RGB4; case TextureInternalFormat::RGB5: - return GL_RGB5; + // Emit the ES-compatible GL_RGB565 rendition: desktop GL_RGB5 is not a legal + // sized internalformat on OpenGL ES backends, GL_RGB565 is (and GL 4.1+ + // accepts it too via ARB_ES2_compatibility). + return GL_RGB565; case TextureInternalFormat::RGB8: return GL_RGB8; case TextureInternalFormat::RGB8Snorm: diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 70e3f62e..5466f24e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -19,6 +19,220 @@ namespace { return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; } + bool IsIdentifierStart(char ch) { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; + } + + MobileGL::String MaskCommentsAndQuotedText(const MobileGL::String& source) { + enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText }; + + MobileGL::String masked = source; + Region region = Region::Code; + char quote = '\0'; + bool escaped = false; + + for (SizeT pos = 0; pos < source.size(); pos++) { + const char ch = source[pos]; + const char next = pos + 1 < source.size() ? source[pos + 1] : '\0'; + + if (region == Region::Code) { + if (ch == '/' && next == '/') { + masked[pos] = ' '; + masked[pos + 1] = ' '; + pos++; + region = Region::SingleLineComment; + } else if (ch == '/' && next == '*') { + masked[pos] = ' '; + masked[pos + 1] = ' '; + pos++; + region = Region::MultiLineComment; + } else if (ch == '"' || ch == '\'') { + masked[pos] = ' '; + quote = ch; + escaped = false; + region = Region::QuotedText; + } + continue; + } + + if (region == Region::SingleLineComment) { + if (ch == '\n' || ch == '\r') { + region = Region::Code; + } else { + masked[pos] = ' '; + } + continue; + } + + if (region == Region::MultiLineComment) { + if (ch == '*' && next == '/') { + masked[pos] = ' '; + masked[pos + 1] = ' '; + pos++; + region = Region::Code; + } else if (ch != '\n' && ch != '\r') { + masked[pos] = ' '; + } + continue; + } + + if (ch != '\n' && ch != '\r') { + masked[pos] = ' '; + } + if (escaped) { + escaped = false; + } else if (ch == '\\') { + escaped = true; + } else if (ch == quote) { + region = Region::Code; + } + } + + return masked; + } + + void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { + while (pos < lineEnd && std::isspace(static_cast(source[pos]))) { + pos++; + } + } + + MobileGL::String ReadDirectiveIdentifier(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { + if (pos >= lineEnd || !IsIdentifierStart(source[pos])) { + return {}; + } + + const SizeT start = pos++; + while (pos < lineEnd && IsIdentifierChar(source[pos])) { + pos++; + } + return source.substr(start, pos - start); + } + + bool HasUtf8Bom(const MobileGL::String& source) { + return source.size() >= 3 && static_cast(source[0]) == 0xef && + static_cast(source[1]) == 0xbb && static_cast(source[2]) == 0xbf; + } + + struct ShaderLanguageInfo { + unsigned version = 110; + MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core; + SizeT versionDirectiveStart = MobileGL::String::npos; + SizeT versionDirectiveEnd = MobileGL::String::npos; + bool hasUtf8Bom = false; + bool enablesGpuShader5 = false; + + bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; } + }; + + ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) { + const MobileGL::String code = MaskCommentsAndQuotedText(source); + ShaderLanguageInfo info; + info.hasUtf8Bom = HasUtf8Bom(source); + + SizeT lineStart = 0; + while (lineStart < code.size()) { + SizeT lineEnd = code.find('\n', lineStart); + const bool hasLineBreak = lineEnd != MobileGL::String::npos; + if (!hasLineBreak) { + lineEnd = code.size(); + } + + SizeT probe = lineStart; + if (lineStart == 0 && info.hasUtf8Bom) { + probe = 3; + } + SkipDirectiveWhitespace(code, probe, lineEnd); + if (probe < lineEnd && code[probe] == '#') { + const SizeT directiveStart = probe; + probe++; + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd); + + if (directive == "version" && !info.HasVersionDirective()) { + SkipDirectiveWhitespace(code, probe, lineEnd); + unsigned version = 0; + bool hasVersionDigits = false; + while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') { + hasVersionDigits = true; + version = version * 10 + static_cast(code[probe] - '0'); + probe++; + } + if (hasVersionDigits) { + info.version = version; + info.versionDirectiveStart = directiveStart; + info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0); + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd); + if (profile == "es" || profile == "ES") { + info.profile = MobileGL::ShaderProfile::ES; + } else if (profile == "compatibility") { + info.profile = MobileGL::ShaderProfile::Compatibility; + } else { + info.profile = MobileGL::ShaderProfile::Core; + } + } + } else if (directive == "extension") { + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String extension = ReadDirectiveIdentifier(code, probe, lineEnd); + SkipDirectiveWhitespace(code, probe, lineEnd); + if (probe < lineEnd && code[probe] == ':') { + probe++; + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String behavior = ReadDirectiveIdentifier(code, probe, lineEnd); + const bool isGpuShader5 = extension == "GL_ARB_gpu_shader5" || + extension == "GL_NV_gpu_shader5"; + const bool enablesExtension = behavior == "enable" || behavior == "require" || + behavior == "warn"; + // Gate the whole source if it ever opts into either extension. This is deliberately + // conservative around conditional directives and keeps legal sample qualifiers intact. + info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension); + } + } + } + + lineStart = lineEnd + (hasLineBreak ? 1 : 0); + } + + return info; + } + + MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) { + if (info.profile == MobileGL::ShaderProfile::ES) { + // Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan + // glslang resource table cannot parse its ESSL built-ins today, even at ESSL 310, whereas the same + // source is accepted through the normalized desktop core path. + return "#version 460 core\n"; + } + + // Keep compatibility-profile handling on its pre-existing 460 path. Vulkan glslang does not accept that + // profile today, and this legacy-sample fix must not broaden or otherwise alter that separate limitation. + if (info.profile == MobileGL::ShaderProfile::Compatibility) { + return "#version 460 compatibility\n"; + } + + const bool useLegacyDesktopVersion = + info.version < 400 && !info.enablesGpuShader5; + return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n"; + } + + void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) { + const MobileGL::String replacement = GetNormalizedVersionDirective(info); + if (info.HasVersionDirective()) { + source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart, + replacement); + if (info.hasUtf8Bom) { + source.erase(0, 3); + } + return; + } + + if (info.hasUtf8Bom) { + source.erase(0, 3); + } + source.insert(0, replacement); + } + bool HasSingleLineFunctionDefinition(const MobileGL::String& source, const MobileGL::String& functionName) { SizeT lineStart = 0; while (lineStart < source.size()) { @@ -153,12 +367,8 @@ namespace { } SizeT FindAfterVersionDirective(const MobileGL::String& source) { - const SizeT versionPos = source.find("#version"); - if (versionPos == MobileGL::String::npos) { - return 0; - } - const SizeT lineEnd = source.find('\n', versionPos); - return lineEnd == MobileGL::String::npos ? source.size() : lineEnd + 1; + const ShaderLanguageInfo info = InspectShaderLanguage(source); + return info.HasVersionDirective() ? info.versionDirectiveEnd : 0; } bool IsExtensionAdvertised(MobileGL::GLExtension extension) { @@ -322,7 +532,7 @@ namespace { void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) { // Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and - // ignored in the forced "#version 460 core" profile, so glslang handles them natively. + // ignored in the normalized desktop core profiles, so glslang handles them natively. ReplaceIdentifier(source, "texture2D", "texture"); ReplaceIdentifier(source, "texture2DProj", "textureProj"); @@ -367,6 +577,11 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { void PreprocessShaderSource(ShaderStage stage, String& source) { + // Normalize while the inspector's source span still refers to the untouched input. Later passes + // remove comments and directives, so any subsequent insertion re-inspects the current source. + const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source); + NormalizeVersionDirective(source, originalLanguage); + // remove multi-line comment size_t commentStartPos = source.find("/*"); while (commentStartPos != String::npos) { @@ -404,43 +619,6 @@ namespace MobileGL { noperspectivePos = source.find(str_np); } - // force #version - ShaderProfile profile = ShaderProfile::Core; - SizeT versionPos = source.find("#version"); - SizeT lineEnd = source.find('\n', versionPos); - - if (versionPos != String::npos) { - String versionLine = source.substr(versionPos, lineEnd - versionPos); - - if (versionLine.find("ES") != String::npos) - profile = ShaderProfile::ES; - else if (versionLine.find("compatibility") != String::npos) - profile = ShaderProfile::Compatibility; - else - profile = ShaderProfile::Core; - } else { - profile = ShaderProfile::Core; - source.insert(0, "#version 460 core\n"); - versionPos = 0; - lineEnd = source.find('\n', versionPos); - } - - SizeT firstLineEnd = lineEnd; - - if (profile != ShaderProfile::ES) { - constexpr const char* versionDirectiveCore = "#version 460 core\n"; - constexpr const char* versionDirectiveCompat = "#version 460 compatibility\n"; - - const char* replacement = - (profile == ShaderProfile::Compatibility) ? versionDirectiveCompat : versionDirectiveCore; - - if (firstLineEnd != String::npos) { - source.replace(versionPos, firstLineEnd - versionPos + 1, replacement); - } else { - source = replacement; - } - } - FilterUnsupportedGpuShaderInt64(source); CoerceUniformBlockPackingToStd140(source); diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index 2b4e6857..107dc0d4 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -310,10 +310,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { // Color sized other case GL_RGB9_E5: case GL_R11F_G11F_B10F: + case GL_RGB565: *outFormat = GL_RGB; break; case GL_RGB10_A2: case GL_RGB5_A1: + case GL_RGBA4: *outFormat = GL_RGBA; break; case GL_RGB10_A2UI: @@ -324,13 +326,11 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { case GL_R3_G3_B2: case GL_RGB4: case GL_RGB5: - case GL_RGB565: case GL_RGB10: case GL_RGB12: *outFormat = GL_RGB; break; case GL_RGBA2: - case GL_RGBA4: case GL_RGBA12: *outFormat = GL_RGBA; break; @@ -544,7 +544,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { case GL_SRGB_ALPHA: *outType = GL_UNSIGNED_BYTE; break; - // Depth case GL_DEPTH_COMPONENT16: *outType = GL_UNSIGNED_SHORT;