mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
Pipe: per-framebuffer state memo, object-held handle hints, per-buffer handle reuse and a cached per-verb gate (ValidateForVerb 0.328 -> 0.282 ms/frame; FCL GL CPU Magma 2.68 -> 2.62, Espryt 3.03 -> 3.00; red-once per piece in its unit case).
This commit is contained in:
@@ -148,6 +148,9 @@ namespace MobileGL::MG_Config {
|
|||||||
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
|
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
|
||||||
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
|
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
|
||||||
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
|
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
|
||||||
|
// P15: the client reuses a bound framebuffer's built set_framebuffer_state while nothing it
|
||||||
|
// reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState).
|
||||||
|
QuirkOverride PipeFramebufferStateMemo = QuirkOverride::Auto;
|
||||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
|
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
|
||||||
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
|
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
|
||||||
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
|
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
|
||||||
|
features.PipeFramebufferStateMemo = QueryEnvQuirkOverride("MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO");
|
||||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||||
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
|
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
|
||||||
|
|||||||
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Bool drawOk = false;
|
Bool drawOk = false;
|
||||||
Bool readOk = false;
|
Bool readOk = false;
|
||||||
if (shared) {
|
if (shared) {
|
||||||
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
||||||
} else {
|
} else {
|
||||||
if (drawFbo) {
|
if (drawFbo) {
|
||||||
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
|
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
|
||||||
}
|
}
|
||||||
if (readFbo) {
|
if (readFbo) {
|
||||||
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
|
readOk = BuildBoundFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!drawOk && !readOk) return 0;
|
if (!drawOk && !readOk) return 0;
|
||||||
@@ -414,6 +414,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
||||||
Uint64 EmissionCount() const { return m_emissions; }
|
Uint64 EmissionCount() const { return m_emissions; }
|
||||||
Uint64 RefusedCount() const { return m_refusals; }
|
Uint64 RefusedCount() const { return m_refusals; }
|
||||||
|
// P15: bound-target builds answered from the memo (BuildBoundFramebufferState).
|
||||||
|
Uint64 MemoHitCount() const { return m_memoHits; }
|
||||||
|
|
||||||
// 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 - MGPipeApplierReset clears the applier's DrawFramebuffer and
|
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
|
||||||
@@ -438,6 +440,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void ResetForTest() {
|
void ResetForTest() {
|
||||||
Reset();
|
Reset();
|
||||||
ResetCounters();
|
ResetCounters();
|
||||||
|
for (auto& memo : m_buildMemo) memo.Valid = false;
|
||||||
m_lastDraw = MGPFramebufferState{};
|
m_lastDraw = MGPFramebufferState{};
|
||||||
m_lastRead = MGPFramebufferState{};
|
m_lastRead = MGPFramebufferState{};
|
||||||
m_lastNamed = MGPFramebufferState{};
|
m_lastNamed = MGPFramebufferState{};
|
||||||
@@ -563,6 +566,40 @@ namespace MobileGL::MG_Pipe {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildFramebufferState for a BOUND target, reusing the last record built for the same
|
||||||
|
// framebuffer and target while nothing the build reads has moved. The build runs per draw
|
||||||
|
// and its answer is a function of:
|
||||||
|
// - the framebuffer: lifetime id (identity) and object version (attachments, draw and
|
||||||
|
// read buffers, no-attachment defaults all move it);
|
||||||
|
// - each attached texture: lifetime id, shape version (sizes, level set, internal
|
||||||
|
// format: completeness and geometry) and its samples / fixed-locations pair, which
|
||||||
|
// move the params version rather than the shape;
|
||||||
|
// - each attached renderbuffer: lifetime id plus its size, format and samples, read
|
||||||
|
// directly (its storage has no version);
|
||||||
|
// - the handles it minted: the slot allocator's reset epoch, and the texture emitter's
|
||||||
|
// table epoch for the sticky bind bits SurfaceOf notes (a dropped table must be
|
||||||
|
// re-noted).
|
||||||
|
// Not for the default framebuffer, whose geometry follows the current surface.
|
||||||
|
// Kill switch MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO=0.
|
||||||
|
Bool BuildBoundFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
|
||||||
|
MGPFramebufferState& out) {
|
||||||
|
const Bool memoOn = MG_Config::Features.PipeFramebufferStateMemo != MG_Config::QuirkOverride::ForceOff &&
|
||||||
|
!fbo.IsDefaultFramebuffer();
|
||||||
|
// Direct-mapped on (framebuffer, target): the build only runs when the bound framebuffer
|
||||||
|
// changed, so a per-target single entry would miss on every A/B/A switch between targets.
|
||||||
|
const Uint64 key = fbo.GetLifetimeId() * 4u + static_cast<Uint64>(target);
|
||||||
|
BuildMemo& memo = m_buildMemo[static_cast<SizeT>((key * 0x9E3779B97F4A7C15ull) >> 60) % m_buildMemo.size()];
|
||||||
|
if (memoOn && memo.Valid && MemoHolds(memo, fbo, target)) {
|
||||||
|
out = memo.State;
|
||||||
|
++m_memoHits;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
memo.Valid = false;
|
||||||
|
if (!BuildFramebufferState(fbo, target, out)) return false;
|
||||||
|
if (memoOn) FillMemo(memo, fbo, target, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
|
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
|
||||||
if (MG_State::pGLContext == nullptr) return false;
|
if (MG_State::pGLContext == nullptr) return false;
|
||||||
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
|
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
|
||||||
@@ -642,6 +679,96 @@ namespace MobileGL::MG_Pipe {
|
|||||||
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
|
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MemoAttachment {
|
||||||
|
Uint32 Point = 0;
|
||||||
|
Bool IsTexture = false;
|
||||||
|
Uint64 LifetimeId = 0;
|
||||||
|
Uint64 ShapeVersion = 0;
|
||||||
|
Int Width = 0, Height = 0, Samples = 0;
|
||||||
|
Uint32 Format = 0;
|
||||||
|
Bool FixedSampleLocations = false;
|
||||||
|
};
|
||||||
|
struct BuildMemo {
|
||||||
|
Bool Valid = false;
|
||||||
|
Uint8 Target = 0;
|
||||||
|
Uint16 ObjectVersion = 0;
|
||||||
|
Uint64 FboLifetimeId = 0;
|
||||||
|
Uint64 AllocatorEpoch = 0;
|
||||||
|
Uint64 TextureTableEpoch = 0;
|
||||||
|
Uint32 AttachmentCount = 0;
|
||||||
|
Array<MemoAttachment, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>
|
||||||
|
Attachments{};
|
||||||
|
MGPFramebufferState State{};
|
||||||
|
};
|
||||||
|
|
||||||
|
static Bool AttachmentMatches(const MemoAttachment& entry,
|
||||||
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||||
|
if (attachment.IsEmpty() || attachment.IsTexture() != entry.IsTexture) return false;
|
||||||
|
if (entry.IsTexture) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
return texture->GetLifetimeId() == entry.LifetimeId &&
|
||||||
|
texture->GetShapeVersion() == entry.ShapeVersion &&
|
||||||
|
texture->GetSamples() == entry.Samples &&
|
||||||
|
texture->HasFixedSampleLocations() == entry.FixedSampleLocations;
|
||||||
|
}
|
||||||
|
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||||
|
return renderbuffer->GetLifetimeId() == entry.LifetimeId && renderbuffer->GetWidth() == entry.Width &&
|
||||||
|
renderbuffer->GetHeight() == entry.Height && renderbuffer->GetSamples() == entry.Samples &&
|
||||||
|
static_cast<Uint32>(renderbuffer->GetInternalFormat()) == entry.Format;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool MemoHolds(const BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target) const {
|
||||||
|
if (memo.Target != static_cast<Uint8>(target) || memo.FboLifetimeId != fbo.GetLifetimeId() ||
|
||||||
|
memo.ObjectVersion != fbo.GetObjectVersion() || memo.AllocatorEpoch != MGPipeSlots().ResetEpoch() ||
|
||||||
|
memo.TextureTableEpoch != MGPipeTextureEmitterInstance().TableEpoch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& all = fbo.GetAllAttachmentObjects();
|
||||||
|
for (Uint32 i = 0; i < memo.AttachmentCount; ++i) {
|
||||||
|
const MemoAttachment& entry = memo.Attachments[i];
|
||||||
|
if (!AttachmentMatches(entry, all[entry.Point])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillMemo(BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target,
|
||||||
|
const MGPFramebufferState& state) {
|
||||||
|
memo.Target = static_cast<Uint8>(target);
|
||||||
|
memo.FboLifetimeId = fbo.GetLifetimeId();
|
||||||
|
memo.ObjectVersion = fbo.GetObjectVersion();
|
||||||
|
memo.AllocatorEpoch = MGPipeSlots().ResetEpoch();
|
||||||
|
memo.TextureTableEpoch = MGPipeTextureEmitterInstance().TableEpoch();
|
||||||
|
memo.AttachmentCount = 0;
|
||||||
|
const auto& all = fbo.GetAllAttachmentObjects();
|
||||||
|
for (SizeT point = 0; point < all.size(); ++point) {
|
||||||
|
const auto& attachment = all[point];
|
||||||
|
if (attachment.IsEmpty()) continue;
|
||||||
|
MemoAttachment& entry = memo.Attachments[memo.AttachmentCount++];
|
||||||
|
entry = MemoAttachment{};
|
||||||
|
entry.Point = static_cast<Uint32>(point);
|
||||||
|
entry.IsTexture = attachment.IsTexture();
|
||||||
|
if (entry.IsTexture) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
entry.LifetimeId = texture->GetLifetimeId();
|
||||||
|
entry.ShapeVersion = texture->GetShapeVersion();
|
||||||
|
entry.Samples = texture->GetSamples();
|
||||||
|
entry.FixedSampleLocations = texture->HasFixedSampleLocations();
|
||||||
|
} else {
|
||||||
|
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||||
|
entry.LifetimeId = renderbuffer->GetLifetimeId();
|
||||||
|
entry.Width = renderbuffer->GetWidth();
|
||||||
|
entry.Height = renderbuffer->GetHeight();
|
||||||
|
entry.Samples = renderbuffer->GetSamples();
|
||||||
|
entry.Format = static_cast<Uint32>(renderbuffer->GetInternalFormat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memo.State = state;
|
||||||
|
memo.Valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct-mapped on (framebuffer lifetime id, target); Named never reaches it.
|
||||||
|
Array<BuildMemo, 16> m_buildMemo{};
|
||||||
|
Uint64 m_memoHits = 0;
|
||||||
Array<Uint64, 2> m_lastEmitted{};
|
Array<Uint64, 2> m_lastEmitted{};
|
||||||
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
|
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
|
||||||
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
|
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
|
||||||
|
|||||||
@@ -2640,6 +2640,44 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// consumer mask gains bit 13 in the same
|
// consumer mask gains bit 13 in the same
|
||||||
// commit, or R-8 withholds the whole family.
|
// commit, or R-8 withholds the whole family.
|
||||||
kMGPipeWiredBufferBindingSubsystem;
|
kMGPipeWiredBufferBindingSubsystem;
|
||||||
|
|
||||||
|
// P15: the dirty bits whose family this build, push mask and consumer emit - every
|
||||||
|
// condition of the per-bit gate in MGPipeValidateForVerb except the dirty bit itself.
|
||||||
|
Uint32 ComputeLiveDirtyBits(Uint64 pushMask) {
|
||||||
|
Uint32 live = 0;
|
||||||
|
for (Uint32 b = 0; b < kMGPipeDirtyCount; ++b) {
|
||||||
|
const Uint64 subsystem = MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(b));
|
||||||
|
if (subsystem != 0 && (pushMask & subsystem) != 0 && (kMGPipeWiredSubsystems & subsystem) != 0 &&
|
||||||
|
P4aFamilyHasItsConsumer(subsystem) && P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
|
||||||
|
P5eFamilyIsLive(subsystem, pushMask)) {
|
||||||
|
live |= MGPipeDirtyBit(static_cast<MGPipeDirty>(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return live;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per verb, these conditions used to be re-asked for every bit (~15 consumer-registration
|
||||||
|
// and dependency-table walks per draw). In monolith their only inputs are the push mask and
|
||||||
|
// the registered consumer, so the answer is kept until either moves. Under a transport the
|
||||||
|
// caps mirror answers and can move with a new session, so it is recomputed every verb.
|
||||||
|
Uint32 LiveDirtyBits(Uint64 pushMask) {
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return ComputeLiveDirtyBits(pushMask);
|
||||||
|
#endif
|
||||||
|
static Bool valid = false;
|
||||||
|
static Uint64 cachedPushMask = 0;
|
||||||
|
static const MGPipeResourceOps* cachedConsumer = nullptr;
|
||||||
|
static Uint32 cachedLive = 0;
|
||||||
|
const MGPipeResourceOps* consumer = MGPipeGetResourceOps();
|
||||||
|
if (!valid || cachedPushMask != pushMask || cachedConsumer != consumer) {
|
||||||
|
cachedLive = ComputeLiveDirtyBits(pushMask);
|
||||||
|
cachedPushMask = pushMask;
|
||||||
|
cachedConsumer = consumer;
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
return cachedLive;
|
||||||
|
}
|
||||||
|
|
||||||
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
|
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
|
||||||
// this a header that set the wrong constant - the sampler bit in the program header,
|
// this a header that set the wrong constant - the sampler bit in the program header,
|
||||||
// say - would switch the wrong family on and every gate would still pass.
|
// say - would switch the wrong family on and every gate would still pass.
|
||||||
@@ -3621,15 +3659,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// binding-point family and asks bit 13's OWN consumer bit rather than the resource
|
// binding-point family and asks bit 13's OWN consumer bit rather than the resource
|
||||||
// family's. It answers true for every subsystem but bit 13, so nothing that emitted
|
// family's. It answers true for every subsystem but bit 13, so nothing that emitted
|
||||||
// before P5e changes.
|
// before P5e changes.
|
||||||
const auto wants = [&](MGPipeDirty bit) {
|
const Uint32 liveDirty = LiveDirtyBits(pushMask) & dirty;
|
||||||
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
|
const auto wants = [&](MGPipeDirty bit) { return (liveDirty & MGPipeDirtyBit(bit)) != 0; };
|
||||||
return subsystem != 0 && (pushMask & subsystem) != 0 &&
|
|
||||||
(kMGPipeWiredSubsystems & subsystem) != 0 &&
|
|
||||||
P4aFamilyHasItsConsumer(subsystem) &&
|
|
||||||
P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
|
|
||||||
P5eFamilyIsLive(subsystem, pushMask) &&
|
|
||||||
(dirty & MGPipeDirtyBit(bit)) != 0;
|
|
||||||
};
|
|
||||||
Uint64 payloadBytes = 0;
|
Uint64 payloadBytes = 0;
|
||||||
|
|
||||||
// A fresh context is a fresh server, and that is true of EVERY subsystem, so it is
|
// A fresh context is a fresh server, and that is true of EVERY subsystem, so it is
|
||||||
|
|||||||
@@ -663,7 +663,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// composite.
|
// composite.
|
||||||
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
|
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
|
||||||
const Uint64 lifetimeId = program.GetLifetimeId();
|
const Uint64 lifetimeId = program.GetLifetimeId();
|
||||||
const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
const MGPipeHandle existing =
|
||||||
|
MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::ShaderCso, lifetimeId, program.PipeHandleHint());
|
||||||
if (!MGPipeHandleIsNull(existing)) return existing;
|
if (!MGPipeHandleIsNull(existing)) return existing;
|
||||||
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
|
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
|
||||||
// an ordinary ShaderCso handle in every other respect - the same kind, the same
|
// an ordinary ShaderCso handle in every other respect - the same kind, the same
|
||||||
|
|||||||
@@ -281,7 +281,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// gating it would make the vertex-input subsystem emit null handles whenever the
|
// gating it would make the vertex-input subsystem emit null handles whenever the
|
||||||
// resource subsystem is off. Only the CALLS are gated (D-A1).
|
// resource subsystem is off. Only the CALLS are gated (D-A1).
|
||||||
MGPipeHandle Acquire(BufferObject& buffer) {
|
MGPipeHandle Acquire(BufferObject& buffer) {
|
||||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
const MGPipeHandle handle =
|
||||||
|
MGPipeSlots().AcquireHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(), buffer.PipeHandleHint());
|
||||||
const SizeT slot = handle.Slot;
|
const SizeT slot = handle.Slot;
|
||||||
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
|
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
|
||||||
m_bySlot[slot].Object = &buffer;
|
m_bySlot[slot].Object = &buffer;
|
||||||
@@ -292,7 +293,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The handle a buffer already has, or the null handle. Never mints - the emission
|
// The handle a buffer already has, or the null handle. Never mints - the emission
|
||||||
// path calls Acquire, the query paths call this.
|
// path calls Acquire, the query paths call this.
|
||||||
MGPipeHandle Find(const BufferObject& buffer) const {
|
MGPipeHandle Find(const BufferObject& buffer) const {
|
||||||
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
return MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(),
|
||||||
|
buffer.PipeHandleHint());
|
||||||
}
|
}
|
||||||
|
|
||||||
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
|
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
|
||||||
|
|||||||
@@ -859,7 +859,9 @@ namespace MobileGL::MG_Pipe {
|
|||||||
unitSampler ? unitSampler.get() : texture->GetSamplerObject().get();
|
unitSampler ? unitSampler.get() : texture->GetSamplerObject().get();
|
||||||
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
|
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
|
||||||
|
|
||||||
entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
entry.Texture = MGPipeSlots().AcquireHinted(
|
||||||
|
MGPipeKind::Texture, texture->GetLifetimeId(),
|
||||||
|
texture->PipeHandleHint(ITextureObject::PipeHintKind::Texture));
|
||||||
// D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView
|
// D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView
|
||||||
// is SAMPLER-bound from then on (sticky; the texture emitter's contract door,
|
// is SAMPLER-bound from then on (sticky; the texture emitter's contract door,
|
||||||
// since this header is included BY TextureEmit.h). One early-out per unit per
|
// since this header is included BY TextureEmit.h). One early-out per unit per
|
||||||
@@ -976,7 +978,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeHandle AcquireSamplerView(const ITextureObject& texture, MGPipeHandle textureHandle,
|
MGPipeHandle AcquireSamplerView(const ITextureObject& texture, MGPipeHandle textureHandle,
|
||||||
Uint64& payloadBytes) {
|
Uint64& payloadBytes) {
|
||||||
const MGPipeHandle handle =
|
const MGPipeHandle handle =
|
||||||
MGPipeSlots().Acquire(MGPipeKind::SamplerViewCso, texture.GetLifetimeId());
|
MGPipeSlots().AcquireHinted(MGPipeKind::SamplerViewCso, texture.GetLifetimeId(),
|
||||||
|
texture.PipeHandleHint(ITextureObject::PipeHintKind::SamplerView));
|
||||||
const SizeT slot = handle.Slot;
|
const SizeT slot = handle.Slot;
|
||||||
if (slot >= m_viewLatch.size()) m_viewLatch.resize(slot + 1);
|
if (slot >= m_viewLatch.size()) m_viewLatch.resize(slot + 1);
|
||||||
ViewLatch& latch = m_viewLatch[slot];
|
ViewLatch& latch = m_viewLatch[slot];
|
||||||
|
|||||||
@@ -223,6 +223,31 @@ namespace MobileGL::MG_Pipe {
|
|||||||
return MGPipeHandle{it->second, entry->Gen};
|
return MGPipeHandle{it->second, entry->Gen};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeIdHinted(MGPipeKind kind, Uint64 lifetimeId,
|
||||||
|
Uint64& hint) const {
|
||||||
|
if (hint != 0 && lifetimeId != 0) {
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeIdHinted");
|
||||||
|
#endif
|
||||||
|
const Uint32 slot = static_cast<Uint32>(hint);
|
||||||
|
const Uint32 gen = static_cast<Uint32>(hint >> 32);
|
||||||
|
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||||
|
if (entry != nullptr && entry->Live && entry->Gen == gen && entry->LifetimeId == lifetimeId)
|
||||||
|
return MGPipeHandle{slot, gen};
|
||||||
|
}
|
||||||
|
const MGPipeHandle found = FindByLifetimeId(kind, lifetimeId);
|
||||||
|
hint = MGPipeHandleIsNull(found) ? 0 : MGPipePackHandleHint(found);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::AcquireHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint) {
|
||||||
|
const MGPipeHandle existing = FindByLifetimeIdHinted(kind, lifetimeId, hint);
|
||||||
|
if (!MGPipeHandleIsNull(existing)) return existing;
|
||||||
|
const MGPipeHandle minted = Acquire(kind, lifetimeId);
|
||||||
|
hint = MGPipeHandleIsNull(minted) ? 0 : MGPipePackHandleHint(minted);
|
||||||
|
return minted;
|
||||||
|
}
|
||||||
|
|
||||||
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
MGPipeRefuseAllocatorFromApplyThread("Acquire");
|
MGPipeRefuseAllocatorFromApplyThread("Acquire");
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeHandle FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const;
|
MGPipeHandle FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const;
|
||||||
// FindByLifetimeId, then AllocateFor when it misses. The ordinary client path.
|
// FindByLifetimeId, then AllocateFor when it misses. The ordinary client path.
|
||||||
MGPipeHandle Acquire(MGPipeKind kind, Uint64 lifetimeId);
|
MGPipeHandle Acquire(MGPipeKind kind, Uint64 lifetimeId);
|
||||||
|
// P15: the same two answers, first trying `hint` - the handle this object was last given,
|
||||||
|
// kept on the frontend object itself (MGPipePackHandleHint). The hint is believed only when
|
||||||
|
// the slot it names is live, at that generation, and owned by this very lifetime id, so a
|
||||||
|
// reset, a free or a recycle can only make it miss; the hint is refreshed on every miss.
|
||||||
|
MGPipeHandle FindByLifetimeIdHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint) const;
|
||||||
|
MGPipeHandle AcquireHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint);
|
||||||
|
|
||||||
// Returns the slot to the free list. The Gen bump happens on the NEXT handout of that
|
// Returns the slot to the free list. The Gen bump happens on the NEXT handout of that
|
||||||
// slot, not here, so a handle that is freed twice cannot skip a generation and the
|
// slot, not here, so a handle that is freed twice cannot skip a generation and the
|
||||||
@@ -179,6 +185,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The monolith's one client allocator. Under split there is one per client context.
|
// The monolith's one client allocator. Under split there is one per client context.
|
||||||
MGPipeSlotAllocator& MGPipeSlots();
|
MGPipeSlotAllocator& MGPipeSlots();
|
||||||
|
|
||||||
|
// A handle packed into the Uint64 a frontend object keeps as its hint (0 = none; slot 0 is
|
||||||
|
// never handed out).
|
||||||
|
inline Uint64 MGPipePackHandleHint(MGPipeHandle handle) {
|
||||||
|
return static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||||
|
}
|
||||||
|
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a
|
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a
|
||||||
// CLIENT-only surface. Its singleton accessor and original three method guards reject apply -
|
// CLIENT-only surface. Its singleton accessor and original three method guards reject apply -
|
||||||
|
|||||||
@@ -590,7 +590,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// gating the mint would make the other subsystems emit null handles in exactly the
|
// gating the mint would make the other subsystems emit null handles in exactly the
|
||||||
// A/B arm that exists to isolate them. Only the CALLS are gated.
|
// A/B arm that exists to isolate them. Only the CALLS are gated.
|
||||||
MGPipeHandle AcquireTexture(Uint64 lifetimeId, ITextureObject* object) {
|
MGPipeHandle AcquireTexture(Uint64 lifetimeId, ITextureObject* object) {
|
||||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId);
|
const MGPipeHandle handle =
|
||||||
|
object != nullptr
|
||||||
|
? MGPipeSlots().AcquireHinted(MGPipeKind::Texture, lifetimeId,
|
||||||
|
object->PipeHandleHint(ITextureObject::PipeHintKind::Texture))
|
||||||
|
: MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId);
|
||||||
Entry& entry = EntryFor(m_textures, handle);
|
Entry& entry = EntryFor(m_textures, handle);
|
||||||
RetireIfRecycled(entry, handle);
|
RetireIfRecycled(entry, handle);
|
||||||
entry.Texture = object;
|
entry.Texture = object;
|
||||||
@@ -1287,7 +1291,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
|
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
|
||||||
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
|
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
|
||||||
// per-context and no re-publication path exists or may exist.
|
// per-context and no re-publication path exists or may exist.
|
||||||
|
// Moves whenever the handle table below is dropped (ResetForTest), so a memo that skipped
|
||||||
|
// NoteTextureBoundAs / AcquireTexture for handles it already noted knows to note again.
|
||||||
|
Uint64 TableEpoch() const { return m_tableEpoch; }
|
||||||
|
|
||||||
void ResetForTest() {
|
void ResetForTest() {
|
||||||
|
++m_tableEpoch;
|
||||||
// EVERY REFERENCE THIS EMITTER OWES IS GIVEN BACK FIRST. A case that dropped the
|
// EVERY REFERENCE THIS EMITTER OWES IS GIVEN BACK FIRST. A case that dropped the
|
||||||
// table without releasing would pin cache entries for the rest of the process and
|
// table without releasing would pin cache entries for the rest of the process and
|
||||||
// the next case's LRU would mint over capacity for reasons it cannot see.
|
// the next case's LRU would mint over capacity for reasons it cannot see.
|
||||||
@@ -1689,6 +1698,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPTextureParams m_lastParams{};
|
MGPTextureParams m_lastParams{};
|
||||||
MGPSubData m_lastSubData{};
|
MGPSubData m_lastSubData{};
|
||||||
|
|
||||||
|
Uint64 m_tableEpoch = 0;
|
||||||
Uint64 m_creates = 0;
|
Uint64 m_creates = 0;
|
||||||
Uint64 m_respecifies = 0;
|
Uint64 m_respecifies = 0;
|
||||||
Uint64 m_paramSets = 0;
|
Uint64 m_paramSets = 0;
|
||||||
|
|||||||
@@ -256,13 +256,29 @@ namespace MobileGL::MG_Pipe {
|
|||||||
for (SizeT i = 0; i < kAttribs; ++i) {
|
for (SizeT i = 0; i < kAttribs; ++i) {
|
||||||
if (vao->GetAttribute(static_cast<Uint>(i)).Enabled) count = static_cast<Uint32>(i) + 1;
|
if (vao->GetAttribute(static_cast<Uint>(i)).Enabled) count = static_cast<Uint32>(i) + 1;
|
||||||
}
|
}
|
||||||
|
// P15: interleaved attributes share one buffer (FCL MC 1.21.5: every chunk draw
|
||||||
|
// re-points ~4 attributes at the next chunk's buffer), so the previous attribute's
|
||||||
|
// answer is reused for the same object - one lookup and one sticky-bit note per
|
||||||
|
// buffer instead of per attribute. Scoped to this loop: same object, same handle.
|
||||||
|
const MG_State::GLState::BufferObject* lastBuffer = nullptr;
|
||||||
|
MGPipeHandle lastHandle = kMGPipeNullHandle;
|
||||||
for (SizeT i = 0; i < count; ++i) {
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
const auto& attrib = vao->GetAttribute(static_cast<Uint>(i));
|
const auto& attrib = vao->GetAttribute(static_cast<Uint>(i));
|
||||||
MGPVertexBuffer& entry = m_entries[i];
|
MGPVertexBuffer& entry = m_entries[i];
|
||||||
entry = MGPVertexBuffer{};
|
entry = MGPVertexBuffer{};
|
||||||
entry.Res = attrib.Buffer ? MGPipeSlots().Acquire(MGPipeKind::Buffer,
|
const Bool sameBuffer = attrib.Buffer && attrib.Buffer.get() == lastBuffer;
|
||||||
attrib.Buffer->GetLifetimeId())
|
if (sameBuffer) {
|
||||||
: kMGPipeNullHandle;
|
entry.Res = lastHandle;
|
||||||
|
} else {
|
||||||
|
entry.Res = attrib.Buffer ? MGPipeSlots().AcquireHinted(MGPipeKind::Buffer,
|
||||||
|
attrib.Buffer->GetLifetimeId(),
|
||||||
|
attrib.Buffer->PipeHandleHint())
|
||||||
|
: kMGPipeNullHandle;
|
||||||
|
if (attrib.Buffer) {
|
||||||
|
lastBuffer = attrib.Buffer.get();
|
||||||
|
lastHandle = entry.Res;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ownedClientBuffers != nullptr && attrib.Enabled && !attrib.Buffer) {
|
if (ownedClientBuffers != nullptr && attrib.Enabled && !attrib.Buffer) {
|
||||||
entry.Res = (*ownedClientBuffers)[i];
|
entry.Res = (*ownedClientBuffers)[i];
|
||||||
}
|
}
|
||||||
@@ -271,7 +287,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// glNamedBuffer* may never be bound at any resource emission, but a draw
|
// glNamedBuffer* may never be bound at any resource emission, but a draw
|
||||||
// that fetches from it resolves it right here, on the GL thread, at every
|
// that fetches from it resolves it right here, on the GL thread, at every
|
||||||
// draw. Sticky, so one draw is enough for the rest of its life.
|
// draw. Sticky, so one draw is enough for the rest of its life.
|
||||||
MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex);
|
if (!sameBuffer) MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex);
|
||||||
// The attribute's own byte offset lives in MGPVertexAttribWire::Offset,
|
// The attribute's own byte offset lives in MGPVertexAttribWire::Offset,
|
||||||
// so the entry's is the BINDING's, which the frontend already folded in.
|
// so the entry's is the BINDING's, which the frontend already folded in.
|
||||||
entry.Offset = 0;
|
entry.Offset = 0;
|
||||||
|
|||||||
@@ -202,6 +202,9 @@ namespace MobileGL {
|
|||||||
// glGenBuffers) and never the heap address (recycled by the allocator): both
|
// glGenBuffers) and never the heap address (recycled by the allocator): both
|
||||||
// let a deleted-and-recreated buffer answer to a dead one's cache entry.
|
// let a deleted-and-recreated buffer answer to a dead one's cache entry.
|
||||||
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
||||||
|
// P15: the pipe client's last Buffer handle for this object (MGPipeSlotAllocator::
|
||||||
|
// AcquireHinted). Only a hint: it is checked against the allocator on every use.
|
||||||
|
Uint64& PipeHandleHint() const { return m_pipeHandleHint; }
|
||||||
// Monotonic counter bumped on every shadow mutation; backends use it to
|
// Monotonic counter bumped on every shadow mutation; backends use it to
|
||||||
// validate cached transient slices.
|
// validate cached transient slices.
|
||||||
Uint64 GetChangeSerial() const;
|
Uint64 GetChangeSerial() const;
|
||||||
@@ -238,6 +241,7 @@ namespace MobileGL {
|
|||||||
|
|
||||||
const Uint m_externalIndex = 0;
|
const Uint m_externalIndex = 0;
|
||||||
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
||||||
|
mutable Uint64 m_pipeHandleHint = 0;
|
||||||
SizeT m_size = 0;
|
SizeT m_size = 0;
|
||||||
BufferUsage m_usage = BufferUsage::StaticDraw;
|
BufferUsage m_usage = BufferUsage::StaticDraw;
|
||||||
// Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and
|
// Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and
|
||||||
|
|||||||
@@ -1281,6 +1281,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// the next glCreateProgram, this distinguishes a deleted-and-recreated program from the
|
// the next glCreateProgram, this distinguishes a deleted-and-recreated program from the
|
||||||
// original, so an identity cache can't false-hit on name recycling.
|
// original, so an identity cache can't false-hit on name recycling.
|
||||||
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
||||||
|
// P15: the pipe client's last ShaderCso handle for this program (MGPipeSlotAllocator::
|
||||||
|
// FindByLifetimeIdHinted). Only a hint: it is checked against the allocator on every use.
|
||||||
|
Uint64& PipeHandleHint() const { return m_pipeHandleHint; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// ---- The one and only join gate for link output (P1 invariant I5) ----
|
// ---- The one and only join gate for link output (P1 invariant I5) ----
|
||||||
@@ -1372,6 +1375,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// writes them.
|
// writes them.
|
||||||
const Uint m_externalIndex = 0;
|
const Uint m_externalIndex = 0;
|
||||||
const Uint64 m_lifetimeId = 0;
|
const Uint64 m_lifetimeId = 0;
|
||||||
|
mutable Uint64 m_pipeHandleHint = 0;
|
||||||
// The attach lists are mutated only in Link()'s GL-thread prologue, which is why
|
// The attach lists are mutated only in Link()'s GL-thread prologue, which is why
|
||||||
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
|
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
|
||||||
Vector<SharedPtr<ShaderObject>> m_shaders;
|
Vector<SharedPtr<ShaderObject>> m_shaders;
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
virtual Bool HasFixedSampleLocations() const = 0;
|
virtual Bool HasFixedSampleLocations() const = 0;
|
||||||
virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0;
|
virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0;
|
||||||
virtual Uint64 GetLifetimeId() const = 0;
|
virtual Uint64 GetLifetimeId() const = 0;
|
||||||
|
// P15: the pipe client's last handles for this texture, one per kind it is looked up under
|
||||||
|
// (MGPipeSlotAllocator::AcquireHinted). Only hints: checked against the allocator on use.
|
||||||
|
enum class PipeHintKind : Uint8 { Texture = 0, SamplerView = 1, Count = 2 };
|
||||||
|
virtual Uint64& PipeHandleHint(PipeHintKind kind) const = 0;
|
||||||
// Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10).
|
// Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10).
|
||||||
// DEPTH_COMPONENT until set, and meaningless for every other format.
|
// DEPTH_COMPONENT until set, and meaningless for every other format.
|
||||||
virtual GLenum GetDepthStencilTextureMode() const = 0;
|
virtual GLenum GetDepthStencilTextureMode() const = 0;
|
||||||
@@ -164,6 +168,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool HasFixedSampleLocations() const override;
|
Bool HasFixedSampleLocations() const override;
|
||||||
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
|
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
|
||||||
Uint64 GetLifetimeId() const override;
|
Uint64 GetLifetimeId() const override;
|
||||||
|
Uint64& PipeHandleHint(PipeHintKind kind) const override { return m_pipeHandleHints[static_cast<SizeT>(kind)]; }
|
||||||
// A plain texture owns its storage; TextureObjectView overrides this.
|
// A plain texture owns its storage; TextureObjectView overrides this.
|
||||||
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override;
|
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override;
|
||||||
Uint GetViewMinLevel() const override { return m_viewMinLevel; }
|
Uint GetViewMinLevel() const override { return m_viewMinLevel; }
|
||||||
@@ -241,6 +246,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
const Uint m_externalIndex;
|
const Uint m_externalIndex;
|
||||||
const Uint64 m_lifetimeId;
|
const Uint64 m_lifetimeId;
|
||||||
|
mutable Uint64 m_pipeHandleHints[static_cast<SizeT>(PipeHintKind::Count)] = {};
|
||||||
const TextureTarget m_target = TextureTarget::Unknown;
|
const TextureTarget m_target = TextureTarget::Unknown;
|
||||||
TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown;
|
||||||
SharedPtr<SamplerObject> m_sampler = nullptr;
|
SharedPtr<SamplerObject> m_sampler = nullptr;
|
||||||
|
|||||||
@@ -1264,6 +1264,111 @@ TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherTh
|
|||||||
<< "an out-of-range slot resized the table instead of being refused";
|
<< "an out-of-range slot resized the table instead of being refused";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================ P15: the bound-target build memo ============================
|
||||||
|
//
|
||||||
|
// EmitFramebufferState reuses the record it built for the same framebuffer and target while
|
||||||
|
// nothing the build reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState). Each
|
||||||
|
// case first proves the memo ENGAGED (an unchanged second emission is a hit), then moves one input
|
||||||
|
// the framebuffer's own version does not see and expects the record to follow it, by field name.
|
||||||
|
namespace {
|
||||||
|
MGPipeHandle HandleOfTexture(const SharedPtr<TextureObject2D>& texture) {
|
||||||
|
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(FramebufferEmit, TheBuildMemoEngagesAndFollowsAnAttachedTexturesRespecify) {
|
||||||
|
FramebufferScope scope;
|
||||||
|
const auto color = MakeColorTexture(80, 32);
|
||||||
|
const auto fbo = MakeShared<FramebufferObject>(20);
|
||||||
|
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||||
|
BindDrawAndRead(fbo, fbo);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
const Uint64 hits = Framebuffers().MemoHitCount();
|
||||||
|
EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), 0u);
|
||||||
|
ASSERT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "an unchanged framebuffer did not reuse its built record";
|
||||||
|
|
||||||
|
// The texture's level 0 grows; the framebuffer's object version does not move.
|
||||||
|
color->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{64, 64, 1}, 64u * 64u * 4u});
|
||||||
|
EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), sizeof(MGPFramebufferState));
|
||||||
|
EXPECT_EQ(Framebuffers().LastDraw().Width, 64u) << "MGPFramebufferState::Width";
|
||||||
|
EXPECT_EQ(Framebuffers().LastDraw().Height, 64u) << "MGPFramebufferState::Height";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The build runs when the bound framebuffer changes, so the memo has to hold more than the last
|
||||||
|
// framebuffer: an A/B/A switch must find A's record again.
|
||||||
|
TEST(FramebufferEmit, TheBuildMemoKeepsEachFramebufferAcrossABindingSwitch) {
|
||||||
|
FramebufferScope scope;
|
||||||
|
const auto colorA = MakeColorTexture(83, 32);
|
||||||
|
const auto colorB = MakeColorTexture(84, 16);
|
||||||
|
const auto a = MakeShared<FramebufferObject>(24);
|
||||||
|
const auto b = MakeShared<FramebufferObject>(25);
|
||||||
|
a->AttachTexture(FramebufferAttachmentType::Color0, colorA, TextureUploadTarget::Texture2D);
|
||||||
|
b->AttachTexture(FramebufferAttachmentType::Color0, colorB, TextureUploadTarget::Texture2D);
|
||||||
|
BindDrawAndRead(a, a);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
BindDrawAndRead(b, b);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
const Uint64 hits = Framebuffers().MemoHitCount();
|
||||||
|
BindDrawAndRead(a, a);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
EXPECT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "switching back to a framebuffer rebuilt its record";
|
||||||
|
EXPECT_EQ(Framebuffers().LastDraw().Width, 32u) << "MGPFramebufferState::Width";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FramebufferEmit, TheBuildMemoFollowsARestoragedAttachedRenderbuffer) {
|
||||||
|
FramebufferScope scope;
|
||||||
|
const auto renderbuffer = MakeShared<RenderbufferObject>(4);
|
||||||
|
renderbuffer->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||||
|
renderbuffer->AllocateStorage(IntVec2{32, 32});
|
||||||
|
const auto fbo = MakeShared<FramebufferObject>(21);
|
||||||
|
fbo->AttachRenderbuffer(FramebufferAttachmentType::Color0, renderbuffer);
|
||||||
|
BindDrawAndRead(fbo, fbo);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
const Uint64 hits = Framebuffers().MemoHitCount();
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
ASSERT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "an unchanged framebuffer did not reuse its built record";
|
||||||
|
|
||||||
|
renderbuffer->AllocateStorage(IntVec2{48, 40});
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
EXPECT_EQ(Framebuffers().LastDraw().Width, 48u) << "MGPFramebufferState::Width";
|
||||||
|
EXPECT_EQ(Framebuffers().LastDraw().Height, 40u) << "MGPFramebufferState::Height";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FramebufferEmit, TheBuildMemoFollowsAnAttachedTexturesSampleCount) {
|
||||||
|
FramebufferScope scope;
|
||||||
|
const auto color = MakeColorTexture(81, 32);
|
||||||
|
const auto fbo = MakeShared<FramebufferObject>(22);
|
||||||
|
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||||
|
BindDrawAndRead(fbo, fbo);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
|
||||||
|
// Samples move the params version, not the shape.
|
||||||
|
color->SetSamples(4);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
EXPECT_EQ(Framebuffers().LastDraw().Samples, 4u) << "MGPFramebufferState::Samples";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FramebufferEmit, TheBuildMemoReNotesBindBitsAfterTheTextureTableIsDropped) {
|
||||||
|
FramebufferScope scope;
|
||||||
|
const auto color = MakeColorTexture(82, 32);
|
||||||
|
const auto fbo = MakeShared<FramebufferObject>(23);
|
||||||
|
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||||
|
BindDrawAndRead(fbo, fbo);
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
const MGPipeHandle handle = HandleOfTexture(color);
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||||
|
ASSERT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0)
|
||||||
|
<< "the attachment's RENDER_TARGET bit was never noted";
|
||||||
|
|
||||||
|
// A dropped table (a replaced session) forgets the sticky bits; the next build must note them again.
|
||||||
|
MGPipeTextureEmitterInstance().ResetForTest();
|
||||||
|
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||||
|
Framebuffers().EmitFramebufferState(Ctx());
|
||||||
|
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0)
|
||||||
|
<< "MGPResourceDesc::BindMask lost RENDER_TARGET after the texture table was dropped";
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
// Before anything logs: the logger reads this variable once, on its first write, and
|
// Before anything logs: the logger reads this variable once, on its first write, and
|
||||||
// caches the handle. The name carries this process's pid, and the file is removed on the
|
// caches the handle. The name carries this process's pid, and the file is removed on the
|
||||||
|
|||||||
@@ -97,6 +97,38 @@ namespace {
|
|||||||
EXPECT_TRUE(allocator.IsLive(MGPipeKind::Buffer, reused));
|
EXPECT_TRUE(allocator.IsLive(MGPipeKind::Buffer, reused));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P15: a hint (the handle a frontend object was last given, kept on the object) is a shortcut
|
||||||
|
// and never an answer of its own. It must give the live handle back, and it must miss - never
|
||||||
|
// name another object's handle - after a free, a recycle of its slot, or a reset.
|
||||||
|
TEST(SlotAllocator, AHandleHintIsOnlyBelievedWhileItsSlotStillBelongsToTheObject) {
|
||||||
|
MGPipeSlotAllocator allocator;
|
||||||
|
constexpr Uint64 kFirst = 1001, kSecond = 1002;
|
||||||
|
|
||||||
|
Uint64 hint = 0;
|
||||||
|
const MGPipeHandle minted = allocator.AcquireHinted(MGPipeKind::Buffer, kFirst, hint);
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(minted));
|
||||||
|
EXPECT_EQ(hint, MGPipePackHandleHint(minted)) << "a mint must leave the hint naming the new handle";
|
||||||
|
EXPECT_TRUE(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, hint) == minted);
|
||||||
|
|
||||||
|
// The slot is freed and handed to another object. The dead object's hint must miss, even
|
||||||
|
// when it is forged to carry the successor's exact slot and generation: the owner moved.
|
||||||
|
allocator.Free(MGPipeKind::Buffer, minted);
|
||||||
|
EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, hint)))
|
||||||
|
<< "a freed object's hint still answered";
|
||||||
|
Uint64 staleHint = MGPipePackHandleHint(minted);
|
||||||
|
const MGPipeHandle successor = allocator.AcquireHinted(MGPipeKind::Buffer, kSecond, staleHint);
|
||||||
|
EXPECT_EQ(successor.Slot, minted.Slot) << "the free list did not hand the slot back";
|
||||||
|
Uint64 forged = MGPipePackHandleHint(successor); // the successor's exact slot and generation
|
||||||
|
EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, forged)))
|
||||||
|
<< "a hint naming another object's live slot answered for the dead object";
|
||||||
|
|
||||||
|
// A reset forgets every handle; a hint from before it can only miss.
|
||||||
|
Uint64 beforeReset = MGPipePackHandleHint(successor);
|
||||||
|
allocator.Reset();
|
||||||
|
EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kSecond, beforeReset)))
|
||||||
|
<< "a hint survived the allocator's reset";
|
||||||
|
}
|
||||||
|
|
||||||
TEST(SlotAllocator, FreedSlotComesBackBeforeHighWaterGrows) {
|
TEST(SlotAllocator, FreedSlotComesBackBeforeHighWaterGrows) {
|
||||||
MGPipeSlotAllocator allocator;
|
MGPipeSlotAllocator allocator;
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,34 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P15: the rebuild reuses the previous attribute's handle when it names the SAME buffer object
|
||||||
|
// (interleaved attributes), and only then. Two buffers interleaved A, A, B, A: every entry must
|
||||||
|
// carry its own buffer's handle, including the A after the B.
|
||||||
|
TEST(VertexInputEmit, InterleavedAttributesOnTwoBuffersEachCarryTheirOwnBuffersHandle) {
|
||||||
|
EmitterScope scope;
|
||||||
|
const SharedPtr<VertexArrayObject> vao = MakeVao(1);
|
||||||
|
const SharedPtr<BufferObject> a = Ctx().CreateBufferObject(1);
|
||||||
|
const SharedPtr<BufferObject> b = Ctx().CreateBufferObject(2);
|
||||||
|
a->Respecify(256, nullptr);
|
||||||
|
b->Respecify(256, nullptr);
|
||||||
|
const SharedPtr<BufferObject>* owners[] = {&a, &a, &b, &a};
|
||||||
|
for (Uint i = 0; i < 4; ++i) {
|
||||||
|
vao->SetAttributeFormat(i, 4, DataType::Float32, false, 16, static_cast<SizeT>(4 * i), false, false, 16);
|
||||||
|
vao->MirrorPointerIntoBinding(i, *owners[i], static_cast<SizeT>(4 * i), 16);
|
||||||
|
vao->BindAttributeBuffer(i, *owners[i]);
|
||||||
|
vao->EnableAttribute(i);
|
||||||
|
}
|
||||||
|
Emitter().EmitVertexBuffers(Ctx(), 0);
|
||||||
|
ASSERT_EQ(Emitter().LastVertexBuffers().Count, 4u);
|
||||||
|
for (Uint i = 0; i < 4; ++i) {
|
||||||
|
const MGPipeHandle expected =
|
||||||
|
MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, (*owners[i])->GetLifetimeId());
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(expected));
|
||||||
|
EXPECT_TRUE(Emitter().LastEntries()[i].Res == expected)
|
||||||
|
<< "MGPVertexBuffer::Res of entry " << i << " is not its own buffer's handle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8: a pointer call's stride 0 means
|
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8: a pointer call's stride 0 means
|
||||||
// "tightly packed" and the frontend already resolved it to the element size, so a zero
|
// "tightly packed" and the frontend already resolved it to the element size, so a zero
|
||||||
// that reaches the wire can only have come from the binding model - where it means every
|
// that reaches the wire can only have come from the binding model - where it means every
|
||||||
|
|||||||
Reference in New Issue
Block a user