Compare commits

...
12 Commits
Author SHA1 Message Date
swung0x48 602da1d131 [Merge] (GLImpl, DirectGLES, DirectVulkan): complete direct state access and the compressed-texture family 2026-08-22 22:47:08 -04:00
swung0x48 06bbaf32b1 [Merge] (GLImpl, DirectGLES, DirectVulkan): take the extension advertisements under the DSA completion 2026-08-22 22:42:47 -04:00
swung0x48 9bcf0a15a0 [Docs] (DirectGLES, DirectVulkan): correct what advertising cube map arrays actually unlocks 2026-08-22 22:03:26 -04:00
swung0x48 26e5a946ac [Test] (GLImpl): pin the DSA name rule on the compressed 3D by-name entry point 2026-08-22 21:56:51 -04:00
swung0x48 dc543fa905 [Feat, Test] (DirectGLES, DirectVulkan, SelfTest): advertise the implemented extensions that were never named 2026-08-22 21:54:17 -04:00
swung0x48 f559d68728 [Feat, Test] (GLImpl): store and patch compressed 3D texture images, and wire the 1D/3D named entry points 2026-08-22 21:49:33 -04:00
swung0x48 48968a663f [Fix, Test] (GLImpl, Util): let a buffer clear take a GL_INT pattern into a normalized format 2026-08-22 21:49:26 -04:00
swung0x48 0fdcb5d6c4 [Feat] (DirectGLES, DirectVulkan): advertise GL_ARB_sync and GL_ARB_shader_atomic_counters 2026-08-22 21:45:13 -04:00
swung0x48 6b25e7a7e3 [Fix] (GLState): report the storage flags glBufferData implies 2026-08-22 21:45:13 -04:00
swung0x48 50d260c840 [Fix] (GLImpl): reject memory-barrier bits the spec does not define 2026-08-22 21:45:13 -04:00
swung0x48 6a8bf4c03c [Fix, Test] (DirectVulkan): resolve a lowered atomic-counter block from the atomic-counter binding points 2026-08-22 21:45:13 -04:00
swung0x48 01098e9dd7 [Fix] (GLImpl): raise the errors ARB_sync specifies for ClientWaitSync, WaitSync and GetSynciv 2026-08-22 21:45:12 -04:00
19 changed files with 858 additions and 57 deletions
@@ -753,7 +753,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no runtime capabilities yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -778,7 +778,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
capabilities.SupportsTextureView);
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
}
} // namespace
@@ -992,7 +992,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported) {
Bool textureViewSupported, Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = {
// The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an
@@ -1030,6 +1030,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
// NotSupported on a feature that works.
E_GL_ARB_draw_elements_base_vertex,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
// buffer the application bound. DirectVulkan reaches the same place through its own
// descriptor resolution, so the string is symmetric.
E_GL_ARB_shader_atomic_counters,
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
// (Better Clouds' GLCompat among them) accept the extension string as an
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
@@ -1039,6 +1055,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
// object-label table are MobileGL's own state, not the host driver's - so it is as
// available here as it is on DirectVulkan, which has advertised it all along.
E_GL_KHR_debug,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
// driver's identical parameters.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
E_GL_ARB_viewport_array,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
@@ -1087,6 +1143,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
// reports. So the string follows the host capability rather than the version.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
// over the SAME storage, so that writes through either are visible through the other and
@@ -83,7 +83,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported);
Bool textureViewSupported, Bool cubeMapArraySupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
@@ -504,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no runtime-gated capabilities); a live
// backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
@@ -512,7 +512,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported) {
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = {
// The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an
@@ -550,11 +551,67 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
// NotSupported on a feature that works.
E_GL_ARB_draw_elements_base_vertex,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
// now reaches the shader on BOTH backends - Magma resolves the lowered
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
// landed, because the counter silently read whatever was bound as SSBO N instead.
E_GL_ARB_shader_atomic_counters,
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
// (Better Clouds' GLCompat among them) accept the extension string as an
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
// available, so withholding it makes MobileGL look less capable than it is.
E_GL_ARB_instanced_arrays,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it. Kept identical to the DirectGLES block so the two
// backends do not disagree about what MobileGL is.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports.
E_GL_ARB_viewport_array,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
@@ -607,6 +664,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
// view type cannot be created without the imageCubeArray device feature - so the string
// follows the feature, not the version, exactly as the per-layer attachment bit does.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
return extensions;
}
@@ -737,7 +806,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
m_vulkanCaps.SupportsImageCubeArray);
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -75,7 +75,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported);
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -17,6 +17,7 @@
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
#include <algorithm>
#include <cstdio>
@@ -923,22 +924,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
binding);
// An atomic counter is not an SSBO the application ever declared: glslang lowers every
// atomic_uint onto a synthesized gl_AtomicCounterBlock_<N> storage block, where N is the
// GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary
// storage-block slot, so resolving it the SSBO way looked up GL_SHADER_STORAGE_BUFFER
// point N' - which is never where glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, N, ...) put
// the buffer. The counter therefore never reached the shader (KHR-GL43
// shader_atomic_counters.advanced-usage-*), and when the application also bound an SSBO at
// the colliding slot the descriptor silently aliased it, so the dispatch wrote over the
// application's own buffer. DirectGLES has always taken this branch explicitly
// (SyncAtomicCounterBuffers); this is the same rule in Magma's descriptor resolution.
//
// Only the SOURCE of the handle differs. The per-counter layout(offset=) is already folded
// into the block's SPIR-V member offsets on this path (FlattenAtomicCounterBlockPass is
// DirectGLES-only), so everything below - residency, the glBindBufferRange window, the
// descriptor fill - is target-agnostic and stays exactly as it was.
const String& blockName = programObj.storageBlockNameByBinding[binding];
const Int atomicCounterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(blockName);
const Bool isAtomicCounterBlock = atomicCounterBinding >= 0;
const BufferTarget bufferTarget =
isAtomicCounterBlock ? BufferTarget::AtomicCounter : BufferTarget::ShaderStorage;
// A block instance array declares one block whose elements take consecutive GL binding
// points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole
// array to that one block - so the element index IS the offset from its binding.
// array to that one block - so the element index IS the offset from its binding. glslang
// synthesizes one counter block per GL binding, so a counter block is never an instance
// array and `element` is always 0 there; the +element rule stays with the SSBO case.
const GLuint frontendBinding =
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
isAtomicCounterBlock
? static_cast<GLuint>(atomicCounterBinding)
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
const Uint32 bindingPointCount =
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
"ResolveStorageBufferDescriptor: frontend SSBO binding %u out of range for block '%s'",
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
frontendBinding, blockName.c_str());
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no buffer bound at frontend binding %u for block '%s'",
frontendBinding, blockName.c_str());
return false;
}
@@ -713,7 +713,34 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
namespace {
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
// GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole.
// Forwarding an undefined bit to the host driver let a caller that had computed its mask
// wrongly (or reused an ES-only bit) get silence instead of the error the spec promises.
constexpr GLbitfield kAllDefinedBarrierBits =
GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT |
GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT |
GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT;
Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) {
if (barriers == GL_ALL_BARRIER_BITS) return true;
if ((barriers & ~kAllDefinedBarrierBits) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function,
"barriers contains bits that are not defined barrier bits."));
return false;
}
return true;
}
} // namespace
void MemoryBarrier(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
MG_State::pGLContext->RecordError(
@@ -725,6 +752,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void MemoryBarrierByRegion(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
if (!memoryBarrierByRegion) {
MG_State::pGLContext->RecordError(
@@ -1047,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
@@ -1835,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits)
+43 -1
View File
@@ -69,8 +69,25 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and
// any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller
// that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a
// flush it never got.
if ((flags & ~static_cast<GLbitfield>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT."));
return GL_WAIT_FAILED;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
// The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning
// the enum alone left glGetError() clean and the failure indistinguishable from a
// genuine wait failure on a live sync.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return GL_WAIT_FAILED;
}
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
@@ -95,6 +112,9 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return;
}
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
@@ -125,8 +145,22 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
// GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE
// and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean
// and write a plausible-looking zero, which is the one failure mode a caller cannot tell
// apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118),
// but a mistyped pname answered a bare 0 that no query ever returns.
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must not be negative."));
return;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
if (length) {
*length = 0;
}
@@ -152,7 +186,15 @@ namespace MobileGL::MG_Impl::GLImpl {
value = static_cast<GLint>(syncObject->flags);
break;
default:
break;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or "
"GL_SYNC_FLAGS."));
if (length) {
*length = 0;
}
return;
}
if (length) {
+251 -5
View File
@@ -4099,11 +4099,171 @@ namespace MobileGL::MG_Impl::GLImpl {
"1D textures are not supported by this implementation"));
}
// The three-dimensional twin of CompressedTexSubImage2D_State: a block-aligned box of the
// compressed image the level shadows is replaced, slice by slice. Same deviation as the 2D form
// - the uncompressed texel shadow beside it is NOT touched, so what changes is the image
// glGetCompressedTexImage hands back, not what the level samples as.
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data) {
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
// ======================= Converting ================================
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// Zero block width doubles as "format is not a specific compressed format", the
// INVALID_ENUM case - one lookup answers both questions.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
// A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
// accepts one.
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"A proxy target has no texture image to modify."));
return;
}
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
if (width < 0 || height < 0 || depth < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"width, height and depth must be non-negative."));
return;
}
if (compressedInfo.blockWidth == 0) {
RecordUnsupportedCompressedFormat(__func__);
return;
}
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (textureMipmapObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
return;
}
// GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
// exactly this compressed format. That is also what makes the block arithmetic below
// sound - the level's grid is measured with THIS format's block size.
const GLenum levelFormat =
textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
if (levelFormat != format) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"format does not match the internal format of the texture image."));
return;
}
const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
const Int levelDepth = std::max(levelSize.z(), 1);
// Subtractions rather than sums for the reason CompressedTexSubImage2D_State spells out:
// offset + extent are both application-supplied GLints and a signed overflow is undefined.
if (xoffset < 0 || yoffset < 0 || zoffset < 0 || width > levelSize.x() - xoffset ||
height > levelSize.y() - yoffset || depth > levelDepth - zoffset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The replaced region does not lie within the texture image."));
return;
}
// GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
// and must either be a whole number of blocks wide/high or run to the image's edge. Every
// format that reaches here is 4x4x1, so the depth axis carries no block alignment rule -
// each slice is its own block grid.
const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
const Bool alignedX = (xoffset % blockWidth == 0) &&
(width % blockWidth == 0 || xoffset + width == levelSize.x());
const Bool alignedY = (yoffset % blockHeight == 0) &&
(height % blockHeight == 0 || yoffset + height == levelSize.y());
if (!alignedX || !alignedY) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The replaced region is not aligned to the format's compressed blocks."));
return;
}
// Exactly the size the format and dimensions imply, which is also what keeps the copy
// below in bounds.
const SizeT expectedImageSize =
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth});
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"imageSize does not match the compressed image size."));
return;
}
// ======================= Processing ================================
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
const void* compressedBytes = CompressedUnpackSource(data);
if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
if (compressedBytes == nullptr) {
// No unpack buffer and a null client pointer: there is nothing to read. GL leaves
// this undefined rather than erroring, and dereferencing it is the one answer that
// is never acceptable.
MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
return;
}
static std::atomic<Bool> announcedNoCodec3D{false};
if (!announcedNoCodec3D.exchange(true)) {
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
"reach the texels this level SAMPLES as. Upload through glTexSubImage3D for "
"that.",
__func__);
}
const SizeT blobSize =
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
const void* existing =
textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
if (blobSize == 0 || existing == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The texture level holds no compressed image to modify."));
return;
}
Vector<Uint8> blob(blobSize);
Memcpy(blob.data(), existing, blobSize);
const SizeT blockByteSize = compressedInfo.blockByteSize;
const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
compressedInfo.blockWidth;
const SizeT levelBlocksY = (static_cast<SizeT>(levelSize.y()) + compressedInfo.blockHeight - 1) /
compressedInfo.blockHeight;
const SizeT levelRowBytes = levelBlocksX * blockByteSize;
const SizeT levelSliceBytes = levelRowBytes * levelBlocksY;
const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
compressedInfo.blockWidth;
const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
compressedInfo.blockHeight;
const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
const SizeT regionRowBytes = regionBlocksX * blockByteSize;
const SizeT regionSliceBytes = regionRowBytes * regionBlocksY;
const auto* source = static_cast<const Uint8*>(compressedBytes);
for (SizeT slice = 0; slice < static_cast<SizeT>(depth); ++slice) {
const SizeT destSliceBase = (static_cast<SizeT>(zoffset) + slice) * levelSliceBytes;
for (SizeT row = 0; row < regionBlocksY; ++row) {
const SizeT destOffset =
destSliceBase + (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
Memcpy(blob.data() + destOffset, source + slice * regionSliceBytes + row * regionRowBytes,
regionRowBytes);
}
}
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
blob.data(), blobSize);
}
// Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a
@@ -4278,15 +4438,83 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordUnsupportedCompressedFormat(__func__);
}
// The three-dimensional twin of CompressedTexImage2D_State, and the same deviation applies: the
// blocks are shadowed verbatim for glGetCompressedTexImage while the texels this level SAMPLES
// as stay zero, because there is no BC/ETC decoder here. A 3D compressed image is a stack of
// `depth` two-dimensional block grids - every format that reaches here has a 4x4x1 block - so
// the blob layout is slice-major and CalculateCompressedTextureImageSize already multiplies by
// depth.
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth, GLint border, GLsizei imageSize, const void* data) {
// ======================= Converting ================================
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
// Zero block width doubles as "internalformat is not a specific compressed format", which is
// the INVALID_ENUM case - one lookup answers both questions.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
if (compressedInfo.blockWidth == 0) {
RecordUnsupportedCompressedFormat(__func__);
return;
}
// GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply,
// otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds.
const SizeT expectedImageSize =
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth});
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"imageSize does not match the compressed image size."));
return;
}
// Object resolution copied from TexImage3D_State rather than routed through
// GetTextureObjectByTarget, for the reason CompressedTexImage2D_State gives: a proxy target
// is legal here and only CreateOrReplaceProxyTextureObject gives it an object to answer the
// level queries from.
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
// ======================= Processing ================================
const TextureInternalFormat textureInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
textureObject->SetInternalFormat(textureInternalFormat);
// A proxy records the format and nothing else - it must never take storage, and it must never
// be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being
// INVALID_OPERATION.
if (isProxy) return;
const SizeT internalBpp =
MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte);
const SizeT internalBytes =
static_cast<SizeT>(width) * static_cast<SizeT>(height) * static_cast<SizeT>(depth) * internalBpp;
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
// AllocateStorage clears any compressed image the level used to hold, so this must run before
// SetMipmapCompressedImage re-arms it.
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
const void* compressedBytes = CompressedUnpackSource(data);
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
expectedImageSize);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
}
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
@@ -5521,6 +5749,14 @@ namespace MobileGL::MG_Impl::GLImpl {
free(processedPixels);
}
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CompressedTexSubImage1D_State(target, level, xoffset, width, format, imageSize, data);
});
}
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -5529,6 +5765,16 @@ namespace MobileGL::MG_Impl::GLImpl {
});
}
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CompressedTexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format,
imageSize, data);
});
}
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -37,8 +37,13 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum format, GLenum type, const void* pixels);
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data);
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data);
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
@@ -87,11 +87,6 @@ void main() {
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
<< "; this needs 3 and 2";
}
if (!AtomicCountersAreWired()) {
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
<< "is still resolved from the shader-storage binding points";
}
m_program = CompileComputeProgram(kCounterComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
}
@@ -105,12 +100,6 @@ void main() {
m_program = 0;
}
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
// frontend half (limits, reflection queries, the link-time offset rules) is
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
@@ -118,7 +118,13 @@ namespace MobileGL::MG_State::GLState {
// record that so backends skip uploading the stale shadow bytes.
m_hasDefinedContent = (data != nullptr) || size == 0;
m_isImmutableStorage = false;
m_storageFlags = 0;
// GL 4.6 core 6.2 defines glBufferData as glBufferStorage with
// DYNAMIC_STORAGE_BIT | MAP_READ_BIT | MAP_WRITE_BIT, so GL_BUFFER_STORAGE_FLAGS has to
// report those three afterwards. Reporting 0 - the value that belongs to a buffer whose
// store has never been specified - told an application that a perfectly writable
// glBufferData buffer accepted neither glBufferSubData nor a map. Only the IMMUTABLE flag
// distinguishes the two cases, and it is cleared just above.
m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
NotifyRespecify();
}
@@ -1087,22 +1087,76 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false);
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false);
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false);
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false, false);
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false, false);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
}
// Cube map arrays are core at the version MobileGL claims, but there is nothing underneath on a
// pre-ES-3.2 driver without EXT/OES_texture_cube_map_array, and no VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
// without the imageCubeArray feature. The string has to follow the capability on both backends -
// and it has to BE there when the capability is, because KHR-GL4*.texture_gather.*-cube-array
// gates on the string with no core-version fallback.
TEST(CubeMapArrayAdvertisement, FollowsTheHostCapabilityOnBothBackends) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto esWithout =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false);
EXPECT_FALSE(contains(esWithout, MobileGL::E_GL_ARB_texture_cube_map_array));
const auto esWith =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, true);
EXPECT_TRUE(contains(esWith, MobileGL::E_GL_ARB_texture_cube_map_array));
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false,
false);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_ARB_texture_cube_map_array));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, true);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_cube_map_array));
}
// The core-plumbing strings carry no capability gate: they name entry points that have been real
// on both backends for as long as the backends have existed, and an application that gates its
// entry-point resolution on the string (LWJGL does) would otherwise call through null. Pinned
// together so a future edit cannot quietly drop one, and pinned on BOTH backends so the two
// cannot disagree about what MobileGL is.
TEST(CorePlumbingAdvertisement, IsUnconditionalAndIdenticalOnBothBackends) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const MobileGL::GLExtension expected[] = {
MobileGL::E_GL_ARB_sync, MobileGL::E_GL_ARB_shader_atomic_counters,
MobileGL::E_GL_ARB_vertex_array_object, MobileGL::E_GL_ARB_sampler_objects,
MobileGL::E_GL_ARB_map_buffer_range, MobileGL::E_GL_ARB_copy_buffer,
MobileGL::E_GL_ARB_copy_image, MobileGL::E_GL_ARB_texture_swizzle,
MobileGL::E_GL_ARB_vertex_type_2_10_10_10_rev, MobileGL::E_GL_ARB_texture_rg,
MobileGL::E_GL_ARB_depth_buffer_float, MobileGL::E_GL_ARB_texture_float,
MobileGL::E_GL_ARB_viewport_array};
// Every gate off: none of these may depend on one.
const auto es = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false,
false);
const auto vk = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false);
for (const auto extension : expected) {
EXPECT_TRUE(contains(es, extension)) << "DirectGLES stopped advertising extension " << extension;
EXPECT_TRUE(contains(vk, extension)) << "DirectVulkan stopped advertising extension " << extension;
}
}
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
@@ -1113,27 +1167,27 @@ TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
};
const auto esWithoutIndirect =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false);
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false);
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
const auto esWithoutBaseInstance =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false);
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false, false);
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
const auto esWithBoth =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false);
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false, false);
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
const auto vkWithoutBaseInstance =
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false);
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
const auto vkWithBoth =
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true, false);
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
}
+56
View File
@@ -711,6 +711,62 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) {
EXPECT_EQ(actual, (Vector<Uint32>{0, pattern, pattern, pattern, 0}));
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core table 8.2 pairs GL_INT with the non-integer base formats as a signed-normalized
// source, so a GL_R8 clear whose pattern arrives as (GL_RED, GL_INT) is legal. The pair used to be
// rejected with INVALID_VALUE, which is the first call
// KHR-GL45.direct_state_access.buffers_functional makes.
TEST_F(BufferTest, ClearNamedBufferSubDataAcceptsSignedNormalizedIntPattern) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::CreateBuffers(1, &buffer);
const Vector<Uint8> initial(24, 0x7F);
MobileGL::MG_Impl::GLImpl::NamedBufferStorage(
buffer, initial.size(), initial.data(),
GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT | GL_MAP_PERSISTENT_BIT);
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const GLint zero = 0;
MobileGL::MG_Impl::GLImpl::ClearNamedBufferSubData(buffer, GL_R8, 0, sizeof(GLint), GL_RED, GL_INT, &zero);
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Vector<Uint8> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
Vector<Uint8> expected(initial);
for (SizeT i = 0; i < sizeof(GLint); ++i) expected[i] = 0;
EXPECT_EQ(actual, expected);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
// The same pair on the bound-target entry point: the DSA and the bound call share
// ClearBufferRange_State, and a regression in either direction has to show up here too.
TEST_F(BufferTest, ClearBufferSubDataAcceptsSignedNormalizedIntPattern) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
const Vector<Uint8> initial(8, 0x7F);
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
// GL_INT is signed-normalized against 2^31-1, so the maximum maps to a saturated GL_R8 texel.
const GLint one = 2147483647;
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(GL_ARRAY_BUFFER, GL_R8, 0, 4, GL_RED, GL_INT, &one);
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Vector<Uint8> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
EXPECT_EQ(actual, (Vector<Uint8>{0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x7F, 0x7F, 0x7F}));
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
TEST_F(BufferTest, ClearBufferSubDataInitializesIrisStaticSsboRange) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
@@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
{
const AsyncModeScope async(true);
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false),
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false),
E_GL_KHR_parallel_shader_compile));
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false),
E_GL_KHR_parallel_shader_compile));
}
{
const AsyncModeScope async(false);
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false),
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
}
+169
View File
@@ -2227,6 +2227,175 @@ TEST_F(TextureTest, CompressedTextureSubImage2DModifiesTheNamedTextureOnly) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
namespace {
// 8x8x8 RGTC1: 2x2 blocks of 8 bytes per slice, so a slice is 32 bytes and the stack is 256.
constexpr GLsizei kRgtc1Size8x8x8 = 256;
constexpr GLsizei kRgtc1Slice8x8 = 32;
GLuint MakeCompressedRgtc1Texture3D() {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture);
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Size8x8x8,
nullptr);
return texture;
}
} // namespace
// glCompressedTexImage3D used to answer GL_INVALID_ENUM to every call, which is what threw
// KHR-GL45.direct_state_access.textures_compressed_subimage out with an InternalError: the CTS
// asserts no error on it. A 3D compressed image is a stack of per-slice block grids, and the whole
// stack has to come back byte for byte.
TEST_F(TextureTest, CompressedTexImage3DShadowsTheWholeStackForReadback) {
Uint8 whole[kRgtc1Size8x8x8];
for (Int i = 0; i < kRgtc1Size8x8x8; ++i) whole[i] = static_cast<Uint8>(i);
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture);
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Size8x8x8,
whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, whole, sizeof(whole)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// An imageSize that is not the one the format and the three dimensions imply - the depth axis
// is the term a 2D-shaped size calculation would drop.
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Slice8x8,
whole);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// Where the incoming blocks land. The box below is one block wide, one block high and two slices
// deep, starting at block (1,1) of slice 3: an implementation that dropped the slice stride, the
// block-row term or the block-column term puts them somewhere else, and a full-image write would
// hide all three.
TEST_F(TextureTest, CompressedTexSubImage3DPlacesBlocksSliceBySlice) {
const GLuint texture = MakeCompressedRgtc1Texture3D();
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 zeros[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, zeros);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const Uint8 box[16] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7};
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 4, 4, 3, 4, 4, 2, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(box)), box);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kRgtc1Size8x8x8] = {};
// slice 3, block row 1, block column 1 -> 3*32 + 1*16 + 1*8, and the same place one slice on.
std::memcpy(expected + 3 * kRgtc1Slice8x8 + 16 + 8, box, 8);
std::memcpy(expected + 4 * kRgtc1Slice8x8 + 16 + 8, box + 8, 8);
Uint8 stored[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// glCompressedTextureSubImage3D was an exported no-op that raised no error at all. It must reach the
// NAMED texture and leave the binding it borrowed exactly as it found it.
TEST_F(TextureTest, CompressedTextureSubImage3DModifiesTheNamedTextureOnly) {
const GLuint bound = MakeCompressedRgtc1Texture3D();
Uint8 boundImage[kRgtc1Size8x8x8];
std::memset(boundImage, 0x11, sizeof(boundImage));
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, boundImage);
const GLuint named = MakeCompressedRgtc1Texture3D();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, bound); // `named` is NOT the bound texture
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 namedImage[kRgtc1Size8x8x8];
std::memset(namedImage, 0x22, sizeof(namedImage));
MG_Impl::GLImpl::CompressedTextureSubImage3D(named, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, namedImage);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, boundImage, sizeof(stored)), 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, named);
std::memset(stored, 0, sizeof(stored));
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored);
EXPECT_EQ(std::memcmp(stored, namedImage, sizeof(stored)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, CompressedTexSubImage3DRejectsTheRegionsGLForbids) {
const GLuint texture = MakeCompressedRgtc1Texture3D();
Uint8 blocks[kRgtc1Size8x8x8] = {};
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A format that is not the one the image is stored in.
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RG_RGTC2, 512, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A start that is not on a block boundary.
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 2, 0, 0, 4, 8, 8, GL_COMPRESSED_RED_RGTC1, 128, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A box that runs past the last slice - the depth bound a 2D-shaped range check never applies.
MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 6, 8, 8, 4, GL_COMPRESSED_RED_RGTC1, 128, blocks);
ExpectSingleGlError(GL_INVALID_VALUE);
(void)texture;
}
// The DSA name rule the CTS's textures_creation pair does not reach for these two entry points: a
// name handed out by glGenTextures has no object until it is first bound, so a by-name call on it is
// INVALID_OPERATION - and, unlike the stub these replaced, it has to SAY so rather than return
// quietly. A glCreateTextures name is a created object and gets past the name check.
TEST_F(TextureTest, CompressedTextureSubImage3DRejectsAGeneratedButNeverBoundName) {
GLuint generated = 0;
MG_Impl::GLImpl::GenTextures(1, &generated);
ASSERT_NE(generated, 0u);
DrainPendingGlErrors();
Uint8 blocks[kRgtc1Size8x8x8] = {};
MG_Impl::GLImpl::CompressedTextureSubImage3D(generated, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A created name is past the name check, so whatever it answers is about the IMAGE (this one
// holds none yet), never about the name.
GLuint created = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &created);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CompressedTextureSubImage3D(created, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1,
kRgtc1Size8x8x8, blocks);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION)
<< "a created 3D texture with no compressed image is an image error, not a name error";
DrainPendingGlErrors();
}
// Core GL defines no compressed format for a 1D target, so both the bound and the by-name entry
// point have to REFUSE the call. The by-name one used to be an exported no-op that raised nothing,
// which is the one answer an application cannot act on.
TEST_F(TextureTest, CompressedTextureSubImage1DRefusesLikeTheBoundCall) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, texture);
MG_Impl::GLImpl::TexImage1D(GL_TEXTURE_1D, 0, GL_R8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
Uint8 blocks[16] = {};
MG_Impl::GLImpl::CompressedTexSubImage1D(GL_TEXTURE_1D, 0, 0, 8, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(blocks)), blocks);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::CompressedTextureSubImage1D(texture, 0, 0, 8, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(blocks)), blocks);
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(TextureTest, CompressedTexSubImage2DRejectsTheRegionsGLForbids) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
Uint8 blocks[kRgtc1Size8x8] = {};
+5 -2
View File
@@ -1269,7 +1269,7 @@ namespace MobileGL::MG_Util::SelfTest {
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
summary.caps.SupportsDrawIndirect,
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance,
summary.caps.SupportsTextureView));
summary.caps.SupportsTextureView, summary.caps.SupportsTextureCubeMapArray));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -2009,6 +2009,7 @@ namespace MobileGL::MG_Util::SelfTest {
Bool samplerAnisotropySupported = false;
Bool drawIndirectFirstInstanceSupported = false;
Bool shaderDrawParametersSupported = false;
Bool imageCubeArraySupported = false;
};
} // namespace
@@ -2300,6 +2301,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
summary.imageCubeArraySupported = features.imageCubeArray == VK_TRUE;
summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE;
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
@@ -2721,7 +2723,8 @@ namespace MobileGL::MG_Util::SelfTest {
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported,
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported));
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported,
summary.imageCubeArraySupported));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
+22
View File
@@ -20,6 +20,28 @@ namespace MobileGL {
// buffer, and the trailing number is the only place the GL binding survives.
inline constexpr const char* ATOMIC_COUNTER_BLOCK_PREFIX = "gl_AtomicCounterBlock";
// "gl_AtomicCounterBlock_5" -> 5; -1 for any name that is not one of these blocks.
// Recovering N from the NAME is not a shortcut, it is the only way: the block reaches
// a backend auto-mapped to whatever storage-block slot the IO mapper had free, and
// that number has no relation to the GL atomic-counter binding the application asked
// for (see TMglGlslIoResolver). A backend that resolves the block from the
// shader-storage binding points therefore binds the wrong buffer - or, worse, the
// application's own SSBO at the same slot.
inline Int AtomicCounterBlockGlBinding(StringView name) {
const SizeT prefixLength = StringView(ATOMIC_COUNTER_BLOCK_PREFIX).size();
// Needs the prefix, the '_' and at least one digit.
if (name.size() <= prefixLength + 1) return -1;
if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1;
if (name[prefixLength] != '_') return -1;
Int binding = 0;
for (SizeT i = prefixLength + 1; i < name.size(); ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
binding = binding * 10 + (name[i] - '0');
if (binding > 0x0FFFFFFF) return -1; // absurd suffix; treat as not-a-counter
}
return binding;
}
// Atomic-counter limits, in ONE place because GL 4.6 requires glGetIntegerv and the
// shading language's gl_MaxAtomicCounter* constants to report the same numbers
// (KHR-GL43.shader_atomic_counters.basic-glsl-built-in compares them directly).
@@ -442,14 +442,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return packed.fieldCount == mapping.channelCount;
}
// GL 4.6 core table 8.2: every unpacked component type pairs with every base format,
// with only two exclusions - an integer format takes integer types only, and the two
// floating types need a non-integer format. This used to be derived from
// GetDirectShadowComponentForType, which answers a different question (is the client
// layout byte-identical to some shadow layout) and has no SNorm32 to hand back for
// (non-integer format, GL_INT). That legal pair was therefore rejected outright, even
// though ConvertUnpackRow decodes it through DecodeComponentToFloat like every other
// normalized type - which is what glClearBufferData(GL_R8, GL_RED, GL_INT) needs.
switch (type) {
case TexturePixelDataType::UnsignedInt5999Rev:
case TexturePixelDataType::UnsignedInt101111Rev:
return !mapping.isInteger && mapping.channelCount == 3;
default: {
ShadowComponent component{};
return GetDirectShadowComponentForType(type, mapping.isInteger, component);
}
case TexturePixelDataType::UnsignedByte:
case TexturePixelDataType::Byte:
case TexturePixelDataType::UnsignedShort:
case TexturePixelDataType::Short:
case TexturePixelDataType::UnsignedInt:
case TexturePixelDataType::Int:
return true;
case TexturePixelDataType::HalfFloat:
case TexturePixelDataType::Float:
return !mapping.isInteger;
default:
return false;
}
}