mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28: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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user