mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1468 lines
84 KiB
C++
1468 lines
84 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.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
|
|
|
|
#include "ManagersInternal.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
const MG_Pipe::MGPipeResourceRecord* PipeRenderbufferRecordForHandle(MG_Pipe::MGPipeHandle res) {
|
|
return PipeRecordAt(MG_Pipe::MGPipeApplier().RenderbufferResources, res);
|
|
}
|
|
|
|
namespace FramebufferImpl {
|
|
BackendFramebufferObject::BackendFramebufferObject() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Identity until a non-identity draw-buffer array forces a relocation. A framebuffer
|
|
// that is never draw-bound never runs the recompute, so the table has to start out
|
|
// matching what the attachment loop will physically do.
|
|
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
|
|
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
|
}
|
|
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
|
m_contextGeneration = g_backendContextGeneration;
|
|
if (m_backendFBOId == 0) {
|
|
MGLOG_E_ONCE("Failed to generate framebuffer object.");
|
|
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
|
} else {
|
|
MGLOG_D("Generated framebuffer object with ID: %u.", m_backendFBOId);
|
|
}
|
|
}
|
|
|
|
BackendFramebufferObject::~BackendFramebufferObject() {
|
|
if (InProcessTeardown()) {
|
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
}
|
|
if (m_backendFBOId == 0) {
|
|
return;
|
|
}
|
|
// Scrub the binding shadow whether or not the id can still be deleted: a
|
|
// recycled name must never satisfy the shadow's dedup.
|
|
NoteFramebufferIdDeleted(m_backendFBOId);
|
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) {
|
|
g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId);
|
|
}
|
|
m_backendFBOId = 0;
|
|
}
|
|
|
|
void BackendFramebufferObject::Bind(FramebufferTarget target) const {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (target == FramebufferTarget::Read)
|
|
BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId);
|
|
else
|
|
BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
|
}
|
|
|
|
namespace {
|
|
// Driver-level framebuffer-binding shadow (see Managers.h). Indexed by
|
|
// FramebufferTarget {Draw, Read}.
|
|
Array<Uint, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindings = {0, 0};
|
|
Array<Bool, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindingKnown = {false, false};
|
|
} // namespace
|
|
|
|
void BindFramebufferId(GLenum fbTarget, Uint id) {
|
|
const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
|
|
const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
|
|
const SizeT drawIdx = SizeT(FramebufferTarget::Draw);
|
|
const SizeT readIdx = SizeT(FramebufferTarget::Read);
|
|
const Bool drawMatches =
|
|
!bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id);
|
|
const Bool readMatches =
|
|
!bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id);
|
|
if (drawMatches && readMatches) {
|
|
return;
|
|
}
|
|
g_GLESFuncs.glBindFramebuffer(fbTarget, id);
|
|
if (bindsDraw) {
|
|
g_driverFBOBindings[drawIdx] = id;
|
|
g_driverFBOBindingKnown[drawIdx] = true;
|
|
}
|
|
if (bindsRead) {
|
|
g_driverFBOBindings[readIdx] = id;
|
|
g_driverFBOBindingKnown[readIdx] = true;
|
|
}
|
|
}
|
|
|
|
Uint CurrentFramebufferBinding(FramebufferTarget target) {
|
|
const SizeT idx = SizeT(target);
|
|
if (!g_driverFBOBindingKnown[idx]) {
|
|
// Cold path: pin the shadow from the driver once (init probes and
|
|
// pre-shadow code bind raw but restore what they found).
|
|
GLint binding = 0;
|
|
g_GLESFuncs.glGetIntegerv(target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING
|
|
: GL_DRAW_FRAMEBUFFER_BINDING,
|
|
&binding);
|
|
g_driverFBOBindings[idx] = static_cast<Uint>(binding);
|
|
g_driverFBOBindingKnown[idx] = true;
|
|
}
|
|
return g_driverFBOBindings[idx];
|
|
}
|
|
|
|
void NoteFramebufferIdDeleted(Uint id) {
|
|
if (id == 0) {
|
|
return;
|
|
}
|
|
for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) {
|
|
if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) {
|
|
g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0
|
|
}
|
|
}
|
|
}
|
|
|
|
void InvalidateFramebufferBindingCache() {
|
|
g_driverFBOBindings = {0, 0};
|
|
g_driverFBOBindingKnown = {false, false};
|
|
// The per-target "already synced" memo describes work pushed into the ES context
|
|
// that is being left or replaced. Both callers (MakeCurrent, DestroyEGLContext)
|
|
// mean the context may have been reset under us, so claim nothing is synced:
|
|
// a null object never matches a real binding, so SyncCurrentFBO re-pushes.
|
|
g_fboSyncedSlotVersions = {0};
|
|
g_fboSyncedObjectVersions = {0};
|
|
g_fboSyncedObjects = {};
|
|
#if MOBILEGL_ESPRYT_FBO_HANDLE_ARM_MEMOS_LINKED
|
|
// E's MAJOR-4 / A9: the handle arm's own memos say the same thing about the same
|
|
// targets, so they are invalidated HERE rather than at each of the nine call sites -
|
|
// six in DirectGLES.cpp and three in SanityTest.cpp, and it was the three that were
|
|
// missed. See the declaration in Managers.h for why the call is gated and which line
|
|
// each package flips.
|
|
InvalidateFramebufferHandleArmMemos();
|
|
#endif
|
|
}
|
|
|
|
void BackendFramebufferObject::InvalidateSyncedState() {
|
|
std::fill(std::begin(m_frontendDrawBuffers), std::end(m_frontendDrawBuffers),
|
|
FramebufferAttachmentType::Unknown);
|
|
std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE);
|
|
// NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every
|
|
// attachment it had, possibly under a non-identity permutation. Declaring the table
|
|
// identity here is safe only because every gate that decides which points get
|
|
// re-issued is invalidated too - the attachment versions below, and under
|
|
// MOBILEGL_PIPE_PUSH the record memos stamped at the end of this function - so the
|
|
// next sync re-attaches all non-empty attachments at their identity points AND
|
|
// (see SyncToBackend's attachment loop) detaches any color point whose frontend owner
|
|
// is empty. Without that detach a stale image would survive under a point the table now
|
|
// claims for a different, empty attachment.
|
|
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
|
|
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
|
}
|
|
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
|
|
m_backendReadBuffer = GL_NONE;
|
|
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
|
|
static_cast<Uint16>(~0u));
|
|
// Every attachment version is invalidated above, so the next walk re-attaches
|
|
// everything regardless; stamp the generation so it does not re-arm twice.
|
|
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
|
|
// The record memos are the gate the PIPE_PUSH arms' attachment walks actually read -
|
|
// the handle arm has nothing narrower under its key - so leaving them standing here
|
|
// would let a re-sync of the same record skip the re-attach this function exists to
|
|
// force. 0 is never a live hash, so a zeroed memo is a guaranteed miss.
|
|
m_syncedRecordHashes = {0};
|
|
m_syncedAttachmentRecordHash = 0;
|
|
m_syncedNoAttachmentDefaultsValid = false;
|
|
}
|
|
|
|
static Bool IsSnormFormat(TextureInternalFormat format) {
|
|
switch (format) {
|
|
case TextureInternalFormat::R8Snorm:
|
|
case TextureInternalFormat::RG8Snorm:
|
|
case TextureInternalFormat::RGB8Snorm:
|
|
case TextureInternalFormat::RGBA8Snorm:
|
|
case TextureInternalFormat::R16Snorm:
|
|
case TextureInternalFormat::RG16Snorm:
|
|
case TextureInternalFormat::RGB16Snorm:
|
|
case TextureInternalFormat::RGBA16Snorm:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static Bool IsUnormFormat(TextureInternalFormat format) {
|
|
switch (format) {
|
|
case TextureInternalFormat::R16:
|
|
case TextureInternalFormat::RG16:
|
|
case TextureInternalFormat::RGB16:
|
|
case TextureInternalFormat::RGBA16:
|
|
return true;
|
|
default:
|
|
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;
|
|
}
|
|
}
|
|
|
|
// THE FOUR CROSS-OBJECT MASKS, ANSWERED FROM THE RECORD (ID-12 DV-5, review M-3 / DV-5).
|
|
//
|
|
// All four reduce to (internal format, TEXTURE TARGET) for a texture attachment and to
|
|
// (internal format) for a renderbuffer one, and MGPSurface now carries both inline -
|
|
// InternalFormat since D-C1 and TextureTarget since c0c widened Pad0 into it. So the
|
|
// handle arm stops reading the frontend attachment objects to compute them, which is
|
|
// what D-C1's "the four cross-object masks fall out at push time with no lookup" meant.
|
|
//
|
|
// ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha / their renderbuffer
|
|
// siblings are UNTOUCHED - two of them are on D-N's byte-identical list. Only what they
|
|
// are ASKED changes, which is the whole point of carrying the target rather than
|
|
// inventing a TextureUploadTarget -> TextureTarget inverse: feeding a D-N-pinned
|
|
// function a guessed input would silently change what every texture is allocated as.
|
|
//
|
|
// Kind IS the gate (c0c: "consulted only when Kind == kMGPipeSurfaceKindTexture"), and
|
|
// kMGPipeSurfaceNoTextureTarget on a texture point is a seam defect - refused loudly and
|
|
// answered false, never guessed. False is the safe direction for all four: the mask
|
|
// turns an emulation ON, and an emulation that does not run leaves the driver's own
|
|
// (correct-for-the-real-format) behavior, while one that runs on the wrong buffer
|
|
// clamps or overwrites texels the application wrote.
|
|
static Bool PushedSurfaceTextureTarget(const MG_Pipe::MGPSurface& surface, TextureTarget* out) {
|
|
if (surface.TextureTarget == MG_Pipe::kMGPipeSurfaceNoTextureTarget) {
|
|
MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} says Kind=texture but carries no "
|
|
"texture target - refusing to guess one for the cross-object masks",
|
|
surface.Res.Slot, surface.Res.Gen);
|
|
return false;
|
|
}
|
|
*out = static_cast<TextureTarget>(surface.TextureTarget);
|
|
return true;
|
|
}
|
|
|
|
static Bool IsSnormFallbackSurface(const MG_Pipe::MGPSurface& surface) {
|
|
const auto format = static_cast<TextureInternalFormat>(surface.InternalFormat);
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
|
|
TextureTarget target = TextureTarget::Unknown;
|
|
return PushedSurfaceTextureTarget(surface, &target) && IsSnormFormat(format) &&
|
|
TextureImpl::ShouldUseCaveatTextureFormat(format, target);
|
|
}
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
|
|
return IsSnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Bool IsUnormFallbackSurface(const MG_Pipe::MGPSurface& surface) {
|
|
const auto format = static_cast<TextureInternalFormat>(surface.InternalFormat);
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
|
|
TextureTarget target = TextureTarget::Unknown;
|
|
return PushedSurfaceTextureTarget(surface, &target) && IsUnormFormat(format) &&
|
|
TextureImpl::ShouldUseCaveatTextureFormat(format, target);
|
|
}
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
|
|
return IsUnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Bool IsAlphaWidenedColorSurface(const MG_Pipe::MGPSurface& surface) {
|
|
const auto format = static_cast<TextureInternalFormat>(surface.InternalFormat);
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
|
|
TextureTarget target = TextureTarget::Unknown;
|
|
return PushedSurfaceTextureTarget(surface, &target) &&
|
|
TextureImpl::BackendTextureFormatAddsAlpha(format, target);
|
|
}
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
|
|
return TextureImpl::BackendRenderbufferFormatAddsAlpha(format);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Bool IsIntegerColorSurface(const MG_Pipe::MGPSurface& surface) {
|
|
// No target and no caveat table: integerness is a property of the format alone, so
|
|
// this one is the same question on both arms.
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
|
return false;
|
|
}
|
|
return IsIntegerColorFormat(static_cast<TextureInternalFormat>(surface.InternalFormat));
|
|
}
|
|
|
|
// The record's surface for an attachment POINT, or null when this record does not
|
|
// describe that point at all. MGPFramebufferState carries Color[8] + Depth + Stencil
|
|
// (D-C1), which is every point a framebuffer can hold on the handle arm: D-C3 refuses
|
|
// bit 9 outright on a driver reporting more than 8 color attachments, and the
|
|
// FRONT/BACK points belong to the DEFAULT framebuffer, which has no twin at all.
|
|
const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record,
|
|
FramebufferAttachmentType point) {
|
|
if (point == FramebufferAttachmentType::Depth) return &record.Depth;
|
|
if (point == FramebufferAttachmentType::Stencil) return &record.Stencil;
|
|
if (point < FramebufferAttachmentType::Color0 || point > FramebufferAttachmentType::Color31) {
|
|
return nullptr;
|
|
}
|
|
const Int index = static_cast<Int>(point) - static_cast<Int>(FramebufferAttachmentType::Color0);
|
|
if (index >= static_cast<Int>(MG_Pipe::kMGPipeMaxColorAttachments)) return nullptr;
|
|
return &record.Color[static_cast<SizeT>(index)];
|
|
}
|
|
|
|
// SyncAttachmentObject's handle arm (review M-3). WHAT MOVES IS THE RESOLUTION: the twin
|
|
// is adopted from MGPSurface::Res through the slot table instead of being looked up by
|
|
// the frontend object's ADDRESS, and the attach SHAPE - layered, level, layer, the
|
|
// upload target and the texture target - is read off the surface instead of off the
|
|
// frontend attachment object.
|
|
//
|
|
// P5e (fb, CONTRACT-P5E.md §5.4): THE FRONTEND ATTACHMENT PARAMETER IS GONE. It was
|
|
// here for two things and P5e retires both. The storage syncs now have by-handle forms
|
|
// (SyncMipmapsToBackendByHandle / SyncToBackendByHandle), so the object is no longer
|
|
// needed to reach the texels; and the record is now the ONLY statement of what is
|
|
// attached, so the cross-checks had nothing left to corroborate against - a second
|
|
// answer that only a monolith build could produce is not a check, it is a second
|
|
// writer. With the object gone, review N-6's hole ("record empty, frontend still
|
|
// holding") is UNREPRESENTABLE rather than refused: the caller's detach and this
|
|
// function's attach now read the SAME test, `Kind == None || Res null`.
|
|
//
|
|
// The two P5c cross-checks that ran HandleOf on the apply thread go with it. The
|
|
// renderbuffer one (the one that was NOT transport-gated, unlike its texture sibling)
|
|
// is deleted by the rekey rather than gated, which is what CONTRACT-P5E §4.4 asks for:
|
|
// gating it would have left a client-allocator probe compiled into a path a run-ahead
|
|
// apply reaches, and there is no longer anything for it to say.
|
|
//
|
|
// P5e (fix1, ID-81 / §5.8): THE STORAGE SYNC IS ARM-SELECTED AND THE MONOLITH ARM KEEPS
|
|
// ITS FRONTEND OBJECT. This function has TWO callers and they are on opposite arms: the
|
|
// handle form (SyncToBackendByHandle) has no frontend object and must not acquire one,
|
|
// while the OBJECT form (SyncToBackend, the push-monolith arm - P4a drives its
|
|
// attachments from the record too, gated on FramebufferSubsystemEnabled() alone, D-C2)
|
|
// is holding the frontend attachment the whole time. fb dropped the parameter for both,
|
|
// which routed the monolith arm into tx2's by-handle storage seam; that seam passes a
|
|
// NULL SharedPtr and relies on the record arm being selected, and under
|
|
// Transport == Monolith it is not - `stateTextureObject->IsTextureView()` then
|
|
// dereferenced null (the device's Lightmap.<init> -> clearColorTexture -> glClear
|
|
// SIGSEGV). So the parameter comes back as MONOLITH GLUE, used for nothing but the
|
|
// storage sync: the attach SHAPE, the empty-point test and the twin adoption stay the
|
|
// record's on both arms, which is what keeps N-6's hole closed and the cross-checks
|
|
// deleted. It is a pointer and it is null on the handle arm, so "which arm am I on" is
|
|
// never inferred from it - MG_Config::Transport decides, as §5.8 requires, and a
|
|
// monolith arm that somehow arrives without one refuses by name instead of attaching a
|
|
// texture whose levels were never pushed.
|
|
static Bool SyncAttachmentSurface(GLenum glFBOTarget, const MG_Pipe::MGPSurface& surface,
|
|
GLenum glBackendAttachment,
|
|
const MG_State::GLState::FramebufferAttachmentObject* monolithAttachment) {
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || MG_Pipe::MGPipeHandleIsNull(surface.Res)) {
|
|
// An empty point: the caller detached it on exactly this test, so attaching
|
|
// nothing here is the whole of the agreement. (The legacy arm does the same.)
|
|
return true;
|
|
}
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
|
|
// The record's handle is validated BEFORE the frontend is consulted (N-3): it is
|
|
// the untrusted half, the frontend cross-check below is the corroboration, and
|
|
// asking in this order is what gives the two adoption refusals a reachable
|
|
// caller at all.
|
|
if (!PipeTwinHandleIsAdoptable(TextureImpl::g_backendTextureObjects, surface.Res, "texture")) {
|
|
return false;
|
|
}
|
|
auto* twinSlot = AdoptTwinByHandle(TextureImpl::g_backendTextureObjects, surface.Res, "texture");
|
|
if (twinSlot == nullptr) {
|
|
return false; // AdoptTwinByHandle named the refusal
|
|
}
|
|
if (!*twinSlot) {
|
|
*twinSlot = MakeShared<TextureImpl::BackendTextureObject>();
|
|
}
|
|
// COPIED OUT of the table: SyncMipmapsToBackend can grow it (a re-mint adopts
|
|
// another handle), and Managers.h:381-385's pointer-invalidation warning applies
|
|
// to exactly this sequence.
|
|
SharedPtr<TextureImpl::BackendTextureObject> backendTextureObject = *twinSlot;
|
|
if (!backendTextureObject) {
|
|
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
|
|
return false;
|
|
}
|
|
// P5e (fb): the storage sync BY HANDLE (tx2's seam). The level shadow it reads
|
|
// is the applier's staged texture store, not a frontend object, which is what
|
|
// let the object parameter go - UNDER A TRANSPORT. P5e (fix1, ID-81): the
|
|
// push-monolith arm keeps the frontend sync it had at f6cfcbd3, token for token,
|
|
// because tx2's record arm inside SyncMipmapsToBackend is itself selected by
|
|
// `Transport != Monolith` and cannot answer here.
|
|
backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res);
|
|
// From here on every draw, clear and blit into this framebuffer can write the
|
|
// texture behind the staged shadow's back (the frontend arm's note, by handle).
|
|
TextureImpl::NoteDriverSideTextureWriteByHandle(surface.Res);
|
|
const auto uploadTarget = static_cast<TextureUploadTarget>(surface.UploadTarget);
|
|
if (surface.Layered != 0) {
|
|
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
|
|
backendTextureObject->GetBackendTextureId(),
|
|
static_cast<GLint>(surface.Level));
|
|
} else if (uploadTarget == TextureUploadTarget::Texture3D ||
|
|
uploadTarget == TextureUploadTarget::Texture2DArray ||
|
|
uploadTarget == TextureUploadTarget::Texture1DArray ||
|
|
uploadTarget == TextureUploadTarget::CubeMapArray ||
|
|
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
|
|
g_GLESFuncs.glFramebufferTextureLayer(
|
|
glFBOTarget, glBackendAttachment, backendTextureObject->GetBackendTextureId(),
|
|
static_cast<GLint>(surface.Level), static_cast<GLint>(surface.Layer));
|
|
} else {
|
|
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
|
if (glTextureTarget == GL_UNKNOWN_MGL) {
|
|
TextureTarget target = TextureTarget::Unknown;
|
|
if (!PushedSurfaceTextureTarget(surface, &target)) {
|
|
return false;
|
|
}
|
|
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
|
}
|
|
// Same cube-face rule as the legacy arm: glBindTexture rejects the face
|
|
// enums, so bind through the owning cube target and attach with the face.
|
|
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
|
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
|
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
|
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
|
backendTextureObject->GetBackendTextureId(),
|
|
static_cast<GLint>(surface.Level));
|
|
}
|
|
return true;
|
|
}
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
|
|
// Same order as the texture arm above, for the same reason (N-3).
|
|
if (!PipeTwinHandleIsAdoptable(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res,
|
|
"renderbuffer")) {
|
|
return false;
|
|
}
|
|
auto* twinSlot =
|
|
AdoptTwinByHandle(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res, "renderbuffer");
|
|
if (twinSlot == nullptr) {
|
|
return false;
|
|
}
|
|
if (!*twinSlot) {
|
|
*twinSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
|
}
|
|
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject = *twinSlot;
|
|
if (!backendRenderbufferObject) {
|
|
MGLOG_E_ONCE("%s: No backend renderbuffer found for FBO attachment.", __func__);
|
|
return false;
|
|
}
|
|
// P5e (fb): the four allocation values were already record-supplied (D-D2); what
|
|
// moves here is the LOOKUP - the handle the surface carries, never HandleOf on
|
|
// an object the apply thread was handed.
|
|
//
|
|
// P5e (fix1, ID-81): arm-selected for the texture arm's reason, and here it is a
|
|
// BEHAVIOR difference even though the by-handle body dereferences nothing: the
|
|
// object form reports a refused allocation to the application through the live
|
|
// GLContext (RecordError) and the by-handle form deliberately does not, because
|
|
// under a transport there is no application on this side to report it to. On the
|
|
// push-monolith arm there is, so the monolith arm keeps the object form.
|
|
backendRenderbufferObject->SyncToBackendByHandle(surface.Res);
|
|
backendRenderbufferObject->Bind();
|
|
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
|
backendRenderbufferObject->GetBackendRenderbufferId());
|
|
return true;
|
|
}
|
|
MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} carries Kind=%u, which is neither a texture "
|
|
"nor a renderbuffer nor an empty point - refusing to attach",
|
|
surface.Res.Slot, surface.Res.Gen, surface.Kind);
|
|
return false;
|
|
}
|
|
|
|
// The read attachment's storage carries an alpha channel its frontend format does not
|
|
// (the three-channel color-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* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
|
return record && IsAlphaWidenedColorSurface(record->ReadSurface);
|
|
}
|
|
|
|
Bool IsFixedPointFallbackReadAttachment() {
|
|
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
|
return record && record->ReadSurface.Kind != MG_Pipe::kMGPipeSurfaceKindNone &&
|
|
IsSnormFormat(static_cast<TextureInternalFormat>(record->ReadSurface.InternalFormat));
|
|
const auto& readFBO =
|
|
MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
|
if (!readFBO) {
|
|
return false;
|
|
}
|
|
const auto readBuffer = readFBO->GetReadBuffer();
|
|
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
|
|
return false;
|
|
}
|
|
// Any signed-normalized attachment, not just the ones currently substituted:
|
|
// ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer
|
|
// hands back the negative half that desktop GL clamps away.
|
|
const auto& attachmentObject = readFBO->GetAttachment(readBuffer);
|
|
if (attachmentObject.IsTexture()) {
|
|
const auto& textureObject = attachmentObject.GetTexture();
|
|
return textureObject && IsSnormFormat(textureObject->GetFormat());
|
|
}
|
|
if (attachmentObject.IsRenderbuffer()) {
|
|
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
|
|
return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const MG_Pipe::MGPFramebufferState* PushedFramebufferRecord(MG_Pipe::MGPipeHandle fbo) {
|
|
// FramebufferRecordFor answers null for the null handle and for a slot nothing has
|
|
// described (both silent - that is every framebuffer before its first emission), and
|
|
// refuses a stale generation loudly while counting it in
|
|
// StaleFramebufferRecordLookups. So there is no second emptiness test here to get out
|
|
// of step with the applier's, and none of the three cases is a binding question.
|
|
return MG_Pipe::MGPipeApplier().FramebufferRecordFor(fbo);
|
|
}
|
|
|
|
// The record's DrawBuffers[] is an ATTACHMENT INDEX with -1 for None (D-C1), which is
|
|
// what the frontend array holds for every framebuffer that has a twin. It deliberately
|
|
// cannot spell FrontLeft / FrontRight / BackLeft / BackRight: those are the DEFAULT
|
|
// framebuffer's own tokens, the default framebuffer has no BackendFramebufferObject at
|
|
// all (it is pDefaultFramebufferInfo->defaultFBO, which is why MGPipeHandles.h reserves
|
|
// {0,1} for it), and the legacy arm's branch for them says so in as many words - "shouldn't
|
|
// remap". So the conversion below is total for every object that reaches this twin, and
|
|
// the record's IsDefault is the thing a later phase would test if that ever changed.
|
|
static void DecodePushedDrawBuffers(const MG_Pipe::MGPFramebufferState& record,
|
|
FramebufferAttachmentType* out) {
|
|
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
|
const Int8 index = record.DrawBuffers[i];
|
|
out[i] = index < 0 ? FramebufferAttachmentType::None
|
|
: static_cast<FramebufferAttachmentType>(
|
|
static_cast<Int>(FramebufferAttachmentType::Color0) + index);
|
|
}
|
|
}
|
|
|
|
// P5e (fb, §5.4): the record half of SyncReadBufferToBackend, lifted out of it so the
|
|
// by-handle sync can run it WITHOUT a frontend object. Nothing inside reads one: the
|
|
// frontend index was only ever a diagnostic, and it is passed as a number now
|
|
// (GlNameForDiag is 0 on the handle arm, which reads as "the record did not say").
|
|
void BackendFramebufferObject::ApplyReadBufferFromRecord(const MG_Pipe::MGPFramebufferState& record,
|
|
Uint glNameForDiag) {
|
|
// D-C1 makes the NULL Res the spelling of "no attachment" and c0c's
|
|
// kMGPipeSurfaceKindNone makes Kind agree with it on a zero-initialized record.
|
|
// Res stays the test - it is the one D-C1 fixes - and Kind is CROSS-CHECKED
|
|
// rather than consulted: the two are one statement the emitter has to make
|
|
// twice, and a record where they disagree is a seam defect, not an empty point.
|
|
// Picking a winner silently is what this refuses to do.
|
|
const Bool readSurfaceEmpty = MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res);
|
|
if (readSurfaceEmpty != (record.ReadSurface.Kind == MG_Pipe::kMGPipeSurfaceKindNone)) {
|
|
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface says Res={%u, %u} and "
|
|
"Kind=%u, which disagree about whether it names anything - refusing "
|
|
"rather than choosing one of them",
|
|
glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen,
|
|
record.ReadSurface.Kind);
|
|
return;
|
|
}
|
|
FramebufferAttachmentType pushedReadBuf = FramebufferAttachmentType::None;
|
|
if (!readSurfaceEmpty) {
|
|
// EVERY FIELD THAT IDENTIFIES THE IMAGE, not the three v1 compared (m-4).
|
|
// The same image legally sits at two color points - a layered attachment of
|
|
// one array and a single slice of it are different surfaces of one Res - so a
|
|
// partial comparison resolves to the lower index and the refusal branch below
|
|
// ("refusing to guess") would then be doing part of the guessing itself.
|
|
// InternalFormat and TextureTarget are properties of the RESOURCE rather than
|
|
// of the point, so they add nothing to the identity and are left out.
|
|
Bool matched = false;
|
|
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
|
const auto& color = record.Color[i];
|
|
if (color.Res == record.ReadSurface.Res && color.Kind == record.ReadSurface.Kind &&
|
|
color.Layered == record.ReadSurface.Layered && color.Level == record.ReadSurface.Level &&
|
|
color.Layer == record.ReadSurface.Layer &&
|
|
color.UploadTarget == record.ReadSurface.UploadTarget) {
|
|
pushedReadBuf = static_cast<FramebufferAttachmentType>(
|
|
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!matched) {
|
|
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface {%u, %u} matches no "
|
|
"color attachment of its own record - refusing to guess a read "
|
|
"buffer",
|
|
glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen);
|
|
return;
|
|
}
|
|
}
|
|
if (pushedReadBuf == m_frontendReadBuffer) {
|
|
return;
|
|
}
|
|
m_frontendReadBuffer = pushedReadBuf;
|
|
|
|
const GLenum glPushedReadBuffer = GetBackendAttachmentType(pushedReadBuf);
|
|
if (m_backendReadBuffer != glPushedReadBuffer) {
|
|
m_backendReadBuffer = glPushedReadBuffer;
|
|
// Still bound as READ first, for the reason the legacy arm gives below:
|
|
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER.
|
|
Bind(FramebufferTarget::Read);
|
|
g_GLESFuncs.glReadBuffer(glPushedReadBuffer);
|
|
}
|
|
}
|
|
|
|
// P5e (fb): the by-handle entry, for SyncCurrentFBOByRecord's "one object on both
|
|
// bindings" skip - the one path that applies a read buffer without doing the rest of
|
|
// the sync. A framebuffer with no record is the caller's seam defect, not a silent
|
|
// no-op, so it says so.
|
|
void BackendFramebufferObject::SyncReadBufferToBackendByHandle(MG_Pipe::MGPipeHandle fbo) {
|
|
const auto* record = PushedFramebufferRecord(fbo);
|
|
if (record == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so its read "
|
|
"buffer cannot be pushed",
|
|
fbo.Slot, fbo.Gen);
|
|
return;
|
|
}
|
|
ApplyReadBufferFromRecord(*record, 0);
|
|
}
|
|
|
|
Bool BackendFramebufferObject::RecomputeBackendColorSlots(
|
|
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) {
|
|
// Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own
|
|
// limit (ValidateColorAttachmentInRange, which reads the clamped
|
|
// GetDynamicParameters().MaxColorAttachments) is never larger than this raw ES cap, so an
|
|
// index the frontend accepted is always < slotCount. Indices at or above it can never own
|
|
// an image and stay on their identity point - never touched, never a GL error.
|
|
const Uint slotCount =
|
|
std::min<Uint>(MAX_COLOR_ATTACHMENT_SLOTS,
|
|
static_cast<Uint>(std::max<Int>(g_GLESCapabilities.MaxColorAttachments, 1)));
|
|
|
|
GLenum newSlots[MAX_COLOR_ATTACHMENT_SLOTS];
|
|
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
|
|
newSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
|
}
|
|
Bool assigned[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
|
|
Bool slotTaken[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
|
|
|
|
// 1. ES pins draw-buffer slot s to GL_COLOR_ATTACHMENTs, so an attachment named by draw
|
|
// buffer slot s has no choice: its image must sit at backend point s. This has to
|
|
// agree with the compaction the caller just pushed through glDrawBuffers.
|
|
for (Uint s = 0; s < FramebufferObject::MAX_DRAW_BUFFERS && s < slotCount; ++s) {
|
|
const auto frontendBuf = stateDrawBuffers[s];
|
|
if (frontendBuf < FramebufferAttachmentType::Color0 ||
|
|
frontendBuf > FramebufferAttachmentType::Color31) {
|
|
continue; // GL_NONE, or a default-framebuffer FRONT/BACK token: never relocated.
|
|
}
|
|
const Uint a = static_cast<Uint>(frontendBuf) - static_cast<Uint>(FramebufferAttachmentType::Color0);
|
|
// Neither guard may ever fire: a duplicate draw buffer is already INVALID_OPERATION
|
|
// and an out-of-range one is rejected by ValidateColorAttachmentInRange. If one did
|
|
// fire the table would disagree with the glDrawBuffers the caller already issued,
|
|
// which is the exact non-injectivity this table exists to remove.
|
|
MOBILEGL_ASSERT(a < slotCount && !assigned[a],
|
|
"Draw buffer %u names color attachment %u which is out of range or duplicated.", s, a);
|
|
if (a >= slotCount || assigned[a]) {
|
|
continue;
|
|
}
|
|
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
|
|
assigned[a] = true;
|
|
slotTaken[s] = true;
|
|
}
|
|
|
|
// 2. Everything else keeps its identity point when that point survived step 1. This is
|
|
// what makes the ordinary drawBuffers[s] == COLOR_ATTACHMENTs case a strict no-op:
|
|
// the table stays identity, nothing moves, no attachment is re-issued.
|
|
for (Uint a = 0; a < slotCount; ++a) {
|
|
if (assigned[a] || slotTaken[a]) {
|
|
continue;
|
|
}
|
|
newSlots[a] = GL_COLOR_ATTACHMENT0 + a;
|
|
assigned[a] = true;
|
|
slotTaken[a] = true;
|
|
}
|
|
|
|
// 3. What is left are attachments whose identity point step 1 took away. Park them on the
|
|
// lowest free point. They are not draw buffers, so nothing is rendered through them;
|
|
// they only have to stay addressable for glReadBuffer and blits, and the map has to
|
|
// stay injective so reading one of them cannot land on another's image.
|
|
for (Uint a = 0; a < slotCount; ++a) {
|
|
if (assigned[a]) {
|
|
continue;
|
|
}
|
|
for (Uint s = 0; s < slotCount; ++s) {
|
|
if (!slotTaken[s]) {
|
|
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
|
|
assigned[a] = true;
|
|
slotTaken[s] = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Bool moved = false;
|
|
for (Uint a = 0; a < MAX_COLOR_ATTACHMENT_SLOTS; ++a) {
|
|
if (m_backendColorSlots[a] == newSlots[a]) {
|
|
continue;
|
|
}
|
|
m_backendColorSlots[a] = newSlots[a];
|
|
moved = true;
|
|
// This attachment's image now belongs at a different backend point. Its frontend
|
|
// version has not changed, so the attachment loop would skip it; force it.
|
|
m_syncedFrontendAttachmentVersions[static_cast<SizeT>(FramebufferAttachmentType::Color0) + a] =
|
|
static_cast<Uint16>(~0u);
|
|
}
|
|
return moved;
|
|
}
|
|
|
|
// ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------
|
|
//
|
|
// WHY IT EXISTS. ScopedDetachedTextureFramebufferAttachments used to answer "which
|
|
// framebuffers currently have this texture attached" by walking every live twin and
|
|
// reading each one's FRONTEND attachment array. Under P5e that walk is the last place
|
|
// the server holds a frontend framebuffer across records - id re-typed ForEachLive and
|
|
// left the StateForHandle read greppable, naming this package as the one that replaces
|
|
// it. The records already carry the answer: every applied set_framebuffer_state names
|
|
// its eleven surfaces, so the relation is maintained where the record is consumed.
|
|
//
|
|
// KEYED ON THE TEXTURE SLOT rather than on {slot, gen}. The question a detach asks is
|
|
// "does the driver have this texture id hanging off some framebuffer", the points are
|
|
// re-derived from the record on every hit, and a stale entry left by a recycled slot
|
|
// therefore costs one record walk that matches nothing. The FRAMEBUFFER handles are
|
|
// stored whole, so a recycled framebuffer slot never answers for its predecessor
|
|
// (FindByHandle refuses a moved Gen).
|
|
//
|
|
// MAINTAINED AT THE SYNC, NOT AT THE APPLY, and that is the same set rather than a
|
|
// smaller one: a framebuffer whose record was never synced has no driver attachment to
|
|
// detach, and every path that can have one runs this sync first.
|
|
|
|
namespace {
|
|
UnorderedMap<Uint32, Vector<MG_Pipe::MGPipeHandle>> g_framebuffersByAttachedTextureSlot;
|
|
UnorderedMap<Uint32, Vector<Uint32>> g_attachedTextureSlotsByFramebufferSlot;
|
|
|
|
void ForgetFramebufferTextureAttachmentsLocked(Uint32 fboSlot) {
|
|
auto it = g_attachedTextureSlotsByFramebufferSlot.find(fboSlot);
|
|
if (it == g_attachedTextureSlotsByFramebufferSlot.end()) return;
|
|
for (const Uint32 textureSlot : it->second) {
|
|
auto listIt = g_framebuffersByAttachedTextureSlot.find(textureSlot);
|
|
if (listIt == g_framebuffersByAttachedTextureSlot.end()) continue;
|
|
auto& list = listIt->second;
|
|
list.erase(std::remove_if(list.begin(), list.end(),
|
|
[&](MG_Pipe::MGPipeHandle h) { return h.Slot == fboSlot; }),
|
|
list.end());
|
|
if (list.empty()) g_framebuffersByAttachedTextureSlot.erase(listIt);
|
|
}
|
|
g_attachedTextureSlotsByFramebufferSlot.erase(it);
|
|
}
|
|
} // namespace
|
|
|
|
void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo, const MG_Pipe::MGPFramebufferState& record) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(fbo)) return;
|
|
ForgetFramebufferTextureAttachmentsLocked(fbo.Slot);
|
|
Vector<Uint32> textureSlots;
|
|
const auto note = [&](const MG_Pipe::MGPSurface& surface) {
|
|
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
|
|
if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return;
|
|
if (std::find(textureSlots.begin(), textureSlots.end(), surface.Res.Slot) != textureSlots.end()) {
|
|
return; // one texture at two points is one entry
|
|
}
|
|
textureSlots.push_back(surface.Res.Slot);
|
|
g_framebuffersByAttachedTextureSlot[surface.Res.Slot].push_back(fbo);
|
|
};
|
|
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i)
|
|
note(record.Color[i]);
|
|
note(record.Depth);
|
|
note(record.Stencil);
|
|
if (textureSlots.empty()) return; // the common Minecraft frame: renderbuffers only
|
|
g_attachedTextureSlotsByFramebufferSlot[fbo.Slot] = std::move(textureSlots);
|
|
}
|
|
|
|
// ---- P5e (fb, §5.4): BackendFramebufferObject::SyncToBackend BY HANDLE ---------------
|
|
//
|
|
// The record's ELEVEN SURFACES ARE THE POINT SET. The object form walks the frontend's
|
|
// 41 attachment points to learn which exist; this one does not have to, because the
|
|
// emitter REFUSES a framebuffer holding a point at or above the wire width
|
|
// (FramebufferEmit.h's two refusals) and the FRONT/BACK tokens belong to the default
|
|
// framebuffer, which has no twin. So Color[0..7] + Depth + Stencil is the whole of it,
|
|
// and a framebuffer that could not be described that way never produced a record at all.
|
|
//
|
|
// THE PER-POINT VERSION MEMO IS GONE and is not replaced. On the object form it is a
|
|
// second, narrower gate under the record hash, hedging against "a frontend attachment
|
|
// moved without the record moving" - which is unrepresentable once the record is the
|
|
// only statement of what is attached (the hash covers every surface, and Fbo is inside
|
|
// it, so a recycled handle cannot be suppressed against its predecessor). Keeping it
|
|
// would have meant keeping the frontend array that carries it, which is the whole point
|
|
// of this arm. The two SERVER-owned gates stay, because no client-side value can answer
|
|
// what they answer: g_attachmentBackendIdGeneration ("did I re-mint a driver id") and
|
|
// the re-entry it drives.
|
|
void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget asTarget) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
const MG_Pipe::MGPFramebufferState* const recordPtr = PushedFramebufferRecord(fbo);
|
|
if (recordPtr == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so it is not "
|
|
"configured here; nothing is bound by this refusal, but the caller's own "
|
|
"bind still targets it",
|
|
fbo.Slot, fbo.Gen);
|
|
return;
|
|
}
|
|
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
|
|
if (record.IsDefault != 0) {
|
|
// The default framebuffer has no twin (pDefaultFramebufferInfo->defaultFBO is the
|
|
// frontend object and MGPipeHandles.h reserves {0,1} for it), so reaching this
|
|
// body with one is a caller that skipped its own IsDefault branch.
|
|
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} describes the DEFAULT framebuffer, "
|
|
"which has no twin to configure",
|
|
fbo.Slot, fbo.Gen);
|
|
return;
|
|
}
|
|
// Kept in step for the monolith-glue overloads that still resolve their record
|
|
// through it (SyncReadBufferToBackend's push arm).
|
|
m_pushedSyncHandle = fbo;
|
|
const GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
|
|
Bind(asTarget);
|
|
|
|
FramebufferObject::FramebufferAttachmentArray stateDrawBuffers{};
|
|
DecodePushedDrawBuffers(record, stateDrawBuffers.data());
|
|
|
|
// 1. Remap draw buffers. Same rule as the object form: glDrawBuffers writes the
|
|
// DRAW-bound framebuffer's state, so a READ-only sync must neither issue it nor
|
|
// stamp the memo (the Minecraft 26.x OIT bug the object form names).
|
|
Bool attachmentPointsMoved = false;
|
|
const Bool drawBufferClean =
|
|
memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(),
|
|
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0;
|
|
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
|
|
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
|
|
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
|
|
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
|
|
int nEffectiveBuffers = 0;
|
|
for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
|
const auto frontendBuf = stateDrawBuffers[i];
|
|
if (frontendBuf == FramebufferAttachmentType::None) {
|
|
m_backendDrawBuffers[i] = GL_NONE;
|
|
continue;
|
|
}
|
|
// DecodePushedDrawBuffers cannot produce a FRONT/BACK token - those are the
|
|
// default framebuffer's own and it has no twin - so the object form's
|
|
// "shouldn't remap" branch has no counterpart here.
|
|
m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i;
|
|
nEffectiveBuffers = i + 1;
|
|
}
|
|
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
|
|
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
|
|
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
|
|
attachmentPointsMoved = true;
|
|
}
|
|
}
|
|
|
|
// The four cross-object masks, off the record's own surfaces (ID-12 DV-5).
|
|
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 ||
|
|
frontendBuf > FramebufferAttachmentType::Color31) {
|
|
continue;
|
|
}
|
|
const MG_Pipe::MGPSurface* surface = PushedSurfaceForAttachment(record, frontendBuf);
|
|
if (surface == nullptr) continue; // unrepresentable: DecodePushedDrawBuffers' range
|
|
if (IsSnormFallbackSurface(*surface)) {
|
|
snormClampOutputMask |= (1u << i);
|
|
} else if (IsUnormFallbackSurface(*surface)) {
|
|
unormClampOutputMask |= (1u << i);
|
|
}
|
|
if (IsAlphaWidenedColorSurface(*surface)) alphaWidenedMask |= (1u << i);
|
|
if (IsIntegerColorSurface(*surface)) integerColorMask |= (1u << i);
|
|
}
|
|
PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask;
|
|
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
|
|
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
|
|
g_integerColorDrawBufferMask = integerColorMask;
|
|
}
|
|
|
|
// 2. Remap read buffer, READ-target only, for the reason the object form gives.
|
|
if (asTarget == FramebufferTarget::Read) {
|
|
ApplyReadBufferFromRecord(record, 0);
|
|
}
|
|
|
|
// 3. The attachments. Three gates, and the key is the object's rather than the
|
|
// binding's: idGenerationMoved ("I re-minted a driver id, so a point may hold a
|
|
// dead one"), attachmentPointsMoved ("the color permutation moved, so a point
|
|
// whose record did not change still has to be re-issued") and the record hash.
|
|
// NOT m_syncedRecordHashes: the per-target array is the object arm's re-arm
|
|
// trigger, and the points written below belong to the driver FBO rather than to
|
|
// the binding they were walked for - see m_syncedAttachmentRecordHash.
|
|
const Bool idGenerationMoved = m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration;
|
|
if (idGenerationMoved) {
|
|
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
|
|
}
|
|
if (idGenerationMoved || attachmentPointsMoved || m_syncedAttachmentRecordHash != record.ContentHash) {
|
|
const auto applyPoint = [&](FramebufferAttachmentType point, const MG_Pipe::MGPSurface& surface) {
|
|
const Bool isColorPoint =
|
|
point >= FramebufferAttachmentType::Color0 && point <= FramebufferAttachmentType::Color31;
|
|
const GLenum glBackendAttachment = isColorPoint
|
|
? GetBackendAttachmentType(point)
|
|
: MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(point);
|
|
if (glBackendAttachment == GL_NONE || glBackendAttachment == GL_UNKNOWN_MGL) return;
|
|
const Bool empty =
|
|
surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || MG_Pipe::MGPipeHandleIsNull(surface.Res);
|
|
// The detach that keeps m_backendColorSlots a permutation of the PHYSICAL
|
|
// layout: a color point whose record says empty must not keep the previous
|
|
// owner's image, or glReadBuffer returns it. Bounded by the driver's cap,
|
|
// because GL_COLOR_ATTACHMENTn above it is INVALID_ENUM.
|
|
if (isColorPoint && empty) {
|
|
const Int index = static_cast<Int>(point) - static_cast<Int>(FramebufferAttachmentType::Color0);
|
|
if (index < g_GLESCapabilities.MaxColorAttachments) {
|
|
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, 0);
|
|
}
|
|
return;
|
|
}
|
|
// No frontend attachment on this arm, by construction: the record IS the
|
|
// point set here (§5.4). Null is what selects nothing - the transport test
|
|
// inside is what selects the by-handle storage syncs.
|
|
SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment,
|
|
/*monolithAttachment=*/nullptr);
|
|
};
|
|
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
|
applyPoint(static_cast<FramebufferAttachmentType>(
|
|
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i)),
|
|
record.Color[i]);
|
|
}
|
|
applyPoint(FramebufferAttachmentType::Depth, record.Depth);
|
|
applyPoint(FramebufferAttachmentType::Stencil, record.Stencil);
|
|
m_syncedAttachmentRecordHash = record.ContentHash;
|
|
NoteFramebufferTextureAttachments(fbo, record);
|
|
}
|
|
|
|
// 4. ARB_framebuffer_no_attachments. With no attachment at all the record's geometry
|
|
// IS the framebuffer's default width/height/layers/samples (FillGeometry), and the
|
|
// driver FBO needs them too: without a default size an attachment-less ES
|
|
// framebuffer is incomplete and every draw into it is dropped. With attachments the
|
|
// defaults are ignored by GL, so they are only pushed for the empty shape.
|
|
Bool hasAnyAttachment = !(record.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
|
|
MG_Pipe::MGPipeHandleIsNull(record.Depth.Res)) ||
|
|
!(record.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
|
|
MG_Pipe::MGPipeHandleIsNull(record.Stencil.Res));
|
|
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && !hasAnyAttachment; ++i) {
|
|
hasAnyAttachment = !(record.Color[i].Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
|
|
MG_Pipe::MGPipeHandleIsNull(record.Color[i].Res));
|
|
}
|
|
if (!hasAnyAttachment) {
|
|
const Array<Int, 5> defaults = {
|
|
static_cast<Int>(record.Width), static_cast<Int>(record.Height), static_cast<Int>(record.Layers),
|
|
std::min<Int>(static_cast<Int>(record.Samples), g_GLESCapabilities.MaxFramebufferSamples),
|
|
static_cast<Int>(record.FixedSampleLocations != 0 ? GL_TRUE : GL_FALSE)};
|
|
if (!m_syncedNoAttachmentDefaultsValid || m_syncedNoAttachmentDefaults != defaults) {
|
|
g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_WIDTH, defaults[0]);
|
|
g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_HEIGHT, defaults[1]);
|
|
if (g_GLESCapabilities.SupportsFramebufferDefaultLayers) {
|
|
g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_LAYERS, defaults[2]);
|
|
}
|
|
g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_SAMPLES, defaults[3]);
|
|
g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS,
|
|
defaults[4]);
|
|
m_syncedNoAttachmentDefaults = defaults;
|
|
m_syncedNoAttachmentDefaultsValid = true;
|
|
}
|
|
}
|
|
|
|
// The walk itself can re-mint a driver id (the storage sync behind a texture
|
|
// surface), invalidating points this walk already attached. Re-enter until the
|
|
// generation is quiescent, exactly as the object form does - every pass syncs each
|
|
// dirty texture clean, so each repeat finds strictly fewer re-mints.
|
|
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
|
|
SyncToBackendByHandle(fbo, asTarget);
|
|
}
|
|
}
|
|
|
|
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
|
|
// Only color attachments are ever relocated; depth/stencil, the default framebuffer's
|
|
// FRONT/BACK names and None map straight through.
|
|
if (frontendAtt < FramebufferAttachmentType::Color0 || frontendAtt > FramebufferAttachmentType::Color31) {
|
|
return MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt);
|
|
}
|
|
// The table is a permutation of the backend color points, so this is the one point that
|
|
// owns this attachment. Searching the draw-buffer array instead returned the identity
|
|
// point for every attachment that was not a draw buffer - which is exactly the point a
|
|
// relocated draw buffer had just taken over, so COLOR_ATTACHMENT0 read back the image of
|
|
// whatever attachment was last made the draw buffer.
|
|
const Uint index = static_cast<Uint>(frontendAtt) - static_cast<Uint>(FramebufferAttachmentType::Color0);
|
|
return m_backendColorSlots[index];
|
|
}
|
|
|
|
TwinRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject, MG_Pipe::MGPipeKind::Framebuffer>
|
|
g_backendFramebufferObjects;
|
|
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSlotVersions = {0};
|
|
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
|
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
|
// must re-sync it even when the binding-slot version has not moved.
|
|
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions = {0};
|
|
Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
|
g_fboSyncedObjects = {};
|
|
Uint64 g_attachmentBackendIdGeneration = 0;
|
|
Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations = {0};
|
|
} // namespace FramebufferImpl
|
|
|
|
namespace ScratchFBOImpl {
|
|
namespace {
|
|
// ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it
|
|
// (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it.
|
|
struct ScratchSet {
|
|
ScratchFramebuffer temp;
|
|
ScratchFramebuffer blitRead;
|
|
ScratchFramebuffer blitDraw;
|
|
Uint completeTinyFBOId = 0;
|
|
Uint completeTinyRBOId = 0;
|
|
};
|
|
PerNativeContext<ScratchSet> g_scratchSets;
|
|
|
|
// Detach every point the shadow no longer vouches for. Used when the
|
|
// shadow is unknown (context reset, texture id deleted while attached).
|
|
void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
|
fb.colorTex = 0;
|
|
fb.colorTarget = 0;
|
|
fb.colorLevel = 0;
|
|
fb.colorLayer = -1;
|
|
fb.depthTex = 0;
|
|
fb.depthTarget = 0;
|
|
fb.depthLevel = 0;
|
|
fb.depthLayer = -1;
|
|
fb.depthHasStencil = false;
|
|
fb.depthIsStencilOnly = false;
|
|
fb.attachmentsKnown = true;
|
|
}
|
|
|
|
void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) {
|
|
if (!fb.attachmentsKnown) {
|
|
ScrubAllAttachments(fb, fbTarget);
|
|
}
|
|
}
|
|
|
|
// The post-attach glGetError probe below must not misread an error some
|
|
// earlier operation left queued; drain before attaching (rare path -
|
|
// only runs when the attachment actually changes).
|
|
void DrainPendingGLErrors() {
|
|
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
|
|
}
|
|
|
|
// Record the color point as detached when the shadow said something was
|
|
// there; the actual detach call is the caller's (it may be replaced by
|
|
// the new attach directly when the point is being overwritten).
|
|
void RecordNoColor(ScratchFramebuffer& fb) {
|
|
fb.colorTex = 0;
|
|
fb.colorTarget = 0;
|
|
fb.colorLevel = 0;
|
|
fb.colorLayer = -1;
|
|
}
|
|
|
|
void RecordNoDepth(ScratchFramebuffer& fb) {
|
|
fb.depthTex = 0;
|
|
fb.depthTarget = 0;
|
|
fb.depthLevel = 0;
|
|
fb.depthLayer = -1;
|
|
fb.depthHasStencil = false;
|
|
fb.depthIsStencilOnly = false;
|
|
}
|
|
} // namespace
|
|
|
|
ScratchFramebuffer& TempFramebuffer() {
|
|
return g_scratchSets.Current().temp;
|
|
}
|
|
ScratchFramebuffer& BlitReadFramebuffer() {
|
|
return g_scratchSets.Current().blitRead;
|
|
}
|
|
ScratchFramebuffer& BlitDrawFramebuffer() {
|
|
return g_scratchSets.Current().blitDraw;
|
|
}
|
|
|
|
Uint EnsureId(ScratchFramebuffer& fb) {
|
|
if (fb.id == 0) {
|
|
g_GLESFuncs.glGenFramebuffers(1, &fb.id);
|
|
// A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw
|
|
// buffers (the ES defaults for a non-default framebuffer).
|
|
fb.attachmentsKnown = true;
|
|
RecordNoColor(fb);
|
|
RecordNoDepth(fb);
|
|
fb.readBuffer = GL_COLOR_ATTACHMENT0;
|
|
fb.drawBuffer = GL_COLOR_ATTACHMENT0;
|
|
}
|
|
return fb.id;
|
|
}
|
|
|
|
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level) {
|
|
PrepareForUse(fb, fbTarget);
|
|
if (fb.depthTex != 0) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
|
RecordNoDepth(fb);
|
|
}
|
|
if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) {
|
|
return;
|
|
}
|
|
if (fb.colorTex != 0) {
|
|
// Detach first: if the new attach fails, the point must read as
|
|
// missing (incomplete FBO), not silently keep the old texture.
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
}
|
|
DrainPendingGLErrors();
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
RecordNoColor(fb);
|
|
return;
|
|
}
|
|
fb.colorTex = tex;
|
|
fb.colorTarget = texTarget;
|
|
fb.colorLevel = level;
|
|
fb.colorLayer = -1;
|
|
}
|
|
|
|
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
|
|
PrepareForUse(fb, fbTarget);
|
|
if (fb.depthTex != 0) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
|
RecordNoDepth(fb);
|
|
}
|
|
if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) {
|
|
return;
|
|
}
|
|
if (fb.colorTex != 0) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
}
|
|
DrainPendingGLErrors();
|
|
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
RecordNoColor(fb);
|
|
return;
|
|
}
|
|
fb.colorTex = tex;
|
|
fb.colorTarget = 0;
|
|
fb.colorLevel = level;
|
|
fb.colorLayer = layer;
|
|
}
|
|
|
|
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
|
Bool withStencil, Bool stencilOnly) {
|
|
PrepareForUse(fb, fbTarget);
|
|
if (fb.colorTex != 0) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
RecordNoColor(fb);
|
|
}
|
|
if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level && fb.depthLayer < 0 &&
|
|
fb.depthHasStencil == withStencil && fb.depthIsStencilOnly == stencilOnly) {
|
|
return;
|
|
}
|
|
if (fb.depthTex != 0) {
|
|
// One call clears both depth and stencil points regardless of how
|
|
// the previous attachment was made.
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
|
}
|
|
DrainPendingGLErrors();
|
|
// A stencil-only texture has no depth point to attach at, and a depth one has no
|
|
// stencil point; GL_DEPTH_STENCIL_ATTACHMENT is only the combined texture's form.
|
|
g_GLESFuncs.glFramebufferTexture2D(
|
|
fbTarget,
|
|
stencilOnly ? GL_STENCIL_ATTACHMENT : (withStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT),
|
|
texTarget, tex, level);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
RecordNoDepth(fb);
|
|
return;
|
|
}
|
|
fb.depthTex = tex;
|
|
fb.depthTarget = texTarget;
|
|
fb.depthLayer = -1;
|
|
fb.depthLevel = level;
|
|
fb.depthHasStencil = withStencil;
|
|
fb.depthIsStencilOnly = stencilOnly;
|
|
}
|
|
|
|
// The layer-point sibling of EnsureColorAttachmentLayer above, for a view's mip chain in
|
|
// a depth-only storage owner. depthTarget == 0 is the marker that this point was made
|
|
// with glFramebufferTextureLayer, exactly as colorTarget == 0 is on the color side, so a
|
|
// later 2D attach to the same level is never deduped against it.
|
|
void EnsureDepthAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
|
|
PrepareForUse(fb, fbTarget);
|
|
if (fb.colorTex != 0) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
RecordNoColor(fb);
|
|
}
|
|
if (fb.depthTex == tex && fb.depthTarget == 0 && fb.depthLevel == level && fb.depthLayer == layer &&
|
|
!fb.depthHasStencil && !fb.depthIsStencilOnly) {
|
|
return;
|
|
}
|
|
if (fb.depthTex != 0) {
|
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
|
}
|
|
DrainPendingGLErrors();
|
|
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_DEPTH_ATTACHMENT, tex, level, layer);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
RecordNoDepth(fb);
|
|
return;
|
|
}
|
|
fb.depthTex = tex;
|
|
fb.depthTarget = 0;
|
|
fb.depthLevel = level;
|
|
fb.depthLayer = layer;
|
|
fb.depthHasStencil = false;
|
|
fb.depthIsStencilOnly = false;
|
|
}
|
|
|
|
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) {
|
|
if (fb.readBuffer == readBuffer) {
|
|
return;
|
|
}
|
|
g_GLESFuncs.glReadBuffer(readBuffer);
|
|
fb.readBuffer = readBuffer;
|
|
}
|
|
|
|
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) {
|
|
if (fb.drawBuffer == drawBuffer) {
|
|
return;
|
|
}
|
|
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
|
|
fb.drawBuffer = drawBuffer;
|
|
}
|
|
|
|
Uint EnsureCompleteTinyFramebufferId() {
|
|
ScratchSet& set = g_scratchSets.Current();
|
|
Uint& g_completeTinyFBOId = set.completeTinyFBOId;
|
|
Uint& g_completeTinyRBOId = set.completeTinyRBOId;
|
|
if (g_completeTinyFBOId != 0) {
|
|
return g_completeTinyFBOId;
|
|
}
|
|
// One-time creation: the renderbuffer binding is context state with no
|
|
// shadow, so save/restore it by query here (cold path only).
|
|
GLint prevRenderbuffer = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
|
g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId);
|
|
g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId);
|
|
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId);
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId);
|
|
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
|
|
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
|
g_completeTinyRBOId);
|
|
const GLenum drawBuffer = GL_COLOR_ATTACHMENT0;
|
|
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
|
|
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Scratch 1x1 framebuffer is incomplete.");
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<Uint>(prevRenderbuffer));
|
|
return g_completeTinyFBOId;
|
|
}
|
|
|
|
void NoteTextureIdDeleted(Uint textureId) {
|
|
if (textureId == 0) {
|
|
return;
|
|
}
|
|
// Textures ARE shared, so the deleted id may sit in any context's scratch FBO.
|
|
g_scratchSets.ForEach([textureId](ScratchSet& set) {
|
|
for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) {
|
|
if (fb->colorTex == textureId || fb->depthTex == textureId) {
|
|
fb->attachmentsKnown = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void OnBackendContextDestroyed() {
|
|
g_scratchSets.Clear();
|
|
}
|
|
} // namespace ScratchFBOImpl
|
|
|
|
namespace PixelStoreImpl {
|
|
namespace {
|
|
PackState g_packState;
|
|
Bool g_packStateKnown = false;
|
|
|
|
void PinPackState(const PackState& value) {
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment);
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength);
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows);
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels);
|
|
g_packState = value;
|
|
g_packStateKnown = true;
|
|
}
|
|
} // namespace
|
|
|
|
void ApplyPackState(const PackState& desired) {
|
|
if (!g_packStateKnown) {
|
|
PinPackState(desired);
|
|
return;
|
|
}
|
|
if (desired.Alignment != g_packState.Alignment) {
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment);
|
|
g_packState.Alignment = desired.Alignment;
|
|
}
|
|
if (desired.RowLength != g_packState.RowLength) {
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength);
|
|
g_packState.RowLength = desired.RowLength;
|
|
}
|
|
if (desired.SkipRows != g_packState.SkipRows) {
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows);
|
|
g_packState.SkipRows = desired.SkipRows;
|
|
}
|
|
if (desired.SkipPixels != g_packState.SkipPixels) {
|
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels);
|
|
g_packState.SkipPixels = desired.SkipPixels;
|
|
}
|
|
}
|
|
|
|
PackState CurrentPackState() {
|
|
if (!g_packStateKnown) {
|
|
// Fresh/unknown context: pin to the GL defaults (what a new context
|
|
// starts with; writing them makes the shadow authoritative either way).
|
|
PinPackState(PackState{});
|
|
}
|
|
return g_packState;
|
|
}
|
|
|
|
void InvalidatePackStateCache() {
|
|
g_packStateKnown = false;
|
|
}
|
|
} // namespace PixelStoreImpl
|
|
|
|
namespace RenderbufferImpl {
|
|
BackendRenderbufferObject::BackendRenderbufferObject() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
|
m_contextGeneration = g_backendContextGeneration;
|
|
if (m_backendRBOId == 0) {
|
|
MGLOG_E_ONCE("Failed to generate renderbuffer object.");
|
|
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
|
}
|
|
}
|
|
|
|
BackendRenderbufferObject::~BackendRenderbufferObject() {
|
|
if (InProcessTeardown()) {
|
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
}
|
|
if (m_backendRBOId == 0) {
|
|
return;
|
|
}
|
|
// No driver-level renderbuffer-binding shadow exists (Bind() always issues the
|
|
// call), so there is nothing to scrub here - only the id to release.
|
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) {
|
|
g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId);
|
|
}
|
|
m_backendRBOId = 0;
|
|
}
|
|
|
|
void BackendRenderbufferObject::Bind() const {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, m_backendRBOId);
|
|
}
|
|
|
|
// P4a (D-D2): the four values BackendRenderbufferObject::SyncToBackend allocates from,
|
|
// spelled once per arm. Macros for the reason the texture-parameter block gives: in the
|
|
// PULL build each expands to the pre-P4a expression at its original site, so that
|
|
// build's tokens and its symbol sizes are unchanged (D-P).
|
|
//
|
|
// ARCHITECTURE.md §4.1 keeps the renderbuffer an INDEPENDENT frontend class while giving
|
|
// it the same discriminated MGPResourceDesc a texture gets, and this is where that pays:
|
|
// the same four fields, read out of the same record type, with Desc.Target telling the
|
|
// applier which of the three per-kind tables the record lives in.
|
|
#define MGB_RBO_FORMAT \
|
|
(pushedRecord != nullptr ? static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat) \
|
|
: stateRBOObject->GetInternalFormat())
|
|
#define MGB_RBO_WIDTH \
|
|
(pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Width) \
|
|
: static_cast<Int>(stateRBOObject->GetWidth()))
|
|
#define MGB_RBO_HEIGHT \
|
|
(pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Height) \
|
|
: static_cast<Int>(stateRBOObject->GetHeight()))
|
|
#define MGB_RBO_SAMPLES \
|
|
(pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Samples) \
|
|
: static_cast<Int>(stateRBOObject->GetSamples()))
|
|
|
|
#undef MGB_RBO_FORMAT
|
|
#undef MGB_RBO_WIDTH
|
|
#undef MGB_RBO_HEIGHT
|
|
#undef MGB_RBO_SAMPLES
|
|
|
|
// P5e (fb, CONTRACT-P5E.md §5.4): the same allocation, keyed on the renderbuffer HANDLE.
|
|
//
|
|
// THE FOUR VALUES WERE ALREADY THE RECORD'S at P4a (D-D2) - what the object form still
|
|
// needed the frontend for was the LOOKUP, `HandleOf(stateRBOObject.get())`, a
|
|
// client-allocator probe on the apply thread. On this arm the handle arrives from the
|
|
// attachment surface that named it, so nothing is probed and nothing is dereferenced.
|
|
// An OVERLOAD rather than a changed signature (the monolith glue keeps its tokens).
|
|
void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
const MG_Pipe::MGPipeResourceRecord* const pushedRecord = PipeRenderbufferRecordForHandle(renderbuffer);
|
|
if (pushedRecord == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: renderbuffer handle {%u, %u} has no applier record, so its "
|
|
"storage cannot be allocated from the pushed descriptor",
|
|
renderbuffer.Slot, renderbuffer.Gen);
|
|
return;
|
|
}
|
|
if (m_isInitialized && m_syncedResourceSerial != 0 && m_syncedResourceSerial == pushedRecord->Serial) {
|
|
return;
|
|
}
|
|
|
|
// A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3): the
|
|
// allocation below re-defines the storage behind the name the framebuffer twins
|
|
// already attached, and no record of THEIRS moves for it, so the attachment memo
|
|
// has to be re-armed the same way a texture re-mint arms it. The first allocation
|
|
// is not a redefinition.
|
|
if (m_isInitialized) {
|
|
++FramebufferImpl::g_attachmentBackendIdGeneration;
|
|
}
|
|
|
|
Bind();
|
|
|
|
const auto internalFormat = static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat);
|
|
const Int width = static_cast<Int>(pushedRecord->Desc.Width);
|
|
const Int height = static_cast<Int>(pushedRecord->Desc.Height);
|
|
const Int samples = static_cast<Int>(pushedRecord->Desc.Samples);
|
|
GLenum glInternalFormat, glType, glFormat;
|
|
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
|
|
|
|
// Drain first, for the object form's reason: a driver that refuses the allocation
|
|
// (a multi-gigabyte renderbuffer is refused routinely) used to leave the twin
|
|
// claiming storage it does not have, and the attachment then rendered nowhere.
|
|
DebugImpl::ErrorLopper::Clear();
|
|
if (samples > 0) {
|
|
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
|
|
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
|
|
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
|
|
static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
|
} else {
|
|
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
|
|
static_cast<GLsizei>(height));
|
|
}
|
|
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
|
|
MGLOG_E_ONCE("Renderbuffer {%u, %u} storage allocation ran out of memory: %dx%d, "
|
|
"samples=%d, format=%s",
|
|
renderbuffer.Slot, renderbuffer.Gen, width, height, samples,
|
|
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
|
|
// NO RecordError HERE. The object form reports the OOM to the application
|
|
// through the live GLContext; on this arm there is no application on this side
|
|
// of the wire to report it to, and gPipeInputs->RecordError is itself one of the
|
|
// BARRIER-PULLED rows (FieldOwnership.def) that rule F forbids an unbarriered
|
|
// apply to touch. The client raises its own errors from its own validator.
|
|
}
|
|
DebugImpl::ErrorLopper::Clear();
|
|
|
|
m_cacheInternalFormat = internalFormat;
|
|
m_cacheWidth = width;
|
|
m_cacheHeight = height;
|
|
m_cacheSamples = samples;
|
|
m_syncedResourceSerial = pushedRecord->Serial;
|
|
m_isInitialized = true;
|
|
}
|
|
|
|
TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject,
|
|
MG_Pipe::MGPipeKind::Renderbuffer>
|
|
g_backendRenderbufferObjects;
|
|
} // namespace RenderbufferImpl
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|