[Fix] (DirectGLES, MG_Util): widen three-channel formats for multisample textures

GLES has no colour-renderable three-channel format beyond RGB8, so
glTexStorage2DMultisample rejects GL_RGB16 (and the SNORM variants) with
GL_INVALID_ENUM and the texture is left with no storage at all - every draw into it
then hit GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and every read came back zero.

The existing fallback machinery could not help: it picks one replacement format per
requested format, from the driver's capabilities, and never re-checks that
replacement against the target it is going to be used with. GL_RGB16's fallback is
GL_RGB32F, which is a perfectly legal ES texture format and a perfectly illegal
multisample storage format, and with EXT_texture_norm16 present no fallback was
selected at all.

Add PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, applied only to
multisample targets, mapping GL_RGB16 to GL_RGBA32F and the three-channel SNORM
formats to GL_RGBA16F. Widening the channel count is safe precisely there and
nowhere else: a multisample texture can never be uploaded to, only rendered into, so
no transfer path has to expand three-channel client data, and the alpha a draw
writes for a three-channel source is already the 1.0 the frontend format implies.

The capability probe recomputes its fallback per target for the same reason, so the
probed format and the format the texture is actually created with stay in agreement.
This commit is contained in:
BZLZHH
2026-08-02 04:10:01 -04:00
parent 6de38c666c
commit 65dbfa6f26
4 changed files with 84 additions and 18 deletions
@@ -210,6 +210,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
}
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
}
String reason;
for (SizeT i = 0; i < reasons.size(); ++i) {
@@ -556,15 +559,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
GLESProbeFormatInfo fallbackInfo;
const Bool hasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
GLESProbeFormatInfo outerFallbackInfo;
const Bool outerHasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
if (!outerHasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
}
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
// A multisample texture can only ever be rendered into, so its storage format
// has to stay colour-renderable; the ordinary fallback for a three-channel
// format is a three-channel one, which ES accepts as a texture but rejects as
// multisample storage. Recompute the fallback per target so those formats get
// widened here and nowhere else.
Flags<PixelFormatNormalizeOptionBit> targetOptions;
if (IsGLESProbeMultisampleTarget(target)) {
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
}
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
Bool hasForcedFallback = outerHasForcedFallback;
if (targetOptions) {
hasForcedFallback = BuildFallbackProbeFormatInfo(
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions,
false, fallbackInfo);
}
}
Bool shouldProbeFallback = hasForcedFallback;
if (!hasForcedFallback) {
Bool nativeRenderable = false;
@@ -600,8 +623,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
if (!hasForcedFallback) {
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
if (!outerHasForcedFallback) {
const Bool nativeRenderbufferComplete =
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
if (nativeRenderbufferComplete) {
@@ -615,16 +638,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
shouldProbeFallbackRenderbuffer = true;
}
}
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat))) {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
}
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples);
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
}
}
}