mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Merge origin/dev (efd7b473) into readback overhaul - unify DirectGLES 2D-array target support under MapToBackendTextureTarget, keep canonical UNorm8 shadows for RGBA4/RGB565 (supersedes packed-word transfer types; GL_RGB565 aliases RGB5), keep upstream GLSL 330 normalization, anisotropy params, error-count semantics and VK clear/scissor fixes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -197,16 +197,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
|
||||
}
|
||||
|
||||
SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
|
||||
VkBufferResource* VkBufferManager::GetOrCreateResource(
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
||||
auto existing = std::static_pointer_cast<VkBufferResource>(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<VkBufferResource*>(existing.get());
|
||||
}
|
||||
auto resource = MakeShared<VkBufferResource>();
|
||||
VkBufferResource* raw = resource.get();
|
||||
bufferObject->SetBackendResource(resource);
|
||||
TrackLiveResource(resource);
|
||||
return resource;
|
||||
return raw;
|
||||
}
|
||||
|
||||
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
private:
|
||||
Bool InitializeTransientArenas();
|
||||
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
|
||||
SharedPtr<VkBufferResource> GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject);
|
||||
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
|
||||
VkMemoryPropertyFlags requiredFlags = 0);
|
||||
|
||||
@@ -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<Int>(framebufferLayers) };
|
||||
framebufferLayers };
|
||||
MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d",
|
||||
static_cast<unsigned long long>(hash),
|
||||
static_cast<unsigned long long>(compatibilityHash),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
@@ -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<unsigned long long>(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<Uint32>(renderPassEntry->extent.x()) &&
|
||||
clearRect.rect.extent.height == static_cast<Uint32>(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<VkClearAttachment> 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<Uint32>(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<GLint>(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<GLint>(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<Uint>(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<Uint32>(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<Uint32>(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<Uint32>(stencil);
|
||||
QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload);
|
||||
}
|
||||
@@ -6863,7 +7003,8 @@ void main() {
|
||||
static_cast<Uint32>(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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -441,7 +441,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,7 +1364,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(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<SizeT>(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();
|
||||
|
||||
@@ -649,7 +649,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void ClearDepth_State(GLclampd depth) {
|
||||
MG_State::pGLContext->SetClearDepth(static_cast<Float>(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<Float>(depth)));
|
||||
}
|
||||
|
||||
void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
|
||||
|
||||
@@ -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<Float>(*(const GLuint*)param);
|
||||
return static_cast<Float>(*(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<GenericErrorInfo>("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<GLint>(*(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<GLuint>(samplerObj->GetMaxAnisotropy());
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(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<GenericErrorInfo>("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) {
|
||||
|
||||
@@ -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<GenericErrorInfo>("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<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerIntParam",
|
||||
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return ValidateSamplerParam(pname, static_cast<GLenum>(param));
|
||||
}
|
||||
|
||||
@@ -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<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& 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<Int>(
|
||||
baseTexelSize.x(),
|
||||
std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
|
||||
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1));
|
||||
Uint mipLevelCount = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(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<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
|
||||
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1)
|
||||
: std::max<Int>(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<SizeT>(levelTexelSize.x()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
@@ -453,6 +471,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& 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<GenericErrorInfo>("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<GLfloat>(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<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
||||
GLfloat param, const char* caller) {
|
||||
if (!textureObject) return;
|
||||
if (!ValidateTextureParameterForTarget(textureObject, pname, static_cast<GLint>(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<GLint>(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<GLint>(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<GLint>(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<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexParameterfv_State",
|
||||
@@ -2368,7 +2418,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(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<MG_State::GLState::TextureObjectMipmap*>(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<GLsizei>(1, width >> level);
|
||||
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
||||
const GLsizei levelDepth = std::max<GLsizei>(1, depth >> level);
|
||||
const GLsizei levelDepth = depthMips ? std::max<GLsizei>(1, depth >> level) : depth;
|
||||
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight,
|
||||
levelDepth);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
@@ -28,6 +29,7 @@ namespace {
|
||||
GLenum errorRaisedByDraw = GL_NO_ERROR;
|
||||
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
std::vector<std::string> extensions;
|
||||
|
||||
GLuint nextBufferId = 1;
|
||||
GLuint nextShaderId = 1;
|
||||
@@ -86,7 +88,7 @@ namespace {
|
||||
*data = 1;
|
||||
break;
|
||||
case GL_NUM_EXTENSIONS:
|
||||
*data = 0;
|
||||
*data = static_cast<GLint>(g_fake.extensions.size());
|
||||
break;
|
||||
default:
|
||||
// Leave the caller's defaults for every other capability query.
|
||||
@@ -114,9 +116,10 @@ namespace {
|
||||
return reinterpret_cast<const GLubyte*>("");
|
||||
}
|
||||
};
|
||||
// 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<const GLubyte*>(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);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <Config.h>
|
||||
@@ -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<GLuint>::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<GLuint>::max());
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, AcquireMemory) {
|
||||
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||
Vector<Uint> bufferNames;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
@@ -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<GLuint>::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<GLuint>::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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,16 +8,21 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
@@ -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<Uint16>(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<Uint16>(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<Uint16>(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<Uint16>(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<GLuint>::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<Uint8>(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<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(2, 2, 2));
|
||||
|
||||
const auto* stored =
|
||||
static_cast<const Uint8*>(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<Uint8>(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<GLenum>(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<MG_State::GLState::TextureObjectMipmap*>(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<MG_State::GLState::TextureObjectMipmap*>(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);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#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<GLuint>::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<Uint> 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<unsigned char>(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<unsigned char>(source[0]) == 0xef &&
|
||||
static_cast<unsigned char>(source[1]) == 0xbb && static_cast<unsigned char>(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<unsigned>(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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user