[Fix, Test] (MG_Backend/DirectGLES, MG_Util, MG_Impl): widen three-channel render targets wherever the driver refuses them

Complementary Reimagined would not load through Espryt on Mali: Iris got
GL_FRAMEBUFFER_UNSUPPORTED building its composite framebuffer, because
colortex1 is RGB8_SNORM and colortex2 is RGB16F - three-channel formats
that no real ES driver can render to (EXT_render_snorm covers R/RG/RGBA
only, and the float extensions exclude the RGB forms). The frontend's
probe cache diagnosed this correctly and then had nothing to offer: the
NoThreeChannelRenderTarget widening machinery existed but was gated to
multisample targets alone. llvmpipe turns out to refuse most of the same
attachments - CI retrace stayed green only because a replay never
branches on glCheckFramebufferStatus - so this was never a desktop-vs-
device split, just an unlit path.

The widening now applies to every color-attachable image, renderbuffers
included, riding the driver-probe branch so the native format is still
tried first and substituted only on refusal. One ThreeChannelWidening
table owns the widened (internalformat, format, type) triple per source
format - the previous per-case branches disagreed with each other and
could emit an unuploadable (RGBA16F, GL_RGB, GL_BYTE) combination or
widen into another three-channel format the driver refuses just the
same. Uploads repack three-component client data to four with the
format's own one in the alpha channel (127 is not 1 for RGB8I - the
integer arms carry integer ones); readback drops the synthetic alpha,
derived from the actual image being read, not the bound framebuffer,
so glGetTexImage through a scratch FBO cannot be confused by an
unrelated widened attachment.

Stored alpha on a widened attachment is now an invariant 1.0 rather
than an accident: the color-mask sync clears the alpha bit per draw
buffer (glColorMaski for MRT mixes), and clears route through
glClearBufferfv with alpha substituted on widened slots only -
scissored clears inherit the discipline for free, integer color
buffers keep their explicit integer-clear path, and glGet still
answers the application's own mask. GL_DST_ALPHA blending, blits and
readback therefore all see 1.0 without further interception.

DriverPost grows the rows this bug earned: EXT_color_buffer_float
detection (previously unreferenced anywhere) with a FAIL row when
absent, the missing EXT_render_snorm row, and a three-channel-
attachment row that reports one representative per widening class -
graded so a half-float-only driver warns about the 32-bit float gap
instead of being declared unsupported.

