Merge origin/dev (readback overhaul c6d22e6e) into default-texture-objects - true per-target default texture objects supersede the readback branch texture-0 silent no-ops: removed the null-slot early-outs in TexImage1D/2D/3D(Multisample) and TexBuffer plus the DefaultTextureOperationsAreSilentNoOps test so name-0 operations actually (re)specify the default objects; deduped the shared state-reset fixes, keeping upstream std::clamp for GL_MAX_UNIFORM_BUFFER_BINDINGS, the Int-typed ActiveTexture combined-units range check, renderbuffer name-0 unbind, and vertex-attrib-0 current-value writes with the attrib-0 round-trip test

This commit is contained in:
2026-07-16 23:50:47 -04:00
38 changed files with 2079 additions and 473 deletions
+3
View File
@@ -232,6 +232,9 @@ namespace MobileGL {
struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
@@ -605,9 +605,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension.
.Extensions = BuildAdvertisedExtensions(false),
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -627,8 +627,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// thread can only observe the extension string after the
// advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedTimerQueryExtension() {
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported());
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
MutableRendererInfo().RendererGLInfo.Extensions =
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
}
} // namespace
@@ -672,11 +673,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
// reconcile the E_GL_ARB_timer_query advertisement (see the comment on
// UpdateAdvertisedTimerQueryExtension for why it cannot happen when
// the extension list is first built).
UpdateAdvertisedTimerQueryExtension();
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
// list is first built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
UpdateDynamicBackendParameters();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -818,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MutableRendererInfo();
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported) {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
@@ -836,6 +837,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
// get plain trilinear.
if (anisotropicFilteringSupported) {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
return extensions;
}
@@ -940,6 +949,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin;
@@ -66,9 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries are (or are not) usable. The
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
+408 -233
View File
@@ -1042,7 +1042,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get());
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue;
GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target);
GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
backendTextureIt->second->Bind(targetGL, unit);
}
@@ -1853,7 +1853,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!exist) {
backendObj = MakeShared<TextureImpl::BackendTextureObject>();
}
backendObj->Bind(target, unit);
backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit);
}
return true;
}
@@ -2143,10 +2143,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
GLenum textureTarget =
MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget());
GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
attachmentObject.GetTextureUploadTarget());
if (textureTarget == GL_UNKNOWN_MGL) {
textureTarget = MG_Util::ConvertTextureTargetToGLEnum(texture->GetTarget());
textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(texture->GetTarget());
}
const GLuint backendFBOId = backendFBO->GetBackendFramebufferId();
@@ -2643,7 +2643,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
backendTexture->Bind(target, unitIndex);
const GLenum backendTarget =
TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target));
backendTexture->Bind(backendTarget, unitIndex);
DebugImpl::ErrorLopper::Clear();
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps.
// Also detach the source texture from synced FBO objects for ANGLE's validation.
@@ -2651,8 +2653,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
// Bind a complete internal FBO that does not reference the source texture.
ScopedCompleteFramebufferBinding completeFramebuffer;
g_GLESFuncs.glGenerateMipmap(target);
RecordGLError("glGenerateMipmap", target, texture->GetFormat());
g_GLESFuncs.glGenerateMipmap(backendTarget);
RecordGLError("glGenerateMipmap", backendTarget, texture->GetFormat());
}
const GLubyte* GetString(GLenum name) {
@@ -3074,88 +3076,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
}
static Int GetFloatReadbackChannelCount(GLenum format) {
switch (format) {
case GL_RED:
return 1;
case GL_RGBA:
return 4;
default:
return 0;
}
}
static Bool ReadPixelsFloatViaUnsignedByte(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
void* pixels) {
if (width <= 0 || height <= 0) {
return true;
}
const Int dstChannels = GetFloatReadbackChannelCount(format);
if (dstChannels == 0) {
return false;
}
const GLenum readFormat = format == GL_RED ? GL_RED : GL_RGBA;
const Int readChannels = format == GL_RED ? 1 : 4;
Vector<Uint8> raw(static_cast<SizeT>(width) * static_cast<SizeT>(height) *
static_cast<SizeT>(readChannels));
GLint prevPixelPackBuffer = 0;
g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer);
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1);
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
g_GLESFuncs.glReadPixels(x, y, width, height, readFormat, GL_UNSIGNED_BYTE, raw.data());
const GLenum readError = g_GLESFuncs.glGetError();
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(prevPixelPackBuffer));
if (readError != GL_NO_ERROR) {
MGLOG_E("ReadPixels: GL_FLOAT fallback read failed: %s",
MG_Util::ConvertGLEnumToString(readError).c_str());
return true;
}
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstPixelBytes = static_cast<SizeT>(dstChannels) * sizeof(Float);
const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT dstOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT packedSize = dstOffset + static_cast<SizeT>(height - 1) * dstRowStride +
static_cast<SizeT>(width) * dstPixelBytes;
Vector<Uint8> packed(packedSize, 0);
for (GLsizei row = 0; row < height; ++row) {
const Uint8* srcRow = raw.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width) *
static_cast<SizeT>(readChannels);
auto* dstRow = reinterpret_cast<Float*>(packed.data() + dstOffset +
static_cast<SizeT>(row) * dstRowStride);
for (GLsizei col = 0; col < width; ++col) {
const Uint8* src = srcRow + static_cast<SizeT>(col) * static_cast<SizeT>(readChannels);
Float* dst = dstRow + static_cast<SizeT>(col) * static_cast<SizeT>(dstChannels);
// TODO: extend readback packing to all desktop GL read formats instead of only normalized RED/RGBA.
for (Int component = 0; component < dstChannels; ++component) {
dst[component] = static_cast<Float>(src[component]) / 255.0f;
}
}
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (pixelPackBufferObject) {
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) {
MGLOG_E("ReadPixels: GL_FLOAT fallback PBO is too small");
return true;
}
pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset);
} else if (pixels != nullptr && !packed.empty()) {
Memcpy(pixels, packed.data(), packed.size());
}
return true;
}
static Bool ReadPixelsDepthFloatViaUnsignedInt(GLint x, GLint y, GLsizei width, GLsizei height, void* pixels) {
if (width <= 0 || height <= 0) {
return true;
@@ -3186,29 +3106,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT packedSize = dstOffset + static_cast<SizeT>(height - 1) * dstRowStride +
static_cast<SizeT>(width) * dstPixelBytes;
Vector<Uint8> packed(packedSize, 0);
for (GLsizei row = 0; row < height; ++row) {
const Uint32* srcRow = raw.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width);
auto* dstRow = reinterpret_cast<Float*>(packed.data() + dstOffset +
static_cast<SizeT>(row) * dstRowStride);
for (GLsizei col = 0; col < width; ++col) {
// TODO: preserve native depth precision when GLES exposes float depth readback directly.
dstRow[col] = static_cast<Float>(static_cast<Double>(srcRow[col]) / 4294967295.0);
}
}
// Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched.
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (pixelPackBufferObject) {
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) {
MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small");
return true;
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small");
return true;
}
Vector<Float> rowBuf(static_cast<SizeT>(width));
for (GLsizei row = 0; row < height; ++row) {
const Uint32* srcRow = raw.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width);
for (GLsizei col = 0; col < width; ++col) {
// TODO: preserve native depth precision when GLES exposes float depth readback directly.
rowBuf[col] = static_cast<Float>(static_cast<Double>(srcRow[col]) / 4294967295.0);
}
const SizeT rowOffset = dstOffset + static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend(
{rowBuf.data(), static_cast<SizeT>(width) * sizeof(Float)}, pboOffset + rowOffset);
} else if (pixels != nullptr) {
Memcpy(static_cast<Uint8*>(pixels) + rowOffset, rowBuf.data(),
static_cast<SizeT>(width) * sizeof(Float));
}
pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset);
} else if (pixels != nullptr && !packed.empty()) {
Memcpy(pixels, packed.data(), packed.size());
}
return true;
}
@@ -3243,29 +3163,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT packedSize = dstOffset + static_cast<SizeT>(height - 1) * dstRowStride +
static_cast<SizeT>(width) * dstPixelBytes;
Vector<Uint8> packed(packedSize, 0);
for (GLsizei row = 0; row < height; ++row) {
const Uint8* srcRow = raw.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width);
auto* dstRow = reinterpret_cast<Uint32*>(packed.data() + dstOffset +
static_cast<SizeT>(row) * dstRowStride);
for (GLsizei col = 0; col < width; ++col) {
// TODO: switch to native uint stencil readback if the GLES backend exposes it.
dstRow[col] = srcRow[col];
}
}
// Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched.
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (pixelPackBufferObject) {
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) {
MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small");
return true;
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small");
return true;
}
Vector<Uint32> rowBuf(static_cast<SizeT>(width));
for (GLsizei row = 0; row < height; ++row) {
const Uint8* srcRow = raw.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width);
for (GLsizei col = 0; col < width; ++col) {
// TODO: switch to native uint stencil readback if the GLES backend exposes it.
rowBuf[col] = srcRow[col];
}
const SizeT rowOffset = dstOffset + static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend(
{rowBuf.data(), static_cast<SizeT>(width) * sizeof(Uint32)}, pboOffset + rowOffset);
} else if (pixels != nullptr) {
Memcpy(static_cast<Uint8*>(pixels) + rowOffset, rowBuf.data(),
static_cast<SizeT>(width) * sizeof(Uint32));
}
pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset);
} else if (pixels != nullptr && !packed.empty()) {
Memcpy(pixels, packed.data(), packed.size());
}
return true;
}
@@ -3297,6 +3217,94 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// Component-array read formats usable as (possibly narrow) wide-read sources.
static Int GetWideReadChannelCount(GLenum format) {
switch (format) {
case GL_RED:
case GL_RED_INTEGER:
return 1;
case GL_RG:
case GL_RG_INTEGER:
return 2;
case GL_RGB:
case GL_RGB_INTEGER:
return 3;
case GL_RGBA:
case GL_RGBA_INTEGER:
return 4;
default:
return 0;
}
}
static Bool IsIntegerReadFormat(GLint format) {
return format == GL_RED_INTEGER || format == GL_RG_INTEGER || format == GL_RGB_INTEGER ||
format == GL_RGBA_INTEGER;
}
// Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA
// layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in
// the source component type.
static void ExpandNarrowWideRead(Vector<Uint8>& data, SizeT pixelCount, Int srcChannels, GLenum componentType) {
const SizeT componentSize = GetReadbackComponentSize(componentType);
if (componentSize == 0 || srcChannels <= 0 || srcChannels >= 4) {
return;
}
Uint8 zeroBits[4] = {0, 0, 0, 0};
Uint8 oneBits[4] = {0, 0, 0, 0};
switch (componentType) {
case GL_UNSIGNED_BYTE:
oneBits[0] = 0xFF;
break;
case GL_BYTE:
oneBits[0] = 0x7F;
break;
case GL_UNSIGNED_SHORT: {
const Uint16 one = 0xFFFF;
Memcpy(oneBits, &one, sizeof(one));
break;
}
case GL_SHORT: {
const Int16 one = 0x7FFF;
Memcpy(oneBits, &one, sizeof(one));
break;
}
case GL_HALF_FLOAT: {
const Uint16 one = 0x3C00;
Memcpy(oneBits, &one, sizeof(one));
break;
}
case GL_FLOAT: {
const Float one = 1.0f;
Memcpy(oneBits, &one, sizeof(one));
break;
}
case GL_UNSIGNED_INT:
case GL_INT: {
const Uint32 one = 1;
Memcpy(oneBits, &one, sizeof(one));
break;
}
default:
break;
}
Vector<Uint8> expanded(pixelCount * 4 * componentSize);
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* src = data.data() + i * static_cast<SizeT>(srcChannels) * componentSize;
Uint8* dst = expanded.data() + i * 4 * componentSize;
for (Int ch = 0; ch < 4; ++ch) {
if (ch < srcChannels) {
Memcpy(dst + static_cast<SizeT>(ch) * componentSize, src + static_cast<SizeT>(ch) * componentSize,
componentSize);
} else {
Memcpy(dst + static_cast<SizeT>(ch) * componentSize, ch == 3 ? oneBits : zeroBits, componentSize);
}
}
}
data = std::move(expanded);
}
static void DrainESErrors() {
for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
@@ -3320,16 +3328,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
return componentType != 0 ? static_cast<GLenum>(componentType) : GL_UNSIGNED_NORMALIZED;
}
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's
// (format, type) layout. Returns false when the combination is not convertible (the caller keeps its
// "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op.
static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
GLenum type, void* pixels) {
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) {
return false;
}
// Covers unknown types, packed field-count/format mismatches and float types on integer formats.
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds `height` rows of
// `width` texels, 4 components x GetReadbackComponentSize(wideType) bytes each.
// honorPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to GetTexImage of 3D
// images only; ReadPixels ignores them (GL 3.3 section 4.3.1).
static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei height,
const ReadbackChannelMapping& mapping, GLenum type, void* pixels,
Bool honorPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
@@ -3338,91 +3344,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
if (width <= 0 || height <= 0) {
return true;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (!pixelPackBufferObject && pixels == nullptr) {
return true;
}
const GLenum attachmentComponentType = QueryReadAttachmentComponentType();
const Bool integerAttachment =
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
if (mapping.isInteger != integerAttachment) {
MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
MG_Util::ConvertGLEnumToString(format).c_str());
return true;
}
// Prefer the implementation-defined pair (full precision on e.g. norm16 buffers), then the
// spec-guaranteed pair for the attachment class.
GLint implFormat = 0;
GLint implType = 0;
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
const GLenum wideFormat = mapping.isInteger ? GL_RGBA_INTEGER : GL_RGBA;
GLenum wideTypeCandidates[3];
Int wideTypeCandidateCount = 0;
if (mapping.isInteger) {
if (implFormat == GL_RGBA_INTEGER && (implType == GL_INT || implType == GL_UNSIGNED_INT)) {
wideTypeCandidates[wideTypeCandidateCount++] = static_cast<GLenum>(implType);
}
wideTypeCandidates[wideTypeCandidateCount++] =
attachmentComponentType == GL_INT ? GL_INT : GL_UNSIGNED_INT;
} else {
if (implFormat == GL_RGBA && CanDecodeWideSourceType(static_cast<GLenum>(implType))) {
wideTypeCandidates[wideTypeCandidateCount++] = static_cast<GLenum>(implType);
}
if (attachmentComponentType == GL_FLOAT) {
wideTypeCandidates[wideTypeCandidateCount++] = GL_FLOAT;
}
wideTypeCandidates[wideTypeCandidateCount++] = GL_UNSIGNED_BYTE;
}
GLint prevPixelPackBuffer = 0;
g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer);
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1);
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
DrainESErrors();
for (Int i = 0; i < wideTypeCandidateCount; ++i) {
const GLenum candidate = wideTypeCandidates[i];
Bool alreadyTried = false;
for (Int j = 0; j < i; ++j) {
alreadyTried = alreadyTried || wideTypeCandidates[j] == candidate;
}
if (alreadyTried) {
continue;
}
const SizeT candidateComponentSize = GetReadbackComponentSize(candidate);
wide.resize(static_cast<SizeT>(width) * static_cast<SizeT>(height) * 4 * candidateComponentSize);
g_GLESFuncs.glReadPixels(x, y, width, height, wideFormat, candidate, wide.data());
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
wideType = candidate;
break;
}
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(prevPixelPackBuffer));
if (wideType == GL_NONE) {
MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return true;
}
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
// rows are written so skip regions of the destination stay untouched. GL_PACK_SKIP_IMAGES
// skips whole 2D images of GL_PACK_IMAGE_HEIGHT (or `height`) rows for 3D readbacks.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT dstSkipOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
const SizeT imageRows = static_cast<SizeT>(packParams.ImageHeight > 0 ? packParams.ImageHeight : height);
const SizeT skipImages =
honorPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * imageRows * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
@@ -3441,7 +3376,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei row = 0; row < height; ++row) {
const Uint8* srcRow = wide.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width) * srcPixelBytes;
const Uint8* srcRow = wide + static_cast<SizeT>(row) * static_cast<SizeT>(width) * srcPixelBytes;
ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
@@ -3462,13 +3397,207 @@ namespace MobileGL::MG_Backend::DirectGLES {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
return true;
}
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's
// (format, type) layout. Returns false when the combination is not convertible (the caller keeps its
// "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op.
static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
GLenum type, void* pixels, Bool honorPackImageParams = false) {
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) {
return false;
}
// Covers unknown types, packed field-count/format mismatches and float types on integer formats.
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
if (width <= 0 || height <= 0) {
return true;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (!pixelPackBufferObject && pixels == nullptr) {
return true;
}
const GLenum attachmentComponentType = QueryReadAttachmentComponentType();
const Bool integerAttachment =
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
if (mapping.isInteger != integerAttachment) {
MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
MG_Util::ConvertGLEnumToString(format).c_str());
return true;
}
// Prefer the implementation-defined pair (full precision on e.g. norm16 buffers, and possibly
// a narrow format like GL_RED/GL_UNSIGNED_SHORT), then the spec/extension-guaranteed pair for
// the attachment class. Narrow reads are expanded to RGBA on the CPU afterwards.
GLint implFormat = 0;
GLint implType = 0;
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
struct WideReadCandidate {
GLenum format;
GLenum type;
};
WideReadCandidate candidates[4];
Int candidateCount = 0;
if (mapping.isInteger) {
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && IsIntegerReadFormat(implFormat) &&
(implType == GL_INT || implType == GL_UNSIGNED_INT)) {
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
}
candidates[candidateCount++] = {
GL_RGBA_INTEGER,
attachmentComponentType == GL_INT ? static_cast<GLenum>(GL_INT) : static_cast<GLenum>(GL_UNSIGNED_INT)};
} else {
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && !IsIntegerReadFormat(implFormat) &&
(CanDecodeWideSourceType(static_cast<GLenum>(implType)) ||
(implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) {
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
}
if (attachmentComponentType == GL_FLOAT) {
candidates[candidateCount++] = {GL_RGBA, GL_FLOAT};
}
if (attachmentComponentType == GL_SIGNED_NORMALIZED) {
// EXT_render_snorm attachments read back as RGBA/BYTE (8-bit) or RGBA/SHORT (16-bit).
candidates[candidateCount++] = {GL_RGBA, GL_SHORT};
candidates[candidateCount++] = {GL_RGBA, GL_BYTE};
} else {
candidates[candidateCount++] = {GL_RGBA, GL_UNSIGNED_BYTE};
}
}
GLint prevPixelPackBuffer = 0;
g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer);
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1);
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
GLenum readFormat = GL_NONE;
Int readChannels = 0;
DrainESErrors();
for (Int i = 0; i < candidateCount; ++i) {
const WideReadCandidate candidate = candidates[i];
Bool alreadyTried = false;
for (Int j = 0; j < i; ++j) {
alreadyTried =
alreadyTried || (candidates[j].format == candidate.format && candidates[j].type == candidate.type);
}
if (alreadyTried) {
continue;
}
const Int channels = GetWideReadChannelCount(candidate.format);
const SizeT candidateComponentSize = GetReadbackComponentSize(candidate.type);
wide.resize(static_cast<SizeT>(width) * static_cast<SizeT>(height) *
static_cast<SizeT>(channels) * candidateComponentSize);
g_GLESFuncs.glReadPixels(x, y, width, height, candidate.format, candidate.type, wide.data());
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
wideType = candidate.type;
readFormat = candidate.format;
readChannels = channels;
break;
}
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(prevPixelPackBuffer));
if (wideType == GL_NONE) {
MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return true;
}
if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
// Unpack the packed words into a float wide buffer (full 10-bit precision on e.g.
// GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV).
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
Vector<Uint8> floatWide(pixelCount * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(floatWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
Uint32 word;
Memcpy(&word, wide.data() + i * 4, sizeof(word));
dst[i * 4 + 0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
}
wide = std::move(floatWide);
wideType = GL_FLOAT;
readChannels = 4;
}
if (readChannels < 4) {
ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType);
}
if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels,
honorPackImageParams)) {
return false;
}
MGLOG_D("Readback conversion: converted %s/%s from wide %s/%s", MG_Util::ConvertGLEnumToString(format).c_str(),
MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(wideFormat).c_str(),
MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(readFormat).c_str(),
MG_Util::ConvertGLEnumToString(wideType).c_str());
return true;
}
// GetTexImage fallback for internal formats the ES driver cannot attach to a framebuffer
// (SNORM, RGB16, RGB9_E5, ...): decodes the canonical CPU shadow-mip storage into wide RGBA
// rows and repacks them into the client layout. Only valid while the shadow copy is
// authoritative, which holds for non-renderable formats (they can never be GPU-written).
static Bool GetTexImageViaShadowConversion(MG_State::GLState::TextureObjectMipmap* textureMipmapObject,
TextureUploadTarget uploadTarget, GLint level, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels) {
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) {
return false;
}
if (GetReadbackDstPixelSize(mapping, type) == 0) {
return false;
}
if (width <= 0 || height <= 0) {
return true;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (!pixelPackBufferObject && pixels == nullptr) {
return true;
}
const void* shadow = textureMipmapObject->MapMipmapData(uploadTarget, level);
if (!shadow) {
return false;
}
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(
textureMipmapObject->GetFormat(), shadow, static_cast<SizeT>(width) * static_cast<SizeT>(height),
wide, isInteger, isSigned)) {
return false;
}
if (mapping.isInteger != isInteger) {
// Spec-invalid combinations are rejected with GL errors at the state layer already.
return false;
}
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels,
/*honorPackImageParams=*/true)) {
return false;
}
MGLOG_D("GetTexImage: converted %s/%s from the CPU shadow copy",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return true;
}
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX;
@@ -3515,7 +3644,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E("ReadPixels: bound READ FBO is not complete");
return;
}
if (!useNativeReadback) {
// ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the
// matching attachment class; every other convertible color layout (including GL_RGBA/GL_FLOAT
// and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type
// the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so
// it always takes the conversion path (which swaps on the CPU).
const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;
const Bool nativeFastPair = !packSwapBytes &&
((format == GL_RGBA && type == GL_UNSIGNED_BYTE) ||
(format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT)));
if (convertible && !nativeFastPair) {
if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) {
MGLOG_D("ReadPixels: finished via client-format conversion");
return;
@@ -3534,10 +3672,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: finished via stencil GL_UNSIGNED_INT fallback");
return;
}
if (type == GL_FLOAT && ReadPixelsFloatViaUnsignedByte(x, y, width, height, format, pixels)) {
MGLOG_D("ReadPixels: finished via GL_FLOAT fallback");
return;
}
// Handle PBO
auto& pixelPackBufferObject =
@@ -3673,18 +3807,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
TempFBOBinder tempFBOBinder(true);
MGLOG_D("GetTexImage: glFramebufferTexture2D(level=%d)", level);
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, backendTexId, level);
// The temp FBO is reused across GetTexImage calls: detach the previous color attachment first
// so a failed attach below leaves the FBO incomplete instead of silently reading stale contents.
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
const GLenum backendAttachTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
MG_Util::ConvertGLEnumToTextureUploadTarget(target));
if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) {
// ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads
// of deeper slices are served from the CPU shadow instead (see the shadow-first branch).
g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, backendTexId, level, 0);
} else {
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget,
backendTexId, level);
}
MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)");
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
MGLOG_D("GetTexImage: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str());
if (fbStatus != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("GetTexImage: READ FBO incomplete");
MGLOG_E("GetTexImage: bound READ FBO is not complete");
return;
}
// Non-renderable internal formats (SNORM, RGB16, RGB9_E5, ...) leave the temp FBO incomplete;
// those readbacks are served from the CPU shadow copy below instead of bailing out.
const Bool tempFBOComplete = fbStatus == GL_FRAMEBUFFER_COMPLETE;
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
@@ -3722,15 +3867,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y());
if (!useNativeReadback) {
if (ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels)) {
// Prefer the client-format conversion for every convertible combination: the "native" ES pairs
// are only guaranteed for matching attachment classes (e.g. GL_RGBA/GL_UNSIGNED_INT is invalid
// for normalized attachments), while the conversion path reads a wide format that is always
// accepted and repacks on the CPU.
if (convertible) {
// 3D/array images read back every slice, but the FBO path can only read one layer:
// multi-slice reads are served from the CPU shadow (depth as extra rows, tight layout).
const GLsizei shadowRows = size.y() * std::max(size.z(), 1);
const Bool multiSlice = size.z() > 1;
if (multiSlice &&
GetTexImageViaShadowConversion(textureMipmapObject,
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
shadowRows, format, type, pixels)) {
MGLOG_D("GetTexImage: finished via shadow conversion");
return;
}
if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels,
/*honorPackImageParams=*/true)) {
MGLOG_D("GetTexImage: finished via client-format conversion");
return;
}
if (GetTexImageViaShadowConversion(textureMipmapObject,
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
shadowRows, format, type, pixels)) {
MGLOG_D("GetTexImage: finished via shadow conversion");
return;
}
if (!tempFBOComplete) {
MGLOG_E("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback");
return;
}
MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return;
}
if (!tempFBOComplete) {
MGLOG_E("GetTexImage: bound READ FBO is not complete");
return;
}
// Handle PBO
auto& pixelPackBufferObject =
+59 -39
View File
@@ -1476,10 +1476,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
return 2;
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB16Snorm:
return 3;
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16:
case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow)
case TextureInternalFormat::RGBA16Snorm:
return 4;
default:
@@ -1574,7 +1577,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
@@ -1695,7 +1698,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? textureMipmapObject->MapMipmapData(uploadTarget, level)
: nullptr;
@@ -1706,20 +1709,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
switch (stateTextureObject->GetTarget()) {
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()), static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
0, glFormat, glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()), static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(levelTexelSize.z()), 0, glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
default:
MGLOG_E("Unhandled texture target %s",
@@ -1783,19 +1788,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
switch (targetInternal) {
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexStorage2D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(baseSize.x()),
static_cast<GLsizei>(baseSize.y()));
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()));
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(baseSize.x()),
static_cast<GLsizei>(baseSize.y()),
static_cast<GLsizei>(baseSize.z()));
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()),
static_cast<GLsizei>(storageSize.z()));
break;
default:
MGLOG_E("Unhandled immutable texture target %s",
@@ -1819,7 +1825,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (levelDirty && levelByteSize != 0) {
auto levelTexelSize =
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level);
Vector<Float> convertedUploadData;
const void* uploadData = PrepareNormFloatFallbackUpload(
@@ -1828,21 +1834,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
switch (targetInternal) {
const IntVec3 uploadSize =
GetBackendUploadSize(targetInternal, levelTexelSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()), glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat, glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(levelTexelSize.z()), glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType, uploadData);
break;
default:
break;
@@ -1869,7 +1877,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? textureMipmapObject->MapMipmapData(uploadTarget, level)
: nullptr;
@@ -1886,23 +1894,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
auto textureTarget = stateTextureObject->GetTarget();
// TODO: handle more texture types
switch (textureTarget) {
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
switch (MapToBackendTextureTarget(textureTarget)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap: {
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()), 0, glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), 0, glFormat, glType, uploadData);
break;
}
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray: {
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(levelTexelSize.z()), 0, glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
}
default: {
@@ -1969,7 +1977,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).x(),
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
@@ -1982,20 +1990,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* uploadData = PrepareNormFloatFallbackUpload(
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
convertedUploadData);
switch (stateTextureObject->GetTarget()) {
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(texelSize.x()),
static_cast<GLsizei>(texelSize.y()), glFormat, glType,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat, glType,
uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(texelSize.x()),
static_cast<GLsizei>(texelSize.y()),
static_cast<GLsizei>(texelSize.z()), glFormat, glType,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType,
uploadData);
break;
default:
@@ -2088,7 +2098,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
m_backendTextureId, stateTextureObject->GetExternalIndex());
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
@@ -2196,7 +2206,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
@@ -2353,11 +2363,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
uploadTarget == TextureUploadTarget::Texture3D ||
uploadTarget == TextureUploadTarget::Texture2DArray ||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
// Single slice/layer of a 3D or array texture: ES has no
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()),
static_cast<GLint>(attachmentObject.GetTextureLayer()));
} else {
auto glTextureTarget =
MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget());
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
attachmentObject.GetTextureUploadTarget());
if (glTextureTarget == GL_UNKNOWN_MGL) {
glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
}
backendTextureObject->Bind(glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
+43 -4
View File
@@ -15,6 +15,7 @@
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
@@ -254,10 +255,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
inline Bool IsSupportedTextureTarget(TextureTarget target) {
if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle ||
target == TextureTarget::Texture1DArray)
return false;
return true;
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
// coordinate padding for 1D/1D-array shaders.
return target != TextureTarget::TextureRectangle;
}
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
default:
return target;
}
}
inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) {
return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target));
}
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
switch (uploadTarget) {
case TextureUploadTarget::Texture1D:
return GL_TEXTURE_2D;
case TextureUploadTarget::Texture1DArray:
return GL_TEXTURE_2D_ARRAY;
default:
return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
}
}
// 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and
// moves the layer count into depth.
inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) {
if (stateTarget == TextureTarget::Texture1DArray) {
return {texelSize.x(), 1, texelSize.y()};
}
return texelSize;
}
inline Bool IsMultisampleTextureTarget(TextureTarget target) {
+6 -69
View File
@@ -19,6 +19,7 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/Math/SmallFloat.h>
#include <cmath>
@@ -557,38 +558,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
namespace {
// Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa
// bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero,
// +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value
// clamp to it. The mantissa is truncated (rounding mode is implementation-defined).
Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) {
const Uint32 bits = std::bit_cast<Uint32>(value);
const Bool negative = (bits & 0x80000000u) != 0;
const Uint32 exponent = (bits >> 23) & 0xFFu;
const Uint32 mantissa = bits & 0x7FFFFFu;
const Uint32 exponentMask = 0x1Fu << mantissaBits;
if (exponent == 0xFFu) {
if (mantissa != 0) {
return exponentMask | 1u; // NaN keeps NaN
}
return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf
}
if (negative) {
return 0u;
}
const Int32 smallExponent = static_cast<Int32>(exponent) - 127 + 15;
if (smallExponent >= 31) { // above the largest finite value -> clamp to it
return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u);
}
if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero
const Uint32 fullMantissa = mantissa | 0x800000u;
const Int32 shift = (23 - mantissaBits) + 1 - smallExponent;
return shift > 23 ? 0u : fullMantissa >> shift;
}
return (static_cast<Uint32>(smallExponent) << mantissaBits) |
(mantissa >> (23u - static_cast<Uint32>(mantissaBits)));
}
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
switch (byteSize) {
case 1: {
@@ -608,43 +577,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
} // namespace
Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); }
Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); }
// RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm
// (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31).
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) {
constexpr Int kMantissaBits = 9;
constexpr Int kExponentBias = 15;
constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B)
Float clamped[3];
for (Int i = 0; i < 3; ++i) {
const Float v = rgb[i];
clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax);
}
const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2]));
Int sharedExponent = 0; // all-zero input keeps the all-zero word
if (maxComponent > 0.0f) {
sharedExponent = std::max(-kExponentBias - 1, static_cast<Int>(std::floor(std::log2(maxComponent)))) +
1 + kExponentBias;
const Float maxScaled = std::floor(
maxComponent / std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits)) +
0.5f);
if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead
++sharedExponent;
}
}
const Float scale = std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits));
Uint32 word = static_cast<Uint32>(sharedExponent) << 27;
for (Int i = 0; i < 3; ++i) {
const auto field = static_cast<Uint32>(std::floor(clamped[i] / scale + 0.5f));
word |= std::min(field, 511u) << (i * kMantissaBits);
}
return word;
}
// Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion
// (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests.
Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); }
Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); }
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); }
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type) {
@@ -488,14 +488,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false),
.IsCompatibilityProfile = false
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
@@ -516,6 +517,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Only advertised when the samplerAnisotropy device feature was granted: without it the
// sampler state is accepted but never applied, and an app trusting the string (LWJGL builds
// GLCapabilities from it) would think it enabled anisotropic filtering.
if (anisotropicFilteringSupported) {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
return extensions;
}
@@ -630,7 +638,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent.
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported());
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -680,6 +689,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
// rather than a maximum the sampler manager will never apply.
m_dynamicParameters.MaxTextureMaxAnisotropy =
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
: 1.0f;
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
@@ -73,7 +73,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -58,11 +58,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = initInfo.device;
m_config = initInfo.config;
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info");
return true;
}
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const {
if (!m_samplerAnisotropySupported) return 1.0f;
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
sampler.GetMagFilter() != SamplerFilterMode::Linear) {
return 1.0f;
}
return std::clamp(sampler.GetMaxAnisotropy(), 1.0f, m_maxSamplerAnisotropy);
}
void VkSamplerManager::Shutdown() {
for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
@@ -99,9 +112,11 @@ 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.
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = ResolveCompareFunc(sampler, texture);
@@ -128,10 +143,11 @@ 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;
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
// different samplers or silently create duplicates.
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
@@ -24,6 +24,10 @@ public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
const VulkanRendererConfig* config = nullptr;
// The samplerAnisotropy device feature was requested and granted at vkCreateDevice.
Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
Float maxSamplerAnisotropy = 1.0f;
};
Bool Initialize(const InitInfo& initInfo);
@@ -49,9 +53,16 @@ private:
const MG_State::GLState::ITextureObject& texture);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
@@ -1890,7 +1890,8 @@ void main() {
m_samplerManager = MakeUnique<VkSamplerManager>();
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
succeeded = m_samplerManager->Initialize({m_device, &m_config});
succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled,
m_physicalDevice.properties.limits.maxSamplerAnisotropy});
MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed.");
succeeded = InitializeBlitResources();
MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed.");
@@ -6508,6 +6509,10 @@ void main() {
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
// Backs GL_TEXTURE_MAX_ANISOTROPY_EXT; optional in Vulkan, so the sampler manager falls back
// to isotropic filtering (and the extension goes unadvertised) when the device lacks it.
deviceFeatures.samplerAnisotropy = supportedDeviceFeatures.samplerAnisotropy;
m_samplerAnisotropyFeatureEnabled = deviceFeatures.samplerAnisotropy == VK_TRUE;
VkDeviceCreateInfo deviceCreateInfo{};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
@@ -227,6 +227,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// frontend. Timestamp support (queue timestampValidBits > 0 and a
// non-zero timestampPeriod) is cached at device creation.
Bool IsTimerQuerySupported() const;
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
// honored rather than accepted-and-ignored.
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
// Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted.
@@ -359,6 +362,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_indexTypeUint8ExtensionEnabled = false;
Bool m_logicOpFeatureEnabled = false;
Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
@@ -60,6 +60,87 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// Whether the backend can actually attach this color format to a framebuffer. Preferred
// source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus
// probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend
// cannot answer for fall back to a conservative static list of formats no ES driver renders to:
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
const SizeT formatIndex = static_cast<SizeT>(format);
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
const SizeT sentinelFormat = static_cast<SizeT>(TextureInternalFormat::RGBA8);
Bool cachePopulated = false;
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated;
++targetIndex) {
cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat],
MG_Backend::FormatCapability::Creatable);
}
if (cachePopulated) {
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
++targetIndex) {
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable) ||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable)) {
return true;
}
}
return false;
}
}
switch (format) {
case TextureInternalFormat::RGB9E5:
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA16Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16
case TextureInternalFormat::RGB12: // stored as RGB16
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::SRGB8:
return false;
default:
return true;
}
}
Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
const auto& attachments = framebufferObject.GetAllAttachmentObjects();
for (SizeT i = 0; i < attachments.size(); ++i) {
const auto type = static_cast<FramebufferAttachmentType>(i);
if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) {
continue;
}
const auto& attachment = attachments[i];
if (!attachment.IsValid()) continue;
TextureInternalFormat format = TextureInternalFormat::Unknown;
if (attachment.IsTexture() && attachment.GetTexture()) {
format = attachment.GetTexture()->GetFormat();
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
format = attachment.GetRenderbuffer()->GetInternalFormat();
}
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
return true;
}
}
return false;
}
void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -556,6 +637,62 @@ namespace MobileGL::MG_Impl::GLImpl {
return framebufferObject;
}
// Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer
// index; the DirectGLES backend attaches it with glFramebufferTextureLayer.
static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment,
GLuint texture, GLint level, GLint layer,
TextureUploadTarget textureUploadTarget) {
if (target == GL_FRAMEBUFFER) {
target = GL_DRAW_FRAMEBUFFER;
}
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer,
textureUploadTarget);
AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer,
textureUploadTarget);
return;
}
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
if (!TextureImpl::ValidateTextureName(texture, true)) return;
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) {
framebufferObject->Detach(attachmentType);
return;
}
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
std::format("Texture object {} is not valid.", texture)));
return;
}
if (layer < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Layer must be non-negative."));
return;
}
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
/*layered=*/false);
}
void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
if (texture == 0) {
const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D;
@@ -563,10 +700,31 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
static_cast<void>(layer);
RecordUnsupportedFramebufferTextureAttachmentError(
__func__,
"Layered framebuffer texture attachments are not represented by the current framebuffer attachment model.");
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::format("Texture object {} is not valid.", texture)));
return;
}
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
switch (textureObject->GetTarget()) {
case TextureTarget::Texture3D:
textureUploadTarget = TextureUploadTarget::Texture3D;
break;
case TextureTarget::Texture2DArray:
textureUploadTarget = TextureUploadTarget::Texture2DArray;
break;
case TextureTarget::Texture2DMultisampleArray:
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
break;
default:
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
return;
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
}
void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
@@ -578,10 +736,15 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
static_cast<void>(zoffset);
RecordUnsupportedFramebufferTextureAttachmentError(
__func__,
"3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model.");
if (textarget != GL_TEXTURE_3D) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"FramebufferTexture3D requires GL_TEXTURE_3D."));
return;
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset,
TextureUploadTarget::Texture3D);
}
void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
@@ -1256,6 +1419,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
}
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1281,6 +1447,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
}
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1630,8 +1799,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// Check framebuffer completeness
if (!framebufferObject->CheckCompleteness()) {
// Check framebuffer completeness (including formats the ES pipeline cannot attach)
if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidFramebufferOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete"));
@@ -1683,6 +1852,33 @@ namespace MobileGL::MG_Impl::GLImpl {
"No color buffer for color format"));
return false;
}
// GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read
// buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error
// for every *_INTEGER readback from a normalized attachment).
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown;
if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
attachmentFormat = readAttachment.GetTexture()->GetFormat();
} else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat();
}
if (attachmentFormat != TextureInternalFormat::Unknown &&
TextureImpl::IsIntegerColorInputFormat(textureInputFormat) !=
TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State",
"Integer-ness of format does not match the read buffer"));
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;
}
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
+18 -9
View File
@@ -529,6 +529,13 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = dynamicParameters.AliasedLineWidthRangeMax;
return;
}
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: {
// EXT_texture_filter_anisotropic queries this as a float; the integer path below widens
// from here, so this case is the authoritative one.
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.MaxTextureMaxAnisotropy;
return;
}
case GL_ALIASED_POINT_SIZE_RANGE:
case GL_POINT_SIZE_RANGE: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
@@ -1894,15 +1901,13 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxTextureSize;
break;
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
// Never advertise more indexed binding points than the state layer's fixed per-target
// array can store (BufferBindingPointCount): glBindBufferBase rejects indices past
// that capacity, and GL CTS's per-case state reset walks every advertised binding
// (gluStateReset), so an over-advertised value aborts whole test batches. The floor
// equals the GL 3.3 core minimum (36), so the clamp never under-advertises.
*params = static_cast<GLint>(std::min<SizeT>(
static_cast<SizeT>(std::max(dynamicParameters.MaxUniformBufferBindings,
kFrontendMinUniformBufferBindings)),
MG_State::GLState::BufferBindingPointCount));
// Never advertise more bindings than the state layer's indexed-binding array can track
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
// (36), so the clamp never under-advertises.
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
break;
case GL_MAX_UNIFORM_BLOCK_SIZE:
*params = dynamicParameters.MaxUniformBlockSize;
@@ -1963,6 +1968,10 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_SAMPLES:
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
break;
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
break;
default:
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
+70 -28
View File
@@ -422,10 +422,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"2D multisample textures must use depth 1."));
return false;
}
// Zero layers is NOT an error for multisample arrays: GL 4.5 8.8 only raises
// INVALID_VALUE for negative dimensions, and GL CTS's per-case state reset
// (gluStateReset) clears the default GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with
// glTexImage3DMultisample(..., depth = 0) after every case.
// Zero layers is NOT an error for multisample arrays: depth == 0 (like width/height
// == 0) deallocates the image - GL 4.5 8.8 only raises INVALID_VALUE for negative
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
if (samples > maxSamples) {
@@ -576,6 +576,14 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_A: {
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param);
if (swizzleValue == TextureSwizzleParam::Unknown) {
// GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside
// [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Invalid texture swizzle value."));
return;
}
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
break;
}
@@ -765,6 +773,23 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
// (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
// sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
// clean). Name 0 resolves to the target's real default texture object, so parameters set on
// it are stored and queryable like on any texture.
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByTargetForParameter(
TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) {
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget);
}
if (textureTarget == TextureTarget::Unknown) {
return nullTextureObject;
}
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
return activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
}
void GenerateMipmap_Backend(GLenum target) {
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
}
@@ -1072,7 +1097,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
switch (pname) {
@@ -1105,6 +1130,12 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_A: {
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param);
if (swizzleValue == TextureSwizzleParam::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value."));
return;
}
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
break;
}
@@ -1155,7 +1186,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
TextureParameterObject_State(textureObject, pname, param, __func__);
@@ -1171,7 +1202,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromFloats(textureObject, params);
break;
@@ -1179,7 +1210,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
@@ -1202,7 +1233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromInts(textureObject, params);
break;
@@ -1210,7 +1241,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) {
return;
@@ -1228,7 +1259,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromIntegerInts(textureObject, params);
break;
@@ -1236,7 +1267,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
@@ -1256,7 +1287,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromUnsignedInts(textureObject, params);
break;
@@ -1267,7 +1298,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
Vec4<TextureSwizzleParam> swizzleParams;
@@ -1318,6 +1349,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
MG_State::pGLContext->RecordError(
@@ -1359,6 +1392,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
MG_State::pGLContext->RecordError(
@@ -1437,6 +1472,8 @@ namespace MobileGL::MG_Impl::GLImpl {
: bindingSlot.GetBoundObject();
// ===================== Error Checking ==============================
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1556,6 +1593,8 @@ namespace MobileGL::MG_Impl::GLImpl {
: bindingSlot.GetBoundObject();
// ===================== Error Checking ==============================
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1660,6 +1699,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1717,7 +1758,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
// nonzero name that is not an existing buffer object is an error. This is reachable on
// the default buffer texture now that binding texture 0 binds a real object.
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
// which the GL CTS state reset detaches with glTexBuffer(..., 0) after every case.
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (buffer != 0 && !bufferObject) {
MG_State::pGLContext->RecordError(
@@ -1733,6 +1775,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = bindingSlot.GetBoundObject();
// ===================== Error Checking ==============================
// Name 0 is the default buffer texture - a real object the (de)attach operates on, not a
// silent no-op; the slot is never empty now that every unit/target holds its default.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
@@ -2625,20 +2669,18 @@ namespace MobileGL::MG_Impl::GLImpl {
void ActiveTexture_State(GLenum texture) {
// ===================== Error Checking ==============================
// GL 3.3 core 3.8: ActiveTexture accepts TEXTUREi for i in
// [0, MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] - NOT a fixed 0..31 range. GL CTS's per-case
// state reset walks every advertised combined unit, so rejecting units the getter
// advertises aborts whole test batches. The backend already clamps its advertised value
// to the state layer's MAX_TEXTURE_IMAGE_UNITS capacity.
GLenum maxCombinedUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS;
if (MG_Backend::pActiveBackendObject != nullptr) {
maxCombinedUnits = std::min<GLenum>(
maxCombinedUnits,
static_cast<GLenum>(
std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits,
1)));
// GL 3.3 core 3.8: the valid range is [GL_TEXTURE0, GL_TEXTURE0 +
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) - NOT a fixed 0..31 range. GL CTS's per-case
// state reset iterates every advertised combined unit, so rejecting units the
// implementation itself reports would leave a sticky GL_INVALID_ENUM behind and abort
// whole test batches. The backend already clamps its advertised value to the state
// layer's MAX_TEXTURE_IMAGE_UNITS capacity.
Int maxCombinedUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
if (MG_Backend::pActiveBackendObject) {
maxCombinedUnits = std::min(
maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits);
}
if (texture < GL_TEXTURE0 || texture >= GL_TEXTURE0 + maxCombinedUnits) {
if (texture < GL_TEXTURE0 || static_cast<Int>(texture - GL_TEXTURE0) >= maxCombinedUnits) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
@@ -175,7 +175,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
static Bool IsIntegerColorInputFormat(TextureInputFormat format) {
Bool IsIntegerColorInputFormat(TextureInputFormat format) {
return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger ||
format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger ||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger ||
@@ -183,7 +183,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
format == TextureInputFormat::AlphaInteger;
}
static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
switch (internalFormat) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
@@ -23,6 +23,8 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border);
Bool IsIntegerColorInputFormat(TextureInputFormat format);
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat);
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type);
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat,
@@ -82,7 +82,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// including index 0 - only an out-of-range index is an error (INVALID_VALUE).
// "Attribute 0 is immutable" was legacy immediate-mode lore; rejecting it broke GL
// CTS's per-case state reset, which writes vertexAttrib4f(0, 0,0,0,1) after every case.
(void)funcName;
static_cast<void>(funcName);
return VertexArrayImpl::ValidateVertexAttributeIndex(index);
}
@@ -12,6 +12,10 @@
#include <string>
#include <vector>
#include <algorithm>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
@@ -31,6 +35,11 @@ namespace {
GLenum pendingError = GL_NO_ERROR;
std::vector<std::string> extensions;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT the fake reports, and whether it was ever asked:
// querying it on a driver without the extension would raise GL_INVALID_ENUM.
GLfloat maxTextureMaxAnisotropy = 16.0f;
bool maxTextureMaxAnisotropyQueried = false;
GLuint nextBufferId = 1;
GLuint nextShaderId = 1;
GLuint nextProgramId = 1;
@@ -122,6 +131,10 @@ namespace {
};
funcs.glGetFloatv = [](GLenum pname, GLfloat* data) {
switch (pname) {
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
g_fake.maxTextureMaxAnisotropyQueried = true;
data[0] = g_fake.maxTextureMaxAnisotropy;
break;
// Two-component range queries.
case GL_ALIASED_LINE_WIDTH_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
@@ -404,6 +417,51 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
ExpectProbeReleasedAllObjects();
}
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(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);
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);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
}
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities absentCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs));
// Never probed (it would be GL_INVALID_ENUM), and reported as "no anisotropy".
EXPECT_FALSE(g_fake.maxTextureMaxAnisotropyQueried);
EXPECT_FLOAT_EQ(absentCaps.MaxTextureMaxAnisotropy, 1.0f);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.maxTextureMaxAnisotropy = 16.0f;
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(g_fake.maxTextureMaxAnisotropyQueried);
EXPECT_FLOAT_EQ(presentCaps.MaxTextureMaxAnisotropy, 16.0f);
}
TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
@@ -775,3 +775,86 @@ TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) {
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u);
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u);
}
// ---- GL CTS packed_pixels readback root-cause regressions --------------------------------------
TEST_F(FramebufferTest, ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8UI, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// GL 3.3 section 4.3.1: normalized format on an integer read buffer -> GL_INVALID_OPERATION
// (GL CTS packed_pixels expects the error for every mismatched combination).
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// The matching integer readback stays valid.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// And the inverse mismatch: integer format on a normalized attachment.
GLuint normalizedTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &normalizedTexture);
MG_Impl::GLImpl::TextureStorage2D(normalizedTexture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, normalizedTexture, 0);
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
TEST_F(FramebufferTest, BindRenderbufferZeroUnbindsWithoutError) {
// The GL CTS state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) and expects no error;
// name 0 used to be reported as an invalid renderbuffer name.
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, FramebufferTexture3DAttachesSliceWithLayerTracking) {
// glFramebufferTexture3D with zoffset used to be rejected outright, leaving a sticky
// GL_INVALID_OPERATION behind (GL CTS packed_pixels varied_rectangle runs on GL_TEXTURE_3D).
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 4, 2);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
ASSERT_NE(framebufferObject, nullptr);
const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0);
ASSERT_TRUE(attachment.IsTexture());
EXPECT_EQ(attachment.GetTextureLayer(), 1);
EXPECT_FALSE(attachment.IsLayered());
}
TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) {
// Without a probing backend the conservative list applies: RGB9_E5 is texture-only, so
// attaching it must not report GL_FRAMEBUFFER_COMPLETE (GL CTS packed_pixels rgb9_e5 expects
// read errors instead of silent unwritten readbacks).
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGB9_E5, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_READ_FRAMEBUFFER),
static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
Uint8 pixelStorage[4 * 4 * 4] = {};
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION);
}
@@ -556,6 +556,81 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) {
}
}
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching
// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing.
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 330
layout(binding = 0) uniform sampler2D InSampler;
in vec2 texCoord;
out vec4 fragColor;
void main() {
fragColor = texture(InSampler, texCoord);
})";
PreprocessShaderSource(ShaderStage::Fragment, source);
// The normal path still emits 330 - the retry must not become the default.
ASSERT_EQ(source.find("#version 330 core"), 0u);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log;
}
// Same source compiled for the OpenGL environment must take the retry too.
ShaderAttrib glAttrib{
.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source, .flags = ShaderCompileBits::CompileForOpenGL};
auto glRes = ShaderCompiler::CompileShader(glAttrib);
if (!glRes) {
FAIL() << "errc: " << glRes.error().errc << "\nlog: " << glRes.error().log;
}
}
TEST_F(ProgramUtilTest, CompileShaderStillFailsWithOriginalDiagnosticsWhenRetryCannotHelp) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 330
in vec2 texCoord;
out vec4 fragColor;
void main() {
fragColor = thisFunctionDoesNotExist(texCoord);
})";
PreprocessShaderSource(ShaderStage::Fragment, source);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
ASSERT_FALSE(res);
EXPECT_EQ(res.error().errc, -2);
EXPECT_NE(res.error().log.find("thisFunctionDoesNotExist"), String::npos) << res.error().log;
}
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
using namespace MG_Util::ShaderTranspiler;
String normalized = "#version 330 core\nvoid main() {}\n";
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
// Already modern: nothing to retarget.
String modern = "#version 460 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
EXPECT_EQ(modern.find("#version 460 core"), 0u);
// ES and compatibility sources keep what they declared.
String es = "#version 300 es\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(es));
EXPECT_EQ(es.find("#version 300 es"), 0u);
String compat = "#version 330 compatibility\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(compat));
EXPECT_EQ(compat.find("#version 330 compatibility"), 0u);
// A commented-out directive is not the real one.
String commented = "// #version 330 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(commented));
EXPECT_EQ(commented.find("#version 460"), String::npos);
}
const char* fs = R"(#version 150
uniform sampler2D InSampler;
+245 -5
View File
@@ -23,7 +23,10 @@
#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>
#include <cstring>
using namespace MobileGL;
@@ -122,6 +125,10 @@ namespace {
return table;
}
const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
return MutableDynamicParameters();
}
// Lets a test stand in a backend limit (e.g. GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT).
static MobileGL::MG_Backend::DynamicBackendParameters& MutableDynamicParameters() {
static MobileGL::MG_Backend::DynamicBackendParameters params = {};
return params;
}
@@ -168,6 +175,30 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
auto backend = MakeUnique<FormatCapabilityBackend>();
FormatCapabilityBackend::MutableDynamicParameters().MaxTextureMaxAnisotropy = 16.0f;
ScopedBackendOverride override(Move(backend));
GLfloat floatValue = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue);
EXPECT_FLOAT_EQ(floatValue, 16.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint integerValue = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &integerValue);
EXPECT_EQ(integerValue, 16);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A backend without anisotropy reports the no-anisotropy floor rather than erroring.
FormatCapabilityBackend::MutableDynamicParameters().MaxTextureMaxAnisotropy = 1.0f;
MG_Impl::GLImpl::GetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue);
EXPECT_FLOAT_EQ(floatValue, 1.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, TextureMaxAnisotropyDefaultsToOneAndRoundTripsWithoutRedundantVersionBumps) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
@@ -1327,10 +1358,13 @@ TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFo
GLenum expectedFormat;
GLenum expectedType;
} cases[] = {
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4},
{GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5},
// 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_SHORT_5_5_5_1},
{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) {
@@ -1363,8 +1397,10 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) {
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D));
EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::Texture1D));
EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
// 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));
}
@@ -1907,3 +1943,207 @@ TEST_F(TextureTest, CtsStyleStateResetOnDefaultTexturesLeavesNoError) {
MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 0, 0, 0, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE_ARRAY reset failed";
}
// ---- GL CTS packed_pixels / texture_swizzle readback root-cause regressions --------------------
TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) {
struct Case {
GLenum requested;
GLenum internalFormat;
GLenum format;
GLenum type;
};
const Case cases[] = {
// Legacy <=8-bit-per-channel formats store as UNorm8 component arrays.
{GL_R3_G3_B2, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
{GL_RGB4, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
{GL_RGB5, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
{GL_RGBA2, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
{GL_RGBA4, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
{GL_RGB5_A1, GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE},
// 10/12-bit channels store as UNorm16 component arrays.
{GL_RGB10, GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT},
{GL_RGB12, GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT},
{GL_RGBA12, GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT},
// RGB10_A2UI keeps its native packed layout (was previously unhandled -> broken uploads).
{GL_RGB10_A2UI, GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV},
};
for (const auto& testCase : cases) {
GLenum internalFormat = 0;
GLenum format = 0;
GLenum type = 0;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(testCase.requested,
PixelFormatNormalizeOptionBit::None,
&internalFormat, &format, &type);
EXPECT_EQ(internalFormat, testCase.internalFormat) << "requested 0x" << std::hex << testCase.requested;
EXPECT_EQ(format, testCase.format) << "requested 0x" << std::hex << testCase.requested;
EXPECT_EQ(type, testCase.type) << "requested 0x" << std::hex << testCase.requested;
}
}
TEST_F(TextureTest, ConvertsUnsignedInt1010102PixelDataType) {
// GL CTS packed_pixels uploads/reads GL_UNSIGNED_INT_10_10_10_2; the GL->MG mapping was missing,
// rejecting every valid combination as GL_INVALID_ENUM.
EXPECT_EQ(MG_Util::ConvertGLEnumToTexturePixelDataType(GL_UNSIGNED_INT_10_10_10_2),
TexturePixelDataType::UnsignedInt1010102);
}
TEST_F(TextureTest, TexParameteriRejectsInvalidSwizzleValue) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// GL CTS texture_swizzle.api_errors: values outside [RED, GREEN, BLUE, ALPHA, ZERO, ONE]
// must raise GL_INVALID_ENUM through the single-value TexParameteri path.
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RGB);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, -1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, BoundTexImage2DEncodesPackedInternalShadowWords) {
// RGB10_A2 / RGB9_E5 / R11F_G11F_B10F shadow bytes hold the ES upload word; uploads from
// component client data must encode instead of raw-copying (GL CTS packed_pixels rgb10_a2,
// rgb9_e5, r11f_g11f_b10f data comparisons).
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint8 rgba8[] = {255, 0, 0, 255};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba8);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
{
const auto* stored = GetBoundTexture2DLevelBytes(texture);
Uint32 word = 0;
std::memcpy(&word, stored, sizeof(word));
EXPECT_EQ(word & 0x3FFu, 1023u); // red = 1.0
EXPECT_EQ((word >> 10) & 0x3FFu, 0u); // green = 0
EXPECT_EQ((word >> 20) & 0x3FFu, 0u); // blue = 0
EXPECT_EQ((word >> 30) & 0x3u, 3u); // alpha = 1.0
}
const Float rgb[] = {1.0f, 0.5f, 0.25f};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 1, 1, 0, GL_RGB, GL_FLOAT, rgb);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
{
const auto* stored = GetBoundTexture2DLevelBytes(texture);
Uint32 word = 0;
std::memcpy(&word, stored, sizeof(word));
EXPECT_EQ(word, MG_Util::EncodeSharedExponentRGB9E5(rgb));
}
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R11F_G11F_B10F, 1, 1, 0, GL_RGB, GL_FLOAT, rgb);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
{
const auto* stored = GetBoundTexture2DLevelBytes(texture);
Uint32 word = 0;
std::memcpy(&word, stored, sizeof(word));
const Uint32 expected = MG_Util::EncodeFloatToUnsignedF11(rgb[0]) |
(MG_Util::EncodeFloatToUnsignedF11(rgb[1]) << 11) |
(MG_Util::EncodeFloatToUnsignedF10(rgb[2]) << 22);
EXPECT_EQ(word, expected);
}
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, BoundTexImage2DDecodesPackedFloatSourceTypes) {
// 5_9_9_9_REV / 10F_11F_11F_REV client data uploaded into a component internal format must be
// decoded per texel (GL CTS packed_pixels uploads every RGB internal format with these types).
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Float rgb[] = {1.0f, 0.5f, 0.25f};
const Uint32 word = MG_Util::EncodeSharedExponentRGB9E5(rgb);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, &word);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
EXPECT_EQ(stored[0], 255); // 1.0
EXPECT_EQ(stored[1], 128); // 0.5
EXPECT_EQ(stored[2], 64); // 0.25
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, UnpackSwapBytesSwapsComponentsNotWholePixels) {
// GL_UNPACK_SWAP_BYTES on the identity-layout copy path used to reverse the whole pixel
// (4 bytes for GL_RG16), garbling multi-component rows (GL CTS packed_pixels varied_rectangle
// GL_UNPACK_SWAP_BYTES cases).
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint16 swapped[] = {0x3412, 0x7856}; // byte-swapped {0x1234, 0x5678}
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SWAP_BYTES, GL_TRUE);
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RG16, 1, 1, 0, GL_RG, GL_UNSIGNED_SHORT, swapped);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
Uint16 red = 0;
Uint16 green = 0;
std::memcpy(&red, stored, sizeof(red));
std::memcpy(&green, stored + 2, sizeof(green));
EXPECT_EQ(red, 0x1234);
EXPECT_EQ(green, 0x5678);
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE);
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) {
// GetTexImage of non-renderable formats reads the CPU shadow; the decode must cover both
// component-array and packed internal layouts.
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
const Uint8 r8[] = {128};
ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::R8, r8, 1, wide,
isInteger, isSigned));
EXPECT_FALSE(isInteger);
{
Float rgba[4];
std::memcpy(rgba, wide.data(), sizeof(rgba));
EXPECT_NEAR(rgba[0], 128.0f / 255.0f, 1e-6f);
EXPECT_EQ(rgba[1], 0.0f);
EXPECT_EQ(rgba[2], 0.0f);
EXPECT_EQ(rgba[3], 1.0f);
}
const Float rgb[] = {1.0f, 0.5f, 0.25f};
const Uint32 e5Word = MG_Util::EncodeSharedExponentRGB9E5(rgb);
ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::RGB9E5, &e5Word, 1,
wide, isInteger, isSigned));
EXPECT_FALSE(isInteger);
{
Float rgba[4];
std::memcpy(rgba, wide.data(), sizeof(rgba));
EXPECT_NEAR(rgba[0], 1.0f, 1.0f / 256.0f);
EXPECT_NEAR(rgba[1], 0.5f, 1.0f / 256.0f);
EXPECT_NEAR(rgba[2], 0.25f, 1.0f / 256.0f);
EXPECT_EQ(rgba[3], 1.0f);
}
const Uint32 uiWord = 1023u | (511u << 10) | (255u << 20) | (2u << 30); // RGB10_A2UI
ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::RGB10A2UI, &uiWord, 1,
wide, isInteger, isSigned));
EXPECT_TRUE(isInteger);
EXPECT_FALSE(isSigned);
{
Uint32 rgba[4];
std::memcpy(rgba, wide.data(), sizeof(rgba));
EXPECT_EQ(rgba[0], 1023u);
EXPECT_EQ(rgba[1], 511u);
EXPECT_EQ(rgba[2], 255u);
EXPECT_EQ(rgba[3], 2u);
}
}
@@ -911,6 +911,13 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
// Only legal to query once the extension has been seen in the loop above, hence not batched
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
if (caps.SupportsTextureFilterAnisotropy) {
GLfloat maxTextureMaxAnisotropy = 1.0f;
glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy);
caps.MaxTextureMaxAnisotropy = std::max(maxTextureMaxAnisotropy, 1.0f);
}
caps.AliasedLineWidthRangeMin = aliasedLineWidthRange[0];
caps.AliasedLineWidthRangeMax = aliasedLineWidthRange[1];
caps.SmoothLineWidthRangeMin = smoothLineWidthRange[0];
@@ -1034,6 +1034,9 @@ namespace MobileGL {
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
Bool SupportsTextureFilterAnisotropy = false;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the
// extension above is present, and left at 1.0 (no anisotropy) otherwise.
Float MaxTextureMaxAnisotropy = 1.0f;
Bool SupportsBaseInstance = false;
// GL_EXT_disjoint_timer_query is present in the extension string.
Bool SupportsDisjointTimerQuery = false;
@@ -124,6 +124,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy;
caps.SmoothLineWidthRangeMin = p.limits.lineWidthRange[0];
caps.SmoothLineWidthRangeMax = p.limits.lineWidthRange[1];
caps.SmoothLineWidthGranularity = p.limits.lineWidthGranularity;
@@ -208,6 +209,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy;
caps.SmoothLineWidthRangeMin = properties.limits.lineWidthRange[0];
caps.SmoothLineWidthRangeMax = properties.limits.lineWidthRange[1];
caps.SmoothLineWidthGranularity = properties.limits.lineWidthGranularity;
@@ -18,6 +18,9 @@ namespace MobileGL {
Int UniformBufferOffsetAlignment = 256;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. Whether it can be used at all depends on
// the samplerAnisotropy feature, which the renderer decides at device creation.
Float MaxSamplerAnisotropy = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
Float SmoothLineWidthRangeMax = 1.0f;
Float SmoothLineWidthGranularity = 1.0f;
@@ -311,6 +311,8 @@ namespace MobileGL {
return TexturePixelDataType::UnsignedInt8888;
case GL_UNSIGNED_INT_8_8_8_8_REV:
return TexturePixelDataType::UnsignedInt8888Rev;
case GL_UNSIGNED_INT_10_10_10_2:
return TexturePixelDataType::UnsignedInt1010102;
case GL_UNSIGNED_INT_10F_11F_11F_REV:
return TexturePixelDataType::UnsignedInt101111Rev;
case GL_UNSIGNED_INT_2_10_10_10_REV:
+116
View File
@@ -0,0 +1,116 @@
// MobileGL - MobileGL/MG_Util/Math/SmallFloat.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <bit>
#include <cmath>
#include <limits>
namespace MobileGL::MG_Util {
// Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa
// bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero,
// +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value
// clamp to it. The mantissa is truncated (rounding mode is implementation-defined).
inline Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) {
const Uint32 bits = std::bit_cast<Uint32>(value);
const Bool negative = (bits & 0x80000000u) != 0;
const Uint32 exponent = (bits >> 23) & 0xFFu;
const Uint32 mantissa = bits & 0x7FFFFFu;
const Uint32 exponentMask = 0x1Fu << mantissaBits;
if (exponent == 0xFFu) {
if (mantissa != 0) {
return exponentMask | 1u; // NaN keeps NaN
}
return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf
}
if (negative) {
return 0u;
}
const Int32 smallExponent = static_cast<Int32>(exponent) - 127 + 15;
if (smallExponent >= 31) { // above the largest finite value -> clamp to it
return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u);
}
if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero
const Uint32 fullMantissa = mantissa | 0x800000u;
const Int32 shift = (23 - mantissaBits) + 1 - smallExponent;
return shift > 23 ? 0u : fullMantissa >> shift;
}
return (static_cast<Uint32>(smallExponent) << mantissaBits) |
(mantissa >> (23u - static_cast<Uint32>(mantissaBits)));
}
inline Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); }
inline Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); }
// Decodes an unsigned small float (5-bit exponent, bias 15, mantissaBits mantissa bits).
inline Float DecodeUnsignedSmallFloatToFloat(Uint32 field, Int mantissaBits) {
const Uint32 exponent = (field >> mantissaBits) & 0x1Fu;
const Uint32 mantissa = field & ((1u << mantissaBits) - 1u);
const Float mantissaScale = 1.0f / static_cast<Float>(1u << mantissaBits);
if (exponent == 0) {
return std::exp2(-14.0f) * static_cast<Float>(mantissa) * mantissaScale;
}
if (exponent == 31) {
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return std::exp2(static_cast<Float>(exponent) - 15.0f) *
(1.0f + static_cast<Float>(mantissa) * mantissaScale);
}
inline Float DecodeUnsignedF11ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 6); }
inline Float DecodeUnsignedF10ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 5); }
// RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm
// (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31).
inline Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) {
constexpr Int kMantissaBits = 9;
constexpr Int kExponentBias = 15;
constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B)
Float clamped[3];
for (Int i = 0; i < 3; ++i) {
const Float v = rgb[i];
clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax);
}
const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2]));
Int sharedExponent = 0; // all-zero input keeps the all-zero word
if (maxComponent > 0.0f) {
sharedExponent = std::max(-kExponentBias - 1, static_cast<Int>(std::floor(std::log2(maxComponent)))) +
1 + kExponentBias;
const Float maxScaled = std::floor(
maxComponent / std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits)) +
0.5f);
if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead
++sharedExponent;
}
}
const Float scale = std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits));
Uint32 word = static_cast<Uint32>(sharedExponent) << 27;
for (Int i = 0; i < 3; ++i) {
const auto field = static_cast<Uint32>(std::floor(clamped[i] / scale + 0.5f));
word |= std::min(field, 511u) << (i * kMantissaBits);
}
return word;
}
// RGB9E5 shared-exponent decode.
inline void DecodeSharedExponentRGB9E5(Uint32 word, Float outRgb[3]) {
constexpr Int kMantissaBits = 9;
constexpr Int kExponentBias = 15;
const Int exponent = static_cast<Int>(word >> 27) - kExponentBias - kMantissaBits;
const Float scale = std::exp2(static_cast<Float>(exponent));
for (Int i = 0; i < 3; ++i) {
outRgb[i] = static_cast<Float>((word >> (i * kMantissaBits)) & 0x1FFu) * scale;
}
}
} // namespace MobileGL::MG_Util
+7 -3
View File
@@ -15,10 +15,10 @@ namespace MobileGL {
SizeT GetSizedInternalFormatSizeInBytes(TextureInternalFormat internal) {
switch (internal) {
case TextureInternalFormat::R8:
case TextureInternalFormat::Red: // UNorm8 shadow layout
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R3G3B2:
return 1;
case TextureInternalFormat::R16:
@@ -27,14 +27,17 @@ namespace MobileGL {
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG8:
case TextureInternalFormat::RG: // UNorm8x2 shadow layout
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::DepthComponent16:
return 2;
case TextureInternalFormat::R3G3B2: // UNorm8x3 shadow layout
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGB: // UNorm8x3 shadow layout
case TextureInternalFormat::RGB8:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::SRGB8:
@@ -43,11 +46,10 @@ namespace MobileGL {
case TextureInternalFormat::DepthComponent24:
return 3;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
case TextureInternalFormat::RGBA: // UNorm8x4 shadow layout
case TextureInternalFormat::RGBA8:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA8I:
@@ -72,6 +74,8 @@ namespace MobileGL {
return 4;
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // UNorm16x3 shadow layout
case TextureInternalFormat::RGB12: // UNorm16x3 shadow layout
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB16I:
+5 -3
View File
@@ -548,8 +548,8 @@ namespace MobileGL::MG_Util::SelfTest {
if (summary.capsValid) {
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
advertisedExtensions = JoinAdvertisedExtensions(
MG_Backend::DirectGLES::BuildAdvertisedExtensions(summary.caps.SupportsDisjointTimerQuery));
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -841,6 +841,7 @@ namespace MobileGL::MG_Util::SelfTest {
String driverVersionString; // raw hex, vendor-encoded (see RunVulkanDriverPost)
Bool shaderSubgroupUsable = false;
Bool timerQueriesSupported = false;
Bool samplerAnisotropySupported = false;
};
} // namespace
@@ -1107,6 +1108,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
} else {
@@ -1279,7 +1281,7 @@ namespace MobileGL::MG_Util::SelfTest {
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
summary.shaderSubgroupUsable, summary.timerQueriesSupported));
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -18,6 +18,7 @@
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
#include "ShaderSourceProcessor.h"
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
@@ -133,27 +134,23 @@ namespace MobileGL {
return Resources;
}
Result<SharedPtr<glslang::TShader>> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) {
auto shaderType = attrib.shaderType;
auto& sourceStr = attrib.sourceStr;
auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType);
if (lang == EShLanguage::EShLangCount) {
ResultInfo r;
r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType);
r.errc = -1;
return std::unexpected(r);
}
// One parse attempt. A glslang::TShader cannot be re-parsed, so a retry has to build a
// fresh one with byte-identical setup - hence a single factored body rather than two
// copies that could drift apart.
static Result<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType,
const String& source,
Flags<ShaderCompileBits> flags) {
SharedPtr<glslang::TShader> res;
auto& tshader = res;
tshader = MakeShared<glslang::TShader>(lang);
const char* src[] = {sourceStr.data()};
// setStrings gets no length array, so it relies on NUL termination: source must be an
// owning buffer that outlives parse(), never a StringView's substring.
const char* src[] = {source.c_str()};
tshader->setStrings(src, 1);
tshader->setNanMinMaxClamp(true);
tshader->setInvertY(true);
tshader->setPreamble("#undef VULKAN\n");
if (attrib.flags & ShaderCompileBits::CompileForOpenGL) {
if (flags & ShaderCompileBits::CompileForOpenGL) {
tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450);
tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450);
tshader->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3);
@@ -182,6 +179,39 @@ namespace MobileGL {
return res;
}
Result<SharedPtr<glslang::TShader>> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) {
auto shaderType = attrib.shaderType;
auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType);
if (lang == EShLanguage::EShLangCount) {
ResultInfo r;
r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType);
r.errc = -1;
return std::unexpected(r);
}
const String source(attrib.sourceStr);
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
if (result) return result;
// Legacy desktop sources are normalized to "#version 330 core", which parses under
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
// using e.g. layout(binding=...) without the matching #extension line compiles on real
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
// broken shader fails both attempts and keeps its original diagnostics.
String retrySource = source;
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
return result;
}
auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags);
if (!retryResult) return result;
MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460",
ConvertGLEnumToString(shaderType).c_str());
return retryResult;
}
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
for (auto& s : attrib.shaders) {
@@ -633,6 +633,21 @@ namespace MobileGL {
InjectDepthRangeBuiltinShim(stage, source);
}
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
// Re-inspect rather than searching for the literal directive: it is not necessarily at
// offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere
// must not be mistaken for the real one.
const ShaderLanguageInfo info = InspectShaderLanguage(source);
if (!info.HasVersionDirective()) return false;
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
// compatibility shaders keep whatever they declared.
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
"#version 460 core\n");
return true;
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -20,6 +20,14 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
void PreprocessShaderSource(ShaderStage stage, String& source);
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving
// the source untouched) for anything else: ES, compatibility, or an already-modern
// declaration. Exists so a shader that only parses under the laxer 460 rules - e.g. it
// uses 420-era syntax without the matching #extension line, which real drivers tend to
// accept - can be retried instead of failing to compile.
Bool RetargetLegacyVersionDirectiveTo460(String& source);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
+341 -15
View File
@@ -8,6 +8,7 @@
#include "PixelStoreProcessor.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Math/SmallFloat.h"
#include <cmath>
namespace MobileGL::MG_Util::PixelStoreProcessor {
@@ -122,13 +123,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
switch (internal) {
case TextureInternalFormat::R8: out = {1, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RG8: out = {2, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::R8:
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RG8:
case TextureInternalFormat::RG: out = {2, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RGB8:
case TextureInternalFormat::RGB:
case TextureInternalFormat::SRGB8: out = {3, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RGBA8:
case TextureInternalFormat::RGBA:
case TextureInternalFormat::SRGB8Alpha8: out = {4, ShadowComponent::UNorm8, false}; return true;
// Legacy desktop-GL sized normalized formats are stored in the closest ES-legal layout
// (see TextureFormatProcessor::NormalizePixelFormat): 8-bit unorm for <=8-bit channels,
// 16-bit unorm for 10/12-bit channels.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5: out = {3, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1: out = {4, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12: out = {3, ShadowComponent::UNorm16, false}; return true;
case TextureInternalFormat::RGBA12: out = {4, ShadowComponent::UNorm16, false}; return true;
case TextureInternalFormat::R8Snorm: out = {1, ShadowComponent::SNorm8, false}; return true;
case TextureInternalFormat::RG8Snorm: out = {2, ShadowComponent::SNorm8, false}; return true;
case TextureInternalFormat::RGB8Snorm: out = {3, ShadowComponent::SNorm8, false}; return true;
@@ -185,12 +203,76 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInternalFormat::RGBA32I: out = {4, ShadowComponent::Int32, true}; return true;
default:
// Packed internal layouts (RGB5A1, RGB10A2, RGB9E5, ...), depth/stencil and unsized formats
// keep the legacy copy path.
// Packed internal layouts (RGB10A2, RGB9E5, ...), depth/stencil and unsized formats
// have no component-array shadow layout (packed ones are handled below).
return false;
}
}
// Packed internal formats whose shadow bytes hold the ES upload word directly
// (GL_UNSIGNED_INT_2_10_10_10_REV / 5_9_9_9_REV / 10F_11F_11F_REV encoding, 4 bytes/texel).
enum class PackedInternalKind {
UNorm2101010Rev, // GL_RGB10_A2
UInt2101010Rev, // GL_RGB10_A2UI
FloatR11G11B10, // GL_R11F_G11F_B10F
FloatRGB9E5, // GL_RGB9_E5
};
struct InternalPackedLayout {
PackedInternalKind kind;
Int channelCount;
Bool isInteger;
};
Bool GetInternalPackedLayout(TextureInternalFormat internal, InternalPackedLayout& out) {
switch (internal) {
case TextureInternalFormat::RGB10A2:
out = {PackedInternalKind::UNorm2101010Rev, 4, false};
return true;
case TextureInternalFormat::RGB10A2UI:
out = {PackedInternalKind::UInt2101010Rev, 4, true};
return true;
case TextureInternalFormat::R11FG11FB10F:
out = {PackedInternalKind::FloatR11G11B10, 3, false};
return true;
case TextureInternalFormat::RGB9E5:
out = {PackedInternalKind::FloatRGB9E5, 3, false};
return true;
default:
return false;
}
}
Uint32 EncodePackedInternalWordFloat(PackedInternalKind kind, const Float rgba[4]) {
switch (kind) {
case PackedInternalKind::UNorm2101010Rev: {
const auto field = [](Float v, Uint32 maxValue) {
return static_cast<Uint32>(std::llround(std::clamp(v, 0.0f, 1.0f) * static_cast<Float>(maxValue)));
};
return field(rgba[0], 1023u) | (field(rgba[1], 1023u) << 10) | (field(rgba[2], 1023u) << 20) |
(field(rgba[3], 3u) << 30);
}
case PackedInternalKind::FloatR11G11B10:
return EncodeFloatToUnsignedF11(rgba[0]) | (EncodeFloatToUnsignedF11(rgba[1]) << 11) |
(EncodeFloatToUnsignedF10(rgba[2]) << 22);
case PackedInternalKind::FloatRGB9E5:
return EncodeSharedExponentRGB9E5(rgba);
default:
return 0;
}
}
Uint32 EncodePackedInternalWordInt(PackedInternalKind kind, const Int64 rgba[4]) {
if (kind != PackedInternalKind::UInt2101010Rev) {
return 0;
}
const auto field = [](Int64 v, Int64 maxValue) {
return static_cast<Uint32>(std::clamp<Int64>(v, 0, maxValue));
};
return field(rgba[0], 1023) | (field(rgba[1], 1023) << 10) | (field(rgba[2], 1023) << 20) |
(field(rgba[3], 3) << 30);
}
struct UnpackChannelMapping {
Int formatPosition[4]; // position of R,G,B,A within the input format's component list; -1 = missing
Int channelCount;
@@ -301,6 +383,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
SizeT inputPixelSize;
SizeT swapGroupSize; // UNPACK_SWAP_BYTES group: packed word size, or the component size
SizeT internalPixelSize;
Bool internalIsPacked;
InternalPackedLayout internalPacked;
};
// Returns true when the (format, type) -> internal-format upload needs a per-texel conversion;
@@ -309,10 +393,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Bool GetUnpackConversionSpec(TextureInternalFormat internal, TextureInputFormat format,
TexturePixelDataType type, UnpackConversionSpec& out) {
InternalShadowLayout layout{};
if (!GetInternalShadowLayout(internal, layout)) return false;
InternalPackedLayout packedInternal{};
const Bool hasComponentLayout = GetInternalShadowLayout(internal, layout);
const Bool hasPackedInternal = !hasComponentLayout && GetInternalPackedLayout(internal, packedInternal);
if (!hasComponentLayout && !hasPackedInternal) return false;
const Bool internalIsInteger = hasComponentLayout ? layout.isInteger : packedInternal.isInteger;
const Int internalChannelCount = hasComponentLayout ? layout.channelCount : packedInternal.channelCount;
UnpackChannelMapping mapping{};
if (!GetUnpackChannelMapping(format, mapping)) return false;
if (mapping.isInteger != layout.isInteger) return false; // rejected upstream; stay safe
if (mapping.isInteger != internalIsInteger) return false; // rejected upstream; stay safe
PackedTypeLayout packed{};
const Bool isPacked = GetPackedTypeLayout(type, packed);
@@ -323,6 +413,13 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
type == TexturePixelDataType::UnsignedInt8888Rev) {
return false;
}
// The client word already equals the packed internal word (memcpy fast path).
if (hasPackedInternal && type == TexturePixelDataType::UnsignedInt2101010Rev &&
(format == TextureInputFormat::RGBA || format == TextureInputFormat::RGBAInteger) &&
(packedInternal.kind == PackedInternalKind::UNorm2101010Rev ||
packedInternal.kind == PackedInternalKind::UInt2101010Rev)) {
return false;
}
} else {
ShadowComponent direct{};
const Bool hasDirect = GetDirectShadowComponentForType(type, mapping.isInteger, direct);
@@ -338,25 +435,46 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TexturePixelDataType::HalfFloat:
if (mapping.isInteger) return false; // rejected upstream
break;
case TexturePixelDataType::UnsignedInt5999Rev:
case TexturePixelDataType::UnsignedInt101111Rev:
// Packed-float RGB source words (decoded in ConvertUnpackRow); only pair with
// GL_RGB, which the state layer already enforces.
if (mapping.isInteger || mapping.channelCount != 3) return false;
// The client word already equals the packed internal word.
if (hasPackedInternal &&
((packedInternal.kind == PackedInternalKind::FloatRGB9E5 &&
type == TexturePixelDataType::UnsignedInt5999Rev) ||
(packedInternal.kind == PackedInternalKind::FloatR11G11B10 &&
type == TexturePixelDataType::UnsignedInt101111Rev))) {
return false;
}
break;
default:
return false;
}
if (hasDirect && direct == layout.component && mapping.channelCount == layout.channelCount &&
IsIdentityChannelOrder(mapping)) {
if (hasComponentLayout && hasDirect && direct == layout.component &&
mapping.channelCount == layout.channelCount && IsIdentityChannelOrder(mapping)) {
return false; // input already matches the shadow layout
}
}
out.mapping = mapping;
out.internal = layout;
out.internal = hasComponentLayout
? layout
: InternalShadowLayout{internalChannelCount, ShadowComponent::UNorm8, internalIsInteger};
out.packed = packed;
out.isPacked = isPacked;
out.type = type;
out.inputPixelSize = GetInputBytesPerPixel(format, type);
const Bool isPackedFloatWord = type == TexturePixelDataType::UnsignedInt5999Rev ||
type == TexturePixelDataType::UnsignedInt101111Rev;
out.swapGroupSize = isPacked ? static_cast<SizeT>(packed.totalBits / 8)
: GetBaseTexturePixelDataTypeSize(type);
: (isPackedFloatWord ? 4 : GetBaseTexturePixelDataTypeSize(type));
out.internalIsPacked = hasPackedInternal;
out.internalPacked = packedInternal;
out.internalPixelSize =
static_cast<SizeT>(layout.channelCount) * GetShadowComponentSize(layout.component);
hasPackedInternal ? 4
: static_cast<SizeT>(layout.channelCount) * GetShadowComponentSize(layout.component);
return true;
}
@@ -564,13 +682,36 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
rgba[ch] = DecodeComponentToInt(s + static_cast<SizeT>(pos) * srcComponentSize, conv.type);
}
}
if (conv.internalIsPacked) {
const Uint32 word = EncodePackedInternalWordInt(conv.internalPacked.kind, rgba);
Memcpy(d, &word, sizeof(word));
continue;
}
for (Int ch = 0; ch < conv.internal.channelCount; ++ch) {
EncodeShadowComponentInt(d + static_cast<SizeT>(ch) * dstComponentSize,
conv.internal.component, rgba[ch]);
}
} else {
Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
if (conv.isPacked) {
if (conv.type == TexturePixelDataType::UnsignedInt5999Rev ||
conv.type == TexturePixelDataType::UnsignedInt101111Rev) {
// Packed-float RGB source word: decode the shared-exponent / small-float fields.
Uint32 word;
Memcpy(&word, s, sizeof(word));
Float comps[3];
if (conv.type == TexturePixelDataType::UnsignedInt5999Rev) {
DecodeSharedExponentRGB9E5(word, comps);
} else {
comps[0] = DecodeUnsignedF11ToFloat(word & 0x7FFu);
comps[1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu);
comps[2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu);
}
for (Int ch = 0; ch < 4; ++ch) {
const Int pos = conv.mapping.formatPosition[ch];
if (pos < 0 || pos >= 3) continue;
rgba[ch] = comps[pos];
}
} else if (conv.isPacked) {
const Uint32 word = ReadPackedWord(s, conv.packed.totalBits);
for (Int ch = 0; ch < 4; ++ch) {
const Int pos = conv.mapping.formatPosition[ch];
@@ -587,6 +728,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
DecodeComponentToFloat(s + static_cast<SizeT>(pos) * srcComponentSize, conv.type);
}
}
if (conv.internalIsPacked) {
const Uint32 word = EncodePackedInternalWordFloat(conv.internalPacked.kind, rgba);
Memcpy(d, &word, sizeof(word));
continue;
}
for (Int ch = 0; ch < conv.internal.channelCount; ++ch) {
EncodeShadowComponentFloat(d + static_cast<SizeT>(ch) * dstComponentSize,
conv.internal.component, rgba[ch]);
@@ -687,9 +833,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
} else {
Memcpy(layerDst, layerSrc, static_cast<SizeT>(copyWidth) * pixelSize);
if (params.SwapBytes && pixelSize > 1 && !isByteType) {
MGLOG_D("%s: SwapBytes", __func__);
SwapBytes(layerDst, pixelSize, static_cast<SizeT>(copyWidth));
if (params.SwapBytes && !isByteType) {
// GL_UNPACK_SWAP_BYTES swaps within each element (component or packed
// word), never across a whole multi-component pixel.
SizeT swapGroup = GetSizedTexturePixelDataTypeSize(inputDataType);
if (swapGroup == 0) swapGroup = GetBaseTexturePixelDataTypeSize(inputDataType);
if (swapGroup > 1) {
MGLOG_D("%s: SwapBytes (group %d)", __func__, static_cast<Int>(swapGroup));
SwapBytes(layerDst, swapGroup,
static_cast<SizeT>(copyWidth) * pixelSize / swapGroup);
}
}
if (params.LSBFirst && isBitmap) {
@@ -788,4 +941,177 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return outputPixels;
}
namespace {
Float DecodeShadowComponentToFloat(const Uint8* p, ShadowComponent component) {
switch (component) {
case ShadowComponent::UNorm8:
return static_cast<Float>(*p) / 255.0f;
case ShadowComponent::SNorm8: {
Int8 v;
Memcpy(&v, p, sizeof(v));
return std::max(static_cast<Float>(v) / 127.0f, -1.0f);
}
case ShadowComponent::UNorm16: {
Uint16 v;
Memcpy(&v, p, sizeof(v));
return static_cast<Float>(v) / 65535.0f;
}
case ShadowComponent::SNorm16: {
Int16 v;
Memcpy(&v, p, sizeof(v));
return std::max(static_cast<Float>(v) / 32767.0f, -1.0f);
}
case ShadowComponent::Half: {
Uint16 v;
Memcpy(&v, p, sizeof(v));
return DecodeHalfBitsToFloat(v);
}
case ShadowComponent::Float32: {
Float v;
Memcpy(&v, p, sizeof(v));
return v;
}
default:
return 0.0f;
}
}
Int64 DecodeShadowComponentToInt(const Uint8* p, ShadowComponent component) {
switch (component) {
case ShadowComponent::UInt8:
return *p;
case ShadowComponent::Int8: {
Int8 v;
Memcpy(&v, p, sizeof(v));
return v;
}
case ShadowComponent::UInt16: {
Uint16 v;
Memcpy(&v, p, sizeof(v));
return v;
}
case ShadowComponent::Int16: {
Int16 v;
Memcpy(&v, p, sizeof(v));
return v;
}
case ShadowComponent::UInt32: {
Uint32 v;
Memcpy(&v, p, sizeof(v));
return v;
}
case ShadowComponent::Int32: {
Int32 v;
Memcpy(&v, p, sizeof(v));
return v;
}
default:
return 0;
}
}
} // namespace
Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount,
Vector<Uint8>& outWide, Bool& outIsInteger, Bool& outIsSigned) {
if (!src) return false;
const Uint8* srcBytes = static_cast<const Uint8*>(src);
InternalShadowLayout layout{};
if (GetInternalShadowLayout(internalFormat, layout)) {
const SizeT componentSize = GetShadowComponentSize(layout.component);
const SizeT srcPixelSize = static_cast<SizeT>(layout.channelCount) * componentSize;
outIsInteger = layout.isInteger;
outIsSigned = layout.component == ShadowComponent::Int8 || layout.component == ShadowComponent::Int16 ||
layout.component == ShadowComponent::Int32;
outWide.resize(pixelCount * 16);
if (layout.isInteger) {
auto* dst = reinterpret_cast<Uint32*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* s = srcBytes + i * srcPixelSize;
for (Int ch = 0; ch < 4; ++ch) {
Int64 v = ch == 3 ? 1 : 0;
if (ch < layout.channelCount) {
v = DecodeShadowComponentToInt(s + static_cast<SizeT>(ch) * componentSize,
layout.component);
}
if (outIsSigned) {
const auto out = static_cast<Int32>(v);
Memcpy(&dst[i * 4 + ch], &out, sizeof(out));
} else {
dst[i * 4 + ch] = static_cast<Uint32>(v);
}
}
}
} else {
auto* dst = reinterpret_cast<Float*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* s = srcBytes + i * srcPixelSize;
for (Int ch = 0; ch < 4; ++ch) {
Float v = ch == 3 ? 1.0f : 0.0f;
if (ch < layout.channelCount) {
v = DecodeShadowComponentToFloat(s + static_cast<SizeT>(ch) * componentSize,
layout.component);
}
dst[i * 4 + ch] = v;
}
}
}
return true;
}
InternalPackedLayout packedInternal{};
if (GetInternalPackedLayout(internalFormat, packedInternal)) {
outIsInteger = packedInternal.isInteger;
outIsSigned = false;
outWide.resize(pixelCount * 16);
if (packedInternal.isInteger) {
auto* dst = reinterpret_cast<Uint32*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
Uint32 word;
Memcpy(&word, srcBytes + i * 4, sizeof(word));
dst[i * 4 + 0] = word & 0x3FFu;
dst[i * 4 + 1] = (word >> 10) & 0x3FFu;
dst[i * 4 + 2] = (word >> 20) & 0x3FFu;
dst[i * 4 + 3] = (word >> 30) & 0x3u;
}
} else {
auto* dst = reinterpret_cast<Float*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
Uint32 word;
Memcpy(&word, srcBytes + i * 4, sizeof(word));
switch (packedInternal.kind) {
case PackedInternalKind::UNorm2101010Rev:
dst[i * 4 + 0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
break;
case PackedInternalKind::FloatR11G11B10:
dst[i * 4 + 0] = DecodeUnsignedF11ToFloat(word & 0x7FFu);
dst[i * 4 + 1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu);
dst[i * 4 + 2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu);
dst[i * 4 + 3] = 1.0f;
break;
case PackedInternalKind::FloatRGB9E5: {
Float rgb[3];
DecodeSharedExponentRGB9E5(word, rgb);
dst[i * 4 + 0] = rgb[0];
dst[i * 4 + 1] = rgb[1];
dst[i * 4 + 2] = rgb[2];
dst[i * 4 + 3] = 1.0f;
break;
}
default:
dst[i * 4 + 0] = dst[i * 4 + 1] = dst[i * 4 + 2] = 0.0f;
dst[i * 4 + 3] = 1.0f;
break;
}
}
}
return true;
}
return false;
}
} // namespace MobileGL::MG_Util::PixelStoreProcessor
@@ -22,4 +22,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
TextureInputFormat dstInputFormat, TexturePixelDataType dstDataType,
IntVec3 dimension, Bool isBitmap, SizeT& outSize);
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle);
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
// readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set
// outIsInteger (outIsSigned tells signed from unsigned). Missing channels read 0 (G/B) and
// 1 / 1.0f (A). Returns false when the format has no canonical shadow layout.
Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount,
Vector<Uint8>& outWide, Bool& outIsInteger, Bool& outIsSigned);
} // namespace MobileGL::MG_Util::PixelStoreProcessor
@@ -19,11 +19,14 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoDepthComponent32;
break;
case GL_RGBA16:
case GL_RGBA12: // stored as RGBA16 (see NormalizePixelFormat)
case GL_RG16:
case GL_R16:
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
break;
case GL_RGB16:
case GL_RGB10: // stored as RGB16 (see NormalizePixelFormat)
case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat)
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
break;
@@ -159,6 +162,30 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
}
*outInternalFormat = internalFormat;
break;
// Legacy desktop-GL sized normalized formats (GL CTS packed_pixels): ES drivers reject them
// as internal formats, so store them in the closest ES-legal format with at least the same
// per-channel precision (extra precision stays inside the CTS comparison epsilon, which is
// derived from the requested format's bit widths). The upload (format, type) below matches
// the canonical shadow layout in PixelStoreProcessor (UNorm8 / UNorm16 component arrays).
case GL_R3_G3_B2:
case GL_RGB4:
case GL_RGB5:
*outInternalFormat = GL_RGB565;
break;
case GL_RGB10:
case GL_RGB12:
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) ||
(options & PixelFormatNormalizeOptionBit::NoRgb16)
? GL_RGB32F
: GL_RGB16;
break;
case GL_RGBA2:
*outInternalFormat = GL_RGBA4;
break;
case GL_RGBA12:
*outInternalFormat =
(options & PixelFormatNormalizeOptionBit::NoNorm16) ? GL_RGBA32F : GL_RGBA16;
break;
default:
*outInternalFormat = internalFormat;
break;
@@ -276,6 +303,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outFormat = GL_RGB;
break;
case GL_SRGB8_ALPHA8:
case GL_SRGB_ALPHA:
*outFormat = GL_RGBA;
break;
@@ -294,6 +322,19 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outFormat = GL_RGBA_INTEGER;
break;
// Legacy desktop-GL sized normalized formats
case GL_R3_G3_B2:
case GL_RGB4:
case GL_RGB5:
case GL_RGB10:
case GL_RGB12:
*outFormat = GL_RGB;
break;
case GL_RGBA2:
case GL_RGBA12:
*outFormat = GL_RGBA;
break;
// Depth
case GL_DEPTH_COMPONENT16:
case GL_DEPTH_COMPONENT24:
@@ -468,20 +509,41 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outType = GL_UNSIGNED_INT_2_10_10_10_REV;
break;
case GL_RGB5_A1:
*outType = GL_UNSIGNED_SHORT_5_5_5_1;
break;
// The shadow mip keeps these formats' packed client bytes (legacy copy path),
// so the canonical transfer type must stay the packed word — the previous
// default (GL_UNSIGNED_BYTE) made the backend read 4 bytes per texel from a
// 2-byte-per-texel shadow (KHR-GL33.pixelstoragemodes teximage rgba4/rgb565
// sliced/garbled uploads).
case GL_RGBA4:
*outType = GL_UNSIGNED_SHORT_4_4_4_4;
break;
case GL_RGB565:
*outType = GL_UNSIGNED_SHORT_5_6_5;
// The shadow stores RGB5_A1 as UNorm8x4 (see PixelStoreProcessor); ES accepts
// GL_RGBA/GL_UNSIGNED_BYTE uploads for this internal format.
*outType = GL_UNSIGNED_BYTE;
break;
// Legacy desktop-GL sized normalized formats: the upload type matches the canonical
// shadow layout (UNorm8 for <=8-bit channels, UNorm16 for 10/12-bit channels).
case GL_R3_G3_B2:
case GL_RGB4:
case GL_RGB5:
case GL_RGB565:
case GL_RGBA2:
case GL_RGBA4:
*outType = GL_UNSIGNED_BYTE;
break;
case GL_RGB10:
case GL_RGB12:
*outType = (options & PixelFormatNormalizeOptionBit::NoNorm16) ||
(options & PixelFormatNormalizeOptionBit::NoRgb16)
? GL_FLOAT
: GL_UNSIGNED_SHORT;
break;
case GL_RGBA12:
*outType = (options & PixelFormatNormalizeOptionBit::NoNorm16) ? GL_FLOAT : GL_UNSIGNED_SHORT;
break;
// Unsized color formats keep their byte-per-channel client layout.
case GL_RGBA:
case GL_RGB:
case GL_RG:
case GL_RED:
case GL_SRGB:
case GL_SRGB_ALPHA:
*outType = GL_UNSIGNED_BYTE;
break;
// Depth
case GL_DEPTH_COMPONENT16:
*outType = GL_UNSIGNED_SHORT;