[Fix] (DirectGLES): generate a three-channel float mip chain on the CPU

glGenerateMipmap requires the level-0 format to be colour-renderable, and ES has no
colour-renderable three-channel float format at all - so an ES driver rejects
GL_RGB16F and GL_RGB32F where every desktop driver accepts them, and the error was
forwarded to the application. KHR-GL40.texture_gather.plain-gather-float-2d-rgb and
its offset- sibling build their texture that way and fail on the leftover error alone.

The blit-based emulation already used for GL_R11F_G11F_B10F is no help: it renders
level n from level n-1, so it needs exactly the renderability that is missing. But a
format the driver cannot render into is a format nothing can have rendered into
either, which makes the frontend's own copy of the texels authoritative for precisely
these formats. So the chain is box-filtered there and the levels are marked dirty; the
backend sync that follows uploads them like any other texture data.

Deliberately narrow: only the two formats whose texels are a plain float array, and
only when they are what the texture actually holds. Every other format keeps the
driver's behaviour, error included.
This commit is contained in:
BZLZHH
2026-08-04 10:54:17 -04:00
parent ae6949e459
commit 5fce287de5
+80 -1
View File
@@ -3953,6 +3953,78 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// ES has no colour-renderable three-channel float format, and its glGenerateMipmap
// requires one, so it rejects GL_RGB16F and GL_RGB32F outright where every desktop
// driver accepts them. Both store a plain array of floats, and a format the driver
// cannot render into is a format nothing can have rendered into - so the frontend's own
// copy of the texels is the authority, and the chain can be filtered there and carried
// down by the ordinary upload path. Returns false for anything else, leaving the
// driver's answer (including its error) in place.
static Bool GenerateThreeChannelFloatMipmapOnCpu(
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (!texture) return false;
const TextureInternalFormat format = texture->GetFormat();
const Bool isHalf = format == TextureInternalFormat::RGB16F;
if (!isHalf && format != TextureInternalFormat::RGB32F) return false;
auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get());
if (mipmapTexture == nullptr) return false;
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
constexpr Int kChannels = 3;
for (const auto uploadTarget : texture->GetUploadTargets()) {
for (Uint level = 1; level < levelCount; ++level) {
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level - 1);
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return false;
auto* src = static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, level - 1));
auto* dst = static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, level));
if (src == nullptr || dst == nullptr) return false;
const SizeT componentBytes = isHalf ? sizeof(Uint16) : sizeof(Float);
const SizeT texelBytes = componentBytes * kChannels;
const auto load = [&](const Uint8* base, Int x, Int y, Int channel) {
const Uint8* texel = base + (static_cast<SizeT>(y) * srcSize.x() + x) * texelBytes +
channel * componentBytes;
if (isHalf) {
Uint16 bits = 0;
Memcpy(&bits, texel, sizeof(bits));
return MG_Util::DecodeHalfBitsToFloat(bits);
}
Float value = 0.0f;
Memcpy(&value, texel, sizeof(value));
return value;
};
// Box filter over the 2x2 source footprint, clamped where a dimension is
// already 1 (GL 4.6 core 8.14.4 leaves the exact filter to the implementation
// and this is the one it describes for power-of-two levels).
for (Int y = 0; y < dstSize.y(); ++y) {
for (Int x = 0; x < dstSize.x(); ++x) {
const Int x0 = std::min(x * 2, srcSize.x() - 1);
const Int x1 = std::min(x * 2 + 1, srcSize.x() - 1);
const Int y0 = std::min(y * 2, srcSize.y() - 1);
const Int y1 = std::min(y * 2 + 1, srcSize.y() - 1);
for (Int channel = 0; channel < kChannels; ++channel) {
const Float average = 0.25f * (load(src, x0, y0, channel) + load(src, x1, y0, channel) +
load(src, x0, y1, channel) + load(src, x1, y1, channel));
Uint8* texel = dst + (static_cast<SizeT>(y) * dstSize.x() + x) * texelBytes +
channel * componentBytes;
if (isHalf) {
const Uint16 bits = MG_Util::EncodeFloatToHalfBits(average);
Memcpy(texel, &bits, sizeof(bits));
} else {
Memcpy(texel, &average, sizeof(average));
}
}
}
}
mipmapTexture->MarkStorageDirty(uploadTarget, level, true);
}
}
return true;
}
void GenerateMipmap(GLenum target) {
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
DebugImpl::OpenGLScopeMarker marker(__func__);
@@ -3962,9 +4034,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto& slot = unit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target));
auto& texture = slot.GetBoundObject();
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
if (texture->GetFormat() == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(texture->GetFormat())) {
if (texture->GetFormat() == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(texture->GetFormat()) ||
texture->GetFormat() == TextureInternalFormat::RGB16F ||
texture->GetFormat() == TextureInternalFormat::RGB32F) {
EnsureGenerateMipmapStorageAllocated(texture);
}
// Filtered on the CPU before the backend sync, so the dirty levels ride down with it.
if (GenerateThreeChannelFloatMipmapOnCpu(texture)) {
TextureImpl::SyncTextureObjectToBackend(texture);
return;
}
auto& backendTexture = TextureImpl::SyncTextureObjectToBackend(texture);
if (IsDepthOnlyFormat(texture->GetFormat())) {