mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c458cd594 | ||
|
|
c2c6a655ea | ||
|
|
9f60aadc1d | ||
|
|
a690032f85 | ||
|
|
173f1dd273 |
+4
-1
@@ -335,7 +335,10 @@ namespace MobileGL::MG_Config {
|
||||
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
||||
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
|
||||
// 0x400 texture resources (texture + renderbuffer resource_*,
|
||||
// set_texture_params) - requires 0x80
|
||||
// set_texture_params) - requires 0x80 AND 0x800
|
||||
// (the built-in sampler CSO a set_texture_params record names is minted by
|
||||
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
|
||||
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
|
||||
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
|
||||
// bind_sampler_states / set_shader_images) - requires 0x400
|
||||
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
|
||||
|
||||
@@ -5252,6 +5252,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (m_imageBindableStorageRequired) {
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Whether this transition re-mints storage that ALREADY EXISTED on the backend: that
|
||||
// is the remint PULL (the levels below are replayed from the client's shadow to fill
|
||||
// the new carrier), and it is what ROADMAP open question 2 counts. A texture reaching
|
||||
// here uninitialised is allocated image-bindable up front and pulls nothing.
|
||||
const Bool hadBackendStorage = m_isInitialized;
|
||||
#endif
|
||||
m_imageBindableStorageRequired = true;
|
||||
m_isInitialized = false;
|
||||
// Every level this object has ALREADY uploaded has to be replayed, because the
|
||||
@@ -5326,6 +5333,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!markedRemintPull) {
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("texture-remint-pull");
|
||||
markedRemintPull = true;
|
||||
// THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): one per
|
||||
// transition that replays a level of storage the backend already held.
|
||||
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
|
||||
}
|
||||
}
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
|
||||
const MG_Pipe::MGPBox wholeLevel{0,
|
||||
|
||||
@@ -389,6 +389,26 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
|
||||
// same objects, so "what was emitted" costs no copy. ----
|
||||
// ---- the death half (P4a final review C-2) ----
|
||||
//
|
||||
// Called by the contract's death helper before the slot is freed (there is no wire
|
||||
// delete for this kind, D-I2, so this is the only client-side thing a framebuffer's
|
||||
// death has to do). The per-object Named latch is the entry: a recycled handle's Gen
|
||||
// already refuses the stale latch, so this is hygiene rather than a fix - the rule
|
||||
// (ID-8) is that whatever mints a handle retires everything it keeps under it at the
|
||||
// death, and every P4a kind takes the same shape. Gen-keyed for a late notice.
|
||||
void NoteFramebufferDied(MGPipeHandle handle) {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return;
|
||||
if (m_named[slot].Gen == handle.Gen) m_named[slot] = NamedEntry{};
|
||||
}
|
||||
// "Does this emitter hold a Named-record latch for this handle at its generation."
|
||||
Bool NamedRecordIsLatched(MGPipeHandle handle) const {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return false;
|
||||
return m_named[slot].Has && m_named[slot].Gen == handle.Gen;
|
||||
}
|
||||
|
||||
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
|
||||
const MGPFramebufferState& LastRead() const { return m_lastRead; }
|
||||
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
@@ -97,6 +98,13 @@ namespace MobileGL::MG_Pipe {
|
||||
entry.Res = binding.Texture ? MGPipeSlots().Acquire(MGPipeKind::Texture,
|
||||
binding.Texture->GetLifetimeId())
|
||||
: kMGPipeNullHandle;
|
||||
// D-A4: a texture named in an emitted MGPImageView is SHADER-IMAGE-bound from
|
||||
// then on - the bit ImageBindableHint is derived from. The bind itself noted it
|
||||
// first (TextureState.h, so the hint precedes the first sync); this is the
|
||||
// letter of the rule and a one-compare early-out once the bit is set.
|
||||
if (!MGPipeHandleIsNull(entry.Res)) {
|
||||
MGPipeNoteTextureBoundAs(entry.Res, static_cast<Uint32>(kMGPipeBindShaderImage));
|
||||
}
|
||||
// THE APPLICATION's format and access, verbatim. The bind-format recast and the
|
||||
// buffer-texture split view are server-side and stay there; so does
|
||||
// SupportsLayeredImageBinding's rule, which asks the BACKEND target after
|
||||
|
||||
@@ -1255,10 +1255,12 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceCreate(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureResourceRespecify(ITextureObject& texture) {
|
||||
void MGPipeEmitTextureResourceRespecify(ITextureObject& texture, MGPipeTextureRespecifyScope scope,
|
||||
Uint32 uploadTarget, Uint32 level) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceRespecify(texture); });
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.EmitResourceRespecify(texture, scope, uploadTarget, level); });
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureParams(ITextureObject& texture) {
|
||||
@@ -1288,6 +1290,21 @@ namespace MobileGL::MG_Pipe {
|
||||
[&](auto& emitter) { emitter.EmitRenderbufferRespecify(renderbuffer); });
|
||||
}
|
||||
|
||||
void MGPipeNoteTextureBoundAs(MGPipeHandle texture, Uint32 bindBit) {
|
||||
// Not gated on FamilyIsLive: the mask is client state (see the declaration), and the
|
||||
// emitter gates the emission it causes.
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.NoteTextureBoundAs(texture, static_cast<Uint16>(bindBit)); });
|
||||
}
|
||||
|
||||
void MGPipeNoteTextureImageBound(ITextureObject& texture) {
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(MGPipeTextureEmitterInstance(), [&](auto& emitter) {
|
||||
emitter.NoteTextureBoundAs(emitter.AcquireTexture(texture.GetLifetimeId(), &texture),
|
||||
static_cast<Uint16>(kMGPipeBindShaderImage));
|
||||
});
|
||||
}
|
||||
|
||||
void MGPipeEmitSamplerCsoCreate(SamplerObject& sampler) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
@@ -1368,11 +1385,24 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// THE EMITTER IS TOLD BETWEEN THE WIRE DELETE AND THE FREE (P4a final review C-2), for
|
||||
// every kind that keeps client state under a handle: a texture's drain entries, pointer,
|
||||
// cache reference and latches; a renderbuffer's entry; a framebuffer's Named latch; a
|
||||
// sampler view's and a shader CSO's record memo. Before this the six helpers freed the slot
|
||||
// and told nobody, so the texture emitter kept the freed ITextureObject* and the level on
|
||||
// the drain list, and `glTexImage2D; glDeleteTextures; <verb>` called a virtual on freed
|
||||
// memory from the next validate point. The forward is the P3a shape
|
||||
// (MGPipeEmitVertexElementsDestroyAndFree's emitter.NoteRecordDestroyed) applied to the
|
||||
// five P4a kinds that have an entry to retire; the content-addressed sampler CSO keeps
|
||||
// none per object (its death is the cache's LRU, ID-17). Unconditional in a push build,
|
||||
// like the mints: the entries exist whether or not the family bit is set.
|
||||
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
||||
// NotifyStateObjectDestroyed takes a KIND and a lifetime id, not an object
|
||||
// (StateObjectDeathNotice.h - one entry point for every kind rather than one ops table
|
||||
@@ -1392,6 +1422,11 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
||||
// The emitter retires its entry while the handle still resolves (C-2): the drain list
|
||||
// drops the dead texture's levels, the raw pointer goes, the built-in sampler's cache
|
||||
// reference is given back, the latches and the sticky mask are cleared.
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteTextureDied(handle); });
|
||||
NotifyAndFree(MGPipeKind::Texture, lifetimeId, handle);
|
||||
// THE SAMPLER VIEW DIES WITH ITS TEXTURE, because it is minted off the same lifetime
|
||||
// id: one SamplerViewCso per ITextureObject (D-F2), re-issued on the same handle
|
||||
@@ -1428,6 +1463,8 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteRenderbufferDied(handle); });
|
||||
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
@@ -1452,6 +1489,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// whatever it owed", which for a framebuffer is the death notice this just raised.
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Framebuffer, lifetimeId);
|
||||
ForwardWhenWired<kMGPipeWiredFramebufferSubsystem>(
|
||||
MGPipeFramebufferEmitterInstance(), [&](auto& emitter) { emitter.NoteFramebufferDied(handle); });
|
||||
NotifyAndFree(MGPipeKind::Framebuffer, lifetimeId, handle);
|
||||
return false;
|
||||
}
|
||||
@@ -1461,6 +1500,11 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
||||
// NOTHING TO RETIRE IN AN EMITTER FOR THIS KIND, stated rather than implied: a sampler
|
||||
// CSO is content-addressed and belongs to a value, so no emitter keeps an entry under
|
||||
// a SamplerObject's handle - the cache's entries are keyed by value and reference
|
||||
// count, and the death of a bound sampler object releases its unit's reference at the
|
||||
// next bind_sampler_states pass (SamplerEmit.h's reconciliation).
|
||||
NotifyAndFree(MGPipeKind::SamplerCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
@@ -1477,6 +1521,8 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
||||
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
|
||||
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
@@ -324,14 +324,12 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// The memo's other half, and the bound-mirror clearing beside it.
|
||||
//
|
||||
// NO PRODUCTION CALLER TODAY, stated rather than implied: since c0b the death path
|
||||
// reads the contract's latch and never asks an emitter. It is kept because the memo
|
||||
// above needs a way to be told, and because everything it clears SELF-HEALS if it is
|
||||
// not called - the slot's Gen moves on reuse, so `RecordGen == handle.Gen` refuses a
|
||||
// stale record latch, and the three bound mirrors below hold a handle whose generation
|
||||
// can never be handed out again, so the next EmitShaderState compares against a
|
||||
// different handle and re-binds. Clearing them here is the cheaper answer, not the
|
||||
// load-bearing one.
|
||||
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the death path
|
||||
// reads the contract's latch for the wire delete and then forwards here, before the
|
||||
// slot is freed, so a dead handle no longer reads as published in this memo between
|
||||
// the death and the recycle and the three bound mirrors never name a dead program.
|
||||
// Gen-keyed, so a late notice for a slot already handed out again clears nothing of
|
||||
// the successor's.
|
||||
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
Vector<Latch>& table = TableOf(handle);
|
||||
|
||||
@@ -767,6 +767,11 @@ namespace MobileGL::MG_Pipe {
|
||||
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
|
||||
|
||||
entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
// 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,
|
||||
// since this header is included BY TextureEmit.h). One early-out per unit per
|
||||
// pass once the bit is set.
|
||||
MGPipeNoteTextureBoundAs(entry.Texture, static_cast<Uint32>(kMGPipeBindSampler));
|
||||
entry.View = AcquireSamplerView(*texture, entry.Texture, bytes);
|
||||
}
|
||||
|
||||
@@ -986,11 +991,11 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
// The memo's other half, for a caller that knows the applier has dropped this record.
|
||||
// NO PRODUCTION CALLER TODAY, and that is stated rather than implied: the death path
|
||||
// goes through the contract's latch, not through here. It is kept because the memo
|
||||
// above needs a way to be told, and because leaving the latch standing SELF-HEALS
|
||||
// anyway - the slot's Gen moves on reuse, so the `RecordGen == handle.Gen` test in
|
||||
// RecordIsPublished and in AcquireSamplerView already refuses a stale entry.
|
||||
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the texture's
|
||||
// helper drops the sampler view minted off the texture's lifetime id and forwards here
|
||||
// before the slot is freed, so a dead handle no longer reads as published in this memo
|
||||
// between the death and the recycle. Gen-keyed, so a late notice for a slot already
|
||||
// handed out again clears nothing of the successor's.
|
||||
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
const SizeT slot = handle.Slot;
|
||||
|
||||
@@ -441,24 +441,89 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// The texture a handle names, or null. A RAW pointer is exact here for
|
||||
// MGPipeResourceTracker::Resolve's reason: the entry exists only between the create the
|
||||
// constructor emits and the destroy the destructor emits, and the Gen compare is what
|
||||
// refuses a stale handle rather than resolving it to whatever now occupies the slot.
|
||||
// constructor emits and the destroy the destructor emits - and since the final review's
|
||||
// C-2 that sentence is ESTABLISHED rather than assumed: the contract's death helper
|
||||
// forwards to NoteTextureDied below before it frees the slot, so a dead handle finds a
|
||||
// null pointer here. The Gen compare refuses a RECYCLED handle rather than resolving it
|
||||
// to whatever now occupies the slot.
|
||||
//
|
||||
// A DEAD SLOT IS REFUSED, LOUDLY. The allocator's generation moves only at the NEXT
|
||||
// hand-out, so between a death and a recycle a dead handle compares equal to the slot's
|
||||
// generation - which is why the guard is IsLive and not GenOfSlot (the review's C-2:
|
||||
// that compare guarded a recycled slot and never a dead one, and the drain then called
|
||||
// a virtual on the freed object once per verb). Reaching this arm at all means a death
|
||||
// path skipped the emitter, which is a seam defect and not traffic: it is counted and
|
||||
// logged once, and the answer is null.
|
||||
ITextureObject* ResolveTexture(MGPipeHandle handle) const {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return nullptr;
|
||||
const Entry& entry = m_textures[slot];
|
||||
if (entry.Texture == nullptr || entry.Gen != handle.Gen) return nullptr;
|
||||
if (MGPipeSlots().GenOfSlot(MGPipeKind::Texture, handle.Slot) != handle.Gen) return nullptr;
|
||||
if (!MGPipeSlots().IsLive(MGPipeKind::Texture, handle)) {
|
||||
++m_deadResolves;
|
||||
MGLOG_E_ONCE("MGPipe: texture handle {slot=%u, gen=%u} is dead but the emitter still holds its "
|
||||
"object - the death path did not retire the entry; refused rather than resolved",
|
||||
handle.Slot, handle.Gen);
|
||||
return nullptr;
|
||||
}
|
||||
return entry.Texture;
|
||||
}
|
||||
|
||||
// ---- the death half (P4a final review C-2) ----
|
||||
//
|
||||
// CALLED BY THE CONTRACT'S DEATH HELPER, after the wire delete went out and BEFORE the
|
||||
// slot is freed (ID-8's order: delete, notice, free - this sits between the first two).
|
||||
// v2 had no such door: the helper freed the slot, the emitter kept the freed
|
||||
// ITextureObject* and the level on the drain list, and `glTexImage2D; glDeleteTextures;
|
||||
// <any verb>` walked freed memory at the next validate point - a SIGABRT ("pure virtual
|
||||
// method called") at the shipping mask. Everything the entry owns goes here: the drain
|
||||
// entries (nothing is owed for a dead texture - its record is gone with the wire delete),
|
||||
// the built-in sampler's cache reference (ID-17: one per entry, released at the death
|
||||
// and no longer at the recycle), the latches and the sticky mask. RetireIfRecycled stays
|
||||
// as the belt for a slot whose death this emitter was never told about.
|
||||
//
|
||||
// Keyed on the GENERATION so a late notice for a slot that has already been handed out
|
||||
// again cannot retire the successor's entry.
|
||||
void NoteTextureDied(MGPipeHandle handle) {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return;
|
||||
Entry& entry = m_textures[slot];
|
||||
if (entry.Gen != handle.Gen) return;
|
||||
if (!entry.DrainKeys.empty()) {
|
||||
// A death inside the drain cannot happen (no SharedPtr drops there), but if one
|
||||
// ever did the loop below is iterating m_drain: the null pointer the reset
|
||||
// leaves is what makes EmitOneLevel answer "nothing owed" and drop the entry.
|
||||
if (!m_draining) {
|
||||
SizeT kept = 0;
|
||||
for (SizeT i = 0; i < m_drain.size(); ++i) {
|
||||
if (m_drain[i].Handle == handle) continue;
|
||||
m_drain[kept++] = m_drain[i];
|
||||
}
|
||||
m_drain.resize(kept);
|
||||
}
|
||||
entry.DrainKeys.clear();
|
||||
}
|
||||
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
|
||||
entry = Entry{};
|
||||
}
|
||||
void NoteRenderbufferDied(MGPipeHandle handle) {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_renderbuffers.size()) return;
|
||||
Entry& entry = m_renderbuffers[slot];
|
||||
if (entry.Gen != handle.Gen) return;
|
||||
entry = Entry{};
|
||||
}
|
||||
|
||||
// ---- the sticky bind mask (D-A4) ----
|
||||
//
|
||||
// ORed, never cleared, and emitted on BOTH resource_create and every
|
||||
// resource_respecify, exactly as P3a's buffer mask is. The four bits nothing set before
|
||||
// P4a get their producers here and in the framebuffer emitter: RENDER_TARGET and
|
||||
// DEPTH_STENCIL from an attachment point, SAMPLER from a resolved sampler view and
|
||||
// SHADER_IMAGE from a resolved image unit (the sampler package's two).
|
||||
// DEPTH_STENCIL from an attachment point (FramebufferEmit.h), SAMPLER from a resolved
|
||||
// sampler view (SamplerEmit.h) and SHADER_IMAGE from glBindImageTexture's state setter
|
||||
// and the resolved image unit (TextureState.h, ImageEmit.h) - the last two through the
|
||||
// contract's MGPipeNoteTextureBoundAs door, since neither may include this header
|
||||
// (final review M-A: before the fix round nothing produced them and the hint was dead).
|
||||
// A MASK CHANGE AFTER THE ALLOCATION IS A METADATA RESPECIFY (ID-18 M4), and without it
|
||||
// the sticky half of D-A4 is a no-op for exactly the textures it was written for. The
|
||||
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
|
||||
@@ -474,6 +539,11 @@ namespace MobileGL::MG_Pipe {
|
||||
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
Entry& entry = EntryFor(m_textures, handle);
|
||||
// The entry is stamped with the generation it is written under, and a predecessor's
|
||||
// entry on a recycled slot is retired first (the same door AcquireTexture takes): a
|
||||
// texture born while the family bit was clear has no create to have done it.
|
||||
RetireIfRecycled(entry, handle);
|
||||
entry.Gen = handle.Gen;
|
||||
const Uint16 before = entry.BindMask;
|
||||
const Uint16 now = static_cast<Uint16>(before | bit);
|
||||
if (now == before) return;
|
||||
@@ -491,6 +561,8 @@ namespace MobileGL::MG_Pipe {
|
||||
void NoteRenderbufferBoundAs(MGPipeHandle handle, Uint16 bit) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
Entry& entry = EntryFor(m_renderbuffers, handle);
|
||||
RetireIfRecycled(entry, handle);
|
||||
entry.Gen = handle.Gen;
|
||||
const Uint16 before = entry.BindMask;
|
||||
const Uint16 now = static_cast<Uint16>(before | bit);
|
||||
if (now == before) return;
|
||||
@@ -523,14 +595,36 @@ namespace MobileGL::MG_Pipe {
|
||||
PublishCreate(MGPipeKind::Texture, handle, entry, desc);
|
||||
}
|
||||
|
||||
// resource_respecify, from every storage-defining entry point. DEDUPED ON THE
|
||||
// DESCRIPTOR ITSELF rather than on a version, because the entry points that reach here
|
||||
// are the ones that move the SHAPE and several of them do not move the descriptor at
|
||||
// all (glTexParameter TEXTURE_BASE_LEVEL bumps the shape version and changes no field
|
||||
// this record carries). A byte compare of an 88-byte POD is cheaper than the emission
|
||||
// it avoids, and it is the same "version-first skip before anything expensive" shape
|
||||
// every other P4a emission takes.
|
||||
void EmitResourceRespecify(ITextureObject& texture) {
|
||||
// resource_respecify, from every storage-defining entry point, WITH THE SCOPE OF THE
|
||||
// STORAGE IT REPLACES (P4a final review C-1; the scopes are PipeMutation.h's).
|
||||
//
|
||||
// THE LEVEL IS PASSED, AND IT IS WIRE'S KEY. The applier keeps a pending-upload set per
|
||||
// (uploadTarget, level) - the client's dirty flags, inverted - and a respecify drops the
|
||||
// entries against the storage it REPLACES: with a null MGPRespecifiedLevel every entry,
|
||||
// with a level exactly that one. v2 passed null at every call, so a level the applier
|
||||
// had ACCEPTED at one verb (the client flag already clear, D-D5 step 1) and that the
|
||||
// next verb's glTexImage2D(level 1) or glGenerateMipmap grow defined AROUND was dropped
|
||||
// with nobody owing its texels: `L0; draw(other); L1; draw(T)` read a black level 0.
|
||||
// The key is built from the SAME packed MGPSubData::Target the drain puts in that
|
||||
// level's record (wire-v3 §5 item 5), so what this drops is what that emission made.
|
||||
//
|
||||
// THREE SCOPES, one call each for the first two and one call PER REMOVED LEVEL for the
|
||||
// chain cut: the applier's key is one (uploadTarget, level), so "every level from N"
|
||||
// is spelled as N.., each after the first landing on an unchanged descriptor - which
|
||||
// the applier classifies as a metadata update that drops nothing but the level it
|
||||
// names. That is the refinement wire's W11 clause takes this round.
|
||||
//
|
||||
// DEDUPED ON THE DESCRIPTOR ITSELF for the whole-resource form only: the entry points
|
||||
// that reach it move the SHAPE and several of them do not move the descriptor at all
|
||||
// (glTexParameter TEXTURE_BASE_LEVEL bumps the shape version and changes no field this
|
||||
// record carries), and a byte compare of an 88-byte POD is cheaper than the emission
|
||||
// it avoids. A PER-LEVEL form is never deduped: the level it redefines is not in the
|
||||
// descriptor (a non-base level's extent moves no field), so an unchanged descriptor
|
||||
// cannot say whether the applier still holds a box against the OLD level - and a box
|
||||
// kept across a shrink is uploaded past the end of the new one. One applier call per
|
||||
// level definition is the cost, and the sub-data that follows moves the serial anyway.
|
||||
void EmitResourceRespecify(ITextureObject& texture, MGPipeTextureRespecifyScope scope,
|
||||
Uint32 uploadTarget, Uint32 level) {
|
||||
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
|
||||
// THE VIEW'S OWNER IS ACQUIRED FIRST, and no Entry& is held across it (m3): the
|
||||
// owner's slot can be higher than this table's size, so AcquireTexture would
|
||||
@@ -568,7 +662,37 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPResourceDesc desc = MGPipeBuildTextureResourceDesc(
|
||||
texture, handle, entry.BindMask, /*storageDefined=*/true, viewOf, bufferHandle, bufOffset,
|
||||
bufSize);
|
||||
if (entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0) return;
|
||||
const Bool unchanged = entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0;
|
||||
|
||||
// THE KEYS THIS CALL DROPS. `keyCount == 0` is the whole resource (a null level
|
||||
// pointer); otherwise `keyCount` keys from `firstLevel` up, all on `uploadTarget`.
|
||||
Uint32 firstLevel = 0;
|
||||
Uint32 keyCount = 0;
|
||||
switch (scope) {
|
||||
case MGPipeTextureRespecifyScope::OneLevel:
|
||||
firstLevel = level;
|
||||
keyCount = 1;
|
||||
break;
|
||||
case MGPipeTextureRespecifyScope::LevelsFrom: {
|
||||
// A cut at 0 leaves nothing: the whole resource. Otherwise the removed levels
|
||||
// are [level, the level count the applier last accepted): LastDesc mirrors
|
||||
// acceptance, and a sub-data for a level the accepted descriptor does not
|
||||
// describe is refused by the applier, so no key above that count can exist. A
|
||||
// cut that removes nothing the applier could hold is deduped like the
|
||||
// whole-resource form; if the descriptor moved anyway the first key carries it.
|
||||
if (level == 0) break;
|
||||
const Uint32 previous = entry.HasLastDesc ? static_cast<Uint32>(entry.LastDesc.Levels) : 0u;
|
||||
if (previous <= level && unchanged) return;
|
||||
firstLevel = level;
|
||||
keyCount = previous > level ? previous - level : 1u;
|
||||
break;
|
||||
}
|
||||
case MGPipeTextureRespecifyScope::WholeResource:
|
||||
default:
|
||||
if (unchanged) return;
|
||||
break;
|
||||
}
|
||||
|
||||
// SELF-HEALING IN BOTH DIRECTIONS, the P3a m12 shape: a texture born while the
|
||||
// subsystem bit was clear has no applier record, and every later respecify would be
|
||||
// REFUSED. A create rather than a respecify, because that is what the record's
|
||||
@@ -589,24 +713,24 @@ namespace MobileGL::MG_Pipe {
|
||||
// about what the APPLIER holds, so a refused respecify must leave LastDesc naming
|
||||
// the descriptor that actually landed, or the next identical call is suppressed
|
||||
// against a record that was never stored.
|
||||
Bool accepted = ApplyRespecify(desc);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
if (!accepted) {
|
||||
// THE SECOND HALF OF THE SELF-HEAL, and the publication latch cannot give
|
||||
// it: the latch answers "did a create for this handle GO OUT", which stays
|
||||
// true after MGPipeApplierReleaseObjectRecords has dropped every object
|
||||
// record - the scope a served context's teardown takes while the frontend
|
||||
// objects live on in the share group. The applier's REFUSAL is the only
|
||||
// signal that says "I hold nothing for this handle", and the acceptance
|
||||
// return is what makes it visible from here at all. One retry, never a
|
||||
// loop: a descriptor the applier refuses on its own merits (a target that
|
||||
// names no resource kind) is refused again and the flags stay set.
|
||||
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
|
||||
texture, handle, entry.BindMask, /*storageDefined=*/false, viewOf,
|
||||
bufferHandle, bufOffset, bufSize);
|
||||
NoteDesc(healDesc, /*isCreate=*/true);
|
||||
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
|
||||
accepted = ApplyRespecify(desc);
|
||||
//
|
||||
// THE PACKED TARGET IS THE DRAIN's (wire-v3 §5 item 5): the contract's packer takes
|
||||
// two Uint32s, low byte the resource target, high byte the upload target (a cube
|
||||
// face), and the applier matches the key against the sub-data records verbatim.
|
||||
const Uint16 packedTarget = MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MGPipeResourceTargetForTextureTarget(texture.GetTarget())), uploadTarget);
|
||||
Bool accepted = false;
|
||||
if (keyCount == 0) {
|
||||
accepted = RespecifyOnce(texture, handle, entry, desc, nullptr, viewOf, bufferHandle, bufOffset,
|
||||
bufSize);
|
||||
} else {
|
||||
for (Uint32 i = 0; i < keyCount; ++i) {
|
||||
MGPRespecifiedLevel key{};
|
||||
key.UploadTarget = packedTarget;
|
||||
key.Level = static_cast<Uint16>(firstLevel + i);
|
||||
accepted = RespecifyOnce(texture, handle, entry, desc, &key, viewOf, bufferHandle, bufOffset,
|
||||
bufSize);
|
||||
if (!accepted) break;
|
||||
}
|
||||
}
|
||||
NoteRespecified(entry, desc, accepted);
|
||||
@@ -638,9 +762,10 @@ namespace MobileGL::MG_Pipe {
|
||||
entry.SamplerVersion == samplerVersion && !entry.ForceParamsResync) {
|
||||
return;
|
||||
}
|
||||
entry.HasParamsLatch = true;
|
||||
entry.ParamsVersion = paramsVersion;
|
||||
entry.SamplerVersion = samplerVersion;
|
||||
// THE LATCH IS TAKEN BELOW, ON ACCEPTANCE (final review m-1, audit F-7) - like the
|
||||
// sub-data and respecify paths, and unlike v2, which advanced it here and left a
|
||||
// refused record (no applier record for the handle, the SD-1/SD-3 shape) unsent
|
||||
// until the next glTexParameter* moved a version.
|
||||
|
||||
// ID-14 / ID-17: THE BUILT-IN SAMPLER COMES FROM C's CONTENT-ADDRESSED CACHE and is
|
||||
// never minted here. v1 took MGPipeSlots().Acquire(SamplerCso, the SamplerObject's
|
||||
@@ -654,9 +779,9 @@ namespace MobileGL::MG_Pipe {
|
||||
// what stops the LRU pulling a handle out from under a standing MGPTextureParams
|
||||
// record: the applier deliberately does not resolve BuiltinSampler, and an eviction
|
||||
// is not a parameter change, so nothing would refuse and nothing would re-emit. The
|
||||
// previous handle is released when the content moves it, and the last one when the
|
||||
// slot is recycled (RetireIfRecycled) - which is the only moment this package can
|
||||
// see a texture die, the death helper being A's.
|
||||
// previous handle is released when the content moves it, and the last one at the
|
||||
// texture's death (NoteTextureDied, reached from the contract's death helper) - or
|
||||
// at the recycle, as the belt, for a death this emitter was not told about.
|
||||
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
|
||||
Uint64 samplerBytes = 0;
|
||||
const MGPipeHandle builtinSampler =
|
||||
@@ -673,10 +798,53 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
const MGPTextureParams params =
|
||||
MGPipeBuildTextureParams(texture, handle, entry.BuiltinSampler, entry.ForceParamsResync);
|
||||
entry.ForceParamsResync = false;
|
||||
m_lastParams = params;
|
||||
++m_paramSets;
|
||||
MGPipeApplySetTextureParams(params);
|
||||
// Not behind MGPipeTextureRecordsReachTheApplier() (see its comment): the call is
|
||||
// dispatched whenever this emitter runs, so the answer is always a real one.
|
||||
Bool accepted = MGPipeApplySetTextureParams(params);
|
||||
if (!accepted) {
|
||||
// THE SELF-HEAL, the respecify path's shape, and the parameters are the one
|
||||
// publication that may be a texture's FIRST: the context's default textures are
|
||||
// constructed before the backend registers its consumer, so no create ever went
|
||||
// out for them, and the application's first glTexParameter* on texture 0 found
|
||||
// no record (the retrace census's residual once this refusal went loud). A
|
||||
// create with no storage gives the record its identity, the storage follows if
|
||||
// the texture has any (a respecify against the create's descriptor is never
|
||||
// deduped away), and the parameters land on the record that now exists. The
|
||||
// same repair covers the served context's teardown scope, where the records are
|
||||
// dropped while the objects live on. One retry, never a loop.
|
||||
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
|
||||
texture, handle, entry.BindMask, /*storageDefined=*/false, kMGPipeNullHandle,
|
||||
kMGPipeNullHandle, 0, 0);
|
||||
NoteDesc(healDesc, /*isCreate=*/true);
|
||||
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
|
||||
const auto* mipmap = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
const Bool hasStorage = mipmap != nullptr
|
||||
? mipmap->GetMipmapLevelCount() > 0
|
||||
: texture.GetStorageType() == MobileGL::TextureStorageType::Buffer;
|
||||
if (hasStorage) {
|
||||
// Can grow the table (a view's owner is acquired inside): no Entry& is held
|
||||
// across it - `entry` is re-fetched below.
|
||||
EmitResourceRespecify(texture, MGPipeTextureRespecifyScope::WholeResource, 0, 0);
|
||||
}
|
||||
accepted = MGPipeApplySetTextureParams(params);
|
||||
}
|
||||
Entry& latched = EntryFor(m_textures, handle);
|
||||
if (!accepted) {
|
||||
// Refused on its merits (a null built-in sampler, no consumer). Nothing latched:
|
||||
// the same versions re-send at the next call. Loud for the reason the sub-data
|
||||
// refusal is loud.
|
||||
++m_refusedParamSets;
|
||||
MGLOG_E_ONCE("MGPipe: set_texture_params for texture %u {slot=%u, gen=%u} was refused; the "
|
||||
"latch is not taken and the parameters are re-sent at the next call",
|
||||
texture.GetExternalIndex(), handle.Slot, handle.Gen);
|
||||
return;
|
||||
}
|
||||
latched.HasParamsLatch = true;
|
||||
latched.ParamsVersion = paramsVersion;
|
||||
latched.SamplerVersion = samplerVersion;
|
||||
latched.ForceParamsResync = false;
|
||||
}
|
||||
|
||||
void EmitRenderbufferCreate(RenderbufferObject& renderbuffer) {
|
||||
@@ -713,7 +881,8 @@ namespace MobileGL::MG_Pipe {
|
||||
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, createDesc);
|
||||
}
|
||||
NoteDesc(desc, /*isCreate=*/false);
|
||||
Bool accepted = ApplyRespecify(desc);
|
||||
// A renderbuffer's storage is always the whole object: no levels, so no key.
|
||||
Bool accepted = ApplyRespecify(desc, nullptr);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
if (!accepted) {
|
||||
// See the texture twin: the applier's refusal is the only thing that can
|
||||
@@ -722,7 +891,7 @@ namespace MobileGL::MG_Pipe {
|
||||
renderbuffer, handle, entry.BindMask, /*storageDefined=*/false);
|
||||
NoteDesc(healDesc, /*isCreate=*/true);
|
||||
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, healDesc);
|
||||
accepted = ApplyRespecify(desc);
|
||||
accepted = ApplyRespecify(desc, nullptr);
|
||||
}
|
||||
}
|
||||
NoteRespecified(entry, desc, accepted);
|
||||
@@ -821,11 +990,16 @@ namespace MobileGL::MG_Pipe {
|
||||
// Records the applier REFUSED. The dirty flag survives one of these, which is the whole
|
||||
// of D-D5 step 1 - so a case that wants to prove the flag survived asserts on this.
|
||||
Uint64 RefusedSubDataCount() const { return m_refusedSubDatas; }
|
||||
// set_texture_params records the applier refused; the latch survives one of these (m-1).
|
||||
Uint64 RefusedParamCount() const { return m_refusedParamSets; }
|
||||
// What create_sampler_state put on the wire on this emitter's behalf, so the csob-blob
|
||||
// accounting does not under-report 100 bytes per built-in sampler mint. set_texture_params
|
||||
// itself returns no byte count - it is not emitted from the validate point's payload
|
||||
// histogram - so this is where the cache's answer lands.
|
||||
Uint64 SamplerCsoPayloadBytes() const { return m_samplerCsoPayloadBytes; }
|
||||
// Dead handles that still held an object when resolved: a death path that skipped the
|
||||
// emitter. 0 on a healthy tree; a case that drives every death path asserts it.
|
||||
Uint64 DeadResolveCount() const { return m_deadResolves; }
|
||||
MGPipeHandle BuiltinSamplerOf(MGPipeHandle handle) const {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return kMGPipeNullHandle;
|
||||
@@ -847,7 +1021,9 @@ namespace MobileGL::MG_Pipe {
|
||||
void ResetCounters() {
|
||||
m_creates = m_respecifies = m_paramSets = m_subDatas = 0;
|
||||
m_refusedSubDatas = 0;
|
||||
m_refusedParamSets = 0;
|
||||
m_samplerCsoPayloadBytes = 0;
|
||||
m_deadResolves = 0;
|
||||
}
|
||||
|
||||
// A unit fixture's per-case reset; the library never calls it. See
|
||||
@@ -915,7 +1091,8 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
static Uint16 MaskOf(const Vector<Entry>& table, MGPipeHandle handle) {
|
||||
const SizeT slot = handle.Slot;
|
||||
return slot < table.size() ? table[slot].BindMask : Uint16{0};
|
||||
if (slot >= table.size() || table[slot].Gen != handle.Gen) return Uint16{0};
|
||||
return table[slot].BindMask;
|
||||
}
|
||||
// A SLOT THE ALLOCATOR HAS HANDED OUT AGAIN CARRIES ITS PREDECESSOR'S ENTRY, and every
|
||||
// field in it is a lie about the new object (m4). The sticky BindMask is the one that
|
||||
@@ -924,10 +1101,10 @@ namespace MobileGL::MG_Pipe {
|
||||
// the dead one's mask and its first descriptor said so. The generation is what
|
||||
// distinguishes them and the reset is here because AcquireTexture is the one door.
|
||||
//
|
||||
// IT IS ALSO THE ONLY MOMENT THIS PACKAGE CAN SEE A TEXTURE DIE. The death helper is
|
||||
// A's (MGPipeEmitTextureDestroyAndFree) and does not forward to this emitter, so the
|
||||
// built-in sampler's cache reference is dropped here - bounded by the number of live
|
||||
// texture slots rather than unbounded, which is the shape ID-17 rule 3 names.
|
||||
// IT IS THE BELT, NOT THE PATH (final review C-2): the death helper forwards to
|
||||
// NoteTextureDied, which retires the entry - drain entries, cache reference, latches,
|
||||
// mask - at the death itself. This stays for a slot whose death this emitter was never
|
||||
// told about, and drops the same reference if one is still standing.
|
||||
void RetireIfRecycled(Entry& entry, MGPipeHandle handle) {
|
||||
if (entry.Gen == handle.Gen) return;
|
||||
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
|
||||
@@ -952,13 +1129,43 @@ namespace MobileGL::MG_Pipe {
|
||||
entry.HasLastDesc = true;
|
||||
}
|
||||
|
||||
static Bool ApplyRespecify(const MGPResourceDesc& desc) {
|
||||
// `level` is null for the whole resource and a key for exactly one level; every caller
|
||||
// says which (final review C-1), and RepublishMask's null is deliberate - a mask move
|
||||
// replaces no storage at all.
|
||||
static Bool ApplyRespecify(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
return MGPipeApplyResourceRespecify(desc, nullptr);
|
||||
return MGPipeApplyResourceRespecify(desc, nullptr, level);
|
||||
}
|
||||
(void)level;
|
||||
return false;
|
||||
}
|
||||
|
||||
// One respecify with one key, and the refusal self-heal beside it. THE SECOND HALF OF
|
||||
// THE SELF-HEAL, and the publication latch cannot give it: the latch answers "did a
|
||||
// create for this handle GO OUT", which stays true after
|
||||
// MGPipeApplierReleaseObjectRecords has dropped every object record - the scope a
|
||||
// served context's teardown takes while the frontend objects live on in the share
|
||||
// group. The applier's REFUSAL is the only signal that says "I hold nothing for this
|
||||
// handle", and the acceptance return is what makes it visible from here at all. One
|
||||
// retry, never a loop: a descriptor the applier refuses on its own merits (a target
|
||||
// that names no resource kind) is refused again and the flags stay set.
|
||||
Bool RespecifyOnce(ITextureObject& texture, MGPipeHandle handle, Entry& entry, const MGPResourceDesc& desc,
|
||||
const MGPRespecifiedLevel* key, MGPipeHandle viewOf, MGPipeHandle bufferHandle,
|
||||
Uint64 bufOffset, Uint64 bufSize) {
|
||||
Bool accepted = ApplyRespecify(desc, key);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
if (!accepted) {
|
||||
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
|
||||
texture, handle, entry.BindMask, /*storageDefined=*/false, viewOf, bufferHandle,
|
||||
bufOffset, bufSize);
|
||||
NoteDesc(healDesc, /*isCreate=*/true);
|
||||
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
|
||||
accepted = ApplyRespecify(desc, key);
|
||||
}
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
static void NoteRespecified(Entry& entry, const MGPResourceDesc& desc, Bool accepted) {
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
if (!accepted) return;
|
||||
@@ -982,7 +1189,10 @@ namespace MobileGL::MG_Pipe {
|
||||
desc.ImageBindableHint = (entry.BindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
|
||||
if (std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0) return;
|
||||
NoteDesc(desc, /*isCreate=*/false);
|
||||
NoteRespecified(entry, desc, ApplyRespecify(desc));
|
||||
// A NULL LEVEL, DELIBERATELY (wire-v3 §5 item 6): a mask move replaces no storage,
|
||||
// and the applier classifies the identical storage fields as a metadata update
|
||||
// that drops nothing. A key here would name a level this call did not touch.
|
||||
NoteRespecified(entry, desc, ApplyRespecify(desc, nullptr));
|
||||
}
|
||||
|
||||
void NoteDesc(const MGPResourceDesc& desc, Bool isCreate) {
|
||||
@@ -1113,7 +1323,9 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 m_paramSets = 0;
|
||||
Uint64 m_subDatas = 0;
|
||||
Uint64 m_refusedSubDatas = 0;
|
||||
Uint64 m_refusedParamSets = 0;
|
||||
Uint64 m_samplerCsoPayloadBytes = 0;
|
||||
mutable Uint64 m_deadResolves = 0;
|
||||
};
|
||||
|
||||
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
|
||||
@@ -1148,7 +1360,10 @@ namespace MobileGL::MG_Pipe {
|
||||
// keeping the inline definition here would not have compiled at all;
|
||||
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
|
||||
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
|
||||
// resource_destroy themselves, so the destructors call ONE helper and not two;
|
||||
// resource_destroy themselves, so the destructors call ONE helper and not two - and
|
||||
// since the final review's C-2 the helper then forwards to NoteTextureDied /
|
||||
// NoteRenderbufferDied above, so no ITextureObject* survives its object in this table
|
||||
// and no dead level survives on the drain list;
|
||||
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
|
||||
// PublishCreate above and read by those helpers.
|
||||
//
|
||||
|
||||
@@ -55,6 +55,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Harness/PipeSlotPeek.cpp
|
||||
Harness/PipeApplyPeek.cpp
|
||||
Harness/P4aSeamPeek.cpp
|
||||
Harness/P4aFinalFixPeek.cpp
|
||||
Scenarios/OrientationScenario.cpp
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
@@ -139,6 +140,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/TextureUploadShapeScenario.cpp
|
||||
Scenarios/ObjectSubsystemControlScenario.cpp
|
||||
Scenarios/P4aSeamAuditScenario.cpp
|
||||
Scenarios/P4aFinalFixScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -822,6 +824,36 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# P4a final review C-2: the dirty-then-delete case, with the allocator SCRIBBLING every freed
|
||||
# block. The defect this pins was a client emitter resolving a dead-but-not-recycled texture
|
||||
# handle to the freed ITextureObject* and calling a virtual on it from the next verb's drain;
|
||||
# whether that reads the object's ghost or faults depends on what the allocator did with the
|
||||
# block, so the ambient registrations above run the case as the application would see it and
|
||||
# these two run it with MALLOC_PERTURB_ set, where a resolved-but-dead pointer faults rather
|
||||
# than passes. Both backends: the death path is backend-neutral by ruling (ID-8).
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_MALLOC_PERTURB_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MALLOC_PERTURB_=165" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_MALLOC_PERTURB_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MALLOC_PERTURB_=165" ${MGL_ITEST_VULKAN_ENV})
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.MallocPerturb."
|
||||
TEST_FILTER "P4aFinalFixScenario.ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_MALLOC_PERTURB_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.MallocPerturb."
|
||||
TEST_FILTER "P4aFinalFixScenario.ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_MALLOC_PERTURB_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||
#
|
||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aFinalFixPeek.cpp
|
||||
// Copyright (c) 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 "P4aFinalFixPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#define MGITEST_P4A_FINALFIX_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_P4A_FINALFIX_PEEK_LIVE)
|
||||
namespace {
|
||||
namespace MGP = MobileGL::MG_Pipe;
|
||||
} // namespace
|
||||
|
||||
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||
// Slot 0 is the reserved null slot; the walk is the same shape PipeApplyPeek.cpp's
|
||||
// params reading takes. A GL name is never an identity on the wire, which is exactly
|
||||
// why it is the right key for a harness that starts from the application's view.
|
||||
for (MobileGL::SizeT slot = 1; slot < applier.TextureResources.size(); ++slot) {
|
||||
const MGP::MGPipeResourceRecord& record = applier.TextureResources[slot];
|
||||
if (!record.Live) continue;
|
||||
if (record.Desc.GlNameForDiag != static_cast<MobileGL::Uint32>(glTextureName)) continue;
|
||||
out->Slot = static_cast<unsigned>(slot);
|
||||
out->Gen = static_cast<unsigned>(record.Gen);
|
||||
out->Serial = static_cast<unsigned long long>(record.Serial);
|
||||
out->BindMask = static_cast<unsigned>(record.Desc.BindMask);
|
||||
out->ImageBindableHint = static_cast<unsigned>(record.Desc.ImageBindableHint);
|
||||
out->Levels = static_cast<unsigned>(record.Desc.Levels);
|
||||
out->PendingUploads = static_cast<unsigned>(record.PendingUploads.size());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out) {
|
||||
if (out == nullptr) return false;
|
||||
namespace Stats = MobileGL::MG_Util::PipeStats;
|
||||
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
|
||||
*out = static_cast<unsigned long long>(Stats::TotalCalls(Stats::CallClass::TextureRemintPulls));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekPipeStatsTextureUploadEmissions(unsigned long long* out) {
|
||||
if (out == nullptr) return false;
|
||||
namespace Stats = MobileGL::MG_Util::PipeStats;
|
||||
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
|
||||
*out = static_cast<unsigned long long>(Stats::TotalCalls(Stats::CallClass::TextureUploadEmissions));
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool PeekPipeTextureResourceRecord(unsigned, PipeTextureResourceRecordPeek*) { return false; }
|
||||
bool PeekPipeStatsTextureRemintPulls(unsigned long long*) { return false; }
|
||||
bool PeekPipeStatsTextureUploadEmissions(unsigned long long*) { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,41 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aFinalFixPeek.h
|
||||
// Copyright (c) 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
|
||||
//
|
||||
// The white-box readings P4aFinalFixScenario.cpp takes, in a translation unit of their own for
|
||||
// P4aSeamPeek.h's reason: a scenario TU includes the GL prototype headers and cannot include
|
||||
// MG_Pipe/PipeApply.h or the Espryt managers beside them, and PipeApplyPeek.cpp is the gates
|
||||
// package's file. Every entry point answers false where the reading cannot be taken (a pull
|
||||
// build, Android, or an applier that holds no record for the name), and a false teaches the
|
||||
// caller nothing - the case declines that half by name and keeps its public-GL verdict.
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// The applier's resource record for a texture, found by its GL name (GlNameForDiag - a
|
||||
// diagnostics-only field, which is exactly what a test harness is).
|
||||
struct PipeTextureResourceRecordPeek {
|
||||
unsigned Slot;
|
||||
unsigned Gen;
|
||||
unsigned long long Serial;
|
||||
unsigned BindMask;
|
||||
unsigned ImageBindableHint;
|
||||
unsigned Levels;
|
||||
unsigned PendingUploads;
|
||||
};
|
||||
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out);
|
||||
|
||||
// The process-wide texture-remint pull count (PipeStats "tex-remint-pulls", `trp=` on the
|
||||
// summary line; ROADMAP open question 2). Arms the PipeStats counters for this process on
|
||||
// the first call, which is what lets a case read the number without a stats-enabled lane.
|
||||
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out);
|
||||
// Espryt's count of texture uploads it actually issued (PipeStats "tex-upload-emissions"):
|
||||
// what tells a CONSUMED pending upload apart from a DROPPED one, since the record's set is
|
||||
// empty either way. Arms the counters the same way.
|
||||
bool PeekPipeStatsTextureUploadEmissions(unsigned long long* out);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,658 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/P4aFinalFixScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - THE THREE FINDINGS OF THE P4a FINAL WHOLE-DIFF REVIEW (final-review-v1.md C-1, C-2,
|
||||
// M-A), each pinned by the public-GL sequence that was red on the tree the review read and is
|
||||
// green with its fix. Every sequence here is legal GL and none of the 80-odd scenarios before
|
||||
// this file drove it, which is how two criticals shipped through a green gate.
|
||||
//
|
||||
// C-1 The client never passed the applier the LEVEL a respecify redefines, so every per-level
|
||||
// glTexImage*D / glGenerateMipmap grow took the applier's whole-resource arm and dropped
|
||||
// EVERY pending upload of the texture - including a level the applier had already
|
||||
// accepted and whose client-side dirty flag was therefore already clear (D-D5 step 1).
|
||||
// Nobody owed those texels any more. The window is "accepted but not yet consumed":
|
||||
// a verb the texture is not reached by (a draw with another texture) drains the level
|
||||
// into the applier, Espryt does not sync the texture, and the next level definition eats
|
||||
// the entry. Two hazard cases (a level-1 definition, a glGenerateMipmap) read a black
|
||||
// level 0 on the handle arm; the three controls beside them (no verb between, level 0
|
||||
// consumed first, an immediate generate) are red on every arm, which is what pins the
|
||||
// window rather than the mip path.
|
||||
// C-2 A dead-but-not-recycled texture handle still resolved to the freed ITextureObject*
|
||||
// inside the client's drain: the death helper freed the slot without telling the emitter,
|
||||
// the drain list kept the level, and the next verb's drain called virtual
|
||||
// GetStorageType() on freed memory - `glTexImage2D; glDeleteTextures; <any verb>` was a
|
||||
// SIGABRT ("pure virtual method called") at the shipping default mask. The same
|
||||
// delete-then-use shape is driven for every kind P4a mints (renderbuffer, sampler object,
|
||||
// program, framebuffer) and for a slot recycled straight after the death (ABA), on both
|
||||
// backends: the death path is backend-neutral by ruling (ID-8) and the DirectVulkan lane
|
||||
// must see it too.
|
||||
// M-A Nothing produced kMGPipeBindSampler / kMGPipeBindShaderImage, so ImageBindableHint was
|
||||
// dead: the applier never saw a texture become image-bound, the metadata respecify
|
||||
// (ID-18 M4) had no live trigger, and the remint pull the hint exists to prevent was
|
||||
// neither prevented nor counted. The case here reads the applier's record around a
|
||||
// glBindImageTexture: the hint arrives as a metadata update that keeps the pending upload
|
||||
// standing beside it, and the picture after the transition is the texels that upload
|
||||
// carried.
|
||||
//
|
||||
// A WHITE-BOX READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES with its
|
||||
// public-GL half (P4aSeamAuditScenario.cpp's shape): a pull build or a backend with no P4a
|
||||
// consumer holds no record to read, and skipping the whole case there would delete the verdict
|
||||
// those lanes carry. The C-1 and M-A cases assert their pictures on DirectGLES only - Espryt is
|
||||
// the one consumer of the texture records this phase wires, so on any other backend the handle
|
||||
// arm is inert by design and the picture proves nothing about it.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/P4aFinalFixPeek.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kInset = 2;
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
out vec2 vUv;
|
||||
void main() {
|
||||
vUv = aPos * 0.5 + 0.5;
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uTex;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = texture(uTex, vUv); }
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
};
|
||||
|
||||
class P4aFinalFixScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
|
||||
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
|
||||
glGenBuffers(1, &m_quadBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||
glBindVertexArray(0);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// The "other" texture: a complete, single-level white texture, so a draw that
|
||||
// samples it is a verb the texture under test is not reached by.
|
||||
m_other = MakeLevel0(255, 255, 255, /*maxLevel=*/0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_other != 0) glDeleteTextures(1, &m_other);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// The C-1 and M-A pictures are about Espryt's consumption of the texture records;
|
||||
// Magma registers no consumer for the P4a families (c0f), so the handle arm is inert
|
||||
// there by design and a green picture proves nothing about the finding. Marks the
|
||||
// case skipped; the caller tests IsSkipped() and returns.
|
||||
void SkipUnlessEspryt(const char* what) {
|
||||
if (Gl().BackendName() == "DirectGLES") return;
|
||||
GTEST_SKIP() << what << " is consumed by DirectGLES only; backend is " << Gl().BackendName();
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> Solid(int size, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(size) * size * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i] = r;
|
||||
texels[i + 1] = g;
|
||||
texels[i + 2] = b;
|
||||
texels[i + 3] = 255;
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
// A 4x4 level 0 of one colour, NEAREST_MIPMAP_NEAREST with the level range clamped
|
||||
// to `maxLevel`, so a single-level texture is complete and a chain is complete once
|
||||
// its levels exist.
|
||||
static GLuint MakeLevel0(std::uint8_t r, std::uint8_t g, std::uint8_t b, int maxLevel, int size = 4) {
|
||||
const std::vector<std::uint8_t> texels = Solid(size, r, g, b);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static void DefineLevel1(GLuint texture, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
const std::vector<std::uint8_t> texels = Solid(2, r, g, b);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// A full-viewport draw sampling `texture` on unit 0 through `program` (the fixture's
|
||||
// by default). The viewport is far larger than the 4x4 base level, so this is
|
||||
// MAGNIFICATION and reads LEVEL 0 whatever the chain holds above it.
|
||||
Image DrawSampled(GLuint texture, GLuint program = 0) {
|
||||
if (program == 0) program = m_program;
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glUseProgram(program);
|
||||
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindVertexArray(0);
|
||||
Gl().EndFrame();
|
||||
return image;
|
||||
}
|
||||
|
||||
::testing::AssertionResult Mostly(const Image& image, const char* color, const std::string& when) {
|
||||
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset, image.Height() - kInset, color,
|
||||
0.0, when);
|
||||
}
|
||||
|
||||
void Report(const char* caseName, const Image& image) {
|
||||
const char* mask = std::getenv("MOBILEGL_PIPE_PUSH");
|
||||
const int cx = image.Width() / 2;
|
||||
const int cy = image.Height() / 2;
|
||||
std::cout << "[ P4aFinalFix ] case=" << caseName << " backend=" << Gl().BackendName()
|
||||
<< " MOBILEGL_PIPE_PUSH=" << (mask ? mask : "(unset)") << " centre=" << image.At(cx, cy)
|
||||
<< " (" << image.ColorName(cx, cy) << ")" << std::endl;
|
||||
}
|
||||
|
||||
// The white-box gate of the M-A case: true when the applier holds a record for the
|
||||
// texture in this process. Prints the decline.
|
||||
bool RecordIsReadable(unsigned glTextureName, const char* what, PipeTextureResourceRecordPeek* out) {
|
||||
if (PeekPipeTextureResourceRecord(glTextureName, out)) return true;
|
||||
std::cout << "[ P4aFinalFix ] white-box reading DECLINED for " << what
|
||||
<< ": the applier holds no record for texture " << glTextureName
|
||||
<< " (a pull build, or a backend with no P4a consumer); the public-GL half of "
|
||||
"the case still runs"
|
||||
<< std::endl;
|
||||
RecordProperty("p4a_finalfix_white_box", "declined");
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_quadBuffer = 0;
|
||||
GLuint m_other = 0;
|
||||
};
|
||||
|
||||
// ======================================================================================
|
||||
// C-1: a per-level definition around a verb the texture is not reached by
|
||||
// ======================================================================================
|
||||
|
||||
// THE HAZARD. L0's upload is accepted at the unrelated draw's validate point (the client
|
||||
// clears its flag), Espryt never syncs T there (it is bound nowhere), then the level-1
|
||||
// definition respecifies the resource. Before the fix that respecify carried no level and
|
||||
// the applier dropped every pending upload; level 0 was allocated undefined.
|
||||
TEST_F(P4aFinalFixScenario, PerLevelDefinitionAcrossAnUnrelatedDraw) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("C-1's per-level respecify");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||
const Image unrelated = DrawSampled(m_other);
|
||||
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
|
||||
DefineLevel1(texture, 255, 0, 0);
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("PerLevelDefinitionAcrossAnUnrelatedDraw", image);
|
||||
EXPECT_TRUE(Mostly(image, "red",
|
||||
"level 0 after a level-1 definition that followed a draw the texture was not "
|
||||
"reached by - its accepted-but-unconsumed upload was dropped by the whole-"
|
||||
"resource arm"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// CONTROL: both levels defined before any verb; both are pending at the first sync.
|
||||
TEST_F(P4aFinalFixScenario, ConsecutiveDefinitionsNoVerbBetween) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("C-1's per-level respecify");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||
DefineLevel1(texture, 255, 0, 0);
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("ConsecutiveDefinitionsNoVerbBetween", image);
|
||||
EXPECT_TRUE(Mostly(image, "red", "level 0 with both levels defined back to back"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// CONTROL: level 0 is consumed by Espryt (T is sampled) before level 1 is defined.
|
||||
TEST_F(P4aFinalFixScenario, LevelZeroConsumedBeforeLevelOne) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("C-1's per-level respecify");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||
const Image first = DrawSampled(texture);
|
||||
EXPECT_TRUE(Mostly(first, "red", "level 0 alone"));
|
||||
DefineLevel1(texture, 255, 0, 0);
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("LevelZeroConsumedBeforeLevelOne", image);
|
||||
EXPECT_TRUE(Mostly(image, "red", "level 0 after level 1 was added to a synced texture"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// THE HAZARD, glGenerateMipmap flavour: the frontend grows the level chain (one
|
||||
// AllocateStorage -> respecify per level) BEFORE the backend generate runs, with level 0
|
||||
// accepted-but-unconsumed. The driver then built the chain from an undefined level 0.
|
||||
TEST_F(P4aFinalFixScenario, GenerateMipmapAcrossAnUnrelatedDraw) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("C-1's per-level respecify");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/1000);
|
||||
const Image unrelated = DrawSampled(m_other);
|
||||
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("GenerateMipmapAcrossAnUnrelatedDraw", image);
|
||||
EXPECT_TRUE(Mostly(image, "red",
|
||||
"level 0 after a glGenerateMipmap that followed a draw the texture was not "
|
||||
"reached by"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// CONTROL for the generate: no verb between the upload and the generate.
|
||||
TEST_F(P4aFinalFixScenario, GenerateMipmapImmediately) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("C-1's per-level respecify");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/1000);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("GenerateMipmapImmediately", image);
|
||||
EXPECT_TRUE(Mostly(image, "red", "level 0 after an immediate glGenerateMipmap"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// ======================================================================================
|
||||
// C-2: delete-then-use, for every kind P4a mints, on both backends
|
||||
// ======================================================================================
|
||||
|
||||
// A level goes dirty, the texture dies before any verb, and the next verb's drain walks
|
||||
// the entry. Before the fix the emitter resolved the dead handle to the freed object and
|
||||
// the drain called a virtual on it: SIGABRT in the first round. Eight rounds, and the
|
||||
// lane registered with MALLOC_PERTURB_ scribbles every freed block so a resolved-but-
|
||||
// dead pointer faults rather than reads the object's ghost.
|
||||
TEST_F(P4aFinalFixScenario, ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain) {
|
||||
if (!Ready()) return;
|
||||
for (int round = 0; round < 8; ++round) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, &texture); // the last reference: the frontend object dies here
|
||||
// Something else is allocated between the death and the drain, so the freed
|
||||
// storage is not simply re-handed to the next object.
|
||||
std::vector<std::uint8_t> churn(4096 + round * 1024, static_cast<std::uint8_t>(round));
|
||||
(void)churn;
|
||||
const Image image = DrawSampled(m_other); // the validate point: the drain runs here
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
if (round == 0) Report("ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain", image);
|
||||
EXPECT_TRUE(Mostly(image, "white", "the draw after a dirty texture died"));
|
||||
}
|
||||
}
|
||||
|
||||
// ABA: the slot the dead texture held is handed straight to the next texture (the free
|
||||
// list is LIFO). The new texture's picture must be its own, and the dead one's drain
|
||||
// entry must not be replayed onto it.
|
||||
TEST_F(P4aFinalFixScenario, ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry) {
|
||||
if (!Ready()) return;
|
||||
{
|
||||
GLuint dead = 0;
|
||||
glGenTextures(1, &dead);
|
||||
glBindTexture(GL_TEXTURE_2D, dead);
|
||||
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, &dead); // dirty, dead, no verb between
|
||||
}
|
||||
const GLuint successor = MakeLevel0(0, 0, 255, /*maxLevel=*/0, /*size=*/8);
|
||||
const Image image = DrawSampled(successor);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry", image);
|
||||
EXPECT_TRUE(Mostly(image, "blue", "the successor of a dead dirty texture on the recycled slot"));
|
||||
const Image other = DrawSampled(m_other);
|
||||
EXPECT_TRUE(Mostly(other, "white", "an unrelated draw after the recycled slot was used"));
|
||||
GLuint cleanup = successor;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// A renderbuffer with defined storage, attached, cleared through its framebuffer, then
|
||||
// both die before the next verb.
|
||||
TEST_F(P4aFinalFixScenario, ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact) {
|
||||
if (!Ready()) return;
|
||||
GLuint renderbuffer = 0;
|
||||
glGenRenderbuffers(1, &renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 8, 8);
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, 8, 8);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const Image cleared = ReadPixels(8, 8);
|
||||
EXPECT_TRUE(RegionIsMostly(cleared, 0, 8, 0, 8, "green", 0.0, "the renderbuffer after the clear"));
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &renderbuffer); // the attachment's last reference went with the FBO
|
||||
const Image image = DrawSampled(m_other);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact", image);
|
||||
EXPECT_TRUE(Mostly(image, "white", "the draw after a renderbuffer and its framebuffer died"));
|
||||
}
|
||||
|
||||
// A sampler object bound to the unit the draw samples through, deleted while bound: GL
|
||||
// unbinds it from every unit at glDeleteSamplers, and the texture's own parameters apply
|
||||
// again. Both draws must be the texture's colour.
|
||||
TEST_F(P4aFinalFixScenario, ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||
GLuint sampler = 0;
|
||||
glGenSamplers(1, &sampler);
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindSampler(0, sampler);
|
||||
const Image withSampler = DrawSampled(texture);
|
||||
EXPECT_TRUE(Mostly(withSampler, "red", "the draw through the bound sampler object"));
|
||||
glDeleteSamplers(1, &sampler); // bound: unbound by the delete, then dies
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact", image);
|
||||
EXPECT_TRUE(Mostly(image, "red", "the draw after the bound sampler object died"));
|
||||
glBindSampler(0, 0);
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// A second program, in use when it is deleted (GL keeps it alive until it is no longer
|
||||
// current), then released by a glUseProgram of the fixture's program: it dies there, and
|
||||
// the draw that follows runs through the survivor.
|
||||
TEST_F(P4aFinalFixScenario, AProgramDeletedWhileInUseLeavesTheNextDrawIntact) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
const GLuint second = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(second, 0u) << error;
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||
const Image throughSecond = DrawSampled(texture, second);
|
||||
EXPECT_TRUE(Mostly(throughSecond, "red", "the draw through the second program"));
|
||||
glDeleteProgram(second); // current: flagged for deletion, still very much alive
|
||||
const Image stillCurrent = DrawSampled(texture, second);
|
||||
EXPECT_TRUE(Mostly(stillCurrent, "red", "the draw through a program flagged for deletion"));
|
||||
const Image image = DrawSampled(texture); // glUseProgram(m_program): the second dies here
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("AProgramDeletedWhileInUseLeavesTheNextDrawIntact", image);
|
||||
EXPECT_TRUE(Mostly(image, "red", "the draw after the deleted program was released"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// A framebuffer handed to the server BY NAME (a DSA clear emits a Named record, ID-19(c))
|
||||
// and deleted before the next verb; its attachment lives on and carries the clear.
|
||||
TEST_F(P4aFinalFixScenario, AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||
glClearNamedFramebufferfv(fbo, GL_COLOR, 0, green);
|
||||
glDeleteFramebuffers(1, &fbo); // unbound and named: dies here
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact", image);
|
||||
EXPECT_TRUE(Mostly(image, "green", "the attachment of a framebuffer that died after a DSA clear"));
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// ======================================================================================
|
||||
// M-A: an image bind after the allocation is a metadata respecify with the hint set
|
||||
// ======================================================================================
|
||||
|
||||
// glTexStorage2D (immutable: no later respecify to ride), a red upload consumed by a draw,
|
||||
// then a blue upload drained by a verb the texture is not reached by (accepted, standing
|
||||
// in the applier's pending set), then glBindImageTexture. The bind must reach the record
|
||||
// as a metadata update - ImageBindableHint 1, the pending upload still standing - and the
|
||||
// draw after it must show the blue that upload carried through the widened carrier the
|
||||
// hint schedules.
|
||||
TEST_F(P4aFinalFixScenario, AnImageBindAfterAllocationReachesTheApplierAsAMetadataRespecify) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("M-A's image-bindable hint");
|
||||
if (IsSkipped()) return;
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxImageUnits < 1) {
|
||||
GTEST_SKIP() << "no image units";
|
||||
return;
|
||||
}
|
||||
|
||||
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR: a texture Espryt allocated BEFORE the
|
||||
// hint reached it is re-minted image-bindable at the bind and its levels replayed
|
||||
// from the client's shadow - one remint pull, counted. Arming the counter here is
|
||||
// what makes it readable without a stats-enabled lane.
|
||||
unsigned long long pullsBefore = 0;
|
||||
const bool pullsReadable = PeekPipeStatsTextureRemintPulls(&pullsBefore);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||
const std::vector<std::uint8_t> red = Solid(4, 255, 0, 0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const Image before = DrawSampled(texture); // allocated and consumed, NOT image-bindable
|
||||
EXPECT_TRUE(Mostly(before, "red", "the immutable texture before the image bind"));
|
||||
|
||||
PipeTextureResourceRecordPeek record{};
|
||||
const bool readable = RecordIsReadable(texture, "M-A's image-bindable hint", &record);
|
||||
if (readable) {
|
||||
EXPECT_EQ(record.ImageBindableHint, 0u) << "nothing has image-bound this texture yet";
|
||||
EXPECT_EQ(record.PendingUploads, 0u) << "the red upload was consumed by the draw";
|
||||
}
|
||||
|
||||
// A blue upload, drained by a verb that does not reach T: accepted, unconsumed.
|
||||
const std::vector<std::uint8_t> blue = Solid(4, 0, 0, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const Image unrelated = DrawSampled(m_other);
|
||||
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
|
||||
if (readable) {
|
||||
ASSERT_TRUE(PeekPipeTextureResourceRecord(texture, &record));
|
||||
EXPECT_EQ(record.PendingUploads, 1u) << "the blue upload was not drained into the applier";
|
||||
}
|
||||
const unsigned long long serialBeforeBind = record.Serial;
|
||||
unsigned long long uploadsBeforeBind = 0;
|
||||
const bool uploadsReadable = PeekPipeStatsTextureUploadEmissions(&uploadsBeforeBind);
|
||||
|
||||
// THE TRANSITION. An immutable texture has no storage-defining respecify left, so the
|
||||
// hint can only arrive as a metadata update (ID-18 M4). Espryt syncs the texture
|
||||
// eagerly inside glBindImageTexture and the widening re-mints its storage, replaying
|
||||
// every defined level from the shadow (the remint pull the counter below counts), so
|
||||
// the standing upload is consumed by that regeneration here and the picture that
|
||||
// follows is blue whatever the metadata respecify did to the record - the KEPT
|
||||
// property is proved further down, on a texture no remint stands in front of.
|
||||
(void)uploadsBeforeBind;
|
||||
(void)uploadsReadable;
|
||||
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
if (readable) {
|
||||
ASSERT_TRUE(PeekPipeTextureResourceRecord(texture, &record));
|
||||
EXPECT_EQ(record.ImageBindableHint, 1u)
|
||||
<< "glBindImageTexture did not reach the applier's record as ImageBindableHint";
|
||||
EXPECT_NE(record.BindMask & (1u << 6), 0u) << "kMGPipeBindShaderImage was not produced";
|
||||
EXPECT_GT(record.Serial, serialBeforeBind) << "the metadata respecify moved no serial";
|
||||
}
|
||||
|
||||
const Image image = DrawSampled(texture);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Report("AnImageBindAfterAllocationReachesTheApplierAsAMetadataRespecify", image);
|
||||
EXPECT_TRUE(Mostly(image, "blue", "the texture after the image bind that followed an unconsumed upload"));
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
unsigned long long pullsAfter = 0;
|
||||
if (pullsReadable && readable && PeekPipeStatsTextureRemintPulls(&pullsAfter)) {
|
||||
EXPECT_EQ(pullsAfter, pullsBefore + 1)
|
||||
<< "the re-mint of a texture allocated before its hint was not counted as a remint pull "
|
||||
"(trp= on the stats line is ROADMAP open question 2's number)";
|
||||
}
|
||||
|
||||
// THE PREVENTION HALF, measured the other way round: a texture whose hint arrives at
|
||||
// the bind, BEFORE its first sync, is allocated image-bindable up front and pulls
|
||||
// nothing - the counter does not move.
|
||||
GLuint early = 0;
|
||||
glGenTextures(1, &early);
|
||||
glBindTexture(GL_TEXTURE_2D, early);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindImageTexture(0, early, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); // before any sync
|
||||
const Image earlyImage = DrawSampled(early);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_TRUE(Mostly(earlyImage, "red", "a texture image-bound before its first sync"));
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
unsigned long long pullsEarly = 0;
|
||||
if (pullsReadable && readable && PeekPipeStatsTextureRemintPulls(&pullsEarly)) {
|
||||
EXPECT_EQ(pullsEarly, pullsAfter)
|
||||
<< "a texture whose hint preceded its first sync was still re-minted (the prevention "
|
||||
"half of the hint did not fire)";
|
||||
}
|
||||
|
||||
// THE METADATA RESPECIFY KEEPS A STANDING UPLOAD, end to end and with no remint in the
|
||||
// way: `early` is image-bindable already, so a NEW sticky bit reaching it - the
|
||||
// RENDER_TARGET bit a DSA attachment produces at its setter (a Named record, ID-19(c)),
|
||||
// with no sync of the texture in between - is a pure metadata update. The blue upload
|
||||
// drained before it must still stand in the record afterwards (or, if a sync did run,
|
||||
// have been uploaded rather than dropped) and reach the driver at the next draw.
|
||||
glBindTexture(GL_TEXTURE_2D, early);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const Image unrelatedAgain = DrawSampled(m_other);
|
||||
EXPECT_TRUE(Mostly(unrelatedAgain, "white", "the unrelated draw"));
|
||||
PipeTextureResourceRecordPeek earlyRecord{};
|
||||
const bool earlyReadable = PeekPipeTextureResourceRecord(early, &earlyRecord);
|
||||
if (earlyReadable) {
|
||||
EXPECT_EQ(earlyRecord.PendingUploads, 1u) << "the blue upload was not drained into the applier";
|
||||
}
|
||||
const unsigned long long earlySerialBefore = earlyRecord.Serial;
|
||||
unsigned long long uploadsBeforeAttach = 0;
|
||||
const bool uploadsCounted = PeekPipeStatsTextureUploadEmissions(&uploadsBeforeAttach);
|
||||
GLuint namedFbo = 0;
|
||||
glCreateFramebuffers(1, &namedFbo);
|
||||
glNamedFramebufferTexture(namedFbo, GL_COLOR_ATTACHMENT0, early, 0);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
if (earlyReadable) {
|
||||
ASSERT_TRUE(PeekPipeTextureResourceRecord(early, &earlyRecord));
|
||||
EXPECT_NE(earlyRecord.BindMask & (1u << 7), 0u)
|
||||
<< "the DSA attachment did not produce kMGPipeBindRenderTarget";
|
||||
EXPECT_GT(earlyRecord.Serial, earlySerialBefore) << "the mask move reached the record as no respecify";
|
||||
unsigned long long uploadsAfterAttach = 0;
|
||||
if (earlyRecord.PendingUploads == 0 && uploadsCounted &&
|
||||
PeekPipeStatsTextureUploadEmissions(&uploadsAfterAttach)) {
|
||||
EXPECT_GT(uploadsAfterAttach, uploadsBeforeAttach)
|
||||
<< "the standing upload vanished from the record without Espryt uploading anything: "
|
||||
"the metadata respecify dropped it";
|
||||
} else {
|
||||
EXPECT_EQ(earlyRecord.PendingUploads, 1u)
|
||||
<< "the metadata respecify dropped the pending upload standing beside it";
|
||||
}
|
||||
}
|
||||
const Image earlyAfter = DrawSampled(early);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_TRUE(Mostly(earlyAfter, "blue", "the upload that stood across a metadata respecify"));
|
||||
glDeleteFramebuffers(1, &namedFbo);
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
GLuint cleanupEarly = early;
|
||||
glDeleteTextures(1, &cleanupEarly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -1100,14 +1100,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// answers the per-record question, but a metadata update allocates nothing, so a
|
||||
// record it classifies as metadata is not acked even when that predicate says the
|
||||
// call may require one.
|
||||
// - NO PendingUploads clear - not the whole vector, and not the redefined level either.
|
||||
// This REFINES the level-scoped clear: identical storage fields clear NOTHING. (The
|
||||
// - NO PendingUploads clear when the call names NO level. This REFINES the whole-resource
|
||||
// clear: identical storage fields with a null MGPRespecifiedLevel clear NOTHING. (The
|
||||
// level-scoped rule exists because clearing the whole vector on a level-1 definition
|
||||
// silently dropped level 0's accepted texels; a metadata update must drop neither.)
|
||||
// - The stored descriptor's BindMask and ImageBindableHint ARE updated - BindMask is
|
||||
// sticky and therefore ORed, never replaced - and the twin re-derives its storage
|
||||
// flags from the new mask on its next sync, recreating backend storage only where the
|
||||
// backend actually needs it. The record itself is not a request to recreate.
|
||||
// silently dropped level 0's accepted texels; a metadata update must drop neither.) A
|
||||
// call that NAMES a level is that level's redefinition whatever the descriptor says -
|
||||
// a non-base level's extent is not a descriptor field - and drops exactly that level
|
||||
// (P4a final review C-1); the client's mask republish passes null on purpose.
|
||||
// - The stored descriptor's BindMask and ImageBindableHint ARE updated: the applier
|
||||
// replaces the descriptor WHOLE with the one the client sent (PipeApply.cpp), and the
|
||||
// mask in it is the CLIENT's sticky OR (TextureEmit.h's entry, never cleared), so the
|
||||
// replacement can never lose a bit the record once carried. The twin re-derives its
|
||||
// storage flags from the new mask on its next sync, recreating backend storage only
|
||||
// where the backend actually needs it. The record itself is not a request to recreate.
|
||||
//
|
||||
// THE STORAGE-DEFINING FIELD SET, named here so that neither side has to guess and a
|
||||
// later field cannot join it by silence. It is every MGPResourceDesc member except the
|
||||
|
||||
@@ -1667,23 +1667,28 @@ namespace MobileGL::MG_Pipe {
|
||||
// the arm this set exists for) -> glTexImage2D(1, data), which under a blanket
|
||||
// clear destroys level 0's entry before anything ever uploaded it.
|
||||
//
|
||||
// - and a METADATA update (ID-18 M4) drops NOTHING, whatever `level` says. It is the
|
||||
// third arm and it refines the first two rather than contradicting them: the rule
|
||||
// is "the uploads against the storage this call REPLACES go with it", and a call
|
||||
// whose storage-defining fields all equal the stored descriptor replaces no
|
||||
// storage, so no level's coordinate system has moved and every pending box is still
|
||||
// described in the space it was accumulated in. B re-emits the descriptor when a
|
||||
// sticky bind bit moves, which can land between a glTexSubImage2D and the sync that
|
||||
// consumes it; eating those texels there would be C1's bug with a different
|
||||
// trigger, and just as silent.
|
||||
// - and a METADATA update (ID-18 M4) with a NULL level drops NOTHING. It refines the
|
||||
// whole-resource arm rather than contradicting it: the rule is "the uploads against
|
||||
// the storage this call REPLACES go with it", and a call whose storage-defining
|
||||
// fields all equal the stored descriptor replaces no storage, so no level's
|
||||
// coordinate system has moved and every pending box is still described in the
|
||||
// space it was accumulated in. B re-emits the descriptor when a sticky bind bit
|
||||
// moves - with a null level, deliberately - which can land between a
|
||||
// glTexSubImage2D and the sync that consumes it; eating those texels there would be
|
||||
// C1's bug with a different trigger, and just as silent.
|
||||
//
|
||||
// - A NAMED LEVEL IS DROPPED WHETHER OR NOT THE DESCRIPTOR MOVED (P4a final review
|
||||
// C-1, refining wire's W11 clause). The level pointer is the CALLER's statement that
|
||||
// it reallocated that level, and the descriptor cannot contradict it: a non-base
|
||||
// level redefined at a new size moves no descriptor field at all (the descriptor
|
||||
// carries the base extent and the level count), so "identical storage fields" says
|
||||
// nothing about that level's coordinate system, and a box kept against the old
|
||||
// level would be uploaded past the end of the new one. The client's mask republish
|
||||
// passes null, so this arm can never eat a standing upload on its behalf.
|
||||
//
|
||||
// A buffer never has a pending upload at all, so all three arms are inert for P3a's
|
||||
// half - which is also why a buffer is never classified as metadata-only (below).
|
||||
if (metadataOnly) {
|
||||
// nothing to drop, deliberately.
|
||||
} else if (level == nullptr) {
|
||||
record->PendingUploads.clear();
|
||||
} else {
|
||||
if (level != nullptr) {
|
||||
// The keys are unique by AccumulatePendingUpload's construction - it looks for the
|
||||
// pair before it appends - so this erases at most one entry and stops.
|
||||
for (auto it = record->PendingUploads.begin(); it != record->PendingUploads.end(); ++it) {
|
||||
@@ -1691,6 +1696,10 @@ namespace MobileGL::MG_Pipe {
|
||||
record->PendingUploads.erase(it);
|
||||
break;
|
||||
}
|
||||
} else if (metadataOnly) {
|
||||
// nothing to drop, deliberately.
|
||||
} else {
|
||||
record->PendingUploads.clear();
|
||||
}
|
||||
|
||||
// resource_respecify is the catalogue's only kNeedsAck call, and the per-record half
|
||||
@@ -2470,11 +2479,11 @@ namespace MobileGL::MG_Pipe {
|
||||
record->Gen = gen;
|
||||
}
|
||||
|
||||
void MGPipeApplySetTextureParams(const MGPTextureParams& params) {
|
||||
Bool MGPipeApplySetTextureParams(const MGPTextureParams& params) {
|
||||
// P4a's belt, and FIRST here because this call's first act is a resolution: with no
|
||||
// consumer no texture create was accepted, so resolving would report the absence as
|
||||
// RefusedObjectCalls - the counter that means a seam defect - for the designed state.
|
||||
if (NoP4aConsumer()) return;
|
||||
if (NoP4aConsumer()) return false;
|
||||
|
||||
// ADDRESSED BY RESOURCE AND BY NOTHING ELSE, which is the whole point of the call: a
|
||||
// texture that is only an FBO attachment, only an image-unit binding or only a
|
||||
@@ -2483,7 +2492,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// moment the parameters move, whether or not anything is bound.
|
||||
MGPipeResourceRecord* record =
|
||||
ResolveObject(g_applier.TextureResources, "set_texture_params", params.Res);
|
||||
if (record == nullptr) return;
|
||||
if (record == nullptr) return false;
|
||||
|
||||
// EVERY ITextureObject OWNS A SamplerObject, so the built-in sampler CSO is not
|
||||
// optional and a null handle is not "no sampler" - it is a record that would have the
|
||||
@@ -2496,7 +2505,7 @@ namespace MobileGL::MG_Pipe {
|
||||
" set_texture_params {slot=%u, gen=%u, glName=%u}: the record names no "
|
||||
"built-in sampler CSO, and every texture object owns one",
|
||||
params.Res.Slot, params.Res.Gen, record->Desc.GlNameForDiag);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// AND THE CSO IT NAMES IS NOT RESOLVED. The sampler subsystem is its own bit and may be
|
||||
// clear while the texture bit is set, so a record that names a CSO this applier has not
|
||||
@@ -2511,6 +2520,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// bytes are CARRIED, never cleared here: the server ORs them into its own flags and
|
||||
// clears its own copy, and the client never clears a server flag.
|
||||
++record->ParamsSerial;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The three of them, and NO STAGE DIMENSION on any of them: MobileGL's texture-unit space
|
||||
|
||||
@@ -784,6 +784,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// same value the emission of that level put in the record. A per-face respecify therefore
|
||||
// drops the face it redefines and leaves the other five standing, and a caller that packs
|
||||
// the pair differently here than it packs it there simply matches nothing.
|
||||
// A NAMED LEVEL IS DROPPED EVEN WHEN EVERY STORAGE-DEFINING FIELD IS UNCHANGED (P4a final
|
||||
// review C-1): the pointer is the caller's statement that it reallocated that level, and
|
||||
// a non-base level's extent is not in the descriptor. Only a NULL level with unchanged
|
||||
// fields is the metadata update that drops nothing (ID-18 M4); the client's mask republish
|
||||
// is the one caller of that shape and passes null on purpose.
|
||||
struct MGPRespecifiedLevel {
|
||||
Uint16 UploadTarget = 0;
|
||||
Uint16 Level = 0;
|
||||
@@ -1008,7 +1013,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// glCopyImageSubData endpoint carry its parameters at all. params.BuiltinSampler may never
|
||||
// be the null handle - every ITextureObject owns a sampler object - so a null is
|
||||
// Fatal{ProtocolCorruption} rather than "no sampler".
|
||||
void MGPipeApplySetTextureParams(const MGPTextureParams& params);
|
||||
//
|
||||
// Returns true when the record took the parameters (P4a final review m-1, audit F-7): the
|
||||
// emitter's version latch advances on this answer and on nothing else, the way the
|
||||
// sub-data and respecify paths latch on theirs, so a refused record - no consumer, no
|
||||
// record for the handle, a null sampler - is re-sent at the next call rather than at the
|
||||
// next glTexParameter*. Source-compatible for the same reason the three resource returns
|
||||
// are: a Bool is ignorable and gen_pipe never parses this header.
|
||||
Bool MGPipeApplySetTextureParams(const MGPTextureParams& params);
|
||||
|
||||
// set_sampler_views / bind_sampler_states / set_shader_images: `tail` is hdr.Count entries
|
||||
// starting at hdr.Start, and hdr.Start + hdr.Count above the unit bound is
|
||||
|
||||
@@ -312,12 +312,44 @@ namespace MobileGL::MG_Pipe {
|
||||
//
|
||||
// Entry points MGPipeTextureEmitter must provide, all taking the frontend object by
|
||||
// reference and returning void:
|
||||
// EmitResourceCreate(ITextureObject&) / EmitResourceRespecify(ITextureObject&)
|
||||
// EmitResourceCreate(ITextureObject&)
|
||||
// EmitResourceRespecify(ITextureObject&, MGPipeTextureRespecifyScope, Uint32 uploadTarget,
|
||||
// Uint32 level)
|
||||
// EmitTextureParams(ITextureObject&)
|
||||
// NoteLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level)
|
||||
// EmitRenderbufferCreate(RenderbufferObject&) / EmitRenderbufferRespecify(RenderbufferObject&)
|
||||
void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// WHICH STORAGE A TEXTURE RESPECIFY REPLACES (P4a final review C-1). The applier scopes
|
||||
// its pending-upload clear on this answer and not on the descriptor, because the
|
||||
// descriptor cannot give it: AllocateStorage is per (uploadTarget, level) and
|
||||
// TruncateMipmapLevels removes every level at or above a cut, while MGPResourceDesc
|
||||
// carries only the base extent and the level count. A level the applier had ACCEPTED at
|
||||
// one verb (the client's dirty flag already clear, D-D5 step 1) and that a later per-level
|
||||
// definition redefined AROUND was dropped by the whole-resource arm with nobody owing its
|
||||
// texels - so every respecify states its scope, and "whole resource" is said, never
|
||||
// defaulted. The emitter builds wire's MGPRespecifiedLevel from the pair, packed exactly
|
||||
// as the drain packs a sub-data record's Target (MGPipePackSubDataTarget), so the key it
|
||||
// drops is the key that level's emission made.
|
||||
enum class MGPipeTextureRespecifyScope : Uint32 {
|
||||
// The whole store is redefined or restated: a format, sample-count or
|
||||
// fixed-sample-locations change, an immutable allocation completing
|
||||
// (SetImmutableLevels), a texture view's creation. Every pending upload goes.
|
||||
WholeResource = 0,
|
||||
// ONE (uploadTarget, level) was (re)allocated: glTexImage*D, glCompressedTexImage*D,
|
||||
// glCopyTexImage*D, one level of a glTexStorage* loop, one level of a generated-mipmap
|
||||
// grow. That level's pending upload goes; every other level's stays. `uploadTarget` and
|
||||
// `level` name it.
|
||||
OneLevel = 1,
|
||||
// The chain was cut: every level of `uploadTarget` at or above `level` is gone and the
|
||||
// levels below it are untouched (glGenerateMipmap fitting the chain, a base-level
|
||||
// redefinition discarding its tail, glTexStorage* fitting the chain to its level
|
||||
// count). `level` is the first level removed; a cut at 0 is the whole resource.
|
||||
LevelsFrom = 2,
|
||||
};
|
||||
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture,
|
||||
MGPipeTextureRespecifyScope scope, Uint32 uploadTarget,
|
||||
Uint32 level);
|
||||
void MGPipeEmitTextureParams(MG_State::GLState::ITextureObject& texture);
|
||||
// The DRAIN LIST's append, on a level's FIRST dirty mark, keyed on the STORAGE OWNER from
|
||||
// day one (D-D4: a view and its owner already share one dirty state, so an upload through
|
||||
@@ -329,6 +361,29 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeEmitRenderbufferResourceCreate(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
void MGPipeEmitRenderbufferResourceRespecify(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
|
||||
// ---- D-A4's two sticky bind-mask producers (P4a final review M-A) ----
|
||||
//
|
||||
// kMGPipeBindSampler is "any texture the sampler-view resolution names in an emitted
|
||||
// MGPBoundView" and kMGPipeBindShaderImage "any texture named in an emitted MGPImageView"
|
||||
// - both the SAMPLER package's emitters (SamplerEmit.h, ImageEmit.h), which the texture
|
||||
// emitter's header includes and which therefore cannot include it back - and, earliest of
|
||||
// all, glBindImageTexture's state setter (TextureState.h, MG_State), which may include no
|
||||
// emit header at all. So the note goes through this door, exactly as the birth hooks do.
|
||||
// Nothing produced either bit before the fix round: ImageBindableHint was always 0, the
|
||||
// metadata respecify (ID-18 M4) had no live trigger, and the remint pull the hint exists to
|
||||
// prevent was neither prevented nor counted.
|
||||
//
|
||||
// UNCONDITIONAL IN A PUSH BUILD, like the mints: the mask is CLIENT state the framebuffer
|
||||
// emitter ORs into whether or not the texture family is on, and the emission a mask move
|
||||
// causes (the metadata respecify) is gated inside the emitter on the family's own pair.
|
||||
void MGPipeNoteTextureBoundAs(MGPipeHandle texture, Uint32 bindBit);
|
||||
// glBindImageTexture. The hint is the PREVENTION half of the texture-remint stall class -
|
||||
// a texture the server knows may be image-bound is allocated image-bindable up front - so it
|
||||
// has to reach the applier before the texture's first sync, i.e. at the bind itself, not at
|
||||
// the validate point's image walk (which notes it as well, D-A4's letter).
|
||||
void MGPipeNoteTextureImageBound(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
|
||||
// ---- sampler CSOs and sampler views: MG_Impl/Pipe/SamplerEmit.h, package C ----
|
||||
//
|
||||
// Entry points MGPipeSamplerEmitter must provide, returning void:
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace MobileGL {
|
||||
// ---- P4a's three client emission points (see TextureObject.h) ----
|
||||
|
||||
void TextureObjectBase::PipePublishDescriptor() {
|
||||
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this);
|
||||
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this, MG_Pipe::MGPipeTextureRespecifyScope::WholeResource,
|
||||
0, 0);
|
||||
// AND THE FRAMEBUFFER AGGREGATE MOVES (P4a fable seam F-3). The resource record
|
||||
// above is only half of what a storage definition changes: set_framebuffer_state
|
||||
// INLINES an attachment's InternalFormat, TextureTarget, extent, Samples and
|
||||
@@ -83,6 +84,24 @@ namespace MobileGL {
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||
}
|
||||
|
||||
void TextureObjectBase::PipePublishLevelDescriptor(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
|
||||
// ONE level was (re)allocated: only that level's pending upload is against
|
||||
// storage that is gone (P4a final review C-1). Every other level's stays.
|
||||
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this, MG_Pipe::MGPipeTextureRespecifyScope::OneLevel,
|
||||
static_cast<Uint32>(uploadTarget),
|
||||
static_cast<Uint32>(mipmapLevel));
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment); // an attached level's extent is inlined (F-3)
|
||||
}
|
||||
|
||||
void TextureObjectBase::PipePublishTruncatedDescriptor(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
// The chain was cut at `levelCount`: the levels above the cut are gone with their
|
||||
// pending uploads, the levels below it are untouched and keep theirs.
|
||||
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this, MG_Pipe::MGPipeTextureRespecifyScope::LevelsFrom,
|
||||
static_cast<Uint32>(uploadTarget),
|
||||
static_cast<Uint32>(levelCount));
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||
}
|
||||
|
||||
void TextureObjectBase::PipePublishParams() {
|
||||
MG_Pipe::MGPipeEmitTextureParams(*this);
|
||||
}
|
||||
@@ -492,8 +511,10 @@ namespace MobileGL {
|
||||
// storage-defining GL entry point - glTexImage*, glCompressedTexImage*,
|
||||
// glTexStorage*, glTextureView and the generated-mip storage grow - reaches
|
||||
// storage through here, which is what makes the emission complete without one call
|
||||
// site per entry point in MG_Impl/GLImpl.
|
||||
PipePublishDescriptor();
|
||||
// site per entry point in MG_Impl/GLImpl. AND IT NAMES THE LEVEL (final review
|
||||
// C-1): this call replaced ONE level's storage, and only that level's pending
|
||||
// upload may go with it.
|
||||
PipePublishLevelDescriptor(uploadTarget, mipmapLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -501,7 +522,9 @@ namespace MobileGL {
|
||||
BumpShapeVersion();
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
// The levels at and above the cut are gone; the ones below keep their pending
|
||||
// uploads (final review C-1).
|
||||
PipePublishTruncatedDescriptor(uploadTarget, levelCount);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -221,11 +221,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
// members rather than free calls so the cube's, the view's and the buffer texture's
|
||||
// translation units keep calling an inherited helper.
|
||||
//
|
||||
// resource_respecify. Called from BumpShapeVersion and from the three parameter
|
||||
// resource_respecify, WHOLE-RESOURCE scope: the format setter and the three parameter
|
||||
// setters that move a DESCRIPTOR field without moving the shape (immutable levels,
|
||||
// sample count, fixed sample locations). The emitter dedupes on the built descriptor,
|
||||
// so an over-call costs one 88-byte compare and never an extra record.
|
||||
// sample count, fixed sample locations), and a view's creation. The emitter dedupes
|
||||
// this form on the built descriptor, so an over-call costs one 88-byte compare and
|
||||
// never an extra record.
|
||||
void PipePublishDescriptor();
|
||||
// The PER-LEVEL and the CHAIN-CUT forms of the same call (P4a final review C-1). The
|
||||
// applier keeps a pending-upload set per (uploadTarget, level) and drops the entries
|
||||
// against the storage a respecify REPLACES - and the descriptor cannot tell it which:
|
||||
// AllocateStorage is per level and TruncateMipmapLevels removes a tail, while the
|
||||
// descriptor carries the base extent and the level count only. So the storage entry
|
||||
// points state the scope themselves; the whole-resource form above is for the calls
|
||||
// that really redefine the whole store. A per-level form is NOT deduped on the
|
||||
// descriptor: a non-base level redefined at a new size moves no descriptor field, and
|
||||
// the applier's box against the old level has to go regardless.
|
||||
void PipePublishLevelDescriptor(TextureUploadTarget uploadTarget, Uint mipmapLevel);
|
||||
void PipePublishTruncatedDescriptor(TextureUploadTarget uploadTarget, Uint levelCount);
|
||||
// set_texture_params, from every mutator that bumps m_textureParamsVersion.
|
||||
void PipePublishParams();
|
||||
// The sub-data DRAIN LIST's append, on a level's first dirty mark. There is no clean
|
||||
|
||||
@@ -34,8 +34,9 @@ namespace MobileGL {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// AFTER the allocation, for TextureObjectWithOneMipmap's reason: BumpShapeVersion
|
||||
// runs first and a descriptor built there would describe the level set this call
|
||||
// is about to change.
|
||||
PipePublishDescriptor();
|
||||
// is about to change. The FACE rides in `uploadTarget`, so the key the emitter
|
||||
// drops is that face's level and no other face's (final review C-1).
|
||||
PipePublishLevelDescriptor(uploadTarget, mipmapLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ namespace MobileGL {
|
||||
BumpShapeVersion();
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
PipePublishTruncatedDescriptor(uploadTarget, levelCount);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
Access = access;
|
||||
Format = format;
|
||||
++Version;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a D-A4 / final review M-A: the EARLIEST producer of kMGPipeBindShaderImage. The
|
||||
// ImageBindableHint the bit feeds is the prevention half of the texture-remint stall
|
||||
// class (a texture the server knows may be image-bound is allocated image-bindable
|
||||
// up front), so it has to reach the applier before the texture's first sync - at
|
||||
// the bind, not at the next validate point's image walk. Push-only through the
|
||||
// contract's door, like every other hook in this directory (G1).
|
||||
if (Texture) MG_Pipe::MGPipeNoteTextureImageBound(*Texture);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -234,7 +234,8 @@ TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
X(FramebufferEmit, ANamedRecordIsSuppressedPerObjectAndNeverAgainstABoundRecord) \
|
||||
X(FramebufferEmit, ADrawBufferTokenAboveTheWireWidthIsRefusedNotTruncated) \
|
||||
X(FramebufferEmit, ALayeredCubeAttachmentDoesNotAssertAFaceItCannotKnow) \
|
||||
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero)
|
||||
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero) \
|
||||
X(FramebufferEmit, ADeadFramebuffersNamedRecordLatchIsRetired)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
@@ -772,6 +773,27 @@ TEST(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZe
|
||||
"only moved field is the attachment's texture target would be suppressed";
|
||||
(void)probe;
|
||||
}
|
||||
// ============================ final review C-2 ============================
|
||||
//
|
||||
// A framebuffer has no wire lifetime (D-I2), so the only client state under its handle is this
|
||||
// emitter's per-object Named latch - and the death helper retires it before the slot is freed,
|
||||
// the shape every P4a kind takes (ID-8). A recycled handle's Gen already refused the stale
|
||||
// latch, so this pins the hygiene rather than a picture.
|
||||
TEST(FramebufferEmit, ADeadFramebuffersNamedRecordLatchIsRetired) {
|
||||
FramebufferScope scope;
|
||||
MGPipeHandle handle{};
|
||||
{
|
||||
const auto fbo = MakeShared<FramebufferObject>(31);
|
||||
const auto color = MakeColorTexture(32, 8);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
handle = MGPipeFramebufferEmitter::HandleFor(*fbo);
|
||||
ASSERT_GT(Framebuffers().EmitFramebufferByName(*fbo), 0u) << "the Named record did not go out";
|
||||
ASSERT_TRUE(Framebuffers().NamedRecordIsLatched(handle));
|
||||
}
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Framebuffer, handle));
|
||||
EXPECT_FALSE(Framebuffers().NamedRecordIsLatched(handle))
|
||||
<< "the dead framebuffer's Named latch survived its death";
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// =========================================================================================
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/Pipe/ImageEmit.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
@@ -347,7 +348,8 @@ TEST(ImageEmit, AMakeCurrentClearsTheImageSetAndAdvancesItsSerial) {
|
||||
X(ImageEmit, AZeroHighWaterMarkEmitsNothingWithoutHashing) \
|
||||
X(ImageEmit, AnAccessModeChangeAloneStillEmitsTheSet) \
|
||||
X(ImageEmit, AnInternalFormatChangeAloneStillEmitsTheSet) \
|
||||
X(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast)
|
||||
X(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast) \
|
||||
X(ImageEmit, AnImageBoundTextureIsMarkedShaderImageBoundAtTheBind)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
@@ -516,6 +518,43 @@ void main() { imageStore(img, ivec2(0), vec4(1.0)); }
|
||||
EXPECT_EQ(Emitter().LastImageViews()[1].InternalFormat, static_cast<Uint32>(GL_RGBA8UI));
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
// FINAL REVIEW M-A: glBindImageTexture IS THE EARLIEST PRODUCER OF kMGPipeBindShaderImage -
|
||||
// the bit the ImageBindableHint is derived from - and the emitted image set's walk is D-A4's
|
||||
// (any texture named in an emitted MGPImageView). The hint is the PREVENTION half of the
|
||||
// texture-remint stall class: a texture the server knows may be image-bound is allocated
|
||||
// image-bindable up front, so it has to arrive before the first sync, i.e. at the bind.
|
||||
// Nothing produced the bit before the fix round.
|
||||
TEST(ImageEmit, AnImageBoundTextureIsMarkedShaderImageBoundAtTheBind) {
|
||||
EmitterScope scope;
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
const GLuint name = MakeImageTexture();
|
||||
const auto& texture = Ctx().GetTextureObject(name);
|
||||
ASSERT_TRUE(texture);
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
EXPECT_EQ(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindShaderImage, 0)
|
||||
<< "nothing has image-bound this texture yet";
|
||||
|
||||
GL::BindImageTexture(0, name, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindShaderImage, 0)
|
||||
<< "glBindImageTexture did not mark the texture image-bound";
|
||||
|
||||
static const char* kOneImage = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, rgba8) uniform image2D img;
|
||||
void main() { imageStore(img, ivec2(0, 0), vec4(1.0)); }
|
||||
)";
|
||||
const GLuint program = MakeComputeProgram(kOneImage);
|
||||
GL::UseProgram(program);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
ASSERT_GE(Emitter().Window(), 1u);
|
||||
EXPECT_TRUE(Emitter().LastImageViews()[0].Res == handle);
|
||||
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindShaderImage, 0)
|
||||
<< "the emitted image set's walk does not carry the bit either";
|
||||
GL::UseProgram(0);
|
||||
GL::BindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
|
||||
@@ -501,7 +501,8 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
|
||||
X(ProgramEmit, AReLinkReIssuesOnTheSameHandle) \
|
||||
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
|
||||
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) \
|
||||
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock)
|
||||
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock) \
|
||||
X(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
@@ -774,6 +775,26 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); }
|
||||
}
|
||||
EXPECT_GT(Emitter().GlobalConstantsSetCount(), setsBefore);
|
||||
}
|
||||
// FINAL REVIEW C-2: the death helper forwards to this emitter before the slot is freed, so
|
||||
// a dead program's handle no longer reads as published in the record memo between the death
|
||||
// and the recycle (the memo's own Gen test covers only the recycle).
|
||||
TEST(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
MGPipeHandle handle{};
|
||||
{
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
Uint64 bytes = 0;
|
||||
handle = Emitter().AcquireShaderCso(*program, bytes);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
ASSERT_TRUE(Emitter().RecordIsPublished(handle));
|
||||
}
|
||||
GL::DeleteProgram(name); // not in use: the frontend object dies here
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, handle));
|
||||
EXPECT_FALSE(Emitter().RecordIsPublished(handle))
|
||||
<< "a dead program still reads as published in the program emitter's memo";
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
@@ -566,7 +567,8 @@ TEST(SamplerEmit, AMakeCurrentTakesTheUnitSetsAndLeavesTheCsoAndViewRecordsStand
|
||||
X(SamplerEmit, ABoundSamplerStateHoldsItsCsoUntilTheUnitMoves) \
|
||||
X(SamplerEmit, AReferencedCsoIsNeverTheLruVictim) \
|
||||
X(SamplerEmit, AFullyPinnedCacheMintsBeyondItsCapacityAndCountsIt) \
|
||||
X(SamplerEmit, AReleaseThisCacheNeverHandedOutIsCountedRatherThanAbsorbed)
|
||||
X(SamplerEmit, AReleaseThisCacheNeverHandedOutIsCountedRatherThanAbsorbed) \
|
||||
X(SamplerEmit, AResolvedSamplerViewMarksItsTextureAsSamplerBound)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
@@ -1088,6 +1090,44 @@ namespace {
|
||||
// turned from a compiled-out assert into a number.
|
||||
EXPECT_EQ(Cache().GetCounters().ReferencedEvictions, 0u);
|
||||
}
|
||||
|
||||
// FINAL REVIEW M-A: THE SAMPLER-VIEW RESOLUTION IS D-A4's PRODUCER OF kMGPipeBindSampler.
|
||||
// "Any texture the sampler-view resolution names in an emitted MGPBoundView" carries the
|
||||
// sticky bit from then on; a texture bound to a unit no sampler uniform resolves does not.
|
||||
// Nothing produced the bit before the fix round.
|
||||
TEST(SamplerEmit, AResolvedSamplerViewMarksItsTextureAsSamplerBound) {
|
||||
EmitterScope scope;
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
const Uint program = MakeSamplerProgram();
|
||||
GLint linked = 0;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
GL::UseProgram(program);
|
||||
const GLint location = GL::GetUniformLocation(program, "sampled");
|
||||
ASSERT_GE(location, 0);
|
||||
GL::Uniform1i(location, 3);
|
||||
|
||||
GLuint sampledName = 0;
|
||||
GLuint unsampledName = 0;
|
||||
const SharedPtr<ITextureObject> sampled = MakeCompleteTexture(sampledName, 4);
|
||||
const SharedPtr<ITextureObject> unsampled = MakeCompleteTexture(unsampledName, 4);
|
||||
BindTextureToUnit(3, sampled);
|
||||
BindTextureToUnit(5, unsampled);
|
||||
|
||||
ASSERT_GT(Emitter().EmitSamplerViews(Ctx()), 0u);
|
||||
const MGPBoundView& resolved = Emitter().LastBoundViews()[3];
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(resolved.Texture));
|
||||
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(resolved.Texture) & kMGPipeBindSampler, 0)
|
||||
<< "the texture a sampler view was resolved for does not carry kMGPipeBindSampler";
|
||||
const MGPipeHandle unsampledHandle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, unsampled->GetLifetimeId());
|
||||
if (!MGPipeHandleIsNull(unsampledHandle)) {
|
||||
EXPECT_EQ(MGPipeTextureEmitterInstance().TextureBindMask(unsampledHandle) & kMGPipeBindSampler, 0)
|
||||
<< "a texture no sampler uniform resolves to was marked sampler-bound";
|
||||
}
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
|
||||
@@ -205,18 +205,27 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
X(TextureEmit, ADestroyedTextureReleasesItsResourceViewAndBuiltinSamplerSlots) \
|
||||
X(TextureEmit, ARenderbufferRespecifyPublishesItsExtentWithoutAVersionCounter) \
|
||||
X(TextureEmit, ABailedLevelStaysDirtyAndStaysOnTheDrainList) \
|
||||
X(TextureEmit, TheApplierStoresTheRegionListTheEmitterBuiltAndNotAnEmptyOne) \
|
||||
X(TextureEmit, ARefusedUploadLeavesTheLevelDirtyAndOnTheDrainList) \
|
||||
X(TextureEmit, AnImmutableTexturesImageBindableHintReachesTheApplierAfterItsAllocation) \
|
||||
X(TextureEmit, ALodWriteOnTheBuiltinSamplerRepublishesTheParams) \
|
||||
X(TextureEmit, ATexturesBuiltinSamplerHoldsOneCacheReferenceAndSwapsItWithTheContent) \
|
||||
X(TextureEmit, ARecycledTextureSlotDoesNotInheritItsPredecessorsBindMask) \
|
||||
X(TextureEmit, TheApplierStoresTheRegionListTheEmitterBuiltAndNotAnEmptyOne) \
|
||||
X(TextureEmit, ARefusedUploadLeavesTheLevelDirtyAndOnTheDrainList) \
|
||||
X(TextureEmit, AnImmutableTexturesImageBindableHintReachesTheApplierAfterItsAllocation) \
|
||||
X(TextureEmit, ALodWriteOnTheBuiltinSamplerRepublishesTheParams) \
|
||||
X(TextureEmit, ATexturesBuiltinSamplerHoldsOneCacheReferenceAndSwapsItWithTheContent) \
|
||||
X(TextureEmit, ARecycledTextureSlotDoesNotInheritItsPredecessorsBindMask) \
|
||||
X(TextureEmit, ALevelMarkedCleanIsCollectedAtTheNextDrain) \
|
||||
X(TextureEmit, WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheApplier) \
|
||||
X(TextureEmit, WithTheSamplerBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \
|
||||
X(TextureEmit, \
|
||||
WithTheBufferResourceBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \
|
||||
X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive)
|
||||
X(TextureEmit, WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheApplier) \
|
||||
X(TextureEmit, WithTheSamplerBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \
|
||||
X(TextureEmit, \
|
||||
WithTheBufferResourceBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \
|
||||
X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive) \
|
||||
X(TextureEmit, ALevelDefinedAfterAnEmittedButUnconsumedUploadKeepsThatUpload) \
|
||||
X(TextureEmit, AChainTruncationKeepsTheSurvivingLevelsPendingUploads) \
|
||||
X(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendingUpload) \
|
||||
X(TextureEmit, ADeadTexturesHandleResolvesToNothingAndLeavesTheDrainList) \
|
||||
X(TextureEmit, ATextureRecycledOntoADeadSlotDoesNotInheritTheDrainEntry) \
|
||||
X(TextureEmit, ADeadRenderbuffersEntryIsRetiredWithItsSlot) \
|
||||
X(TextureEmit, ARefusedParamsRecordDoesNotAdvanceTheLatch) \
|
||||
X(TextureEmit, ADeadTexturesSamplerViewLatchIsRetiredAtItsDeath) \
|
||||
X(TextureEmit, ATextureBornBeforeTheConsumerRegisteredGetsItsRecordFromItsFirstParamsPublication)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
@@ -1233,6 +1242,11 @@ TEST(TextureEmit, ARecycledTextureSlotDoesNotInheritItsPredecessorsBindMask) {
|
||||
Textures().NoteTextureBoundAs(firstHandle, kMGPipeBindShaderImage);
|
||||
ASSERT_NE(Textures().TextureBindMask(firstHandle) & kMGPipeBindRenderTarget, 0);
|
||||
}
|
||||
// Since the final review's C-2 the reference goes back AT THE DEATH, through the death
|
||||
// helper's forward, and not first at the recycle.
|
||||
EXPECT_EQ(cache.RefCountOf(firstCso), 0u)
|
||||
<< "the dead texture's cache reference survived its death; the death helper did not reach the emitter";
|
||||
EXPECT_EQ(Textures().TextureBindMask(firstHandle), 0u) << "a dead handle still reads its sticky mask";
|
||||
const auto second = MakeTexture2D(22, 8);
|
||||
const MGPipeHandle secondHandle = Textures().FindTexture(*second);
|
||||
ASSERT_EQ(secondHandle.Slot, firstHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
||||
@@ -1275,6 +1289,304 @@ TEST(TextureEmit, ALevelMarkedCleanIsCollectedAtTheNextDrain) {
|
||||
EXPECT_EQ(Textures().DrainListSize(), 1u)
|
||||
<< "a re-dirtied level did not go back on the drain list, so its texels are owed for ever";
|
||||
}
|
||||
// ============================ final review C-1 ============================
|
||||
//
|
||||
// THE CLIENT PASSES THE LEVEL IT REDEFINES. AllocateStorage is per (uploadTarget, level) while
|
||||
// the descriptor carries only the base extent and the level count, so only the caller can tell
|
||||
// the applier WHICH storage a respecify replaces (wire C1's MGPRespecifiedLevel); before the fix
|
||||
// every texture respecify took the whole-resource arm and dropped every pending upload of the
|
||||
// texture - including a level the applier had already accepted and whose client flag was
|
||||
// therefore already clear (D-D5 step 1). Driven through the real AllocateStorage.
|
||||
TEST(TextureEmit, ALevelDefinedAfterAnEmittedButUnconsumedUploadKeepsThatUpload) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeShared<TextureObject2D>(90);
|
||||
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
// glTexImage2D(level 0, data)
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{64, 64, 1}, 64 * 64 * 4});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{64, 64, 1});
|
||||
// A verb the texture is not reached by: the drain emits level 0, the applier accepts, the
|
||||
// client clears its flag. Nothing has consumed the entry.
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
const MGPipeResourceRecord* record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
ASSERT_EQ(Textures().RefusedSubDataCount(), 0u);
|
||||
ASSERT_EQ(record->PendingUploads.size(), 1u);
|
||||
ASSERT_FALSE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
|
||||
// glTexImage2D(level 1, data): a DIFFERENT level.
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 1, MipmapInput{IntVec3{32, 32, 1}, 32 * 32 * 4});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 1, IntVec3{0, 0, 0}, IntVec3{32, 32, 1});
|
||||
record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
Bool levelZeroPending = false;
|
||||
for (const auto& pending : record->PendingUploads) {
|
||||
if (pending.Level == 0) levelZeroPending = true;
|
||||
}
|
||||
EXPECT_TRUE(levelZeroPending)
|
||||
<< "defining level 1 dropped level 0's accepted-but-unconsumed pending upload (PendingUploads.size()="
|
||||
<< record->PendingUploads.size() << ") while level 0's client dirty flag is "
|
||||
<< (texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0) ? "set" : "CLEAR - the texels are owed by nobody");
|
||||
// And after the next drain both levels stand in the set.
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
Bool zeroAfter = false;
|
||||
Bool oneAfter = false;
|
||||
for (const auto& pending : record->PendingUploads) {
|
||||
if (pending.Level == 0) zeroAfter = true;
|
||||
if (pending.Level == 1) oneAfter = true;
|
||||
}
|
||||
EXPECT_TRUE(oneAfter);
|
||||
EXPECT_TRUE(zeroAfter) << "level 0's texels are lost: not pending, flag clear";
|
||||
}
|
||||
|
||||
// A chain truncation - glGenerateMipmap fitting the chain, a base redefinition discarding its
|
||||
// tail - removes the levels at and above the cut and nothing below it. Before the fix it was a
|
||||
// whole-resource respecify and took level 0's standing upload with the tail.
|
||||
TEST(TextureEmit, AChainTruncationKeepsTheSurvivingLevelsPendingUploads) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(93, 64, /*levels=*/3);
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{64, 64, 1});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 2, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
const MGPipeResourceRecord* record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
ASSERT_EQ(record->PendingUploads.size(), 2u);
|
||||
ASSERT_FALSE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
|
||||
|
||||
texture->TruncateMipmapLevels(TextureUploadTarget::Texture2D, 1);
|
||||
record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
EXPECT_EQ(record->Desc.Levels, 1u);
|
||||
Bool zeroPending = false;
|
||||
Bool twoPending = false;
|
||||
for (const auto& pending : record->PendingUploads) {
|
||||
if (pending.Level == 0) zeroPending = true;
|
||||
if (pending.Level == 2) twoPending = true;
|
||||
}
|
||||
EXPECT_TRUE(zeroPending) << "truncating the chain above level 0 dropped level 0's standing upload";
|
||||
EXPECT_FALSE(twoPending) << "a level the truncation removed kept a pending upload against storage that is gone";
|
||||
}
|
||||
|
||||
// A non-base level redefined at a new size moves NO descriptor field (the descriptor carries
|
||||
// the base extent and the level count), so the emitter's descriptor dedupe used to swallow the
|
||||
// respecify and the applier kept a box sized for the OLD level - which Espryt would have
|
||||
// uploaded past the end of the new one. A per-level respecify reaches the applier whether or
|
||||
// not the descriptor moved, and drops exactly that level.
|
||||
TEST(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendingUpload) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(96, 16, /*levels=*/2);
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 1, IntVec3{0, 0, 0}, IntVec3{8, 8, 1});
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
const MGPipeResourceRecord* record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
ASSERT_EQ(record->PendingUploads.size(), 2u);
|
||||
const Uint64 serialBefore = record->Serial;
|
||||
|
||||
// glTexImage2D(level 1) at 4x4: the base is still 16x16 and the chain still two levels.
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 1, MipmapInput{IntVec3{4, 4, 1}, 4 * 4 * 4});
|
||||
record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
EXPECT_GT(record->Serial, serialBefore) << "the per-level respecify never reached the applier";
|
||||
Bool zeroPending = false;
|
||||
Bool onePending = false;
|
||||
for (const auto& pending : record->PendingUploads) {
|
||||
if (pending.Level == 0) zeroPending = true;
|
||||
if (pending.Level == 1) onePending = true;
|
||||
}
|
||||
EXPECT_TRUE(zeroPending) << "redefining level 1 dropped level 0's standing upload";
|
||||
EXPECT_FALSE(onePending) << "level 1's 8x8 box survived its redefinition onto a 4x4 level";
|
||||
}
|
||||
|
||||
// ============================ final review C-2 ============================
|
||||
//
|
||||
// A DEAD HANDLE RESOLVES TO NOTHING AND THE DRAIN LIST DROPS IT AT THE DEATH. The allocator's
|
||||
// generation moves only at the next hand-out, so between a death and a recycle the dead handle
|
||||
// compared equal to the slot's generation and the emitter answered the freed ITextureObject*;
|
||||
// the drain then called a virtual on it once per verb until something recycled the slot.
|
||||
TEST(TextureEmit, ADeadTexturesHandleResolvesToNothingAndLeavesTheDrainList) {
|
||||
TextureScope scope;
|
||||
MGPipeHandle handle{};
|
||||
{
|
||||
const auto texture = MakeShared<TextureObject2D>(91);
|
||||
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{16, 16, 1}, 16 * 16 * 4});
|
||||
// glTexSubImage2D: the level goes on the drain list; NO verb follows before the delete.
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
|
||||
handle = Textures().FindTexture(*texture);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||
} // glDeleteTextures: the last SharedPtr drops, ~TextureObjectBase frees the slot
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Texture, handle));
|
||||
EXPECT_EQ(Textures().ResolveTexture(handle), nullptr)
|
||||
<< "ResolveTexture hands back the freed ITextureObject* of a dead-but-not-recycled handle";
|
||||
EXPECT_EQ(Textures().DrainListSize(), 0u)
|
||||
<< "the dead texture's level is still on the drain list, so the next verb walks it";
|
||||
// And the next drain has nothing to say about it: no record, no refusal, no emission.
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
EXPECT_EQ(Textures().SubDataCount(), 0u);
|
||||
EXPECT_EQ(Textures().RefusedSubDataCount(), 0u);
|
||||
// The null came from the DEATH PATH retiring the entry, not from the loud refusal that
|
||||
// guards a death path that skipped the emitter.
|
||||
EXPECT_EQ(Textures().DeadResolveCount(), 0u)
|
||||
<< "the dead handle was refused by ResolveTexture's guard, so the death helper never told the emitter";
|
||||
}
|
||||
|
||||
// The sampler view minted off the texture's lifetime id (D-F2) has its own record memo in the
|
||||
// sampler emitter; the texture's death retires it through the view's death helper.
|
||||
TEST(TextureEmit, ADeadTexturesSamplerViewLatchIsRetiredAtItsDeath) {
|
||||
TextureScope scope;
|
||||
MGPipeHandle viewHandle{};
|
||||
{
|
||||
const auto texture = MakeTexture2D(89, 8);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
Uint64 bytes = 0;
|
||||
viewHandle = MGPipeSamplerEmitterInstance().AcquireSamplerView(*texture, handle, bytes);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(viewHandle));
|
||||
ASSERT_TRUE(MGPipeSamplerEmitterInstance().RecordIsPublished(viewHandle));
|
||||
}
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::SamplerViewCso, viewHandle));
|
||||
EXPECT_FALSE(MGPipeSamplerEmitterInstance().RecordIsPublished(viewHandle))
|
||||
<< "a dead sampler view still reads as published in the sampler emitter's memo";
|
||||
}
|
||||
|
||||
// ABA: the recycled slot's new texture owns the drain list entry it makes and none of its
|
||||
// predecessor's.
|
||||
TEST(TextureEmit, ATextureRecycledOntoADeadSlotDoesNotInheritTheDrainEntry) {
|
||||
TextureScope scope;
|
||||
MGPipeHandle deadHandle{};
|
||||
{
|
||||
const auto dead = MakeTexture2D(97, 8);
|
||||
dead->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{8, 8, 1});
|
||||
deadHandle = Textures().FindTexture(*dead);
|
||||
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||
}
|
||||
EXPECT_EQ(Textures().DrainListSize(), 0u) << "the death did not retire the drain entry";
|
||||
const auto successor = MakeTexture2D(98, 32);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*successor);
|
||||
ASSERT_EQ(handle.Slot, deadHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
||||
ASSERT_NE(handle.Gen, deadHandle.Gen);
|
||||
EXPECT_EQ(Textures().DrainListSize(), 0u) << "the successor inherited a drain entry it never made";
|
||||
successor->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{32, 32, 1});
|
||||
EXPECT_EQ(Textures().DrainListSize(), 1u);
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
EXPECT_EQ(Textures().SubDataCount(), 1u) << "exactly the successor's level went out";
|
||||
EXPECT_TRUE(Textures().LastSubData().Res == handle);
|
||||
EXPECT_EQ(Textures().LastSubData().UnionBox.W, 32u);
|
||||
EXPECT_EQ(Textures().DrainListSize(), 0u);
|
||||
}
|
||||
|
||||
// The renderbuffer table has no pointer to dangle but the same stale entry: a dead
|
||||
// renderbuffer's sticky mask must not be readable through its dead handle.
|
||||
TEST(TextureEmit, ADeadRenderbuffersEntryIsRetiredWithItsSlot) {
|
||||
TextureScope scope;
|
||||
MGPipeHandle handle{};
|
||||
{
|
||||
const auto renderbuffer = MakeShared<RenderbufferObject>(94);
|
||||
handle = Textures().FindRenderbuffer(*renderbuffer);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
Textures().NoteRenderbufferBoundAs(handle, kMGPipeBindRenderTarget);
|
||||
ASSERT_NE(Textures().RenderbufferBindMask(handle) & kMGPipeBindRenderTarget, 0);
|
||||
}
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Renderbuffer, handle));
|
||||
EXPECT_EQ(Textures().RenderbufferBindMask(handle), 0u)
|
||||
<< "a dead renderbuffer's entry still answers through its dead handle";
|
||||
const auto successor = MakeShared<RenderbufferObject>(99);
|
||||
const MGPipeHandle successorHandle = Textures().FindRenderbuffer(*successor);
|
||||
ASSERT_EQ(successorHandle.Slot, handle.Slot);
|
||||
EXPECT_EQ(Textures().RenderbufferBindMask(successorHandle), 0u);
|
||||
}
|
||||
|
||||
// ============================ final review m-1 (audit F-7) ============================
|
||||
//
|
||||
// set_texture_params LATCHES ON ACCEPTANCE, like the sub-data and respecify paths. A record the
|
||||
// applier refused used to advance the version latch anyway, so the parameters were not re-sent
|
||||
// until the next glTexParameter* moved a version. A refusal for a missing record is HEALED now
|
||||
// (the case after this one), so the property is driven through a refusal on the merits: with no
|
||||
// backend consumer the applier's belt refuses the parameters AND the healing create, and the
|
||||
// emitter is driven directly (the contract hook would not even emit without the consumer).
|
||||
TEST(TextureEmit, ARefusedParamsRecordDoesNotAdvanceTheLatch) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(95, 8);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
ASSERT_NE(AppliedTexture(handle), nullptr);
|
||||
const Uint64 serialBefore = AppliedTexture(handle)->ParamsSerial;
|
||||
|
||||
// A parameter moves (a LOD write on the built-in sampler, which the format setter's earlier
|
||||
// publication did not carry) while no consumer is registered: refused, and not healable.
|
||||
texture->GetSamplerObject()->SetLodBias(0.5f);
|
||||
const Uint64 paramsBefore = Textures().ParamCount();
|
||||
{
|
||||
ScopedNoResourceOps noConsumer;
|
||||
Textures().EmitTextureParams(*texture);
|
||||
}
|
||||
EXPECT_EQ(Textures().ParamCount(), paramsBefore + 1) << "the record was not even emitted";
|
||||
EXPECT_EQ(Textures().RefusedParamCount(), 1u) << "the emitter did not see the refusal";
|
||||
EXPECT_EQ(AppliedTexture(handle)->ParamsSerial, serialBefore) << "the refused record moved the serial";
|
||||
|
||||
// The consumer is back and the same parameters, no version moved, are published again:
|
||||
// with the latch taken on the REFUSED call this returns early and the record never learns
|
||||
// the LOD write.
|
||||
MG_Pipe::MGPipeEmitTextureParams(*texture);
|
||||
EXPECT_EQ(Textures().ParamCount(), paramsBefore + 2)
|
||||
<< "a refused set_texture_params advanced the latch, so the parameters are not re-sent";
|
||||
const MGPipeResourceRecord* record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr);
|
||||
EXPECT_EQ(record->ParamsSerial, serialBefore + 1) << "the record never learned the LOD write";
|
||||
EXPECT_EQ(record->Params.LodBias, 0.5f);
|
||||
}
|
||||
|
||||
// THE RETRACE CENSUS's ONE RESIDUAL after m-1 went loud: a texture born while the family was
|
||||
// not live - the context's default textures are constructed before the backend registers its
|
||||
// consumer - has no record, and when the application's first glTexParameter* lands on it
|
||||
// (texture 0) the record is refused and, before m-1, silently latched away for ever. The
|
||||
// params path now heals the record the way the respecify path does: a create with no storage
|
||||
// for the identity, the storage itself if the texture has any, then the parameters.
|
||||
TEST(TextureEmit, ATextureBornBeforeTheConsumerRegisteredGetsItsRecordFromItsFirstParamsPublication) {
|
||||
TextureScope scope;
|
||||
SharedPtr<TextureObject2D> texture;
|
||||
{
|
||||
ScopedNoResourceOps noConsumer;
|
||||
texture = MakeTexture2D(88, 8); // born, formatted and allocated with no consumer: no create
|
||||
}
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
ASSERT_EQ(AppliedTexture(handle), nullptr) << "the case needs a texture the applier never heard of";
|
||||
ASSERT_FALSE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle));
|
||||
|
||||
// glTexParameterf(GL_TEXTURE_LOD_BIAS) on it, with the consumer present now.
|
||||
texture->GetSamplerObject()->SetLodBias(0.25f);
|
||||
MG_Pipe::MGPipeEmitTextureParams(*texture);
|
||||
EXPECT_EQ(Textures().RefusedParamCount(), 0u)
|
||||
<< "the parameters of a texture born before the consumer were refused instead of healing its record";
|
||||
const MGPipeResourceRecord* record = AppliedTexture(handle);
|
||||
ASSERT_NE(record, nullptr) << "no record was healed";
|
||||
EXPECT_TRUE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle));
|
||||
EXPECT_EQ(record->Desc.Width, 8u) << "the healed record carries no storage although the texture has some";
|
||||
EXPECT_EQ(record->Desc.Levels, 1u);
|
||||
EXPECT_EQ(record->ParamsSerial, 1u);
|
||||
EXPECT_EQ(record->Params.LodBias, 0.25f);
|
||||
// And a texture with NO storage at all - the default texture's shape - heals to a record
|
||||
// with the identity only, which is what its parameters need and all a create says.
|
||||
const auto bare = MakeShared<TextureObject2D>(87);
|
||||
{
|
||||
// its create went out with the consumer present, so take the record away again to model
|
||||
// a birth the applier never saw
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
bare->GetSamplerObject()->SetLodBias(0.75f);
|
||||
MG_Pipe::MGPipeEmitTextureParams(*bare);
|
||||
const MGPipeHandle bareHandle = Textures().FindTexture(*bare);
|
||||
const MGPipeResourceRecord* bareRecord = AppliedTexture(bareHandle);
|
||||
ASSERT_NE(bareRecord, nullptr) << "a storage-less texture's parameters healed no record";
|
||||
EXPECT_EQ(bareRecord->Desc.Width, 0u);
|
||||
EXPECT_EQ(bareRecord->Params.LodBias, 0.75f);
|
||||
EXPECT_EQ(Textures().RefusedParamCount(), 0u);
|
||||
}
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// =========================================================================================
|
||||
@@ -1902,7 +2214,7 @@ TEST(TextureEmit, ARespecifyThatRedefinesNoStorageCarriesTheStickyMaskAndKeepsTh
|
||||
// glTexStorage2D: an IMMUTABLE store, which is the whole reason this arm exists.
|
||||
MGPResourceDesc allocated = TextureDesc(texture, 64, 151);
|
||||
allocated.Immutable = 1;
|
||||
allocated.Levels = 1;
|
||||
allocated.Levels = 2; // level 1 exists for the named-level clause below
|
||||
allocated.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
allocated.BindMask = static_cast<Uint16>(kMGPipeBindSampler);
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(allocated, nullptr));
|
||||
@@ -1931,17 +2243,29 @@ TEST(TextureEmit, ARespecifyThatRedefinesNoStorageCarriesTheStickyMaskAndKeepsTh
|
||||
<< "the serial is the whole publication of a metadata update - the twin re-derives its "
|
||||
"storage flags from the new mask on the strength of it";
|
||||
|
||||
// AND THE LEVEL POINTER DOES NOT CHANGE THE ANSWER. This is where ID-18 M4 refines C1:
|
||||
// C1's rule drops the uploads against the storage a call REPLACES, and a call that replaces
|
||||
// no storage replaces no level's coordinate system either, whatever level it names.
|
||||
const MGPRespecifiedLevel levelZero{kTex2D, 0};
|
||||
// A SECOND MASK MOVE WITH A NULL LEVEL STILL DROPS NOTHING - the client's mask republish
|
||||
// passes null on purpose (wire-v3 §5 item 6) and this is its shape.
|
||||
MGPResourceDesc maskedAgain = masked;
|
||||
maskedAgain.BindMask = static_cast<Uint16>(masked.BindMask | kMGPipeBindRenderTarget);
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, &levelZero));
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, nullptr));
|
||||
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
|
||||
<< "a metadata update dropped the level it named";
|
||||
<< "a metadata update with no level dropped a standing upload";
|
||||
EXPECT_EQ(TextureRecordOf(11).Desc.BindMask, maskedAgain.BindMask);
|
||||
|
||||
// BUT A NAMED LEVEL IS DROPPED WHETHER OR NOT THE DESCRIPTOR MOVED (P4a final review C-1,
|
||||
// refining the W11 clause that stood here): the pointer is the caller's statement that it
|
||||
// reallocated that level, and the descriptor cannot contradict it - a non-base level
|
||||
// redefined at a new size moves no descriptor field, so "identical storage fields" says
|
||||
// nothing about that level's coordinate system. Level 1's entry goes; level 0's stays.
|
||||
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels));
|
||||
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 2u);
|
||||
const MGPRespecifiedLevel levelOne{kTex2D, 1};
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, &levelOne));
|
||||
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
|
||||
<< "a level-scoped respecify on an unchanged descriptor did not drop the level it named";
|
||||
EXPECT_EQ(TextureRecordOf(11).PendingUploads[0].Level, 0u) << "it dropped the wrong level";
|
||||
const MGPRespecifiedLevel levelZero{kTex2D, 0};
|
||||
|
||||
// THE NEGATIVE CONTROL, in the same case: move ONE storage-defining field and the same call
|
||||
// is a redefinition again, which takes the level it names with it.
|
||||
MGPResourceDesc reallocated = maskedAgain;
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
|
||||
"framebuffer-emissions", "sampler-view-emissions", "sampler-state-emissions",
|
||||
"shader-image-emissions", "client-tex-upload-emissions",
|
||||
"shader-image-emissions", "client-tex-upload-emissions", "tex-remint-pulls",
|
||||
#endif
|
||||
};
|
||||
const char* const kGateNames[kGateCount] = {
|
||||
@@ -453,6 +453,10 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
line += " sie=" + std::to_string(calls[static_cast<Uint32>(CallClass::ShaderImageEmissions)]);
|
||||
line += " ctu=" +
|
||||
std::to_string(calls[static_cast<Uint32>(CallClass::ClientTextureUploadEmissions)]);
|
||||
// trp is the texture-remint pull count (ROADMAP open question 2): every one is a texture
|
||||
// Espryt had already allocated and then had to re-mint image-bindable, replaying its
|
||||
// levels from the client's shadow, because ImageBindableHint reached it too late.
|
||||
line += " trp=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureRemintPulls)]);
|
||||
#endif
|
||||
line += "] gates[";
|
||||
for (Uint32 i = 0; i < kGateCount; ++i) {
|
||||
|
||||
@@ -156,6 +156,14 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
// hides is ~+6 ms/frame, so an emission-shape divergence has to be a difference of two
|
||||
// numbers rather than something only a GPU can see.
|
||||
ClientTextureUploadEmissions,
|
||||
// THE TEXTURE-REMINT PULL RATE (ROADMAP open question 2; P4a final review M-A). Counted
|
||||
// by Espryt once per transition in which a texture that ALREADY HAD backend storage is
|
||||
// re-minted image-bindable and its defined levels are replayed from the client's shadow
|
||||
// (RequireImageBindableStorage) - the reach-back a split cannot make (D-M) and the one
|
||||
// ImageBindableHint exists to prevent. A texture whose hint arrived before its first
|
||||
// sync is allocated image-bindable up front and never counts. `trp=` on the summary
|
||||
// line; the number that decides MOBILEGL_PIPE_TEXEL_RETAIN_MB's default.
|
||||
TextureRemintPulls,
|
||||
#endif
|
||||
Count
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user