Gates: 606/606 unit at default and with the async kill switch; full
retrace, both backends - the complementary fixtures now run with the
widening ACTIVE on llvmpipe and pass with a slightly better SSIM than
before; ext caselist DirectGLES holds 3914/4867 with zero set drift
while 54 cases move from NotSupported to genuinely passing; on the
Mali-G77 device, Complementary Reimagined builds its pipeline and
renders in-world through Espryt (md5-verified build), BSL still green.
A new ThreeChannelAttachmentScenario pins the frontend answer -
COMPLETE where it used to say UNSUPPORTED - on the real driver.
This commit is contained in:
BZLZHH
2026-08-09 15:28:34 -04:00
parent 0f394fa46f
commit d8d7530011
17 changed files with 2066 additions and 143 deletions
@@ -210,7 +210,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES"); reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
} }
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no three-channel multisample storage format on OpenGL ES"); reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
} }
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) { if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
reasons.push_back("EXT_render_snorm not supported"); reasons.push_back("EXT_render_snorm not supported");
@@ -553,26 +553,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) { for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex); const auto target = static_cast<TextureTarget>(targetIndex);
// A multisample texture can only ever be rendered into, so its storage format // Colour-attachable targets need a colour-renderable fallback; the ordinary
// has to stay colour-renderable; the ordinary fallback for a three-channel // fallback for a three-channel format is another three-channel one, which ES
// format is a three-channel one, which ES accepts as a texture but rejects as // accepts as a texture but never as an attachment. Recompute the fallback per
// multisample storage. Recompute the fallback per target so those formats get // target so those formats get widened where the target demands it.
// widened here and nowhere else. const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
Flags<PixelFormatNormalizeOptionBit> targetOptions; TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex);
if (IsGLESProbeMultisampleTarget(target)) { // Multisample storage has no three-channel form on ES at all, so its widening
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; // is unconditional and skips the native probe (which cannot succeed). Every
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) { // other target keeps the widening on the DRIVER branch, behind the native
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget; // probe: `shouldProbeFallback = !nativeCreated || !nativeRenderable` below is
} // what makes the substitution conditional on the driver actually refusing, so
} // a driver that does render to a three-channel image keeps allocating it byte
// for byte. That is a per-format runtime answer, NOT a desktop-vs-device
// split: llvmpipe renders to GL_RGB16F but refuses GL_RGB8_SNORM, GL_SRGB8,
// GL_RGB32F and the RGB integer formats, so the CI driver widens those eight
// too. Re-run the retrace fixtures and the glcts suites on any change here.
const Bool widenUnconditionally = IsGLESProbeMultisampleTarget(target);
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo; GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
Bool hasForcedFallback = outerHasForcedFallback; Bool hasForcedFallback = outerHasForcedFallback;
if (targetOptions) { if (renderTargetOptions) {
hasForcedFallback = BuildFallbackProbeFormatInfo( // Folded into the forced options only when a forced fallback already
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo); // applies, so the render-target bits never *create* one: ANGLE's forced
if (!hasForcedFallback) { // GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions, false, // widening, but a non-ANGLE driver must not lose its native probe.
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
(outerHasForcedFallback || widenUnconditionally) ? forcedOptions | renderTargetOptions
: forcedOptions;
hasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions, true,
fallbackInfo); fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat,
driverOptions | renderTargetOptions, false, fallbackInfo);
}
// HONEST STATUS OF THE FORCED PATH. A forced fallback is only ever built
// for ANGLE (GetForcedPixelFormatNormalizeOptions returns nothing for any
// other renderer), and it SKIPS the native probe entirely - the widened
// format is asserted rather than measured on this device. That assertion
// is validated on exactly one configuration, the android-angle retrace
// golden; it is NOT covered by the headless llvmpipe suites, which take
// the driver branch below and prove nothing about ANGLE's answers. So log
// the choice at INFO rather than the usual MGLOG_D caveat: on any other
// ANGLE device the device report is the only evidence there is of which
// storage format the image really got. Once per format on the ordinary 2D
// target - repeating it for all ten targets would bury the report.
if (hasForcedFallback && target == TextureTarget::Texture2D &&
(MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(
requestedInternalFormat, renderTargetOptions) &
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)) {
MGLOG_I("Three-channel widening (FORCED path, no native probe): %s stored as %s. "
"Reason: %s. Device-validated on the android-angle golden only.",
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str(),
fallbackInfo.Reason.c_str());
} }
} }
@@ -618,8 +652,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex(); const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback; // A renderbuffer exists only to be attached, so it needs the same three-channel
if (!outerHasForcedFallback) { // widening the colour-attachable texture targets get - and on the same terms: the
// native storage is probed first, so a driver that renders to it keeps it.
const Flags<PixelFormatNormalizeOptionBit> renderbufferOptions =
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, renderbufferTargetIndex);
GLESProbeFormatInfo renderbufferFallbackInfo = outerFallbackInfo;
Bool renderbufferHasForcedFallback = outerHasForcedFallback;
if (renderbufferOptions) {
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
outerHasForcedFallback ? forcedOptions | renderbufferOptions : forcedOptions;
renderbufferHasForcedFallback = BuildFallbackProbeFormatInfo(
requestedInternalFormat, forcedProbeOptions, true, renderbufferFallbackInfo);
if (!renderbufferHasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderbufferOptions,
false, renderbufferFallbackInfo);
}
}
Bool shouldProbeFallbackRenderbuffer = renderbufferHasForcedFallback;
if (!renderbufferHasForcedFallback) {
const Bool nativeRenderbufferComplete = const Bool nativeRenderbufferComplete =
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1); ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
if (nativeRenderbufferComplete) { if (nativeRenderbufferComplete) {
@@ -633,16 +685,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
shouldProbeFallbackRenderbuffer = true; shouldProbeFallbackRenderbuffer = true;
} }
} }
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL && if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) { ProbeRenderbuffer(gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex, if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat))) { GetRenderbufferFeatureCaps(logicalFormat))) {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo); LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
} }
const Int maxSamples = const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat); GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples); gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
} }
} }
} }
+222 -40
View File
@@ -1401,7 +1401,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
if (currentFBO == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) { if (currentFBO == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
// Default FBO, nothing to sync // Default FBO, nothing to sync - except the widened-attachment mask, which is
// only ever WRITTEN by SyncToBackend and would otherwise still describe the
// user FBO that was draw-bound before. The window surface is a real RGBA
// buffer, so nothing here is ever widened.
if (target == FramebufferTarget::Draw) {
g_alphaWidenedDrawBufferMask = 0;
g_integerColorDrawBufferMask = 0;
}
StampSyncedFBO(target, slotVersion, objectVersion, currentPtr); StampSyncedFBO(target, slotVersion, objectVersion, currentPtr);
continue; continue;
} }
@@ -1459,20 +1466,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
// direct_state_access.renderbuffers_storage. One unconditional push settles the whole // direct_state_access.renderbuffers_storage. One unconditional push settles the whole
// block rather than the one cap that happened to be noticed. // block rather than the one cap that happened to be noticed.
static Bool g_forceFullRenderStateResync = true; static Bool g_forceFullRenderStateResync = true;
// Which draw buffers' alpha channel the colour mask last pushed to the driver had forced
// OFF - i.e. the value of `appliedWidenMask` in the last SyncRenderState that reached the
// colour-mask block. NOT derivable from the frontend parameter block: it depends on the
// bound DRAW framebuffer's attachment formats and on whether the caller is a draw or a
// clear, neither of which bumps the frontend render-state version. Without it a
// clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits
// the clear's undoctored mask.
static Uint32 g_syncedColorMaskAlphaWidenMask = 0;
void InvalidateSyncedRenderState() { void InvalidateSyncedRenderState() {
g_forceFullRenderStateResync = true; g_forceFullRenderStateResync = true;
g_hasSyncedRenderState = false; g_hasSyncedRenderState = false;
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1); g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1); g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
} }
void SyncRenderState() { void SyncRenderState(Bool forColorClear) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion(); Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
const Bool forceFullPush = g_forceFullRenderStateResync; const Bool forceFullPush = g_forceFullRenderStateResync;
g_forceFullRenderStateResync = false; g_forceFullRenderStateResync = false;
if (!forceFullPush && g_hasSyncedRenderState && currentRenderStateVersion == g_syncedRenderStateVersion) { // The alpha discipline for widened colour attachments (see the header comment on
// SyncRenderState): a DRAW must not be able to move the stored alpha off 1.0, a CLEAR
// is what puts it there. So the draw path masks alpha off on every widened draw
// buffer and the clear path masks nothing.
const Uint32 appliedWidenMask = forColorClear ? 0u : FramebufferImpl::g_alphaWidenedDrawBufferMask;
const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask;
if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState &&
currentRenderStateVersion == g_syncedRenderStateVersion) {
return; return;
} }
@@ -1778,23 +1800,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
if (tailSpanDirty) { // Color mask. Uniform masks use the non-indexed glColorMask (works everywhere); divergent if (tailSpanDirty || colorMaskWidenDirty) { // Color mask. Uniform masks use the non-indexed glColorMask
// per-draw-buffer masks use the indexed glColorMaski when draw_buffers_indexed is // (works everywhere); divergent per-draw-buffer masks use the indexed glColorMaski when
// available, otherwise fall back to broadcasting draw buffer 0. Mirrors the blend block. // draw_buffers_indexed is available, otherwise fall back to broadcasting draw buffer 0.
// Mirrors the blend block.
using FBO = MG_State::GLState::FramebufferObject; using FBO = MG_State::GLState::FramebufferObject;
const auto& targetMasks = parameters.ColorMasks; const auto& targetMasks = parameters.ColorMasks;
const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks; const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks;
Bool anyDirty = forceFullPush; // What the DRIVER is told for draw buffer i. Identical to the application's mask
// except on a widened attachment during a draw, where alpha is forced off; the
// frontend's own array is never written, so glGet(GL_COLOR_WRITEMASK) keeps
// answering with the application's value.
const auto driverMask = [&](Uint i) -> BoolVec4 {
BoolVec4 m = targetMasks[i];
if (i < 32 && (appliedWidenMask & (1u << i)) != 0) {
m.w() = false;
}
return m;
};
Bool anyDirty = forceFullPush || colorMaskWidenDirty;
Bool allSame = true; Bool allSame = true;
const BoolVec4 driverMask0 = driverMask(0);
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (targetMasks[i] != syncedMasks[i]) anyDirty = true; if (targetMasks[i] != syncedMasks[i]) anyDirty = true;
if (i > 0 && targetMasks[i] != targetMasks[0]) allSame = false; if (i > 0 && driverMask(i) != driverMask0) allSame = false;
} }
if (anyDirty) { if (anyDirty) {
if (allSame || !g_GLESCapabilities.SupportsIndexedColorMask) { if (allSame || !g_GLESCapabilities.SupportsIndexedColorMask) {
const BoolVec4& m = targetMasks[0]; // Without draw_buffers_indexed there is only one mask for the whole
// framebuffer, so a widened draw buffer 0 costs every other buffer its
// alpha writes. ES 3.2 makes glColorMaski core and ES 3.1 has it as
// EXT/OES; the only devices that reach this line are ES 3.0-class, where
// MRT with a mixed widened/native colour attachment set is already rare.
const BoolVec4& m = driverMask0;
g_GLESFuncs.glColorMask(ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()), g_GLESFuncs.glColorMask(ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
ToGLBoolean(m.w())); ToGLBoolean(m.w()));
} else { } else {
@@ -1802,14 +1843,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT : g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
: g_GLESFuncs.glColorMaskiOES; : g_GLESFuncs.glColorMaskiOES;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (forceFullPush || targetMasks[i] != syncedMasks[i]) { // colorMaskWidenDirty forces every slot: the previous push may have
const BoolVec4& m = targetMasks[i]; // been the non-indexed glColorMask above (which set all of them), and
// the per-slot diff below only knows about the application's array.
if (forceFullPush || colorMaskWidenDirty || targetMasks[i] != syncedMasks[i]) {
const BoolVec4 m = driverMask(i);
colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()), colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
ToGLBoolean(m.w())); ToGLBoolean(m.w()));
} }
} }
} }
} }
g_syncedColorMaskAlphaWidenMask = appliedWidenMask;
} }
if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present. if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present.
@@ -2089,6 +2134,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (!framebuffer || framebuffer == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) { if (!framebuffer || framebuffer == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
// Same reset as SyncCurrentFBO's default-framebuffer branch: SyncToBackend is the
// only writer of the widened-attachment mask, so a path that skips it has to say so
// explicitly. It matters here because the DSA clears and glBlitFramebuffer briefly
// sync a DIFFERENT framebuffer as DRAW and then restore the application's through
// ForceBindCurrentFBO - which lands right here when that one is the default.
if (target == FramebufferTarget::Draw) {
FramebufferImpl::g_alphaWidenedDrawBufferMask = 0;
FramebufferImpl::g_integerColorDrawBufferMask = 0;
}
FramebufferImpl::BindFramebufferId( FramebufferImpl::BindFramebufferId(
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
return; return;
@@ -3009,7 +3063,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif #endif
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
FramebufferImpl::SyncCurrentFBO(); FramebufferImpl::SyncCurrentFBO();
RenderStateImpl::SyncRenderState(); // A colour clear is exactly the operation that is allowed to write a widened
// attachment's alpha - it is what puts the 1.0 there that every later draw is masked
// away from. SyncCurrentFBO ran first, so g_alphaWidenedDrawBufferMask already describes
// the framebuffer this clear will land on.
RenderStateImpl::SyncRenderState(/*forColorClear=*/(mask & GL_COLOR_BUFFER_BIT) != 0);
BindCurrentFBO(FramebufferTarget::Draw); BindCurrentFBO(FramebufferTarget::Draw);
@@ -3047,10 +3105,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor; const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor;
const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f || const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f ||
cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f; cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f;
// A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE
// clear colour to every draw buffer - so a framebuffer that mixes a widened
// attachment with a native one cannot be served by doctoring glClearColor. Take the
// same per-draw-buffer glClearBufferfv route the out-of-range case already uses and
// substitute the alpha only where it belongs. Scissor and the colour write mask apply
// to glClearBufferfv exactly as they do to glClear, so a scissored clear stays
// scissored and an application that masked alpha off still gets its way (the storage
// then keeps the 1.0 an earlier clear left, which is the same answer).
//
// glClearBufferfv on an INTEGER colour buffer is GL_INVALID_OPERATION, so a
// framebuffer with one of those as a draw buffer keeps plain glClear - which ES
// leaves undefined for integer colour buffers anyway, and which an application that
// wants a defined answer must replace with glClearBufferuiv/iv (those DO substitute
// the widened alpha). The out-of-range trigger is left exactly as it was.
const Uint32 widenedDrawBuffers = FramebufferImpl::g_alphaWidenedDrawBufferMask;
const Bool widenedColorClear =
widenedDrawBuffers != 0 && FramebufferImpl::g_integerColorDrawBufferMask == 0;
GLint clearDrawFbo = 0; GLint clearDrawFbo = 0;
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo); g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo);
if (outOfRange && clearDrawFbo != 0) { if ((outOfRange || widenedColorClear) && clearDrawFbo != 0) {
const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), cc.w()};
GLint maxDrawBuffers = 0; GLint maxDrawBuffers = 0;
GLint clearedCount = 0; GLint clearedCount = 0;
GLint firstDb = -1; GLint firstDb = -1;
@@ -3060,6 +3134,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast<GLenum>(i), &db); g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast<GLenum>(i), &db);
if (i == 0) firstDb = db; if (i == 0) firstDb = db;
if (db != GL_NONE) { if (db != GL_NONE) {
const Bool widened = i < 32 && (widenedDrawBuffers & (1u << i)) != 0;
const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), widened ? 1.0f : cc.w()};
g_GLESFuncs.glClearBufferfv(GL_COLOR, i, value); g_GLESFuncs.glClearBufferfv(GL_COLOR, i, value);
++clearedCount; ++clearedCount;
} }
@@ -5316,37 +5392,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glClearBufferfi(buffer, drawbuffer, depth, stencil); g_GLESFuncs.glClearBufferfi(buffer, drawbuffer, depth, stencil);
} }
namespace {
using FramebufferImpl::SubstituteWidenedClearAlpha;
// A colour attachment the backend widened from three channels to four has to end up
// holding alpha 1.0 - the value GL reports for a channel the application's format does
// not have - so an explicit per-buffer clear of it writes 1.0 rather than whatever the
// application passed (SubstituteWidenedClearAlpha, in Managers.h). Draws can never move
// it again: their alpha write mask is forced off, see SyncRenderState. That pairing is
// what makes GL_DST_ALPHA blending, glReadPixels and glBlitFramebuffer all see the right
// value without any of them being intercepted.
// Whether draw buffer `drawbuffer` of the framebuffer currently bound as DRAW is such an
// attachment. Answered from the mask SyncCurrentFBO just recomputed, so it costs nothing.
Bool IsWidenedBoundDrawBuffer(GLenum buffer, GLint drawbuffer) {
return buffer == GL_COLOR && drawbuffer >= 0 && drawbuffer < 32 &&
(FramebufferImpl::g_alphaWidenedDrawBufferMask & (1u << drawbuffer)) != 0;
}
// The same question for an explicitly named framebuffer (the DSA clears), which is NOT
// the one g_alphaWidenedDrawBufferMask describes at the point these run.
Bool IsWidenedNamedDrawBuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer) {
using FBO = MG_State::GLState::FramebufferObject;
if (buffer != GL_COLOR || !framebuffer || drawbuffer < 0 ||
drawbuffer >= static_cast<GLint>(FBO::MAX_DRAW_BUFFERS)) {
return false;
}
const auto frontendBuf = framebuffer->GetDrawBuffers()[static_cast<SizeT>(drawbuffer)];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
return false;
}
return FramebufferImpl::IsAlphaWidenedColorAttachment(framebuffer->GetAttachment(frontendBuf));
}
} // namespace
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
FramebufferImpl::SyncCurrentFBO(); FramebufferImpl::SyncCurrentFBO();
RenderStateImpl::SyncRenderState(); RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
BindCurrentFBO(FramebufferTarget::Draw); BindCurrentFBO(FramebufferTarget::Draw);
g_GLESFuncs.glClearBufferfv(buffer, drawbuffer, value); GLfloat widenedValue[4] = {};
g_GLESFuncs.glClearBufferfv(
buffer, drawbuffer,
SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), 1.0f, widenedValue));
} }
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
FramebufferImpl::SyncCurrentFBO(); FramebufferImpl::SyncCurrentFBO();
RenderStateImpl::SyncRenderState(); RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
// SyncCurrentFBO early-outs for the default framebuffer, so without this // SyncCurrentFBO early-outs for the default framebuffer, so without this
// bind a user-FBO -> default-FBO switch would leave the clear landing on // bind a user-FBO -> default-FBO switch would leave the clear landing on
// the stale driver DRAW binding (the fi/fv/uiv siblings all bind too). // the stale driver DRAW binding (the fi/fv/uiv siblings all bind too).
BindCurrentFBO(FramebufferTarget::Draw); BindCurrentFBO(FramebufferTarget::Draw);
g_GLESFuncs.glClearBufferiv(buffer, drawbuffer, value); GLint widenedValue[4] = {};
g_GLESFuncs.glClearBufferiv(
buffer, drawbuffer,
SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLint(1), widenedValue));
} }
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
FramebufferImpl::SyncCurrentFBO(); FramebufferImpl::SyncCurrentFBO();
RenderStateImpl::SyncRenderState(); RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
BindCurrentFBO(FramebufferTarget::Draw); BindCurrentFBO(FramebufferTarget::Draw);
g_GLESFuncs.glClearBufferuiv(buffer, drawbuffer, value); GLuint widenedValue[4] = {};
g_GLESFuncs.glClearBufferuiv(
buffer, drawbuffer,
SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLuint(1), widenedValue));
} }
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
@@ -5355,9 +5476,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::OpenGLScopeMarker marker(__func__); DebugImpl::OpenGLScopeMarker marker(__func__);
#endif #endif
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
RenderStateImpl::SyncRenderState(); RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true); SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
GLfloat widenedValue[4] = {};
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer), 1.0f,
widenedValue);
g_GLESFuncs.glClearBufferfv(buffer, drawbuffer, value); g_GLESFuncs.glClearBufferfv(buffer, drawbuffer, value);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
@@ -5389,9 +5513,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::OpenGLScopeMarker marker(__func__); DebugImpl::OpenGLScopeMarker marker(__func__);
#endif #endif
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
RenderStateImpl::SyncRenderState(); RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true); SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
GLint widenedValue[4] = {};
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer),
GLint(1), widenedValue);
g_GLESFuncs.glClearBufferiv(buffer, drawbuffer, value); g_GLESFuncs.glClearBufferiv(buffer, drawbuffer, value);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
@@ -5406,9 +5533,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::OpenGLScopeMarker marker(__func__); DebugImpl::OpenGLScopeMarker marker(__func__);
#endif #endif
TextureImpl::SyncNeccessaryTextures(); TextureImpl::SyncNeccessaryTextures();
RenderStateImpl::SyncRenderState(); RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true); SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
GLuint widenedValue[4] = {};
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer),
GLuint(1), widenedValue);
g_GLESFuncs.glClearBufferuiv(buffer, drawbuffer, value); g_GLESFuncs.glClearBufferuiv(buffer, drawbuffer, value);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
@@ -5681,16 +5811,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
format == GL_RGBA_INTEGER; format == GL_RGBA_INTEGER;
} }
// Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA // The bit pattern of 1.0 in a wide-read component type: what GL reports for a channel the
// layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in // attachment's format does not have.
// the source component type. static void FillWideReadOneBits(GLenum componentType, Uint8* oneBits) {
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) { switch (componentType) {
case GL_UNSIGNED_BYTE: case GL_UNSIGNED_BYTE:
oneBits[0] = 0xFF; oneBits[0] = 0xFF;
@@ -5727,6 +5850,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
default: default:
break; break;
} }
}
// Overwrites the alpha of a 4-channel wide read with the format's implied 1.0. Used for an
// attachment the backend widened from three channels to keep it colour-renderable: the storage
// has a real alpha channel holding whatever the draw wrote, but the format the application
// asked for has none, and GL reads a missing channel back as one.
static void ForceWideReadAlphaToOne(Vector<Uint8>& data, SizeT pixelCount, GLenum componentType) {
const SizeT componentSize = GetReadbackComponentSize(componentType);
if (componentSize == 0 || data.size() < pixelCount * 4 * componentSize) {
return;
}
Uint8 oneBits[4] = {0, 0, 0, 0};
FillWideReadOneBits(componentType, oneBits);
for (SizeT i = 0; i < pixelCount; ++i) {
Memcpy(data.data() + (i * 4 + 3) * componentSize, oneBits, componentSize);
}
}
// 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};
FillWideReadOneBits(componentType, oneBits);
Vector<Uint8> expanded(pixelCount * 4 * componentSize); Vector<Uint8> expanded(pixelCount * 4 * componentSize);
for (SizeT i = 0; i < pixelCount; ++i) { for (SizeT i = 0; i < pixelCount; ++i) {
@@ -5771,9 +5923,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's // 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 // (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. // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op.
// `forceOpaqueAlpha`: the source image is a three-channel format the backend widened to four to
// keep it colour-renderable, so its alpha channel holds whatever the draw wrote and has to be
// answered with the 1.0 the application's format implies. Passed in rather than derived here:
// glReadPixels reads the bound READ framebuffer, but glGetTexImage reads a texture through a
// scratch framebuffer, so the frontend's READ binding describes a different image entirely -
// consulting it there would both miss real widenings and corrupt readbacks of ordinary
// textures taken while some unrelated widened attachment happened to be bound.
static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
GLenum type, void* pixels, Bool honorPackImageParams = false, GLenum type, void* pixels, Bool honorPackImageParams,
Bool applyFixedPointReadClamp = true) { Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha) {
ReadbackChannelMapping mapping{}; ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) { if (!GetReadbackChannelMapping(format, mapping)) {
return false; return false;
@@ -5900,6 +6059,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType); ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType);
} }
// Undo the three-channel widening (see the parameter's comment). Deliberately not gated on
// applyFixedPointReadClamp: that flag implements GL_CLAMP_READ_COLOR, which glGetTexImage
// is exempt from, whereas "a format without alpha reads as 1.0" is the format's own
// semantics and applies to every read.
if (forceOpaqueAlpha) {
ForceWideReadAlphaToOne(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), wideType);
}
// GL clamps a read from a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR // GL clamps a read from a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR
// defaults to GL_FIXED_ONLY). Formats the backend substitutes with a floating-point // defaults to GL_FIXED_ONLY). Formats the backend substitutes with a floating-point
// one keep the out-of-range value the app stored, so apply the clamp here - a // one keep the out-of-range value the app stored, so apply the clamp here - a
@@ -6058,11 +6225,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so // 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). // it always takes the conversion path (which swaps on the CPU).
const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes; const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;
const Bool nativeFastPair = !packSwapBytes && // The read buffer is what glReadPixels reads, so the frontend's READ binding is exactly
// the right thing to ask here.
const Bool forceOpaqueAlpha = FramebufferImpl::IsAlphaWidenedFallbackReadAttachment();
// An attachment widened from three channels to stay colour-renderable also has to leave
// the fast pair: only the conversion path knows to answer its alpha with the 1.0 the
// application's format implies instead of whatever the draw wrote into the added channel.
const Bool nativeFastPair = !packSwapBytes && !forceOpaqueAlpha &&
((format == GL_RGBA && type == GL_UNSIGNED_BYTE) || ((format == GL_RGBA && type == GL_UNSIGNED_BYTE) ||
(format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT))); (format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT)));
if (convertible && !nativeFastPair) { if (convertible && !nativeFastPair) {
if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) { if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels,
/*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true,
forceOpaqueAlpha)) {
MGLOG_D("ReadPixels: finished via client-format conversion"); MGLOG_D("ReadPixels: finished via client-format conversion");
return; return;
} }
@@ -6121,7 +6296,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: native read of %s/%s failed (%s), retrying via client-format conversion", MGLOG_D("ReadPixels: native read of %s/%s failed (%s), retrying via client-format conversion",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(),
MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); MG_Util::ConvertGLEnumToString(nativeReadError).c_str());
if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) { if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels,
/*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true,
forceOpaqueAlpha)) {
MGLOG_D("ReadPixels: finished via client-format conversion after native failure"); MGLOG_D("ReadPixels: finished via client-format conversion after native failure");
return; return;
} }
@@ -6302,6 +6479,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// for normalized attachments), while the conversion path reads a wide format that is always // for normalized attachments), while the conversion path reads a wide format that is always
// accepted and repacks on the CPU. // accepted and repacks on the CPU.
if (convertible) { if (convertible) {
// The image being read is this texture, not whatever the application left bound to
// GL_READ_FRAMEBUFFER, so the widening question has to be asked of the texture.
const Bool forceOpaqueAlpha =
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image // GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
// readbacks (cube-map arrays address as arrays); 2D targets must ignore // readbacks (cube-map arrays address as arrays); 2D targets must ignore
// them (GL 3.3 section 6.1.4). // them (GL 3.3 section 6.1.4).
@@ -6347,7 +6528,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset; void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset;
if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst, if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst,
/*honorPackImageParams=*/false, /*honorPackImageParams=*/false,
/*applyFixedPointReadClamp=*/false)) { /*applyFixedPointReadClamp=*/false, forceOpaqueAlpha)) {
allSlicesRead = false; allSlicesRead = false;
break; break;
} }
@@ -6369,7 +6550,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels, if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels,
applyPackImageParams, applyPackImageParams,
/*applyFixedPointReadClamp=*/false)) { /*applyFixedPointReadClamp=*/false,
forceOpaqueAlpha)) {
MGLOG_D("GetTexImage: finished via client-format conversion"); MGLOG_D("GetTexImage: finished via client-format conversion");
return; return;
} }
@@ -188,6 +188,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
void OnBackendContextDestroyed(); void OnBackendContextDestroyed();
} // namespace XfbImpl } // namespace XfbImpl
namespace RenderStateImpl {
// Pushes the frontend's render-state block to the ES driver, diffed against what was
// last pushed.
//
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
// mask handed to the driver. A draw into a colour attachment the backend widened from
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
// move the stored alpha away from the 1.0 the application's three-channel format
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
// gets there in the first place, so it must be allowed to write alpha - hence the flag
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
// frontend version.
//
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
} // namespace RenderStateImpl
extern MG_External::EGLFunctionsTable g_EGLFuncs; extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs; extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities; extern MG_External::GLESCapabilities g_GLESCapabilities;
+319 -13
View File
@@ -1869,6 +1869,154 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
// Components per texel the frontend format's client data carries. Only the three-channel
// formats that can be widened to a four-channel render target need an answer (see
// PrepareChannelWidenedUpload); everything else keeps its own layout and reports 0.
Uint GetWidenableClientComponentCount(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::SRGB8:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
return 3;
default:
return 0;
}
}
// True when the widened format's client data is integer rather than normalized. The two
// classes share every narrow component type - GL_RGB8I and GL_RGB8_SNORM are both uploaded
// as GL_BYTE - but their "1.0" differs: an integer channel's one is the integer 1, a
// normalized channel's is the saturated field. The type alone cannot tell them apart, so
// the source format has to.
Bool IsIntegerWidenableFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
return true;
default:
return false;
}
}
// The bit pattern of 1.0 in an upload component type: what a format without alpha reads
// back as, and therefore what the synthetic fourth channel of a widened render target has
// to hold. Integer components carry the integer one, not a saturated field - and since
// GL_BYTE/GL_SHORT/GL_UNSIGNED_BYTE/GL_UNSIGNED_SHORT serve both classes, `integerData`
// is what decides, not the type.
static Bool GetUploadComponentOneBits(GLenum uploadType, Bool integerData, Uint8* outOneBits,
SizeT* outComponentSize) {
switch (uploadType) {
case GL_BYTE: {
const Int8 one = integerData ? Int8(1) : Int8(0x7F);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_BYTE: {
const Uint8 one = integerData ? Uint8(1) : Uint8(0xFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_SHORT: {
const Int16 one = integerData ? Int16(1) : Int16(0x7FFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_SHORT: {
const Uint16 one = integerData ? Uint16(1) : Uint16(0xFFFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_HALF_FLOAT: {
const Uint16 one = 0x3C00; // half 1.0
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_FLOAT: {
const Float one = 1.0f;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_INT: {
const Int32 one = 1;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_INT: {
const Uint32 one = 1;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
default:
return false;
}
}
// A three-channel format widened to four to keep a colour attachment renderable (see
// NormalizePixelFormat) is described to the driver as a four-component transfer, so the
// three-component client data has to be repacked with an alpha of 1.0 - otherwise the
// driver walks three texels' worth of data per four-texel row and the image shears.
// `componentCount` is the SOURCE component count and `byteSize` the source's size, so this
// runs after any type conversion (which keeps the component count) has already happened.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum uploadType,
Vector<Uint8>& widenedData, Bool integerData) {
Uint8 oneBits[8] = {};
SizeT componentSize = 0;
if (componentCount != 3 || data == nullptr || byteSize == 0 ||
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
return data;
}
const SizeT srcTexelBytes = componentSize * componentCount;
// Sized from the level, never from the source: the driver reads a full
// width*height*depth*4 components for the transfer it was handed, so a source that
// somehow holds fewer texels must still leave a full destination behind (its tail
// reads as transparent black with the format's implied opaque alpha) rather than a
// short buffer the driver would run off the end of.
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / srcTexelBytes);
widenedData.assign(texelCount * componentSize * 4, 0);
const auto* src = static_cast<const Uint8*>(data);
Uint8* dst = widenedData.data();
for (SizeT i = 0; i < texelCount; ++i, dst += componentSize * 4) {
if (i < copyTexelCount) {
Memcpy(dst, src, srcTexelBytes);
src += srcTexelBytes;
}
Memcpy(dst + srcTexelBytes, oneBits, componentSize);
}
return widenedData.data();
}
static const void* PrepareNormFloatFallbackUpload(TextureInternalFormat format, static const void* PrepareNormFloatFallbackUpload(TextureInternalFormat format,
const IntVec3& texelSize, const IntVec3& texelSize,
const void* data, const void* data,
@@ -1914,6 +2062,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
return convertedData.data(); return convertedData.data();
} }
// The two shadow -> upload conversions a fallback storage format can need, in order:
// the component type first (SNORM/UNORM shadows into the float the fallback stores), then
// the component count (three-channel client data into a four-channel widened render
// target). They compose: GL_RGB8_SNORM on a driver with no renderable three-channel
// format becomes GL_RGBA16F, so its Int8x3 shadow is converted to Float x3 and then
// repacked as Float x4 with alpha 1.0.
//
// Both scratch buffers belong to the caller so they outlive the returned pointer; the
// return value is `data` itself whenever neither conversion applies, which is what the
// sub-rect upload fast path tests for.
static const void* PrepareFallbackUpload(TextureInternalFormat format, TextureTarget target,
const IntVec3& texelSize, const void* data, SizeT byteSize,
GLenum uploadType, Vector<Float>& convertedData,
Vector<Uint8>& widenedData) {
const void* uploadData =
PrepareNormFloatFallbackUpload(format, texelSize, data, byteSize, uploadType, convertedData);
// The component-count switch first: it rules out every format that cannot be widened
// (which is nearly all of them, including GL_RGBA8) without touching the capability
// cache, so an ordinary atlas upload does not pay for a per-level cache lookup.
const Uint componentCount = GetWidenableClientComponentCount(format);
if (componentCount == 0 || !TextureImpl::BackendTextureFormatAddsAlpha(format, target)) {
return uploadData;
}
// The type conversion above rewrites the level into `convertedData` at four bytes per
// component while keeping the component count, so the widening's source size is that
// buffer's, not the shadow's.
const SizeT uploadByteSize = (!convertedData.empty() && uploadData == convertedData.data())
? convertedData.size() * sizeof(Float)
: byteSize;
return PrepareChannelWidenedUpload(componentCount, texelSize, uploadData, uploadByteSize, uploadType,
widenedData, IsIntegerWidenableFormat(format));
}
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE // RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is // leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors, // implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
@@ -2121,9 +2302,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
? textureMipmapObject->MapMipmapData(uploadTarget, level) ? textureMipmapObject->MapMipmapData(uploadTarget, level)
: nullptr; : nullptr;
Vector<Float> convertedUploadData; Vector<Float> convertedUploadData;
const void* uploadData = PrepareNormFloatFallbackUpload( Vector<Uint8> widenedUploadData;
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType, const void* uploadData = PrepareFallbackUpload(
convertedUploadData); textureMipmapObject->GetFormat(), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData; Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData); uploadData, levelByteSize, &glType, packedUploadData);
@@ -2253,9 +2435,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level); auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level);
Vector<Float> convertedUploadData; Vector<Float> convertedUploadData;
const void* uploadData = PrepareNormFloatFallbackUpload( Vector<Uint8> widenedUploadData;
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType, const void* uploadData = PrepareFallbackUpload(
convertedUploadData); textureMipmapObject->GetFormat(), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData; Vector<Uint8> packedUploadData;
uploadData = uploadData =
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
@@ -2312,9 +2495,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
? textureMipmapObject->MapMipmapData(uploadTarget, level) ? textureMipmapObject->MapMipmapData(uploadTarget, level)
: nullptr; : nullptr;
Vector<Float> convertedUploadData; Vector<Float> convertedUploadData;
const void* uploadData = PrepareNormFloatFallbackUpload( Vector<Uint8> widenedUploadData;
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType, const void* uploadData = PrepareFallbackUpload(
convertedUploadData); textureMipmapObject->GetFormat(), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData; Vector<Uint8> packedUploadData;
uploadData = uploadData =
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
@@ -2422,9 +2606,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
const void* mipData = textureMipmapObject->MapMipmapData(uploadTarget, level); const void* mipData = textureMipmapObject->MapMipmapData(uploadTarget, level);
Vector<Float> convertedUploadData; Vector<Float> convertedUploadData;
const void* uploadData = PrepareNormFloatFallbackUpload( Vector<Uint8> widenedUploadData;
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType, const void* uploadData = PrepareFallbackUpload(
convertedUploadData); textureMipmapObject->GetFormat(), targetInternal, texelSize, mipData, byteSize,
glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData; Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize, uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
uploadData, byteSize, &glType, packedUploadData); uploadData, byteSize, &glType, packedUploadData);
@@ -2827,7 +3012,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
}); });
// A three-channel format widened to four for a multisample target (see // A three-channel format widened to four to keep the image colour-renderable (see
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and // NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
// whatever the draw that filled it wrote there is not what GL would report: a format // whatever the draw that filled it wrote there is not what GL would report: a format
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the // without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
@@ -3139,6 +3324,113 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false; return false;
} }
// The colour attachment glReadPixels/glGetTexImage would read from, or nullptr when the
// read buffer names no colour attachment at all.
static const MG_State::GLState::FramebufferAttachmentObject* GetReadColorAttachment() {
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return nullptr;
}
const auto readBuffer = readFBO->GetReadBuffer();
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
return nullptr;
}
return &readFBO->GetAttachment(readBuffer);
}
Bool IsAlphaWidenedColorAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(),
textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
TextureImpl::BackendRenderbufferFormatAddsAlpha(renderbufferObject->GetInternalFormat());
}
return false;
}
Uint32 g_alphaWidenedDrawBufferMask = 0;
Uint32 g_integerColorDrawBufferMask = 0;
static Bool IsIntegerColorFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32I:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32I:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return true;
default:
return false;
}
}
static Bool IsIntegerColorAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsIntegerColorFormat(textureObject->GetFormat());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject && IsIntegerColorFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo) {
using FBO = MG_State::GLState::FramebufferObject;
const auto& drawBuffers = fbo.GetDrawBuffers();
Uint32 mask = 0;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = drawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
if (IsAlphaWidenedColorAttachment(fbo.GetAttachment(frontendBuf))) {
mask |= (1u << i);
}
}
return mask;
}
// The read attachment's storage carries an alpha channel its frontend format does not
// (the three-channel colour-renderable widening). GL answers such a read with 1.0, but
// the storage holds whatever the draw wrote there, so the readback has to overwrite it.
Bool IsAlphaWidenedFallbackReadAttachment() {
const auto* attachmentObject = GetReadColorAttachment();
if (attachmentObject == nullptr) {
return false;
}
return IsAlphaWidenedColorAttachment(*attachmentObject);
}
Bool IsFixedPointFallbackReadAttachment() { Bool IsFixedPointFallbackReadAttachment() {
const auto& readFBO = const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
@@ -3344,6 +3636,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (asTarget == FramebufferTarget::Draw) { if (asTarget == FramebufferTarget::Draw) {
Uint32 snormClampOutputMask = 0; Uint32 snormClampOutputMask = 0;
Uint32 unormClampOutputMask = 0; Uint32 unormClampOutputMask = 0;
Uint32 alphaWidenedMask = 0;
Uint32 integerColorMask = 0;
for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) { for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = stateDrawBuffers[i]; const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 || if (frontendBuf < FramebufferAttachmentType::Color0 ||
@@ -3356,9 +3650,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (IsUnormFallbackAttachment(attachmentObject)) { } else if (IsUnormFallbackAttachment(attachmentObject)) {
unormClampOutputMask |= (1u << i); unormClampOutputMask |= (1u << i);
} }
// Independent of the two above: a widened attachment can be SNORM
// (GL_RGB8_SNORM -> GL_RGBA16F, which also clamps) or not (GL_SRGB8 ->
// GL_SRGB8_ALPHA8, which does not), so it gets its own bit rather than an
// `else if` branch of theirs.
if (IsAlphaWidenedColorAttachment(attachmentObject)) {
alphaWidenedMask |= (1u << i);
}
if (IsIntegerColorAttachment(attachmentObject)) {
integerColorMask |= (1u << i);
}
} }
PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask; PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask;
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask; PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
g_integerColorDrawBufferMask = integerColorMask;
} }
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so // 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
+80
View File
@@ -515,6 +515,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap; return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
} }
// Components per texel the frontend format's client data carries, for the three-channel
// formats that can be widened to a four-channel colour-renderable target; 0 for everything
// else. See PrepareChannelWidenedUpload.
Uint GetWidenableClientComponentCount(TextureInternalFormat format);
// True when a widenable format's components are integer rather than normalized, which is
// what decides the synthetic alpha's value: GL_RGB8I and GL_RGB8_SNORM are both uploaded
// as GL_BYTE, but their 1.0 is 1 and 0x7F respectively.
Bool IsIntegerWidenableFormat(TextureInternalFormat format);
// Repacks three-component client data as four components with an alpha of 1.0 in
// `uploadType`, for a format the backend widened to keep a colour attachment renderable.
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
// scratch buffer and has to outlive the returned pointer.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
Bool integerData = false);
struct StateTextureBasicInfo { // Used for tracking texture state changes struct StateTextureBasicInfo { // Used for tracking texture state changes
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown; TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
SizeT width = 0; SizeT width = 0;
@@ -714,6 +733,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
// has to apply the clamp itself. // has to apply the clamp itself.
Bool IsFixedPointFallbackReadAttachment(); Bool IsFixedPointFallbackReadAttachment();
// True when the read buffer names a three-channel attachment the backend actually stores
// in a four-channel format (the colour-renderable widening). A format without alpha reads
// back as 1.0, so the readback path has to overwrite the alpha the draw left behind -
// unconditionally, since this is the format's own semantics rather than the
// GL_CLAMP_READ_COLOR rule the clamp above implements.
Bool IsAlphaWidenedFallbackReadAttachment();
// True when this attachment's storage carries an alpha channel its frontend format does
// not (the three-channel colour-renderable widening).
Bool IsAlphaWidenedColorAttachment(const MG_State::GLState::FramebufferAttachmentObject& attachmentObject);
// Bit i set = DRAW BUFFER i of `fbo` resolves to a colour attachment the backend widened
// from three channels to four. Indexed by draw-buffer slot, not by attachment point,
// because that is what glColorMaski / glClearBufferfv address.
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo);
// The same mask for whatever is currently bound to GL_DRAW_FRAMEBUFFER, recomputed by
// SyncCurrentFBO (BackendFramebufferObject::SyncToBackend for the DRAW target, and reset
// to 0 on the default framebuffer). Read by the draw/clear state sync, so it is only
// trustworthy after SyncCurrentFBO has run in the same entry point.
//
// WHY IT EXISTS (the dst-alpha discipline). A widened attachment has a real alpha channel
// the application's format does not, and GL says a missing channel reads as 1.0. Readback
// can paper over that (ForceWideReadAlphaToOne), but GL_DST_ALPHA /
// GL_ONE_MINUS_DST_ALPHA blending and glBlitFramebuffer read the STORED alpha inside the
// driver where no interception is possible. So the stored alpha is kept at 1.0 instead:
// a clear touching a widened buffer writes alpha 1.0, and every draw into it has its
// alpha write mask forced off, so nothing can ever move it again. The application's own
// colour mask is untouched - glGet(GL_COLOR_WRITEMASK) still reports what it set.
extern Uint32 g_alphaWidenedDrawBufferMask;
// Bit i set = DRAW BUFFER i of the framebuffer bound as DRAW resolves to a colour
// attachment with an INTEGER format. Recomputed beside the mask above and for its sake:
// glClearBufferfv on an integer colour buffer is GL_INVALID_OPERATION, so the
// per-draw-buffer clear route the widening needs has to stand down when one is present.
// (glClear on an integer colour buffer is left undefined by ES in the first place, and
// an application that wants a defined answer has to call glClearBufferuiv/iv - which does
// carry the widened alpha substitution.)
extern Uint32 g_integerColorDrawBufferMask;
// The colour a clear has to hand the driver for one draw buffer: the application's value,
// except that a widened attachment's alpha is replaced by the 1.0 its three-channel
// format implies. `one` is 1.0 encoded in the clear call's own component type - the
// integer clears carry the integer 1, the float clear carries 1.0f.
//
// Returns `value` itself when nothing is substituted, so the ordinary path allocates and
// copies nothing; `scratch` is the caller's buffer and has to outlive the returned
// pointer. Free of GL state on purpose, so the substitution can be unit-tested exactly as
// the driver sees it.
template <typename T>
const T* SubstituteWidenedClearAlpha(const T* value, Bool widened, T one, T (&scratch)[4]) {
if (!widened || value == nullptr) {
return value;
}
scratch[0] = value[0];
scratch[1] = value[1];
scratch[2] = value[2];
scratch[3] = one;
return scratch;
}
// What SyncCurrentFBO last pushed for each target, as a (binding, object, revision) // What SyncCurrentFBO last pushed for each target, as a (binding, object, revision)
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and // triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are // ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
+69 -36
View File
@@ -61,29 +61,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions); requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
} }
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
// ES texture format but not a legal multisample storage format. Widening to four channels
// is safe here precisely because there is no transfer path that would have to expand
// three-channel client data, and the alpha the draw writes for a three-channel source is
// already the 1.0 the frontend format implies.
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
}
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
Flags<PixelFormatNormalizeOptionBit> options;
if (!TargetRequiresRenderableFormat(targetIndex)) {
return options;
}
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
return options;
}
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat, Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
SizeT targetIndex, SizeT targetIndex,
Bool caveat, Bool caveat,
@@ -141,14 +118,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat); const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
Flags<PixelFormatNormalizeOptionBit> options; Flags<PixelFormatNormalizeOptionBit> options;
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) { if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat, options = GetRuntimeFallbackNormalizeOptions(
GetRenderTargetNormalizeOptions(targetIndex)); requestedInternalFormat,
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
} }
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType); NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
} }
} // namespace } // namespace
namespace TextureImpl { namespace TextureImpl {
// Every image that can back a colour attachment needs a colour-renderable storage format,
// and ES has no renderable three-channel format at all: a three-channel float fallback is
// a legal ES texture but neither legal multisample storage nor a legal attachment, so
// GL_RGB8_SNORM / GL_RGB16F / ... have to be widened to four channels for any of them.
// This used to cover the multisample pair alone, on the grounds that only those can never
// be uploaded to; the transfer paths now expand three-channel client data themselves
// (Managers.cpp PrepareFallbackUpload) and hide the added alpha again on sample and
// readback, so the same substitution is available everywhere.
//
// The widening only ever *happens* where the driver refuses the native form (see
// PopulateFormatCapabilitiesImpl: outside multisample storage it rides the driver branch,
// behind the native probe), so a driver that does render to a three-channel image keeps
// allocating it byte for byte.
//
// Do NOT read that as "nothing changes off-device". Measured on Mesa 26.1.6 llvmpipe
// (the headless CI driver), an ES 3.2 GL_TEXTURE_2D colour attachment is COMPLETE for
// GL_RGB8 and GL_RGB16F but INCOMPLETE_ATTACHMENT for GL_RGB8_SNORM, GL_SRGB8 and every
// RGB integer format, and UNSUPPORTED for GL_RGB32F. Those eight formats therefore DO
// take the widened path on llvmpipe, which is where the retrace fixtures and the glcts
// green suites run - the substitution is driver-conditional, not desktop-exempt.
//
// A buffer texture is the one image that can never be an attachment; its storage is the
// buffer object's, and widening it would misdescribe the application's data.
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
if (targetIndex >= kFormatCapabilityTargetCount) {
return false;
}
if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) {
return true;
}
return static_cast<TextureTarget>(targetIndex) != TextureTarget::TextureBuffer;
}
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex) {
Flags<PixelFormatNormalizeOptionBit> options;
if (!TargetRequiresRenderableFormat(targetIndex)) {
return options;
}
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
return options;
}
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat, void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, TextureTarget target) { GLenum* outFormat, GLenum* outType, TextureTarget target) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
@@ -178,20 +202,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex()); return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
} }
namespace {
Bool BackendFormatAddsAlpha(TextureInternalFormat internalFormat, SizeT targetIndex) {
if (!TargetRequiresRenderableFormat(targetIndex)) {
return false;
}
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
return false;
}
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
}
} // namespace
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) { Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
const SizeT targetIndex = const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target); target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
if (!TargetRequiresRenderableFormat(targetIndex)) { return BackendFormatAddsAlpha(internalFormat, targetIndex);
return false; }
}
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) { Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
return false; return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
}
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const Flags<PixelFormatNormalizeOptionBit> options =
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
GetRenderTargetNormalizeOptions(targetIndex));
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
} }
} // namespace TextureImpl } // namespace TextureImpl
namespace PrgramImpl { namespace PrgramImpl {
+17 -3
View File
@@ -9,6 +9,8 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
namespace DebugImpl { namespace DebugImpl {
@@ -34,6 +36,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace VertexArrayImpl } // namespace VertexArrayImpl
namespace TextureImpl { namespace TextureImpl {
// Whether images on this format-capability target can back a colour attachment, and so
// need a colour-renderable storage format even when the frontend asked for a
// three-channel one ES never renders to. Shared by the capability probe (which passes the
// capabilities it has just queried, before the globals are published) and by the
// allocation path (which reads the active backend's), so the format the cache was probed
// with is always the format the image is created with.
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat, void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, GLenum* outFormat, GLenum* outType,
TextureTarget target = TextureTarget::Unknown); TextureTarget target = TextureTarget::Unknown);
@@ -41,10 +53,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum* outFormat, GLenum* outType); GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target); Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
// True when the format the texture is actually created with has an alpha channel the // True when the format the image is actually created with has an alpha channel the
// frontend format does not (the three-channel multisample widening). GL reads such a // frontend format does not (the three-channel colour-renderable widening). GL reads such
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE. // a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
// any readback of the image has to overwrite the alpha the draw happened to leave there.
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target); Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat); Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl } // namespace TextureImpl
@@ -106,9 +106,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in; // `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters // kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
// because a capability recorded for one of them says nothing about the others: DirectGLES // because a capability recorded for one of them says nothing about the others: DirectGLES
// widens three-channel formats to four channels to keep them renderable as *multisample* // decides each target's substitution against that target's own probe, and a buffer texture
// storage, and a format that survives only through that substitution is still texture-only // never gets one at all. This is also where the three-channel widening becomes visible to
// on every ordinary target. // the application - a GL_RGB8_SNORM colour attachment on a driver with no renderable
// three-channel format answers COMPLETE because the backend stores it as GL_RGBA16F and
// recorded FramebufferRenderable in CaveatCaps.
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) { Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
const SizeT formatIndex = static_cast<SizeT>(format); const SizeT formatIndex = static_cast<SizeT>(format);
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) { if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
@@ -52,6 +52,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/MultiDrawScenario.cpp Scenarios/MultiDrawScenario.cpp
Scenarios/AsyncCompileScenario.cpp Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/ThreeChannelAttachmentScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -0,0 +1,300 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ThreeChannelAttachmentScenario.cpp
// 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
//
// Scenario - THREE-CHANNEL COLOUR ATTACHMENTS, on a live driver.
//
// The bug: no OpenGL ES driver renders to a three-channel image. EXT_render_snorm covers
// R/RG/RGBA only, EXT_color_buffer_float excludes RGB16F, and RGB integer formats are not
// colour-renderable anywhere. Complementary Reimagined declares colortex1 = RGB8_SNORM and
// colortex2 = RGB16F, so every framebuffer Iris built from them answered
// GL_FRAMEBUFFER_UNSUPPORTED and Iris refused to load the shaderpack. DirectGLES now stores such
// an attachment in its four-channel sibling (GL_RGB8_SNORM -> GL_RGBA16F) and reports the
// substitution as a caveat capability, which is what makes glCheckFramebufferStatus say COMPLETE.
//
// WHY THIS SCENARIO EXISTS RATHER THAN A UNIT TEST. The unit tests in
// MG_Test/Framebuffer/FramebufferTest.cpp drive a HAND-BUILT capability cache: they prove the
// frontend accepts a caveat capability, and prove the colour-mask/clear discipline that keeps a
// widened attachment's stored alpha at 1.0, but they cannot prove that a real driver's probe
// actually PRODUCES that caveat. Only a live glCheckFramebufferStatus can, and the answer is
// per-driver, not per-platform:
//
// Mesa llvmpipe (the headless CI driver), ES 3.2, GL_TEXTURE_2D colour attachment:
// COMPLETE GL_RGB8, GL_RGB16F, GL_R11F_G11F_B10F, every RGBA*
// INCOMPLETE_ATTACHMENT GL_RGB8_SNORM, GL_SRGB8, every RGB integer format
// UNSUPPORTED GL_RGB32F
//
// So the widening is LIVE on llvmpipe - "the desktop build is unaffected" was simply wrong, and
// the CI retraces were green before the fix only because retrace ignores what
// glCheckFramebufferStatus returns. This scenario is the gate that actually looks.
//
// DirectGLES only. DirectVulkan's format story is its own (Vulkan exposes R8G8B8_SNORM on almost
// nothing, and Magma substitutes on different terms); asserting Espryt's answers there would
// only pin a coincidence.
#include <cmath>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
// Two outputs so the mixed case is covered: draw buffer 0 is a natively renderable
// four-channel format whose alpha the application owns, draw buffer 1 is the widened
// three-channel one whose alpha the format says is 1.0. Both alphas are deliberately
// NOT 1.0 in the shader, so an implementation that simply passed the value through would
// fail the second assertion.
constexpr const char* kFS = R"(#version 330 core
layout(location = 0) out vec4 oNative;
layout(location = 1) out vec4 oWidened;
void main() {
oNative = vec4(1.0, 0.0, 0.0, 0.25);
oWidened = vec4(0.0, 1.0, 0.0, 0.75);
}
)";
constexpr int kSize = 16;
class ThreeChannelAttachmentScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() != "DirectGLES") {
GTEST_SKIP() << "three-channel widening is a DirectGLES substitution; backend is "
<< Gl().BackendName();
}
}
// A single-level 2D texture in `internalFormat`, or 0 when the driver rejects the
// storage outright (which is a different failure from rejecting the ATTACHMENT).
static GLuint MakeTexture(GLenum internalFormat) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize);
if (glGetError() != GL_NO_ERROR) {
glDeleteTextures(1, &texture);
return 0;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
static GLenum SingleAttachmentStatus(GLenum internalFormat) {
const GLuint texture = MakeTexture(internalFormat);
if (texture == 0) return GL_NONE;
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
const GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &texture);
return status;
}
};
// THE regression gate for the frontend's answer: this is the exact call Iris makes, and
// GL_FRAMEBUFFER_UNSUPPORTED here is the whole shaderpack load failure.
TEST_F(ThreeChannelAttachmentScenario, ThreeChannelColorAttachmentsReportComplete) {
if (!Ready() || IsSkipped()) return;
// GL_RGB8 is the control: colour-renderable in ES core, so it must pass with or
// without any substitution. If it ever fails, nothing below means anything.
EXPECT_EQ(SingleAttachmentStatus(GL_RGB8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "GL_RGB8 is ES-core colour-renderable";
// Complementary Reimagined's colortex1 and colortex2.
EXPECT_EQ(SingleAttachmentStatus(GL_RGB8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "colortex1 (RGB8_SNORM) must be renderable through the four-channel widening";
EXPECT_EQ(SingleAttachmentStatus(GL_RGB16F), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "colortex2 (RGB16F) must be renderable, natively or through the widening";
// The other formats the widening covers. GL_RGB32F only reaches a renderable
// four-channel form when EXT_color_buffer_float is present, so a half-float-only
// driver legitimately answers UNSUPPORTED for it - see the POST's per-format row.
EXPECT_EQ(SingleAttachmentStatus(GL_SRGB8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(SingleAttachmentStatus(GL_RGB8UI), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
}
// The other half: the substitution has to be INVISIBLE. A three-channel format has no
// alpha, so GL answers 1.0 for it - and that answer has to hold after a draw that wrote
// something else into the widened storage's real alpha channel, which is what the
// colour-mask discipline in SyncRenderState is for. GL_DST_ALPHA blending and
// glBlitFramebuffer read that stored alpha inside the driver, where no readback fixup can
// reach it, so "the storage really holds 1.0" is the only workable invariant.
TEST_F(ThreeChannelAttachmentScenario, WidenedAttachmentReadsBackOpaqueWhileItsNeighbourKeepsItsAlpha) {
if (!Ready() || IsSkipped()) return;
std::string error;
const GLuint program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
const GLuint nativeTexture = MakeTexture(GL_RGBA16F);
const GLuint widenedTexture = MakeTexture(GL_RGB8_SNORM);
ASSERT_NE(nativeTexture, 0u);
ASSERT_NE(widenedTexture, 0u);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, nativeTexture, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, widenedTexture, 0);
const GLenum drawBuffers[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, drawBuffers);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
// Alpha 0.0 on purpose: the widened attachment must come back 1.0 anyway, and the
// native one must come back 0.0 where the draw does not cover it.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0;
GLuint vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
glReadBuffer(GL_COLOR_ATTACHMENT1);
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
EXPECT_NEAR(pixels[0], 0.0f, 0.02f) << "widened attachment red";
EXPECT_NEAR(pixels[1], 1.0f, 0.02f) << "widened attachment green";
EXPECT_NEAR(pixels[2], 0.0f, 0.02f) << "widened attachment blue";
EXPECT_NEAR(pixels[3], 1.0f, 0.001f)
<< "a three-channel format has no alpha channel, so GL must report 1.0 for it";
glReadBuffer(GL_COLOR_ATTACHMENT0);
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
EXPECT_NEAR(pixels[0], 1.0f, 0.02f) << "native attachment red";
EXPECT_NEAR(pixels[3], 0.25f, 0.02f)
<< "the alpha discipline must not leak onto a natively renderable attachment";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteTextures(1, &nativeTexture);
glDeleteTextures(1, &widenedTexture);
glDeleteProgram(program);
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
}
// The case above can be satisfied by the readback fixup alone (ForceWideReadAlphaToOne
// rewrites glReadPixels' alpha), so it does NOT prove the STORED alpha is 1.0. This one
// does, by asking the driver to read that alpha itself: GL_DST_ALPHA blending multiplies
// by the destination alpha inside the raster pipeline, where nothing MobileGL does can
// intervene. Same reason GL_ONE_MINUS_DST_ALPHA and glBlitFramebuffer are covered for
// free once this holds - and the reason the discipline is a write mask rather than a
// readback patch.
//
// Ablation-checked on llvmpipe, each half separately: disable the alpha doctoring in
// SyncRenderState and the opaque draw leaves 0.25 in the stored alpha; disable the clear
// substitution in Clear() and it stays at the application's 0.0. Either way this case
// reads back the wrong number, which is what makes it a gate rather than a description.
TEST_F(ThreeChannelAttachmentScenario, DstAlphaBlendingSeesOneInAWidenedAttachment) {
if (!Ready() || IsSkipped()) return;
static constexpr const char* kSingleOutFS = R"(#version 330 core
out vec4 oColor;
uniform vec4 uColor;
void main() { oColor = uColor; }
)";
std::string error;
const GLuint program = CompileProgram(kVS, kSingleOutFS, &error);
ASSERT_NE(program, 0u) << error;
const GLint colorLocation = glGetUniformLocation(program, "uColor");
ASSERT_GE(colorLocation, 0);
const GLuint widenedTexture = MakeTexture(GL_RGB8_SNORM);
ASSERT_NE(widenedTexture, 0u);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, widenedTexture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0;
GLuint vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glViewport(0, 0, kSize, kSize);
// The clear's alpha is 0.0 and the draw's is 0.25 - neither is the 1.0 the format
// implies, so both halves of the discipline have to fire for the blend below to see
// 1.0: the clear substitutes it, and the draw is masked away from it.
glDisable(GL_BLEND);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// dst = stored alpha; src factor GL_DST_ALPHA, dst factor GL_ZERO, source white
// => the destination colour becomes (storedAlpha, storedAlpha, storedAlpha).
glEnable(GL_BLEND);
glBlendFunc(GL_DST_ALPHA, GL_ZERO);
glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisable(GL_BLEND);
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
EXPECT_NEAR(pixels[0], 1.0f, 0.02f)
<< "GL_DST_ALPHA read the stored alpha of a three-channel attachment; it must be 1.0";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteTextures(1, &widenedTexture);
glDeleteProgram(program);
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
}
} // namespace
} // namespace MGITest
@@ -13,10 +13,13 @@
#include "Includes.h" #include "Includes.h"
#include "Init.h" #include "Init.h"
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h> #include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h> #include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h> #include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
@@ -858,3 +861,394 @@ TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) {
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage); MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION);
} }
// ---- Three-channel colour attachments: the Complementary Reimagined / Iris load failure --------
//
// Complementary declares colortex1 = RGB8_SNORM and colortex2 = RGB16F. No real OpenGL ES driver
// renders to a three-channel image (EXT_render_snorm covers R/RG/RGBA only; EXT_color_buffer_float
// excludes RGB16F), so the DirectGLES probe records those formats as creatable-but-not-renderable
// and the frontend answered every framebuffer built from them GL_FRAMEBUFFER_UNSUPPORTED - which
// Iris turns into a hard "Draw buffers [0, 1] Status: 36061" load failure. The backend now records
// the four-channel substitution it will actually allocate as a caveat capability, and the frontend
// has to accept that as renderable.
namespace {
class ThreeChannelAttachmentBackend final : public MG_Backend::BackendObject {
public:
// `substituted` stands in for a driver where the four-channel widening probe succeeded, i.e.
// for what PopulateFormatCapabilitiesImpl records on Mali. false is the pre-fix state: the
// native form is creatable, nothing is renderable, and no fallback was ever built.
explicit ThreeChannelAttachmentBackend(Bool substituted) {
auto& cache = MutableFormatCapabilities();
const auto texture2DIndex = MG_Backend::GetFormatCapabilityTargetIndex(TextureTarget::Texture2D);
// IsColorInternalFormatRenderable only trusts the cache once it looks populated, which
// it decides from RGBA8 being creatable somewhere. Without this the static deny-list
// answers instead and the caveat below would never be consulted.
const auto rgba8Index = static_cast<SizeT>(TextureInternalFormat::RGBA8);
cache.FullCaps[texture2DIndex][rgba8Index] |= MG_Backend::FormatCapability::Creatable;
cache.FullCaps[texture2DIndex][rgba8Index] |= MG_Backend::FormatCapability::FramebufferRenderable;
cache.FullCaps[texture2DIndex][rgba8Index] |= MG_Backend::FormatCapability::ColorAttachment;
for (const TextureInternalFormat format :
{TextureInternalFormat::RGB8Snorm, TextureInternalFormat::RGB16F}) {
const auto formatIndex = static_cast<SizeT>(format);
// Creatable and samplable as an ordinary texture, but the driver's
// glCheckFramebufferStatus said no - exactly Mali r32p1's answer.
cache.FullCaps[texture2DIndex][formatIndex] |= MG_Backend::FormatCapability::Creatable;
cache.FullCaps[texture2DIndex][formatIndex] |= MG_Backend::FormatCapability::Sampled;
if (substituted) {
cache.CaveatCaps[texture2DIndex][formatIndex] |=
MG_Backend::FormatCapability::FramebufferRenderable;
cache.CaveatCaps[texture2DIndex][formatIndex] |= MG_Backend::FormatCapability::ColorAttachment;
}
}
}
void Initialize() override {}
Bool InitCapabilities() override { return true; }
Bool InitWindowSurface() override { return true; }
const RendererInfo& GetRendererInfo() const override {
static RendererInfo info = {};
return info;
}
String GetBackendAPIVersionString() const override { return {}; }
const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override {
static MG_Backend::GlobalBackendFunctionsTable table = {};
return table;
}
const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
static MG_Backend::DynamicBackendParameters params = {};
return params;
}
BackendType GetBackendType() const override { return BackendType::Unknown; }
};
class ScopedBackendOverride {
public:
explicit ScopedBackendOverride(UniquePtr<MG_Backend::BackendObject> backend):
m_previous(Move(MG_Backend::pActiveBackendObject)) {
MG_Backend::pActiveBackendObject = Move(backend);
}
~ScopedBackendOverride() { MG_Backend::pActiveBackendObject = Move(m_previous); }
private:
UniquePtr<MG_Backend::BackendObject> m_previous;
};
GLenum CheckSingleColorAttachmentStatus(GLenum internalFormat) {
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, internalFormat, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
return MG_Impl::GLImpl::CheckNamedFramebufferStatus(framebuffer, GL_DRAW_FRAMEBUFFER);
}
} // namespace
TEST_F(FramebufferTest, ThreeChannelColorAttachmentsAreUnsupportedWithoutTheWidenedSubstitution) {
// The pre-fix behaviour, pinned so a regression is a red test rather than a shaderpack that
// silently stops loading: no caveat capability, so nothing makes these renderable.
ScopedBackendOverride backend(MakeUnique<ThreeChannelAttachmentBackend>(/*substituted=*/false));
EXPECT_EQ(CheckSingleColorAttachmentStatus(GL_RGB8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
EXPECT_EQ(CheckSingleColorAttachmentStatus(GL_RGB16F), static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ThreeChannelColorAttachmentsAreCompleteThroughTheWidenedSubstitution) {
ScopedBackendOverride backend(MakeUnique<ThreeChannelAttachmentBackend>(/*substituted=*/true));
// Complementary's colortex1 (RGB8_SNORM) and colortex2 (RGB16F): both must come out COMPLETE,
// because the backend stores them as GL_RGBA16F. Shipping only the first would move the
// failure one composite pass down instead of fixing it.
EXPECT_EQ(CheckSingleColorAttachmentStatus(GL_RGB8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(CheckSingleColorAttachmentStatus(GL_RGB16F), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, TwoAttachmentCompositeFramebufferMatchesIrisComplementaryPass) {
// The exact framebuffer Iris failed on: Complementary's `composite` pass draws to colortex7
// (RGBA16F, natively renderable) and colortex1 (RGB8_SNORM, only renderable widened). Iris
// logs it as "Draw buffers [0, 1]" - a two-attachment FBO, not colortex 0 and 1.
ScopedBackendOverride backend(MakeUnique<ThreeChannelAttachmentBackend>(/*substituted=*/true));
GLuint framebuffer = 0;
GLuint colortex7 = 0;
GLuint colortex1 = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &colortex7);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &colortex1);
MG_Impl::GLImpl::TextureStorage2D(colortex7, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::TextureStorage2D(colortex1, 1, GL_RGB8_SNORM, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, colortex7, 0);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT1, colortex1, 0);
EXPECT_EQ(MG_Impl::GLImpl::CheckNamedFramebufferStatus(framebuffer, GL_DRAW_FRAMEBUFFER),
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
// Both entry points answer from the same helpers, and CheckFramebufferStatus is what Iris
// actually calls; they are near-verbatim duplicates, so assert they agree.
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER),
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ---- Widened attachments: the stored-alpha discipline -----------------------------------------
//
// A widened attachment has a real alpha channel the application's three-channel format does not,
// and GL says a channel a format lacks reads back as 1.0. glReadPixels and glGetTexImage can be
// made to say that (ForceWideReadAlphaToOne), but GL_DST_ALPHA / GL_ONE_MINUS_DST_ALPHA blending
// and glBlitFramebuffer read the STORED alpha inside the driver, where nothing can intercept it.
// So the stored alpha is held at 1.0 instead: a clear writes 1.0 into it, and every draw has that
// buffer's alpha write mask forced off so nothing can move it again.
//
// These cases pin the two halves of that pairing at the seam where they are visible - what the ES
// driver is actually handed - and pin the invariant that the application's own colour mask is
// never touched.
namespace {
struct RecordedColorMask {
Bool seen = false;
GLboolean r = GL_FALSE, g = GL_FALSE, b = GL_FALSE, a = GL_FALSE;
};
constexpr Uint kRecordedDrawBuffers = 8;
RecordedColorMask g_driverIndexedColorMasks[kRecordedDrawBuffers];
RecordedColorMask g_driverUniformColorMask;
void ResetRecordedColorMasks() {
for (auto& recorded : g_driverIndexedColorMasks) recorded = {};
g_driverUniformColorMask = {};
}
void StubColorMask(GLboolean r, GLboolean g, GLboolean b, GLboolean a) {
g_driverUniformColorMask = {true, r, g, b, a};
// The non-indexed call sets every draw buffer, so record it as such: a later assertion
// about draw buffer 1 must not read a stale indexed record the uniform push overwrote.
for (auto& recorded : g_driverIndexedColorMasks) recorded = {true, r, g, b, a};
}
void StubColorMaski(GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a) {
if (index < kRecordedDrawBuffers) g_driverIndexedColorMasks[index] = {true, r, g, b, a};
}
void StubViewport(GLint, GLint, GLsizei, GLsizei) {}
void StubScissor(GLint, GLint, GLsizei, GLsizei) {}
void StubEnable(GLenum) {}
void StubDisable(GLenum) {}
void StubEnablei(GLenum, GLuint) {}
void StubDisablei(GLenum, GLuint) {}
void StubBlendFuncSeparate(GLenum, GLenum, GLenum, GLenum) {}
void StubBlendFuncSeparatei(GLuint, GLenum, GLenum, GLenum, GLenum) {}
void StubBlendEquationSeparate(GLenum, GLenum) {}
void StubBlendEquationSeparatei(GLuint, GLenum, GLenum) {}
void StubBlendColor(GLfloat, GLfloat, GLfloat, GLfloat) {}
void StubDepthFunc(GLenum) {}
void StubDepthMask(GLboolean) {}
void StubDepthRangef(GLfloat, GLfloat) {}
void StubStencilFuncSeparate(GLenum, GLenum, GLint, GLuint) {}
void StubStencilMaskSeparate(GLenum, GLuint) {}
void StubStencilOpSeparate(GLenum, GLenum, GLenum, GLenum) {}
void StubClearColor(GLfloat, GLfloat, GLfloat, GLfloat) {}
void StubClearDepthf(GLfloat) {}
void StubClearStencil(GLint) {}
void StubCullFace(GLenum) {}
void StubFrontFace(GLenum) {}
void StubPolygonOffset(GLfloat, GLfloat) {}
void StubLineWidth(GLfloat) {}
void StubSampleCoverage(GLfloat, GLboolean) {}
// Replaces the ES function table with no-ops that record only what these cases assert on.
// The table is ZEROED first on purpose: SyncRenderState is long, and a call it makes that
// this fixture did not anticipate must crash here rather than silently reach a stale pointer
// into a driver that this process never made current.
class ScopedRenderStateDriverStubs {
public:
ScopedRenderStateDriverStubs():
m_funcs(MG_Backend::DirectGLES::g_GLESFuncs), m_caps(MG_Backend::DirectGLES::g_GLESCapabilities) {
auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
gl = MG_External::GLESFunctionsTable{};
gl.glViewport = StubViewport;
gl.glScissor = StubScissor;
gl.glEnable = StubEnable;
gl.glDisable = StubDisable;
gl.glEnablei = StubEnablei;
gl.glDisablei = StubDisablei;
gl.glBlendFuncSeparate = StubBlendFuncSeparate;
gl.glBlendFuncSeparatei = StubBlendFuncSeparatei;
gl.glBlendEquationSeparate = StubBlendEquationSeparate;
gl.glBlendEquationSeparatei = StubBlendEquationSeparatei;
gl.glBlendColor = StubBlendColor;
gl.glDepthFunc = StubDepthFunc;
gl.glDepthMask = StubDepthMask;
gl.glDepthRangef = StubDepthRangef;
gl.glStencilFuncSeparate = StubStencilFuncSeparate;
gl.glStencilMaskSeparate = StubStencilMaskSeparate;
gl.glStencilOpSeparate = StubStencilOpSeparate;
gl.glClearColor = StubClearColor;
gl.glClearDepthf = StubClearDepthf;
gl.glClearStencil = StubClearStencil;
gl.glCullFace = StubCullFace;
gl.glFrontFace = StubFrontFace;
gl.glPolygonOffset = StubPolygonOffset;
gl.glLineWidth = StubLineWidth;
gl.glSampleCoverage = StubSampleCoverage;
gl.glColorMask = StubColorMask;
gl.glColorMaski = StubColorMaski;
auto& caps = MG_Backend::DirectGLES::g_GLESCapabilities;
caps.SupportsIndexedColorMask = true;
caps.SupportsSrgbWriteControl = false;
caps.SupportsPolygonMode = false;
caps.SupportsDualSourceBlend = true;
ResetRecordedColorMasks();
// The viewport and scissor blocks fall back to querying the surface size when the
// frontend's rectangle is degenerate, and there is no surface in this process.
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
MG_Impl::GLImpl::Scissor(0, 0, 4, 4);
MG_Backend::DirectGLES::RenderStateImpl::InvalidateSyncedRenderState();
}
~ScopedRenderStateDriverStubs() {
MG_Backend::DirectGLES::FramebufferImpl::g_alphaWidenedDrawBufferMask = 0;
MG_Backend::DirectGLES::g_GLESFuncs = m_funcs;
MG_Backend::DirectGLES::g_GLESCapabilities = m_caps;
// The shadow now describes pushes that went to the stubs, not to any driver.
MG_Backend::DirectGLES::RenderStateImpl::InvalidateSyncedRenderState();
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}
private:
MG_External::GLESFunctionsTable m_funcs;
MG_External::GLESCapabilities m_caps;
};
} // namespace
TEST_F(FramebufferTest, WidenedDrawBufferIsIdentifiedPerDrawBufferSlotNotPerAttachmentPoint) {
ScopedBackendOverride backend(MakeUnique<ThreeChannelAttachmentBackend>(/*substituted=*/true));
// Complementary's `composite` framebuffer again: draw buffer 0 is a natively renderable
// RGBA8, draw buffer 1 is the widened RGB8_SNORM. Only the second may be doctored.
GLuint framebuffer = 0;
GLuint colortex7 = 0;
GLuint colortex1 = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &colortex7);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &colortex1);
MG_Impl::GLImpl::TextureStorage2D(colortex7, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::TextureStorage2D(colortex1, 1, GL_RGB8_SNORM, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, colortex7, 0);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT1, colortex1, 0);
auto& framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
ASSERT_NE(framebufferObject, nullptr);
framebufferObject->SetDrawBuffer(0, FramebufferAttachmentType::Color0);
framebufferObject->SetDrawBuffer(1, FramebufferAttachmentType::Color1);
EXPECT_EQ(MG_Backend::DirectGLES::FramebufferImpl::ComputeAlphaWidenedDrawBufferMask(*framebufferObject),
1u << 1);
// Swapping the draw-buffer array moves the bit with the SLOT, not with the attachment point:
// glColorMaski and glClearBufferfv both address slots.
framebufferObject->SetDrawBuffer(0, FramebufferAttachmentType::Color1);
framebufferObject->SetDrawBuffer(1, FramebufferAttachmentType::Color0);
EXPECT_EQ(MG_Backend::DirectGLES::FramebufferImpl::ComputeAlphaWidenedDrawBufferMask(*framebufferObject),
1u << 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff) {
ScopedRenderStateDriverStubs driver;
MG_Backend::DirectGLES::FramebufferImpl::g_alphaWidenedDrawBufferMask = 1u << 1;
// What the application asked for: write every channel of every draw buffer.
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
// What the driver was told. Draw buffer 0 is untouched; draw buffer 1 loses alpha.
ASSERT_TRUE(g_driverIndexedColorMasks[0].seen);
EXPECT_EQ(g_driverIndexedColorMasks[0].r, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[0].g, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[0].b, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[0].a, GL_TRUE);
ASSERT_TRUE(g_driverIndexedColorMasks[1].seen);
EXPECT_EQ(g_driverIndexedColorMasks[1].r, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[1].g, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[1].b, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[1].a, GL_FALSE) << "a widened draw buffer must not take alpha writes";
// And what the application sees back. The doctoring lives entirely on the push; the frontend
// state it is derived from is never written, so glGet still answers with the app's value.
GLboolean appMask[4] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE};
MG_Impl::GLImpl::GetBooleanv(GL_COLOR_WRITEMASK, appMask);
EXPECT_EQ(appMask[0], GL_TRUE);
EXPECT_EQ(appMask[1], GL_TRUE);
EXPECT_EQ(appMask[2], GL_TRUE);
EXPECT_EQ(appMask[3], GL_TRUE) << "glGet(GL_COLOR_WRITEMASK) must report the application's mask";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitutesOne) {
ScopedRenderStateDriverStubs driver;
MG_Backend::DirectGLES::FramebufferImpl::g_alphaWidenedDrawBufferMask = 1u << 1;
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// A draw first, so the mask really is doctored when the clear arrives...
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_EQ(g_driverIndexedColorMasks[1].a, GL_FALSE);
// ...and now the clear, with NOTHING changed in the frontend parameter block. The frontend's
// render-state version has not moved, so only the purpose-aware memo can force this push -
// without it the clear would inherit the draw's alpha-off mask and never write the 1.0.
ResetRecordedColorMasks();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true);
ASSERT_TRUE(g_driverIndexedColorMasks[1].seen) << "the clear must re-push the colour mask";
EXPECT_EQ(g_driverIndexedColorMasks[1].a, GL_TRUE) << "a clear is what puts the 1.0 in the stored alpha";
// The value that clear writes: the application's RGB, alpha replaced by the 1.0 the
// three-channel format implies, and only on the widened buffer.
const GLfloat appColor[4] = {0.25f, 0.5f, 0.75f, 0.0f};
GLfloat scratch[4] = {};
const GLfloat* widened =
MG_Backend::DirectGLES::FramebufferImpl::SubstituteWidenedClearAlpha(appColor, true, 1.0f, scratch);
EXPECT_EQ(widened[0], 0.25f);
EXPECT_EQ(widened[1], 0.5f);
EXPECT_EQ(widened[2], 0.75f);
EXPECT_EQ(widened[3], 1.0f);
const GLfloat* untouched =
MG_Backend::DirectGLES::FramebufferImpl::SubstituteWidenedClearAlpha(appColor, false, 1.0f, scratch);
EXPECT_EQ(untouched, appColor) << "a native attachment's clear must not even be copied";
// An integer widened format (GL_RGB8UI -> GL_RGBA8UI) carries the INTEGER one, not a
// saturated field: glClearBufferuiv takes the value verbatim.
const GLuint appIntegerColor[4] = {7u, 8u, 9u, 0u};
GLuint integerScratch[4] = {};
const GLuint* widenedInteger = MG_Backend::DirectGLES::FramebufferImpl::SubstituteWidenedClearAlpha(
appIntegerColor, true, GLuint(1), integerScratch);
EXPECT_EQ(widenedInteger[3], 1u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffer) {
// The doctoring only ever REMOVES alpha writes on a widened buffer; it must never add them
// back on a buffer the application masked itself, and must never touch a native one.
ScopedRenderStateDriverStubs driver;
MG_Backend::DirectGLES::FramebufferImpl::g_alphaWidenedDrawBufferMask = 1u << 1;
MG_Impl::GLImpl::ColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
MG_Impl::GLImpl::ColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
MG_Impl::GLImpl::ColorMaski(2, GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
EXPECT_EQ(g_driverIndexedColorMasks[0].a, GL_FALSE) << "the application's own alpha mask survives";
EXPECT_EQ(g_driverIndexedColorMasks[1].a, GL_FALSE) << "the widened buffer loses alpha";
EXPECT_EQ(g_driverIndexedColorMasks[2].r, GL_FALSE);
EXPECT_EQ(g_driverIndexedColorMasks[2].g, GL_TRUE);
EXPECT_EQ(g_driverIndexedColorMasks[2].b, GL_FALSE);
EXPECT_EQ(g_driverIndexedColorMasks[2].a, GL_TRUE) << "a native buffer keeps its alpha writes";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
+291
View File
@@ -15,6 +15,7 @@
#include <Config.h> #include <Config.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/Managers.h> #include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h> #include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
@@ -27,6 +28,7 @@
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h> #include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h> #include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Math/SmallFloat.h> #include <MG_Util/Math/SmallFloat.h>
#include <MG_Util/Texture/PixelStoreProcessor.h> #include <MG_Util/Texture/PixelStoreProcessor.h>
#include <MG_Util/Texture/TextureFormatProcessor.h> #include <MG_Util/Texture/TextureFormatProcessor.h>
@@ -2885,3 +2887,292 @@ TEST_F(TextureTest, NamedTextureCallKeepsUnitBindingAccountingCoherent) {
MG_Impl::GLImpl::DeleteTextures(2, names); MG_Impl::GLImpl::DeleteTextures(2, names);
DrainPendingGlErrors(); DrainPendingGlErrors();
} }
// ---- Three-channel colour-renderable widening (Complementary Reimagined / Iris) ----------------
//
// No real OpenGL ES driver renders to a three-channel image, so a colour attachment the
// application asked for as GL_RGB8_SNORM or GL_RGB16F has to be stored in the four-channel
// sibling. The bit that says so used to be reachable for multisample storage only, which is why
// an ordinary GL_TEXTURE_2D attachment in one of those formats had no fallback at all and the
// frontend could only answer GL_FRAMEBUFFER_UNSUPPORTED.
TEST_F(TextureTest, ColorAttachableTargetsRequestTheThreeChannelWidening) {
using MobileGL::MG_Backend::DirectGLES::TextureImpl::GetRenderTargetNormalizeOptions;
using MobileGL::MG_Backend::DirectGLES::TextureImpl::TargetRequiresRenderableFormat;
MG_External::GLESCapabilities capabilities{};
capabilities.SupportsRenderSnorm = true;
capabilities.SupportsNorm16Texture = true;
// Every image that can be a colour attachment, not just the multisample pair: an ordinary 2D
// texture is what Iris attaches, and it used to be excluded.
for (const TextureTarget target : {TextureTarget::Texture2D, TextureTarget::Texture3D,
TextureTarget::TextureCubeMap, TextureTarget::Texture2DArray,
TextureTarget::TextureCubeMapArray, TextureTarget::Texture2DMultisample,
TextureTarget::Texture2DMultisampleArray, TextureTarget::Texture1D,
TextureTarget::Texture1DArray, TextureTarget::TextureRectangle}) {
const SizeT targetIndex = MobileGL::MG_Backend::GetFormatCapabilityTargetIndex(target);
EXPECT_TRUE(TargetRequiresRenderableFormat(targetIndex))
<< "target " << MG_Util::ConvertTextureTargetToString(target);
EXPECT_TRUE(GetRenderTargetNormalizeOptions(capabilities, targetIndex) &
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)
<< "target " << MG_Util::ConvertTextureTargetToString(target);
}
// A renderbuffer exists only to be attached.
EXPECT_TRUE(TargetRequiresRenderableFormat(MobileGL::MG_Backend::GetRenderbufferFormatCapabilityTargetIndex()));
// A buffer texture is the one image that can never be an attachment; its storage belongs to
// the buffer object, so widening it would misdescribe the application's data.
const SizeT bufferIndex = MobileGL::MG_Backend::GetFormatCapabilityTargetIndex(TextureTarget::TextureBuffer);
EXPECT_FALSE(TargetRequiresRenderableFormat(bufferIndex));
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, bufferIndex));
// Without EXT_render_snorm a 16-bit SNORM render target cannot keep its encoding either.
MG_External::GLESCapabilities noSnormCapabilities{};
const SizeT texture2DIndex = MobileGL::MG_Backend::GetFormatCapabilityTargetIndex(TextureTarget::Texture2D);
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noSnormCapabilities, texture2DIndex) &
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
}
TEST_F(TextureTest, ThreeChannelRenderTargetOptionAppliesToEveryDeniedThreeChannelFormat) {
using MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions;
const Flags<PixelFormatNormalizeOptionBit> requested =
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
// GL_RGB16F in particular matched no case at all, so no option could ever apply to it and it
// fell through NormalizePixelFormat's default passthrough unchanged.
for (const GLenum internalFormat : {GL_RGB8_SNORM, GL_RGB16_SNORM, GL_RGB16, GL_RGB10, GL_RGB12, GL_RGB16F,
GL_RGB32F, GL_SRGB8, GL_RGB8I, GL_RGB8UI, GL_RGB16I, GL_RGB16UI, GL_RGB32I,
GL_RGB32UI}) {
EXPECT_TRUE(GetApplicablePixelFormatNormalizeOptions(internalFormat, requested) &
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)
<< "internalformat 0x" << std::hex << internalFormat;
}
// Four-channel and shared-exponent formats are not widened: RGBA8_SNORM has its own always-on
// fallback, and GL_RGB9_E5 has no four-channel sibling that would not need the shared exponent
// unpacked on every transfer (nothing renders to it on desktop GL either).
for (const GLenum internalFormat : {GL_RGBA8_SNORM, GL_RGBA16F, GL_RGBA8, GL_RGB8, GL_RGB9_E5}) {
EXPECT_FALSE(GetApplicablePixelFormatNormalizeOptions(internalFormat, requested) &
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)
<< "internalformat 0x" << std::hex << internalFormat;
}
}
TEST_F(TextureTest, ThreeChannelWideningRetargetsInternalFormatAndTransferPairTogether) {
using MG_Util::TextureFormatProcessor::NormalizePixelFormat;
struct Case {
GLenum requested;
Flags<PixelFormatNormalizeOptionBit> options;
GLenum internalFormat;
GLenum format;
GLenum type;
};
const Flags<PixelFormatNormalizeOptionBit> widen = PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
const Flags<PixelFormatNormalizeOptionBit> widenNoSnorm16 =
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget |
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
const Case cases[] = {
// Complementary's colortex1 and colortex2. The transfer pair used to stay three-channel
// and keep the *source* component type, emitting (GL_RGBA16F, GL_RGB, GL_BYTE) - which ES
// rejects for glTexImage2D outright, and which only went unnoticed because the bit was
// reachable for multisample storage alone (glTexStorage*Multisample takes no pair).
{GL_RGB8_SNORM, widen, GL_RGBA16F, GL_RGBA, GL_FLOAT},
{GL_RGB16F, widen, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT},
{GL_RGB32F, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
// 16-bit SNORM keeps its encoding where EXT_render_snorm can render to it; a half float's
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
{GL_RGB16_SNORM, widen, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA16F, GL_RGBA, GL_FLOAT},
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16.
{GL_RGB16, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
{GL_RGB10, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
{GL_RGB12, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
// sRGB and the integer formats: the base format has to move to the four-channel one of the
// right class, GL_RGBA_INTEGER included.
{GL_SRGB8, widen, GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE},
{GL_RGB8I, widen, GL_RGBA8I, GL_RGBA_INTEGER, GL_BYTE},
{GL_RGB8UI, widen, GL_RGBA8UI, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE},
{GL_RGB16I, widen, GL_RGBA16I, GL_RGBA_INTEGER, GL_SHORT},
{GL_RGB16UI, widen, GL_RGBA16UI, GL_RGBA_INTEGER, GL_UNSIGNED_SHORT},
{GL_RGB32I, widen, GL_RGBA32I, GL_RGBA_INTEGER, GL_INT},
{GL_RGB32UI, widen, GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT},
// The widening outranks the other fallbacks, which all pick a three-channel storage the
// driver still refuses to render to (GL_RGB8_SNORM -> GL_RGB16F, GL_RGB16 -> GL_RGB32F).
{GL_RGB8_SNORM, widen | PixelFormatNormalizeOptionBit::NoSnorm8, GL_RGBA16F, GL_RGBA, GL_FLOAT},
{GL_RGB16, widen | PixelFormatNormalizeOptionBit::NoNorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
// Control: without the bit nothing moves. The bit is only ever set for a target whose
// native probe failed, so this is the shape every driver that does render to the
// three-channel form keeps - per format, not per platform (llvmpipe renders to GL_RGB16F
// but not to GL_RGB8_SNORM, GL_SRGB8, GL_RGB32F or the RGB integer formats).
{GL_RGB8_SNORM, PixelFormatNormalizeOptionBit::None, GL_RGB8_SNORM, GL_RGB, GL_BYTE},
{GL_RGB16F, PixelFormatNormalizeOptionBit::None, GL_RGB16F, GL_RGB, GL_HALF_FLOAT},
{GL_RGB32F, PixelFormatNormalizeOptionBit::None, GL_RGB32F, GL_RGB, GL_FLOAT},
{GL_SRGB8, PixelFormatNormalizeOptionBit::None, GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE},
// Not widened even under the bit: no four-channel shared-exponent sibling exists.
{GL_RGB9_E5, widen, GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV},
// Four-channel formats are unaffected by the bit; RGBA8_SNORM keeps its own fallback.
{GL_RGBA8_SNORM, widen, GL_RGBA8_SNORM, GL_RGBA, GL_BYTE},
{GL_RGBA8_SNORM, widen | PixelFormatNormalizeOptionBit::NoRGBA8Snorm, GL_RGBA16F, GL_RGBA, GL_FLOAT},
};
for (const auto& testCase : cases) {
GLenum internalFormat = 0;
GLenum format = 0;
GLenum type = 0;
NormalizePixelFormat(testCase.requested, testCase.options, &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, WidenedRenderTargetUploadExpandsThreeChannelDataWithOpaqueAlpha) {
using MobileGL::MG_Backend::DirectGLES::TextureImpl::GetWidenableClientComponentCount;
using MobileGL::MG_Backend::DirectGLES::TextureImpl::PrepareChannelWidenedUpload;
// Only the three-channel formats that can be widened report a source component count; the
// repack is what keeps the driver from walking three texels' worth of data per four-texel row.
for (const TextureInternalFormat format :
{TextureInternalFormat::RGB8Snorm, TextureInternalFormat::RGB16F, TextureInternalFormat::RGB32F,
TextureInternalFormat::RGB16Snorm, TextureInternalFormat::RGB16, TextureInternalFormat::SRGB8,
TextureInternalFormat::RGB8UI, TextureInternalFormat::RGB32I}) {
EXPECT_EQ(GetWidenableClientComponentCount(format), 3u)
<< MG_Util::ConvertTextureInternalFormatToString(format);
}
EXPECT_EQ(GetWidenableClientComponentCount(TextureInternalFormat::RGBA8), 0u);
EXPECT_EQ(GetWidenableClientComponentCount(TextureInternalFormat::RGBA8Snorm), 0u);
EXPECT_EQ(GetWidenableClientComponentCount(TextureInternalFormat::RGB9E5), 0u);
const IntVec3 texelSize(2, 1, 1);
// GL_RGB8_SNORM -> GL_RGBA16F: PrepareNormFloatFallbackUpload has already turned the Int8
// shadow into floats, so what arrives here is three floats per texel.
{
const Float source[] = {0.25f, -0.5f, 0.75f, -1.0f, 0.0f, 1.0f};
Vector<Uint8> widened;
const auto* result = static_cast<const Float*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_FLOAT, widened));
ASSERT_NE(result, static_cast<const void*>(source));
ASSERT_EQ(widened.size(), 8 * sizeof(Float));
const Float expected[] = {0.25f, -0.5f, 0.75f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f};
for (SizeT i = 0; i < 8; ++i) {
EXPECT_FLOAT_EQ(result[i], expected[i]) << "component " << i;
}
}
// GL_RGB16F -> GL_RGBA16F uploads halves untouched, so the synthetic alpha is the half
// encoding of 1.0 rather than a saturated field.
{
const Uint16 source[] = {0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006};
Vector<Uint8> widened;
const auto* result = static_cast<const Uint16*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_HALF_FLOAT, widened));
ASSERT_NE(result, static_cast<const void*>(source));
const Uint16 expected[] = {0x0001, 0x0002, 0x0003, 0x3C00, 0x0004, 0x0005, 0x0006, 0x3C00};
for (SizeT i = 0; i < 8; ++i) {
EXPECT_EQ(result[i], expected[i]) << "component " << i;
}
}
// GL_SRGB8 -> GL_SRGB8_ALPHA8: fixed-point one is the saturated field.
{
const Uint8 source[] = {1, 2, 3, 4, 5, 6};
Vector<Uint8> widened;
const auto* result = static_cast<const Uint8*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened));
const Uint8 expected[] = {1, 2, 3, 0xFF, 4, 5, 6, 0xFF};
ASSERT_NE(result, static_cast<const void*>(source));
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
}
// GL_RGB16_SNORM -> GL_RGBA16_SNORM keeps GL_SHORT, whose 1.0 is the positive maximum.
{
const Int16 source[] = {-1, 2, -3, 4, -5, 6};
Vector<Uint8> widened;
const auto* result = static_cast<const Int16*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_SHORT, widened));
const Int16 expected[] = {-1, 2, -3, 0x7FFF, 4, -5, 6, 0x7FFF};
ASSERT_NE(result, static_cast<const void*>(source));
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
}
// An integer format's added channel carries the integer one, not a saturated field.
{
const Uint32 source[] = {10, 20, 30, 40, 50, 60};
Vector<Uint8> widened;
const auto* result = static_cast<const Uint32*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_UNSIGNED_INT, widened, /*integerData=*/true));
const Uint32 expected[] = {10, 20, 30, 1, 40, 50, 60, 1};
ASSERT_NE(result, static_cast<const void*>(source));
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
}
// GL_RGB8I -> GL_RGBA8I uploads as GL_BYTE, the very type GL_RGB8_SNORM uses, so the type
// alone cannot decide the added channel's value: the integer format's one is 1, the
// signed-normalized format's is 0x7F. Getting this wrong is invisible through sampling and
// glGetTexImage (both answer the alpha with the format's implied one) but escapes through a
// blit or glCopyTexSubImage out of the widened attachment.
{
const Int8 source[] = {-1, 2, -3, 4, -5, 6};
Vector<Uint8> widened;
const auto* asInteger = static_cast<const Int8*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_BYTE, widened, /*integerData=*/true));
const Int8 expectedInteger[] = {-1, 2, -3, 1, 4, -5, 6, 1};
ASSERT_NE(asInteger, static_cast<const void*>(source));
EXPECT_EQ(std::memcmp(asInteger, expectedInteger, sizeof(expectedInteger)), 0);
Vector<Uint8> widenedNorm;
const auto* asNormalized = static_cast<const Int8*>(PrepareChannelWidenedUpload(
3, texelSize, source, sizeof(source), GL_BYTE, widenedNorm, /*integerData=*/false));
const Int8 expectedNormalized[] = {-1, 2, -3, 0x7F, 4, -5, 6, 0x7F};
EXPECT_EQ(std::memcmp(asNormalized, expectedNormalized, sizeof(expectedNormalized)), 0);
}
// Which class a widenable format belongs to.
for (const TextureInternalFormat format :
{TextureInternalFormat::RGB8I, TextureInternalFormat::RGB8UI, TextureInternalFormat::RGB16I,
TextureInternalFormat::RGB16UI, TextureInternalFormat::RGB32I, TextureInternalFormat::RGB32UI}) {
EXPECT_TRUE(MobileGL::MG_Backend::DirectGLES::TextureImpl::IsIntegerWidenableFormat(format))
<< MG_Util::ConvertTextureInternalFormatToString(format);
}
for (const TextureInternalFormat format :
{TextureInternalFormat::RGB8Snorm, TextureInternalFormat::RGB16Snorm, TextureInternalFormat::RGB16,
TextureInternalFormat::RGB16F, TextureInternalFormat::RGB32F, TextureInternalFormat::SRGB8}) {
EXPECT_FALSE(MobileGL::MG_Backend::DirectGLES::TextureImpl::IsIntegerWidenableFormat(format))
<< MG_Util::ConvertTextureInternalFormatToString(format);
}
// The destination is sized from the level, never from the source. The driver reads a full
// width*height*4 components for the transfer it was handed, so a short source must still
// leave a full buffer behind - sizing it from the source would hand the driver a buffer it
// runs off the end of.
{
const Float shortSource[] = {0.5f, 0.25f, 0.125f};
Vector<Uint8> widened;
const auto* result = static_cast<const Float*>(PrepareChannelWidenedUpload(
3, IntVec3(2, 2, 1), shortSource, sizeof(shortSource), GL_FLOAT, widened));
ASSERT_NE(result, static_cast<const void*>(shortSource));
ASSERT_EQ(widened.size(), 4 * 4 * sizeof(Float));
const Float expected[] = {0.5f, 0.25f, 0.125f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
for (SizeT i = 0; i < 16; ++i) {
EXPECT_FLOAT_EQ(result[i], expected[i]) << "component " << i;
}
}
// No widening in effect (or nothing to convert): the caller's pointer comes straight back, so
// the sub-rect upload fast path still recognises an unconverted level.
{
const Float source[] = {1.0f, 2.0f, 3.0f, 4.0f};
Vector<Uint8> widened;
EXPECT_EQ(PrepareChannelWidenedUpload(4, texelSize, source, sizeof(source), GL_FLOAT, widened),
static_cast<const void*>(source));
EXPECT_EQ(PrepareChannelWidenedUpload(0, texelSize, source, sizeof(source), GL_FLOAT, widened),
static_cast<const void*>(source));
EXPECT_EQ(PrepareChannelWidenedUpload(3, texelSize, nullptr, 0, GL_FLOAT, widened), nullptr);
}
}
@@ -838,6 +838,12 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) { if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) {
caps.SupportsRenderSnorm = true; caps.SupportsRenderSnorm = true;
} }
if (std::strcmp(extension, "GL_EXT_color_buffer_float") == 0) {
caps.SupportsColorBufferFloat = true;
}
if (std::strcmp(extension, "GL_EXT_color_buffer_half_float") == 0) {
caps.SupportsColorBufferHalfFloat = true;
}
if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) { if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) {
caps.SupportsSrgbWriteControl = true; caps.SupportsSrgbWriteControl = true;
} }
@@ -1034,6 +1034,16 @@ namespace MobileGL {
// GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable // GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable
// (and usable as multisample texture storage) rather than texture-only. // (and usable as multisample texture storage) rather than texture-only.
Bool SupportsRenderSnorm = false; Bool SupportsRenderSnorm = false;
// GL_EXT_color_buffer_float is present, so GL_RGBA16F / GL_RGBA32F / GL_R11F_G11F_B10F
// (and the R/RG float formats) are colour-renderable. ES 3.x core makes them
// texture-only, and every Iris shaderpack renders into at least R11F_G11F_B10F, so
// without this no shaderpack can work at all.
Bool SupportsColorBufferFloat = false;
// GL_EXT_color_buffer_half_float is present: the half-float subset of the above, for
// drivers that ship only the smaller extension. Note it does NOT rescue GL_RGB16F -
// the extension nominally lists it but disclaims it under ES 3.x, and real drivers
// reject it, which is why three-channel float attachments are widened instead.
Bool SupportsColorBufferHalfFloat = false;
// GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off. // GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off.
// GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded, // GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded,
// while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw. // while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw.
+146
View File
@@ -16,7 +16,12 @@
// Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no // Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no
// MG_State code: it runs standalone, before MG_State::Init(). // MG_State code: it runs standalone, before MG_State::Init().
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h> #include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h> #include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
@@ -450,6 +455,35 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("GL_EXT_texture_norm16", builder.Warn("GL_EXT_texture_norm16",
"not supported; 16-bit normalized texture formats need emulation"); "not supported; 16-bit normalized texture formats need emulation");
} }
if (caps.SupportsRenderSnorm) {
builder.Pass("GL_EXT_render_snorm",
"supported (signed-normalized formats are colour-renderable, so an "
"SNORM render target keeps its own encoding instead of a float substitute)");
} else {
builder.Warn("GL_EXT_render_snorm",
"not supported; signed-normalized formats are texture-only, so every SNORM "
"render target is stored as a float (GL_RGBA8_SNORM/GL_RGB8_SNORM -> "
"GL_RGBA16F) and its fragment outputs are clamped to [-1,1] in software");
}
// FAIL, not WARN: ES 3.x core makes every float format texture-only, and every Iris
// shaderpack renders into at least GL_R11F_G11F_B10F (Complementary's colortex0, BSL's
// colortex0). Without this extension there is no substitute format left - a half float
// is not renderable either - so shaderpacks cannot work at all on such a driver.
if (caps.SupportsColorBufferFloat) {
builder.Pass("GL_EXT_color_buffer_float",
"supported (GL_R11F_G11F_B10F / GL_RGBA16F / GL_RGBA32F are "
"colour-renderable, which is what every shaderpack renders into)");
} else if (caps.SupportsColorBufferHalfFloat) {
builder.Warn("GL_EXT_color_buffer_float",
"not supported, but GL_EXT_color_buffer_half_float is; 16-bit float render "
"targets work, 32-bit float ones (GL_RGBA32F, and the GL_RGBA16 fallback "
"that lands on it) do not");
} else {
builder.Fail("GL_EXT_color_buffer_float",
"not supported, and neither is GL_EXT_color_buffer_half_float; no floating-point "
"format is colour-renderable on this driver, so no shaderpack can create its "
"render targets (Iris reports GL_FRAMEBUFFER_UNSUPPORTED and refuses to load)");
}
// INFO, never WARN: this is the HOST driver's ability to compile its own ESSL on // INFO, never WARN: this is the HOST driver's ability to compile its own ESSL on
// its own threads, and MobileGL's asynchronous compilation does not depend on it // its own threads, and MobileGL's asynchronous compilation does not depend on it
@@ -783,6 +817,117 @@ namespace MobileGL::MG_Util::SelfTest {
} }
} }
// No real ES driver renders to a three-channel image, but desktop GL applications ask for
// one constantly - Complementary Reimagined's colortex1 is GL_RGB8_SNORM and its colortex2
// is GL_RGB16F, and Iris refuses to load when a framebuffer built from them is not
// COMPLETE. DirectGLES substitutes the four-channel sibling, and this row names the
// outcome per format so the failure mode is a five-second read instead of an
// investigation. Answered from the capability cache that was just probed on this very
// driver, so it costs no extra GL work.
void ReportThreeChannelColorAttachments(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
const MG_Backend::FormatCapabilityCache& cache) {
// GL_RGB8 is the control: it is ES-core renderable, and it is exactly why BSL loads on
// the same driver where Complementary does not. The rest are one representative of
// each widening class - signed-normalized, half float, 32-bit float, sRGB, integer -
// so the row says which CLASS of shaderpack target a device cannot serve rather than
// just "three-channel formats".
constexpr TextureInternalFormat kProbedFormats[] = {
TextureInternalFormat::RGB8, TextureInternalFormat::RGB8Snorm, TextureInternalFormat::RGB16F,
TextureInternalFormat::RGB32F, TextureInternalFormat::SRGB8, TextureInternalFormat::RGB8UI};
const SizeT targetIndex = MG_Backend::GetFormatCapabilityTargetIndex(TextureTarget::Texture2D);
const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
MG_Backend::DirectGLES::TextureImpl::GetRenderTargetNormalizeOptions(caps, targetIndex);
String nativeList;
String widenedList;
String unusableList;
// GL_RGB8 is colour-renderable in ES 3.0 CORE. A driver that answers no to it is
// broken (or the probe itself is), and that is the ONLY three-channel verdict that
// deserves a FAIL on its own - see the verdict block below.
Bool controlFormatBroken = false;
const auto append = [](String& list, const String& entry) {
if (!list.empty()) list += ", ";
list += entry;
};
for (const TextureInternalFormat probedFormat : kProbedFormats) {
const SizeT formatIndex = static_cast<SizeT>(probedFormat);
const String name = MG_Util::ConvertTextureInternalFormatToString(probedFormat);
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable)) {
append(nativeList, name);
continue;
}
if (probedFormat == TextureInternalFormat::RGB8) {
controlFormatBroken = true;
}
if (MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable)) {
GLenum widenedInternalFormat = GL_UNKNOWN_MGL;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
MG_Util::ConvertTextureInternalFormatToGLEnum(probedFormat), renderTargetOptions,
&widenedInternalFormat, nullptr, nullptr);
append(widenedList, name + " -> " + MG_Util::ConvertGLEnumToString(widenedInternalFormat));
continue;
}
append(unusableList, name);
}
String detail;
if (!nativeList.empty()) detail += "renderable natively: " + nativeList;
if (!widenedList.empty()) {
if (!detail.empty()) detail += "; ";
detail += "widened to stay renderable: " + widenedList;
}
if (!unusableList.empty()) {
if (!detail.empty()) detail += "; ";
detail += "NOT renderable and not substitutable: " + unusableList;
}
// The verdict deliberately does NOT track "every probed format came out usable".
//
// GL_RGB32F widens to GL_RGBA32F, and GL_RGBA32F is colour-renderable only under
// GL_EXT_color_buffer_float. A perfectly healthy half-float-only driver (the common
// mobile shape: EXT_color_buffer_half_float and nothing more) therefore reports
// GL_RGB32F as unusable while every format a shaderpack actually renders into works.
// FAILing that device would make the POST's hardest verdict fire on a configuration
// MobileGL runs fine on, which is exactly how a report stops being read.
//
// So FAIL is reserved for the two answers that really are broken:
// * the ES-core control (GL_RGB8) is not renderable - the probe or the driver is
// wrong about something much more basic than three-channel widening; and
// * a widenable format has no usable fallback ON A DRIVER THAT ADVERTISES
// GL_EXT_color_buffer_float - the extension promises the widened float targets
// are renderable, so a gap here is a real, unexplained refusal.
// Everything else is a WARN carrying the exact per-format status, which is what the
// row is for. The "no float render targets at all" case is already a FAIL of its own
// on the GL_EXT_color_buffer_float row above; repeating it here would only double-count.
if (controlFormatBroken) {
builder.Fail("Three-channel colour attachments",
detail + " - GL_RGB8 is colour-renderable in OpenGL ES 3.0 core, so a driver "
"that refuses it cannot render to ANY three-channel attachment and the "
"capability probe itself is suspect");
} else if (!unusableList.empty() && caps.SupportsColorBufferFloat) {
builder.Fail("Three-channel colour attachments",
detail + " - GL_EXT_color_buffer_float is supported, so the widened "
"four-channel float targets are required to be renderable; a framebuffer "
"using one of the formats above still reports GL_FRAMEBUFFER_UNSUPPORTED, "
"which Iris turns into a hard load failure");
} else if (!unusableList.empty()) {
builder.Warn("Three-channel colour attachments",
detail + " - without GL_EXT_color_buffer_float the 32-bit float widening has no "
"renderable target left, so a shaderpack asking for one of the formats "
"above gets GL_FRAMEBUFFER_UNSUPPORTED; the half-float and fixed-point "
"ones above still work");
} else if (!widenedList.empty()) {
builder.Warn("Three-channel colour attachments",
detail + " - the substitution costs the extra alpha channel's memory and is "
"hidden from the application by an ALPHA->ONE swizzle");
} else {
builder.Pass("Three-channel colour attachments", detail);
}
}
// Everything the "MobileGL reported ..." rows need from the GLES device probe. // Everything the "MobileGL reported ..." rows need from the GLES device probe.
struct GlesProbeSummary { struct GlesProbeSummary {
Bool capsValid = false; Bool capsValid = false;
@@ -913,6 +1058,7 @@ namespace MobileGL::MG_Util::SelfTest {
builder.report.formatCapabilities.emplace(); builder.report.formatCapabilities.emplace();
MG_Backend::DirectGLES::PopulateFormatCapabilities( MG_Backend::DirectGLES::PopulateFormatCapabilities(
glesFuncs, caps, builder.report.formatCapabilities.value()); glesFuncs, caps, builder.report.formatCapabilities.value());
ReportThreeChannelColorAttachments(builder, caps, builder.report.formatCapabilities.value());
} while (false); } while (false);
} }
@@ -58,12 +58,99 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
case GL_R8_SNORM: case GL_R8_SNORM:
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
break; break;
// The rest of the three-channel formats no real ES driver renders to. They have no
// other fallback: none of the driver/forced option bits names them, so before the
// render-target widening existed for ordinary targets an FBO attachment in one of
// them could only ever be answered GL_FRAMEBUFFER_UNSUPPORTED (Complementary
// Reimagined's colortex2 = RGB16F).
//
// GL_RGB9_E5 is deliberately absent: its four-channel sibling would have to be a
// half float, which means unpacking the shared exponent on every transfer, and
// nothing renders to a shared-exponent format on desktop GL either.
case GL_RGB16F:
case GL_RGB32F:
case GL_SRGB8:
case GL_RGB8I:
case GL_RGB8UI:
case GL_RGB16I:
case GL_RGB16UI:
case GL_RGB32I:
case GL_RGB32UI:
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
break;
default: default:
break; break;
} }
return applicableOptions; return applicableOptions;
} }
namespace {
// The four-channel sibling a three-channel format is widened to when the target has to
// stay colour-renderable, together with the transfer pair that describes client data for
// it. Kept in one place because all three of NormalizePixelFormat's switches have to agree:
// reporting the widened storage but the original three-channel base format emitted
// inconsistent triples such as (GL_RGBA16F, GL_RGB, GL_BYTE), which is
// GL_INVALID_OPERATION for glTexImage2D on ES. That only ever went unnoticed because the
// bit was reachable for multisample storage alone, and glTexStorage*Multisample takes no
// transfer pair at all.
struct ThreeChannelWidening {
GLenum InternalFormat = GL_UNKNOWN_MGL;
GLenum Format = GL_UNKNOWN_MGL;
GLenum Type = GL_UNKNOWN_MGL;
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
};
ThreeChannelWidening GetThreeChannelRenderTargetWidening(GLenum internalFormat,
Flags<PixelFormatNormalizeOptionBit> options) {
if (!(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)) {
return {};
}
switch (internalFormat) {
// Signed-normalized: matches what the always-on NoRGBA8Snorm fallback already does to
// GL_RGBA8_SNORM, so the two SNORM8 formats land on the same storage.
case GL_RGB8_SNORM:
return {GL_RGBA16F, GL_RGBA, GL_FLOAT};
case GL_RGB16_SNORM:
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
// signed-normalized encoding whenever the driver can render to it.
return (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
? ThreeChannelWidening{GL_RGBA16F, GL_RGBA, GL_FLOAT}
: ThreeChannelWidening{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT};
// Unsigned-normalized 16-bit (and the legacy 10/12-bit formats stored as RGB16):
// GL_RGB32F is a legal ES texture format but is not colour-renderable either.
case GL_RGB16:
case GL_RGB10:
case GL_RGB12:
return {GL_RGBA32F, GL_RGBA, GL_FLOAT};
// Floating point.
case GL_RGB16F:
return {GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT};
case GL_RGB32F:
return {GL_RGBA32F, GL_RGBA, GL_FLOAT};
// sRGB: GL_SRGB8_ALPHA8 keeps the sRGB encoding of the colour channels and stores
// the added alpha linearly, which is exactly the three-channel format's semantics.
case GL_SRGB8:
return {GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE};
// Integer.
case GL_RGB8I:
return {GL_RGBA8I, GL_RGBA_INTEGER, GL_BYTE};
case GL_RGB8UI:
return {GL_RGBA8UI, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE};
case GL_RGB16I:
return {GL_RGBA16I, GL_RGBA_INTEGER, GL_SHORT};
case GL_RGB16UI:
return {GL_RGBA16UI, GL_RGBA_INTEGER, GL_UNSIGNED_SHORT};
case GL_RGB32I:
return {GL_RGBA32I, GL_RGBA_INTEGER, GL_INT};
case GL_RGB32UI:
return {GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT};
default:
return {};
}
}
} // namespace
void NormalizePixelFormat(GLenum internalFormat, Flags<PixelFormatNormalizeOptionBit> options, void NormalizePixelFormat(GLenum internalFormat, Flags<PixelFormatNormalizeOptionBit> options,
GLenum* outInternalFormat, GLenum* outFormat, GLenum* outType) { GLenum* outInternalFormat, GLenum* outFormat, GLenum* outType) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
@@ -95,13 +182,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outInternalFormat = internalFormat; *outInternalFormat = internalFormat;
break; break;
case GL_RGB16: case GL_RGB16:
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
// GL_RGB32F is a legal ES texture format but is not colour-renderable, so
// glTexStorage2DMultisample rejects it and the attachment ends up with no
// storage at all.
*outInternalFormat = GL_RGBA32F;
break;
}
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) || if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
(options & PixelFormatNormalizeOptionBit::NoRgb16)) { (options & PixelFormatNormalizeOptionBit::NoRgb16)) {
*outInternalFormat = GL_RGB32F; *outInternalFormat = GL_RGB32F;
@@ -132,14 +212,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outInternalFormat = internalFormat; *outInternalFormat = internalFormat;
break; break;
case GL_RGB16_SNORM: case GL_RGB16_SNORM:
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
// signed-normalized encoding whenever the driver can render to it.
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
? GL_RGBA16F
: GL_RGBA16_SNORM;
break;
}
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) || if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) || (options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) { (options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
@@ -173,10 +245,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outInternalFormat = internalFormat; *outInternalFormat = internalFormat;
break; break;
case GL_RGB8_SNORM: case GL_RGB8_SNORM:
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
*outInternalFormat = GL_RGBA16F;
break;
}
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) { if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
*outInternalFormat = GL_RGB16F; *outInternalFormat = GL_RGB16F;
break; break;
@@ -615,5 +683,18 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
break; break;
} }
} }
// Applied last, over whatever the three switches above chose: widening a three-channel
// format to keep a colour attachment renderable outranks every other fallback, because
// the others all pick a three-channel storage the driver still refuses to render to
// (GL_RGB8_SNORM -> GL_RGB16F under NoSnorm8, GL_RGB16 -> GL_RGB32F under NoNorm16).
// All three outputs move together: reporting the widened storage while leaving the
// three-channel base format and its component type in place produced triples like
// (GL_RGBA16F, GL_RGB, GL_BYTE), which ES rejects for glTexImage2D outright.
if (const ThreeChannelWidening widening = GetThreeChannelRenderTargetWidening(internalFormat, options)) {
if (outInternalFormat) *outInternalFormat = widening.InternalFormat;
if (outFormat) *outFormat = widening.Format;
if (outType) *outType = widening.Type;
}
} }
} // namespace MobileGL::MG_Util::TextureFormatProcessor } // namespace MobileGL::MG_Util::TextureFormatProcessor
@@ -20,9 +20,14 @@ namespace MobileGL {
NoRGB16Snorm = 1 << 6, NoRGB16Snorm = 1 << 6,
// The target must be colour-renderable and ES has no renderable three-channel // The target must be colour-renderable and ES has no renderable three-channel
// form of the requested format, so it has to be widened to the four-channel one. // form of the requested format, so it has to be widened to the four-channel one.
// Only meaningful for multisample textures: those can never be uploaded to, only // Set for any colour-attachable target whose native three-channel form the driver
// rendered into, so the extra alpha comes from the draw (1.0 for an RGB source) // refused to render to (multisample storage always, since ES has no three-channel
// and no transfer path has to expand three-channel client data. // multisample format at all; every other target only after its native probe failed).
// The widening is visible to every transfer path, so it also retargets the (format,
// type) pair NormalizePixelFormat reports: the upload has to describe four
// components in the widened storage's component type, the backend has to expand
// three-channel client data with an alpha of 1.0, and sampling/readback has to hide
// the added alpha again (BackendTextureFormatAddsAlpha).
NoThreeChannelRenderTarget = 1 << 7, NoThreeChannelRenderTarget = 1 << 7,
// Pairs with the bit above: the widened four-channel format has to stay renderable AND // Pairs with the bit above: the widened four-channel format has to stay renderable AND
// keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and // keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and