mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Feat] (Pipe): push the bound framebuffers with a resolved read surface, inline attachment formats and a content hash that covers the draw-buffer array
This commit is contained in:
@@ -26,9 +26,18 @@
|
|||||||
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
|
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
|
||||||
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
|
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||||
|
#include <MG_Impl/Pipe/Tracker.h>
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
#include <MG_Pipe/PipeApply.h>
|
#include <MG_Pipe/PipeApply.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <xxhash.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace MobileGL::MG_Pipe {
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
@@ -39,23 +48,412 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// before the call that carries it exists.
|
// before the call that carries it exists.
|
||||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = 0;
|
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = 0;
|
||||||
|
|
||||||
// set_framebuffer_state. STUB AT THE CONTRACT COMMIT: it emits nothing and returns 0
|
inline Bool MGPipeFramebufferSubsystemEnabled() {
|
||||||
// payload bytes, so the validate point's ladder has its final shape and the package that
|
return (kMGPipeWiredFramebufferSubsystem & kMGPipeSubsystemFramebuffer) != 0 &&
|
||||||
// fills this in never edits PipeFill.cpp.
|
(MG_Config::Features.PipePush & kMGPipeSubsystemFramebuffer) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-C1: the MGPSurface builder, one pure function, one statement per field
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MGPSurface::Kind. MGPipeKind is REUSED rather than a second three-value enum minted
|
||||||
|
// beside it: it already spells Texture and Renderbuffer, its None is 0, and a
|
||||||
|
// zero-initialised MGPSurface therefore already IS the empty attachment point the contract
|
||||||
|
// describes ({Res = kMGPipeNullHandle, Kind = None} and every other field zero). If the
|
||||||
|
// contract package later wants a dedicated enumeration beside MGPSurface it is a rename,
|
||||||
|
// not a re-encoding.
|
||||||
|
inline constexpr Uint8 kMGPipeSurfaceKindNone = static_cast<Uint8>(MGPipeKind::None);
|
||||||
|
inline constexpr Uint8 kMGPipeSurfaceKindTexture = static_cast<Uint8>(MGPipeKind::Texture);
|
||||||
|
inline constexpr Uint8 kMGPipeSurfaceKindRenderbuffer = static_cast<Uint8>(MGPipeKind::Renderbuffer);
|
||||||
|
static_assert(kMGPipeSurfaceKindNone == 0,
|
||||||
|
"a zero-initialised MGPSurface must already be the empty attachment point");
|
||||||
|
|
||||||
|
// The upload target an attachment names, RESOLVED: an attachment made through an entry
|
||||||
|
// point that carries no face token stores TextureUploadTarget::Unknown, and the record goes
|
||||||
|
// out fully resolved - nothing in it may require a lookup on the far side. This is the same
|
||||||
|
// fallback FramebufferAttachmentObject::GetSize already applies to answer its own question.
|
||||||
|
inline MobileGL::TextureUploadTarget MGPipeResolveAttachmentUploadTarget(
|
||||||
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||||
|
MobileGL::TextureUploadTarget resolved = attachment.GetTextureUploadTarget();
|
||||||
|
if (resolved != MobileGL::TextureUploadTarget::Unknown) return resolved;
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
if (!texture) return MobileGL::TextureUploadTarget::Unknown;
|
||||||
|
const auto& targets = texture->GetUploadTargets();
|
||||||
|
return targets.empty() ? MobileGL::TextureUploadTarget::Unknown : targets[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ONE PURE FUNCTION, ONE STATEMENT PER FIELD, and that shape is a gate requirement rather
|
||||||
|
// than taste: G7's scripted control stops this conversion copying exactly one member
|
||||||
|
// (MGPSurface::Layered) and expects the framebuffer suite to go red NAMING that field. A
|
||||||
|
// loop or a memcpy would make the control unanswerable.
|
||||||
|
//
|
||||||
|
// `res` is handed in because resolving it needs the slot allocator and this function stays
|
||||||
|
// pure; `internalFormat` is INLINE in the record on purpose, so the four cross-object masks
|
||||||
|
// fall out at push time with no lookup on the far side.
|
||||||
|
inline MGPSurface MGPipeBuildSurface(const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||||
|
MGPipeHandle res) {
|
||||||
|
MGPSurface surface{};
|
||||||
|
if (attachment.IsEmpty()) return surface;
|
||||||
|
surface.Res = res;
|
||||||
|
if (attachment.IsTexture()) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
surface.Kind = kMGPipeSurfaceKindTexture;
|
||||||
|
surface.InternalFormat = static_cast<Uint32>(texture->GetFormat());
|
||||||
|
surface.Layered = attachment.IsLayered() ? 1 : 0;
|
||||||
|
surface.Level = static_cast<Uint16>(std::max<Int>(attachment.GetTextureLevel(), 0));
|
||||||
|
surface.Layer = static_cast<Uint32>(std::max<Int>(attachment.GetTextureLayer(), 0));
|
||||||
|
surface.UploadTarget = static_cast<Uint16>(MGPipeResolveAttachmentUploadTarget(attachment));
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||||
|
surface.Kind = kMGPipeSurfaceKindRenderbuffer;
|
||||||
|
surface.InternalFormat = static_cast<Uint32>(renderbuffer->GetInternalFormat());
|
||||||
|
surface.Layered = 0;
|
||||||
|
surface.Level = 0;
|
||||||
|
surface.Layer = 0;
|
||||||
|
surface.UploadTarget = 0;
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGPFramebufferState::DrawBuffers[i]: an index INTO THIS RECORD'S OWN Color[] array, and
|
||||||
|
// -1 for NONE, which is the field's documented convention read literally.
|
||||||
|
//
|
||||||
|
// THE FOUR DEFAULT-FRAMEBUFFER TOKENS map to 0, and that is a deliberate narrowing rather
|
||||||
|
// than an oversight: a default framebuffer has one colour surface, this record carries it
|
||||||
|
// in Color[0] (see MGPipeBuildFramebufferState), and IsDefault is what tells the server
|
||||||
|
// which framebuffer it is looking at. The distinction the narrowing loses is FRONT versus
|
||||||
|
// BACK and LEFT versus RIGHT, which MobileGL's frontend never gives a default framebuffer
|
||||||
|
// in the first place - FramebufferObject's constructor seeds BackLeft and nothing writes
|
||||||
|
// another. A phase that needs stereo has to widen the field, not re-encode this one.
|
||||||
|
inline Int8 MGPipeDrawBufferIndex(MobileGL::FramebufferAttachmentType buffer) {
|
||||||
|
using MobileGL::FramebufferAttachmentType;
|
||||||
|
if (buffer == FramebufferAttachmentType::None) return -1;
|
||||||
|
if (buffer >= FramebufferAttachmentType::Color0 && buffer <= FramebufferAttachmentType::ColorMax) {
|
||||||
|
return static_cast<Int8>(static_cast<Int>(buffer) - static_cast<Int>(FramebufferAttachmentType::Color0));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-C4: ContentHash, and the one input it must not swallow
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// XXH64 over the WHOLE record with ContentHash itself zeroed, computed field-wise into a
|
||||||
|
// zero-initialised staging copy so that no padding byte can enter the hash. Two jobs: the
|
||||||
|
// server's render-pass memo key, and this client's emission suppressor.
|
||||||
|
//
|
||||||
|
// IT MUST COVER Fbo. A recycled framebuffer handle whose successor happens to carry an
|
||||||
|
// identical attachment set would otherwise be suppressed against its predecessor; Fbo
|
||||||
|
// carries Gen, so it cannot be.
|
||||||
|
//
|
||||||
|
// IT MUST COVER DrawBuffers[8], and this is the trap worth naming. The backend derives the
|
||||||
|
// fragColor BROADCAST COUNT from the draw-buffer array, and it does that at the verb, from
|
||||||
|
// the framebuffer state it then holds, precisely so a program can relink inside the same
|
||||||
|
// draw. A hash that did not cover the array would let a suppressed set_framebuffer_state
|
||||||
|
// mean "the draw buffers did not move" when they had, and the shader would be specialised
|
||||||
|
// for the previous output shape. With the array in the hash, a suppression provably means
|
||||||
|
// the array did not move, which provably means the broadcast count did not move.
|
||||||
|
inline void MGPipeCopySurfaceForHash(MGPSurface& dst, const MGPSurface& src) {
|
||||||
|
dst.Res = src.Res;
|
||||||
|
dst.InternalFormat = src.InternalFormat;
|
||||||
|
dst.Kind = src.Kind;
|
||||||
|
dst.Layered = src.Layered;
|
||||||
|
dst.Level = src.Level;
|
||||||
|
dst.Layer = src.Layer;
|
||||||
|
dst.UploadTarget = src.UploadTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Uint64 MGPipeFramebufferStateContentHash(const MGPFramebufferState& state) {
|
||||||
|
MGPFramebufferState staging{};
|
||||||
|
staging.Fbo = state.Fbo;
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
MGPipeCopySurfaceForHash(staging.Color[i], state.Color[i]);
|
||||||
|
}
|
||||||
|
MGPipeCopySurfaceForHash(staging.Depth, state.Depth);
|
||||||
|
MGPipeCopySurfaceForHash(staging.Stencil, state.Stencil);
|
||||||
|
MGPipeCopySurfaceForHash(staging.ReadSurface, state.ReadSurface);
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
staging.DrawBuffers[i] = state.DrawBuffers[i];
|
||||||
|
}
|
||||||
|
staging.Width = state.Width;
|
||||||
|
staging.Height = state.Height;
|
||||||
|
staging.Layers = state.Layers;
|
||||||
|
staging.Samples = state.Samples;
|
||||||
|
staging.FixedSampleLocations = state.FixedSampleLocations;
|
||||||
|
staging.IsDefault = state.IsDefault;
|
||||||
|
staging.Complete = state.Complete;
|
||||||
|
staging.Target = state.Target;
|
||||||
|
// staging.ContentHash stays 0 - that is the whole point.
|
||||||
|
return XXH64(&staging, sizeof(staging), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// The emitter
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
class MGPipeFramebufferEmitter {
|
class MGPipeFramebufferEmitter {
|
||||||
public:
|
public:
|
||||||
using GLContext = MG_State::GLState::GLContext;
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||||
|
using FramebufferAttachmentType = MobileGL::FramebufferAttachmentType;
|
||||||
|
|
||||||
|
// The handle for `fbo`. kMGPipeDefaultFramebuffer ({0,1}) for the default framebuffer,
|
||||||
|
// which is what retires the four pDefaultFramebufferInfo->defaultFBO identity
|
||||||
|
// comparisons into an ordinary handle compare; a client-minted {slot, gen} otherwise.
|
||||||
|
//
|
||||||
|
// Minted, never gated: a framebuffer handle is CLIENT state and costs one free-list pop.
|
||||||
|
static MGPipeHandle HandleFor(const FramebufferObject& fbo) {
|
||||||
|
if (fbo.IsDefaultFramebuffer()) return kMGPipeDefaultFramebuffer;
|
||||||
|
return MGPipeSlots().Acquire(MGPipeKind::Framebuffer, fbo.GetLifetimeId());
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||||
Uint64 EmitFramebufferState(GLContext& ctx) {
|
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||||
(void)ctx;
|
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
|
||||||
return 0;
|
const auto& drawFbo = ctx.GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).GetBoundObject();
|
||||||
|
const auto& readFbo = ctx.GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).GetBoundObject();
|
||||||
|
if (!drawFbo && !readFbo) return 0;
|
||||||
|
|
||||||
|
// ONE OBJECT BOUND TO BOTH TARGETS IS ONE RECORD WITH Target = Both, and that is
|
||||||
|
// not an optimisation: Espryt's "same FBO as draw" skip is the habitat of the
|
||||||
|
// read-buffer defect class, and a record that says which target it describes turns
|
||||||
|
// "apply the draw buffers only for the draw target" from call-site discipline into
|
||||||
|
// a one-line test on the far side.
|
||||||
|
const Bool shared = drawFbo && readFbo && drawFbo.get() == readFbo.get();
|
||||||
|
|
||||||
|
MGPFramebufferState drawState{};
|
||||||
|
MGPFramebufferState readState{};
|
||||||
|
Bool drawOk = false;
|
||||||
|
Bool readOk = false;
|
||||||
|
if (shared) {
|
||||||
|
drawOk = BuildFramebufferState(*drawFbo, *drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
||||||
|
} else {
|
||||||
|
if (drawFbo) {
|
||||||
|
drawOk = BuildFramebufferState(*drawFbo, readFbo ? *readFbo : *drawFbo,
|
||||||
|
MGPipeFramebufferTarget::Draw, drawState);
|
||||||
|
}
|
||||||
|
if (readFbo) {
|
||||||
|
readOk = BuildFramebufferState(*readFbo, *readFbo, MGPipeFramebufferTarget::Read, readState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!drawOk && !readOk) return 0;
|
||||||
|
|
||||||
|
// THE SUPPRESSOR SLOT IS FED THE COMBINED ANSWER and the per-target latches decide
|
||||||
|
// which of the two records actually goes out. The slot exists so that
|
||||||
|
// InvalidateAll() on a fresh context reaches this family like every other, and so
|
||||||
|
// that "nothing moved" costs one compare rather than two.
|
||||||
|
const Uint64 drawHash = drawOk ? drawState.ContentHash : 0;
|
||||||
|
const Uint64 readHash = readOk ? readState.ContentHash : 0;
|
||||||
|
const Uint64 combined =
|
||||||
|
MGPipeMixShutter(MGPipeMixShutter(drawHash, readHash), shared ? 1u : 0u);
|
||||||
|
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetFramebufferState,
|
||||||
|
combined)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
if (shared) {
|
||||||
|
if (drawOk && (drawHash != m_lastEmitted[kDraw] || drawHash != m_lastEmitted[kRead])) {
|
||||||
|
bytes += Emit(drawState);
|
||||||
|
m_lastEmitted[kDraw] = drawHash;
|
||||||
|
m_lastEmitted[kRead] = drawHash;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
if (drawOk && drawHash != m_lastEmitted[kDraw]) {
|
||||||
|
bytes += Emit(drawState);
|
||||||
|
m_lastEmitted[kDraw] = drawHash;
|
||||||
|
}
|
||||||
|
if (readOk && readHash != m_lastEmitted[kRead]) {
|
||||||
|
bytes += Emit(readState);
|
||||||
|
m_lastEmitted[kRead] = readHash;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
|
||||||
|
// same objects, so "what was emitted" costs no copy. ----
|
||||||
|
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
|
||||||
|
const MGPFramebufferState& LastRead() const { return m_lastRead; }
|
||||||
|
Uint64 EmissionCount() const { return m_emissions; }
|
||||||
|
Uint64 RefusedCount() const { return m_refusals; }
|
||||||
|
|
||||||
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||||
// LATCHES reset here - the applier's object records survive a make-current and
|
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
|
||||||
// re-publishing them would move their serials for nothing.
|
// ReadFramebuffer working state, so these mirrors have to go with them or the first
|
||||||
void Reset() {}
|
// emission after a make-current would be suppressed as unchanged and the server would
|
||||||
|
// draw into the previous context's framebuffer. The suppressor slot is invalidated by
|
||||||
|
// the validate point's own InvalidateAll(), beside this call.
|
||||||
|
void Reset() {
|
||||||
|
m_lastEmitted[kDraw] = 0;
|
||||||
|
m_lastEmitted[kRead] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() { m_emissions = m_refusals = 0; }
|
||||||
|
|
||||||
|
void ResetForTest() {
|
||||||
|
Reset();
|
||||||
|
ResetCounters();
|
||||||
|
m_lastDraw = MGPFramebufferState{};
|
||||||
|
m_lastRead = MGPFramebufferState{};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr SizeT kDraw = 0;
|
||||||
|
static constexpr SizeT kRead = 1;
|
||||||
|
|
||||||
|
Uint64 Emit(const MGPFramebufferState& state) {
|
||||||
|
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read)) {
|
||||||
|
m_lastRead = state;
|
||||||
|
} else {
|
||||||
|
m_lastDraw = state;
|
||||||
|
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state;
|
||||||
|
}
|
||||||
|
MGPipeApplySetFramebufferState(state);
|
||||||
|
++m_emissions;
|
||||||
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1);
|
||||||
|
}
|
||||||
|
return sizeof(MGPFramebufferState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `fbo` is the object this record describes; `readFbo` is the object whose OWN read
|
||||||
|
// buffer resolves ReadSurface, and for a Draw record that is the read-bound object
|
||||||
|
// rather than this one.
|
||||||
|
//
|
||||||
|
// THE RESOLVED READ SURFACE IS WHAT STRUCTURALLY CLOSES THE read-buffer-shared-FBO
|
||||||
|
// DEFECT CLASS: the record carries the surface, not an index, and it is resolved from
|
||||||
|
// the READ framebuffer's own read buffer, so the "same FBO as draw" skip that used to
|
||||||
|
// lose it cannot be expressed.
|
||||||
|
Bool BuildFramebufferState(const FramebufferObject& fbo, const FramebufferObject& readFbo,
|
||||||
|
MGPipeFramebufferTarget target, MGPFramebufferState& out) {
|
||||||
|
// D-C3, THE CLIENT HALF OF THE BRING-UP REFUSAL. The wire array is 8 wide and
|
||||||
|
// GetDynamicParameters().MaxColorAttachments is the driver's raw ES cap, not
|
||||||
|
// clamped to 8 on the GLES path. An attachment point at or above the wire width
|
||||||
|
// cannot be carried at all, so the record is REFUSED and the legacy arm runs -
|
||||||
|
// truncating it silently is exactly the bug class this phase is closing. The
|
||||||
|
// backend half of the same refusal (bit 9 declined at its first lookup, with one
|
||||||
|
// ERROR naming the cap) rides ResolveFramebufferSubsystemArm.
|
||||||
|
for (Int point = static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||||
|
static_cast<Int>(kMGPipeMaxColorAttachments);
|
||||||
|
point <= static_cast<Int>(FramebufferAttachmentType::ColorMax); ++point) {
|
||||||
|
if (fbo.GetAttachment(static_cast<FramebufferAttachmentType>(point)).IsEmpty()) continue;
|
||||||
|
MGLOG_E_ONCE("MGPipe: framebuffer %u has an attachment at colour point %d, which is at or "
|
||||||
|
"above the wire width of %u - set_framebuffer_state is refused rather than "
|
||||||
|
"truncated and the legacy arm runs",
|
||||||
|
fbo.GetExternalIndex(),
|
||||||
|
point - static_cast<Int>(FramebufferAttachmentType::Color0),
|
||||||
|
static_cast<Uint>(kMGPipeMaxColorAttachments));
|
||||||
|
++m_refusals;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = MGPFramebufferState{};
|
||||||
|
out.Fbo = HandleFor(fbo);
|
||||||
|
out.Target = static_cast<Uint8>(target);
|
||||||
|
out.IsDefault = fbo.IsDefaultFramebuffer() ? 1 : 0;
|
||||||
|
|
||||||
|
// THE COLOUR POINTS. A default framebuffer keeps its one colour surface under
|
||||||
|
// BackLeft rather than under Color0, and the record has exactly one place to put
|
||||||
|
// it: Color[0], which is also the index MGPipeDrawBufferIndex maps that token to,
|
||||||
|
// so the array and the draw-buffer indices agree by construction.
|
||||||
|
if (out.IsDefault != 0) {
|
||||||
|
out.Color[0] = SurfaceOf(fbo, FramebufferAttachmentType::BackLeft);
|
||||||
|
} else {
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
out.Color[i] = SurfaceOf(fbo, static_cast<FramebufferAttachmentType>(
|
||||||
|
static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||||
|
static_cast<Int>(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Depth = SurfaceOf(fbo, FramebufferAttachmentType::Depth);
|
||||||
|
out.Stencil = SurfaceOf(fbo, FramebufferAttachmentType::Stencil);
|
||||||
|
out.ReadSurface = SurfaceOf(readFbo, readFbo.GetReadBuffer());
|
||||||
|
|
||||||
|
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
out.DrawBuffers[i] = MGPipeDrawBufferIndex(drawBuffers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
FillGeometry(fbo, out);
|
||||||
|
// Complete is FramebufferObject::CheckCompleteness(), the FRONTEND-ONLY answer, and
|
||||||
|
// never glCheckFramebufferStatus's: that entry point additionally consults the
|
||||||
|
// backend's probed format-capability cache, and a client emitting it would be
|
||||||
|
// reading the backend from the client side - the exact coupling this boundary
|
||||||
|
// exists to remove. glCheckFramebufferStatus keeps answering from the frontend
|
||||||
|
// exactly as it does today.
|
||||||
|
out.Complete = fbo.CheckCompleteness() ? 1 : 0;
|
||||||
|
out.ContentHash = MGPipeFramebufferStateContentHash(out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPSurface SurfaceOf(const FramebufferObject& fbo, FramebufferAttachmentType type) {
|
||||||
|
if (type == FramebufferAttachmentType::None || type == FramebufferAttachmentType::Unknown) {
|
||||||
|
return MGPSurface{};
|
||||||
|
}
|
||||||
|
const auto& attachment = fbo.GetAttachment(type);
|
||||||
|
if (attachment.IsEmpty()) return MGPSurface{};
|
||||||
|
MGPipeTextureEmitter& textures = MGPipeTextureEmitterInstance();
|
||||||
|
// D-A4's two producers: an attachment point is what sets RENDER_TARGET and
|
||||||
|
// DEPTH_STENCIL, the two sticky bind bits nothing set before P4a. Sticky and ORed,
|
||||||
|
// so a texture that was ever a colour attachment keeps saying so, and the mask is
|
||||||
|
// republished on the resource's next respecify.
|
||||||
|
const Uint16 bit = (type == FramebufferAttachmentType::Depth ||
|
||||||
|
type == FramebufferAttachmentType::Stencil)
|
||||||
|
? static_cast<Uint16>(kMGPipeBindDepthStencil)
|
||||||
|
: static_cast<Uint16>(kMGPipeBindRenderTarget);
|
||||||
|
MGPipeHandle res = kMGPipeNullHandle;
|
||||||
|
if (attachment.IsTexture()) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
res = textures.AcquireTexture(texture->GetLifetimeId(), texture.get());
|
||||||
|
textures.NoteTextureBoundAs(res, bit);
|
||||||
|
} else if (attachment.IsRenderbuffer()) {
|
||||||
|
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||||
|
res = textures.AcquireRenderbuffer(renderbuffer->GetLifetimeId());
|
||||||
|
textures.NoteRenderbufferBoundAs(res, bit);
|
||||||
|
}
|
||||||
|
return MGPipeBuildSurface(attachment, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The attachments' common extent, and the ARB_framebuffer_no_attachments defaults when
|
||||||
|
// there is no attachment at all (GL 4.6 core table 23.24 - the shape a framebuffer with
|
||||||
|
// no attachments rasterizes at).
|
||||||
|
static void FillGeometry(const FramebufferObject& fbo, MGPFramebufferState& out) {
|
||||||
|
Bool found = false;
|
||||||
|
for (const auto& attachment : fbo.GetAllAttachmentObjects()) {
|
||||||
|
if (attachment.IsEmpty()) continue;
|
||||||
|
const IntVec3 size = attachment.GetSize();
|
||||||
|
if (!found) {
|
||||||
|
out.Width = static_cast<Uint16>(std::clamp<Int>(size.x(), 0, 0xFFFF));
|
||||||
|
out.Height = static_cast<Uint16>(std::clamp<Int>(size.y(), 0, 0xFFFF));
|
||||||
|
out.Layers = static_cast<Uint16>(
|
||||||
|
attachment.IsLayered() ? std::clamp<Int>(size.z(), 1, 0xFFFF) : 1);
|
||||||
|
if (attachment.IsTexture()) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
out.Samples = static_cast<Uint16>(std::max<Int>(texture->GetSamples(), 0));
|
||||||
|
out.FixedSampleLocations = texture->HasFixedSampleLocations() ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
out.Samples = static_cast<Uint16>(
|
||||||
|
std::max<Int>(attachment.GetRenderbuffer()->GetSamples(), 0));
|
||||||
|
out.FixedSampleLocations = 1;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) return;
|
||||||
|
out.Width = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultWidth(), 0, 0xFFFF));
|
||||||
|
out.Height = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultHeight(), 0, 0xFFFF));
|
||||||
|
out.Layers = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultLayers(), 0, 0xFFFF));
|
||||||
|
out.Samples = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultSamples(), 0, 0xFFFF));
|
||||||
|
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<Uint64, 2> m_lastEmitted{};
|
||||||
|
MGPFramebufferState m_lastDraw{};
|
||||||
|
MGPFramebufferState m_lastRead{};
|
||||||
|
Uint64 m_emissions = 0;
|
||||||
|
Uint64 m_refusals = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline MGPipeFramebufferEmitter& MGPipeFramebufferEmitterInstance() {
|
inline MGPipeFramebufferEmitter& MGPipeFramebufferEmitterInstance() {
|
||||||
|
|||||||
Reference in New Issue
Block a user