mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[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:
@@ -210,7 +210,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
|
||||
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
reasons.push_back("EXT_render_snorm not supported");
|
||||
@@ -553,26 +553,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||
// A multisample texture can only ever be rendered into, so its storage format
|
||||
// has to stay colour-renderable; the ordinary fallback for a three-channel
|
||||
// format is a three-channel one, which ES accepts as a texture but rejects as
|
||||
// multisample storage. Recompute the fallback per target so those formats get
|
||||
// widened here and nowhere else.
|
||||
Flags<PixelFormatNormalizeOptionBit> targetOptions;
|
||||
if (IsGLESProbeMultisampleTarget(target)) {
|
||||
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
}
|
||||
// Colour-attachable targets need a colour-renderable fallback; the ordinary
|
||||
// fallback for a three-channel format is another three-channel one, which ES
|
||||
// accepts as a texture but never as an attachment. Recompute the fallback per
|
||||
// target so those formats get widened where the target demands it.
|
||||
const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex);
|
||||
// Multisample storage has no three-channel form on ES at all, so its widening
|
||||
// is unconditional and skips the native probe (which cannot succeed). Every
|
||||
// other target keeps the widening on the DRIVER branch, behind the native
|
||||
// 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;
|
||||
Bool hasForcedFallback = outerHasForcedFallback;
|
||||
if (targetOptions) {
|
||||
hasForcedFallback = BuildFallbackProbeFormatInfo(
|
||||
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
|
||||
if (!hasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions, false,
|
||||
if (renderTargetOptions) {
|
||||
// Folded into the forced options only when a forced fallback already
|
||||
// applies, so the render-target bits never *create* one: ANGLE's forced
|
||||
// GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs
|
||||
// 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);
|
||||
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();
|
||||
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
|
||||
if (!outerHasForcedFallback) {
|
||||
// A renderbuffer exists only to be attached, so it needs the same three-channel
|
||||
// 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 =
|
||||
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
||||
if (nativeRenderbufferComplete) {
|
||||
@@ -633,16 +685,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
shouldProbeFallbackRenderbuffer = true;
|
||||
}
|
||||
}
|
||||
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||
if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||
ProbeRenderbuffer(gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||
GetRenderbufferFeatureCaps(logicalFormat))) {
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||
}
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
||||
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1401,7 +1401,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
@@ -1459,20 +1466,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// direct_state_access.renderbuffers_storage. One unconditional push settles the whole
|
||||
// block rather than the one cap that happened to be noticed.
|
||||
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() {
|
||||
g_forceFullRenderStateResync = true;
|
||||
g_hasSyncedRenderState = false;
|
||||
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
|
||||
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
|
||||
}
|
||||
void SyncRenderState() {
|
||||
void SyncRenderState(Bool forColorClear) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
const Bool forceFullPush = g_forceFullRenderStateResync;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1778,23 +1800,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Color mask. Uniform masks use the non-indexed glColorMask (works everywhere); divergent
|
||||
// per-draw-buffer masks use the indexed glColorMaski when draw_buffers_indexed is
|
||||
// available, otherwise fall back to broadcasting draw buffer 0. Mirrors the blend block.
|
||||
if (tailSpanDirty || colorMaskWidenDirty) { // Color mask. Uniform masks use the non-indexed glColorMask
|
||||
// (works everywhere); divergent per-draw-buffer masks use the indexed glColorMaski when
|
||||
// draw_buffers_indexed is available, otherwise fall back to broadcasting draw buffer 0.
|
||||
// Mirrors the blend block.
|
||||
using FBO = MG_State::GLState::FramebufferObject;
|
||||
const auto& targetMasks = parameters.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;
|
||||
const BoolVec4 driverMask0 = driverMask(0);
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
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 (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()),
|
||||
ToGLBoolean(m.w()));
|
||||
} else {
|
||||
@@ -1802,14 +1843,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
|
||||
: g_GLESFuncs.glColorMaskiOES;
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
if (forceFullPush || targetMasks[i] != syncedMasks[i]) {
|
||||
const BoolVec4& m = targetMasks[i];
|
||||
// colorMaskWidenDirty forces every slot: the previous push may have
|
||||
// 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()),
|
||||
ToGLBoolean(m.w()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g_syncedColorMaskAlphaWidenMask = appliedWidenMask;
|
||||
}
|
||||
|
||||
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);
|
||||
#endif
|
||||
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(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
return;
|
||||
@@ -3009,7 +3063,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
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);
|
||||
|
||||
@@ -3047,10 +3105,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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 ||
|
||||
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;
|
||||
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo);
|
||||
if (outOfRange && clearDrawFbo != 0) {
|
||||
const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), cc.w()};
|
||||
if ((outOfRange || widenedColorClear) && clearDrawFbo != 0) {
|
||||
GLint maxDrawBuffers = 0;
|
||||
GLint clearedCount = 0;
|
||||
GLint firstDb = -1;
|
||||
@@ -3060,6 +3134,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast<GLenum>(i), &db);
|
||||
if (i == 0) firstDb = db;
|
||||
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);
|
||||
++clearedCount;
|
||||
}
|
||||
@@ -5316,37 +5392,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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) {
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
FramebufferImpl::SyncCurrentFBO();
|
||||
RenderStateImpl::SyncRenderState();
|
||||
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
||||
|
||||
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) {
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
FramebufferImpl::SyncCurrentFBO();
|
||||
RenderStateImpl::SyncRenderState();
|
||||
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
||||
|
||||
// SyncCurrentFBO early-outs for the default framebuffer, so without this
|
||||
// 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).
|
||||
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) {
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
FramebufferImpl::SyncCurrentFBO();
|
||||
RenderStateImpl::SyncRenderState();
|
||||
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
||||
|
||||
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,
|
||||
@@ -5355,9 +5476,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
RenderStateImpl::SyncRenderState();
|
||||
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
||||
|
||||
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
||||
GLfloat widenedValue[4] = {};
|
||||
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer), 1.0f,
|
||||
widenedValue);
|
||||
g_GLESFuncs.glClearBufferfv(buffer, drawbuffer, value);
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
||||
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__);
|
||||
#endif
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
RenderStateImpl::SyncRenderState();
|
||||
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
||||
|
||||
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
||||
GLint widenedValue[4] = {};
|
||||
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer),
|
||||
GLint(1), widenedValue);
|
||||
g_GLESFuncs.glClearBufferiv(buffer, drawbuffer, value);
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
||||
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__);
|
||||
#endif
|
||||
TextureImpl::SyncNeccessaryTextures();
|
||||
RenderStateImpl::SyncRenderState();
|
||||
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
||||
|
||||
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
||||
GLuint widenedValue[4] = {};
|
||||
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer),
|
||||
GLuint(1), widenedValue);
|
||||
g_GLESFuncs.glClearBufferuiv(buffer, drawbuffer, value);
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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};
|
||||
// The bit pattern of 1.0 in a wide-read component type: what GL reports for a channel the
|
||||
// attachment's format does not have.
|
||||
static void FillWideReadOneBits(GLenum componentType, Uint8* oneBits) {
|
||||
switch (componentType) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
oneBits[0] = 0xFF;
|
||||
@@ -5727,6 +5850,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
default:
|
||||
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);
|
||||
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
|
||||
// (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.
|
||||
// `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,
|
||||
GLenum type, void* pixels, Bool honorPackImageParams = false,
|
||||
Bool applyFixedPointReadClamp = true) {
|
||||
GLenum type, void* pixels, Bool honorPackImageParams,
|
||||
Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha) {
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(format, mapping)) {
|
||||
return false;
|
||||
@@ -5900,6 +6059,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
// 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
|
||||
@@ -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
|
||||
// it always takes the conversion path (which swaps on the CPU).
|
||||
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_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT)));
|
||||
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");
|
||||
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",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).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");
|
||||
return;
|
||||
}
|
||||
@@ -6302,6 +6479,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// for normalized attachments), while the conversion path reads a wide format that is always
|
||||
// accepted and repacks on the CPU.
|
||||
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
|
||||
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
|
||||
// them (GL 3.3 section 6.1.4).
|
||||
@@ -6347,7 +6528,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset;
|
||||
if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst,
|
||||
/*honorPackImageParams=*/false,
|
||||
/*applyFixedPointReadClamp=*/false)) {
|
||||
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha)) {
|
||||
allSlicesRead = false;
|
||||
break;
|
||||
}
|
||||
@@ -6369,7 +6550,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels,
|
||||
applyPackImageParams,
|
||||
/*applyFixedPointReadClamp=*/false)) {
|
||||
/*applyFixedPointReadClamp=*/false,
|
||||
forceOpaqueAlpha)) {
|
||||
MGLOG_D("GetTexImage: finished via client-format conversion");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,6 +188,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void OnBackendContextDestroyed();
|
||||
} // 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::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -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,
|
||||
const IntVec3& texelSize,
|
||||
const void* data,
|
||||
@@ -1914,6 +2062,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
// 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,
|
||||
@@ -2121,9 +2302,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
? textureMipmapObject->MapMipmapData(uploadTarget, level)
|
||||
: nullptr;
|
||||
Vector<Float> convertedUploadData;
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> widenedUploadData;
|
||||
const void* uploadData = PrepareFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), targetInternal, levelTexelSize, pData,
|
||||
levelByteSize, glType, convertedUploadData, widenedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
uploadData, levelByteSize, &glType, packedUploadData);
|
||||
@@ -2253,9 +2435,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level);
|
||||
Vector<Float> convertedUploadData;
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> widenedUploadData;
|
||||
const void* uploadData = PrepareFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), targetInternal, levelTexelSize, pData,
|
||||
levelByteSize, glType, convertedUploadData, widenedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData =
|
||||
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
@@ -2312,9 +2495,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
? textureMipmapObject->MapMipmapData(uploadTarget, level)
|
||||
: nullptr;
|
||||
Vector<Float> convertedUploadData;
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> widenedUploadData;
|
||||
const void* uploadData = PrepareFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), targetInternal, levelTexelSize, pData,
|
||||
levelByteSize, glType, convertedUploadData, widenedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData =
|
||||
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
@@ -2422,9 +2606,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
const void* mipData = textureMipmapObject->MapMipmapData(uploadTarget, level);
|
||||
Vector<Float> convertedUploadData;
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> widenedUploadData;
|
||||
const void* uploadData = PrepareFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), targetInternal, texelSize, mipData, byteSize,
|
||||
glType, convertedUploadData, widenedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
|
||||
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());
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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
|
||||
@@ -3139,6 +3324,113 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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() {
|
||||
const auto& readFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
@@ -3344,6 +3636,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (asTarget == FramebufferTarget::Draw) {
|
||||
Uint32 snormClampOutputMask = 0;
|
||||
Uint32 unormClampOutputMask = 0;
|
||||
Uint32 alphaWidenedMask = 0;
|
||||
Uint32 integerColorMask = 0;
|
||||
for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) {
|
||||
const auto frontendBuf = stateDrawBuffers[i];
|
||||
if (frontendBuf < FramebufferAttachmentType::Color0 ||
|
||||
@@ -3356,9 +3650,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else if (IsUnormFallbackAttachment(attachmentObject)) {
|
||||
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_unormFallbackClampOutputMask = unormClampOutputMask;
|
||||
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
|
||||
g_integerColorDrawBufferMask = integerColorMask;
|
||||
}
|
||||
|
||||
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
|
||||
|
||||
@@ -515,6 +515,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
SizeT width = 0;
|
||||
@@ -714,6 +733,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// has to apply the clamp itself.
|
||||
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)
|
||||
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
|
||||
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
|
||||
|
||||
@@ -61,29 +61,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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,
|
||||
SizeT targetIndex,
|
||||
Bool caveat,
|
||||
@@ -141,14 +118,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
||||
GetRenderTargetNormalizeOptions(targetIndex));
|
||||
options = GetRuntimeFallbackNormalizeOptions(
|
||||
requestedInternalFormat,
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
}
|
||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
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,
|
||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -178,20 +202,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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) {
|
||||
const SizeT targetIndex =
|
||||
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||
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(targetIndex));
|
||||
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
||||
return BackendFormatAddsAlpha(internalFormat, targetIndex);
|
||||
}
|
||||
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
namespace PrgramImpl {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#pragma once
|
||||
#include <Includes.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 DebugImpl {
|
||||
@@ -34,6 +36,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace VertexArrayImpl
|
||||
|
||||
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,
|
||||
GLenum* outFormat, GLenum* outType,
|
||||
TextureTarget target = TextureTarget::Unknown);
|
||||
@@ -41,10 +53,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLenum* outFormat, GLenum* outType);
|
||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
|
||||
// True when the format the texture is actually created with has an alpha channel the
|
||||
// frontend format does not (the three-channel multisample widening). GL reads such a
|
||||
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
|
||||
// True when the format the image is actually created with has an alpha channel the
|
||||
// frontend format does not (the three-channel colour-renderable widening). GL reads such
|
||||
// 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 BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
|
||||
@@ -106,9 +106,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
||||
// 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
|
||||
// widens three-channel formats to four channels to keep them renderable as *multisample*
|
||||
// storage, and a format that survives only through that substitution is still texture-only
|
||||
// on every ordinary target.
|
||||
// decides each target's substitution against that target's own probe, and a buffer texture
|
||||
// never gets one at all. This is also where the three-channel widening becomes visible to
|
||||
// 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) {
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||
|
||||
@@ -52,6 +52,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/MultiDrawScenario.cpp
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
)
|
||||
|
||||
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 "Init.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_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.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_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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/BackendObjects.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/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/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/Texture/PixelStoreProcessor.h>
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
@@ -2885,3 +2887,292 @@ TEST_F(TextureTest, NamedTextureCallKeepsUnitBindingAccountingCoherent) {
|
||||
MG_Impl::GLImpl::DeleteTextures(2, names);
|
||||
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) {
|
||||
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) {
|
||||
caps.SupportsSrgbWriteControl = true;
|
||||
}
|
||||
|
||||
@@ -1034,6 +1034,16 @@ namespace MobileGL {
|
||||
// GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable
|
||||
// (and usable as multisample texture storage) rather than texture-only.
|
||||
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.
|
||||
// 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.
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
// 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().
|
||||
#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/TextureEnumConverter.h>
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
@@ -450,6 +455,35 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Warn("GL_EXT_texture_norm16",
|
||||
"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
|
||||
// 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.
|
||||
struct GlesProbeSummary {
|
||||
Bool capsValid = false;
|
||||
@@ -913,6 +1058,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.report.formatCapabilities.emplace();
|
||||
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
||||
glesFuncs, caps, builder.report.formatCapabilities.value());
|
||||
ReportThreeChannelColorAttachments(builder, caps, builder.report.formatCapabilities.value());
|
||||
} while (false);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,12 +58,99 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
case GL_R8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
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:
|
||||
break;
|
||||
}
|
||||
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,
|
||||
GLenum* outInternalFormat, GLenum* outFormat, GLenum* outType) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -95,13 +182,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
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) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRgb16)) {
|
||||
*outInternalFormat = GL_RGB32F;
|
||||
@@ -132,14 +212,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
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) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
@@ -173,10 +245,6 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
*outInternalFormat = GL_RGBA16F;
|
||||
break;
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
*outInternalFormat = GL_RGB16F;
|
||||
break;
|
||||
@@ -615,5 +683,18 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
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
|
||||
|
||||
@@ -20,9 +20,14 @@ namespace MobileGL {
|
||||
NoRGB16Snorm = 1 << 6,
|
||||
// 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.
|
||||
// Only meaningful for multisample textures: those can never be uploaded to, only
|
||||
// rendered into, so the extra alpha comes from the draw (1.0 for an RGB source)
|
||||
// and no transfer path has to expand three-channel client data.
|
||||
// Set for any colour-attachable target whose native three-channel form the driver
|
||||
// refused to render to (multisample storage always, since ES has no three-channel
|
||||
// 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,
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user