[Fix, Test] (Pipe): read the shader composite band's own counters in the composite leak case, arm the ABA flip from anywhere under a backend, match Espryt's exact refusal line and pin the bit-10-requires-bit-11 arm

This commit is contained in:
2026-09-08 19:08:12 -04:00
committed by rereview
parent 9c3e8ffd32
commit ea38cfcb99
7 changed files with 635 additions and 115 deletions
@@ -227,22 +227,70 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return MG_Config::Features.PipeHandleAbaControl;
}
// The per-kind form of the answer above. `kind` is MG_Pipe::MGPipeKind.
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
//
// Deliberately a SWITCH over the kinds this backend mints rather than a default of "true":
// a kind added to MGPipeKind without a decision here lands in the `default` arm and is
// reported as NOT covered, which is the safe direction - an uncovered kind whose control
// asserts the correct pixels is a control that has not armed yet, while a covered-by-default
// kind whose control asserts a corruption nobody can produce is a red lane.
inline Bool MagmaPipeAbaControlCoversKind(MG_Pipe::MGPipeKind kind) {
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
// "covered" direction is the expensive one - a control asserting a corruption nobody can
// produce is a permanently red always-on lane - so an undecided kind must never read true,
// and with no `default:` there is no arm for it to read true from.
//
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
// asserts say so in both directions, and the file no longer compiles if the tables and this
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
// and could not make anything red or green.)
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
switch (kind) {
// The two MagmaPipeIdentityTables really mints.
case MG_Pipe::MGPipeKind::VertexElementsCso:
case MG_Pipe::MGPipeKind::Buffer:
return MagmaPipeAbaControlDefeatsIdentity();
default:
// P4a's six, and every other kind: not minted on this backend, so not defeatable.
return true;
// P4a's six object classes: still reached from their frontend objects on this
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
// the knob to defeat.
case MG_Pipe::MGPipeKind::Texture:
case MG_Pipe::MGPipeKind::Renderbuffer:
case MG_Pipe::MGPipeKind::Framebuffer:
case MG_Pipe::MGPipeKind::SamplerCso:
case MG_Pipe::MGPipeKind::SamplerViewCso:
case MG_Pipe::MGPipeKind::ShaderCso:
// ...and everything else this backend does not mint a handle for.
case MG_Pipe::MGPipeKind::None:
case MG_Pipe::MGPipeKind::Xfb:
case MG_Pipe::MGPipeKind::RenderStateCso:
case MG_Pipe::MGPipeKind::Fence:
case MG_Pipe::MGPipeKind::Query:
case MG_Pipe::MGPipeKind::Context:
case MG_Pipe::MGPipeKind::KindCount:
return false;
}
return false;
}
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
"defeat for it");
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
"Wiring one of them is what flips this assert, this predicate and that arm - and "
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
"carries the answer into the lane");
// The per-kind form of MagmaPipeAbaControlDefeatsIdentity(): true only where there is both a
// key to defeat here AND the operator asked for it.
inline Bool MagmaPipeAbaControlCoversKind(MG_Pipe::MGPipeKind kind) {
return MagmaPipeAbaControlKindIsRekeyedHere(kind) && MagmaPipeAbaControlDefeatsIdentity();
}
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
+93 -23
View File
@@ -408,7 +408,9 @@ function(mgl_itest_probe_for_symbol outVar directory symbolRegex)
set(${outVar} "${mglItestProbeHit}" PARENT_SCOPE)
endfunction()
# The same probe, over a CONJUNCTION: the first source in the directory that names BOTH regexes.
# The same probe, over a CONJUNCTION: BOTH regexes matched ANYWHERE UNDER the directory, not
# necessarily in the same file. outVar is set to "<file matching A> + <file matching B>" when both
# were found and to the empty string otherwise.
#
# P4a needs it for exactly one question and the question cannot be asked any other way. "Does
# MOBILEGL_PIPE_HANDLE_ABA_CONTROL defeat the identity of P4a's OBJECT kinds on this backend?" is
@@ -418,23 +420,46 @@ endfunction()
# program, and every one of them would assert a corruption nothing on the tree can produce - a hard
# red on an always-on integration-gpu lane. Nor is it answered by "some source names a P4a subsystem
# bit", which will become true for a backend that honours the mask long before anyone wires the
# knob. What the arm actually needs is ONE SOURCE THAT DOES BOTH, and that is what this asks.
# knob. What the arm needs is BOTH FACTS TO BE TRUE OF THE BACKEND.
#
# DIRECTORY-WIDE RATHER THAN PER FILE, and that is review finding F-M5 rather than a preference.
# Requiring one file to carry both makes the arming depend on the FILE LAYOUT a later package
# chooses: a backend that wires the knob in Managers.cpp while its P4a subsystem constants live in
# SlotTables.h satisfies the question and fails the probe, the six controls keep printing wired=0
# and asserting the correct pixels, and NOTHING fails, warns or records that the expected flip did
# not happen - the one failure mode a control whose flip is in the future has. The false-positive
# this trades against is a backend that reads the knob somewhere and names a P4a bit somewhere else
# without connecting them; that costs a red lane an engineer must look at, which is the direction
# that gets noticed. Both spellings of the answer are printed, so the configure log says which file
# supplied which half.
#
# Same staleness guarantees as the single-regex probe: CONFIGURE_DEPENDS on the glob, and every file
# it finds appended to CMAKE_CONFIGURE_DEPENDS.
function(mgl_itest_probe_for_two_symbols outVar directory symbolRegexA symbolRegexB)
file(GLOB_RECURSE mglItestProbeSources CONFIGURE_DEPENDS
"${directory}/*.h" "${directory}/*.hpp" "${directory}/*.cpp" "${directory}/*.c")
set(mglItestProbeHit "")
set(mglItestProbeHitA "")
set(mglItestProbeHitB "")
foreach(mglItestProbeSource IN LISTS mglItestProbeSources)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${mglItestProbeSource}")
file(STRINGS "${mglItestProbeSource}" mglItestProbeLinesA REGEX "${symbolRegexA}")
file(STRINGS "${mglItestProbeSource}" mglItestProbeLinesB REGEX "${symbolRegexB}")
if (mglItestProbeLinesA AND mglItestProbeLinesB AND NOT mglItestProbeHit)
set(mglItestProbeHit "${mglItestProbeSource}")
if (NOT mglItestProbeHitA)
file(STRINGS "${mglItestProbeSource}" mglItestProbeLinesA REGEX "${symbolRegexA}")
if (mglItestProbeLinesA)
set(mglItestProbeHitA "${mglItestProbeSource}")
endif()
endif()
if (NOT mglItestProbeHitB)
file(STRINGS "${mglItestProbeSource}" mglItestProbeLinesB REGEX "${symbolRegexB}")
if (mglItestProbeLinesB)
set(mglItestProbeHitB "${mglItestProbeSource}")
endif()
endif()
endforeach()
set(${outVar} "${mglItestProbeHit}" PARENT_SCOPE)
if (mglItestProbeHitA AND mglItestProbeHitB)
set(${outVar} "${mglItestProbeHitA} + ${mglItestProbeHitB}" PARENT_SCOPE)
else()
set(${outVar} "" PARENT_SCOPE)
endif()
endfunction()
if (MOBILEGL_PIPE_PUSH)
@@ -545,10 +570,13 @@ if (MOBILEGL_PIPE_PUSH)
"HandleRecycle's six P4a cases will SKIP their Handles arm on it")
endif()
# ...and whether the ABA knob reaches those kinds THERE. A conjunction, for the reason
# mgl_itest_probe_for_two_symbols states: reading the knob is not the same as steering
# P4a's keys with it, and arming this marker on the weaker evidence turns six always-on
# entries into a permanent red.
# ...and whether the ABA knob reaches those kinds THERE. A conjunction over the WHOLE
# backend directory, for the reason mgl_itest_probe_for_two_symbols states: reading the
# knob is not the same as steering P4a's keys with it, so the weaker single-regex evidence
# would turn six always-on entries into a permanent red - but requiring one FILE to carry
# both halves would let a backend satisfy the question and miss the probe, and the six
# controls would then keep asserting the correct pixels with nothing recording that the
# flip was forgotten (F-M5).
mgl_itest_probe_for_two_symbols(MGL_ITEST_OBJECT_ABA
"${MGL_ITEST_ROOT}/MobileGL/MG_Backend/${mglItestObjectBackend}"
"PipeHandleAbaControl"
@@ -559,10 +587,11 @@ if (MOBILEGL_PIPE_PUSH)
list(APPEND MGL_ITEST_CAPABILITY_ENV
"MGITEST_HANDLE_ABA_OBJECTS_${mglItestObjectBackend}=1")
else()
message(STATUS "Integration tests: no ${mglItestObjectBackend} source both reads "
"Features.PipeHandleAbaControl and names a P4a subsystem bit - "
"HandleRecycle's six P4a cases will assert the CORRECT pixels on the "
"AbaControl arm and say that it is not a control for them yet")
message(STATUS "Integration tests: MobileGL/MG_Backend/${mglItestObjectBackend} does not "
"both read Features.PipeHandleAbaControl and name a P4a subsystem bit "
"somewhere under it - HandleRecycle's six P4a cases will assert the "
"CORRECT pixels on the AbaControl arm and say that it is not a control "
"for them yet")
endif()
endforeach()
@@ -583,6 +612,26 @@ if (MOBILEGL_PIPE_PUSH)
"ObjectSubsystemControl's emission case will SKIP")
endif()
# ...and the CLIENT half of the texture-upload shape, separately, because it is a different
# question with a different owner's file behind it (review F-m7). ctu= is emitted by PipeStats
# in EVERY push build whether or not anything increments it, so "the counter read zero" and
# "no client emitter exists" are the same number and TextureUploadShape cannot tell them apart
# from the summary line alone. Once package B's texture emitter lands, an emitter that stopped
# emitting would read exactly like no emitter at all and the two-sided assertion the scenario
# exists for would pass while comparing nothing. This probe is what separates them.
mgl_itest_probe_for_symbol(MGL_ITEST_CLIENT_TEXTURE_UPLOAD_EMITTER
"${MGL_ITEST_ROOT}/MobileGL/MG_Impl/Pipe" "ClientTextureUploadEmissions")
if (MGL_ITEST_CLIENT_TEXTURE_UPLOAD_EMITTER)
message(STATUS "Integration tests: the client texture-upload counter has an emitter "
"(${MGL_ITEST_CLIENT_TEXTURE_UPLOAD_EMITTER}) - TextureUploadShape's "
"two-sided assertion is live")
list(APPEND MGL_ITEST_CAPABILITY_ENV "MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT=1")
else()
message(STATUS "Integration tests: no MG_Impl/Pipe source emits "
"ClientTextureUploadEmissions - TextureUploadShape records the SERVER shape "
"only and asserts that ctu= is zero")
endif()
mgl_itest_probe_for_symbol(MGL_ITEST_MAGMA_ABA
"${MGL_ITEST_ROOT}/MobileGL/MG_Backend/DirectVulkan" "PipeHandleAbaControl")
if (MGL_ITEST_MAGMA_ABA)
@@ -1351,13 +1400,20 @@ gtest_discover_tests(MobileGLIntegrationTest
# constant P3a shipped survives as P4a's A/B control, and a lane that spelled its own bit pattern
# would stop being the shape that ships the first time the default moved.
#
# THE THIRD LANE IS THE DEPENDENCY REFUSAL (D-K2) and it is the one entry in this family that is not
# vacuous before the emitters land: 0x9ff sets the sampler subsystem (bit 11) WITHOUT the texture
# resource subsystem (bit 10) that every MGPBoundView::Texture and MGPImageView::Res depends on, and
# the bring-up must log ONE error naming both bits and run the legacy sampler arm. That is a
# decision made from the bitmask alone, so it is assertable on a tree where nothing emits yet - and
# a half-honoured mask is invisible in the pixels by construction, which is why it needs an entry
# rather than a code comment.
# THE THIRD AND FOURTH LANES ARE THE DEPENDENCY REFUSALS (D-K2) and they are the entries in this
# family that are not vacuous before the emitters land, because both are decisions made from the
# bitmask alone. A half-honoured mask is invisible in the pixels by construction, which is why each
# needs an entry rather than a code comment.
#
# 0x9ff sets the sampler subsystem (bit 11) WITHOUT the texture resource subsystem (bit 10) that
# every MGPBoundView::Texture and MGPImageView::Res depends on.
# 0x5ff sets the texture resource subsystem (bit 10) WITHOUT the sampler subsystem (bit 11):
# D-K2's fourth row (ID-15). MGPTextureParams::BuiltinSampler is a SamplerCso handle and
# only bit 11 mints sampler CSOs, so bit 10 alone would emit a null there and the applier's
# Fatal is the next thing that happens. The brief called this pair harmless; P4a as built
# says otherwise, and the refusal belongs in the texture family's arm resolver.
#
# In both, the bring-up must log ONE error naming both bits and run the legacy arm.
#
# ONE CASE PER LANE, through TEST_FILTER, and it is a constraint rather than a preference: each case
# READS the library's log and the log is a per-LANE resource (the library opens it fopen(path, "w"),
@@ -1387,6 +1443,11 @@ mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_ENVIRONMENT
"MOBILEGL_PIPE_PUSH=0x9ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-refused-DirectGLES.log"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_TEXTURE_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=refused-texture"
"MOBILEGL_PIPE_PUSH=0x5ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-refused-texture-DirectGLES.log"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.On."
@@ -1415,6 +1476,15 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.RefusedTexture."
TEST_FILTER "ObjectSubsystemControlScenario.ATextureBitWithoutTheSamplerBitIsRefusedAndNamed"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_TEXTURE_ENVIRONMENT}"
)
# --- The texture upload shape: RECORDED, NOT GATED in P4a (D-D4) ----------------------
#
@@ -49,12 +49,34 @@ namespace MGITest {
bool PeekPipeSlotHighWater(PipeSlotKind kind, unsigned* outHighWater) {
if (outHighWater == nullptr) return false;
// The ORDINARY space only, for every kind including ShaderCso (contract-v2.md 4.3).
*outHighWater = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().HighWater(Translate(kind)));
return true;
}
bool PeekPipeCompositeSlotLiveCount(unsigned* outLive) {
if (outLive == nullptr) return false;
*outLive = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().CompositeLiveCount());
return true;
}
bool PeekPipeCompositeSlotHighWater(unsigned* outHighWater) {
if (outHighWater == nullptr) return false;
*outHighWater = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().CompositeHighWater());
return true;
}
bool PeekPipeCompositeSlotBandBase(unsigned* outBandBase) {
if (outBandBase == nullptr) return false;
*outBandBase = static_cast<unsigned>(MobileGL::MG_Pipe::kMGPipeShaderCsoCompositeSlotBase);
return true;
}
#else
bool PeekPipeSlotLiveCount(PipeSlotKind, unsigned*) { return false; }
bool PeekPipeSlotHighWater(PipeSlotKind, unsigned*) { return false; }
bool PeekPipeCompositeSlotLiveCount(unsigned*) { return false; }
bool PeekPipeCompositeSlotHighWater(unsigned*) { return false; }
bool PeekPipeCompositeSlotBandBase(unsigned*) { return false; }
#endif
} // namespace MGITest
@@ -45,13 +45,20 @@ namespace MGITest {
SamplerCso,
SamplerViewCso,
// ShaderCso covers BOTH the ordinary program slots and the program-pipeline COMPOSITES
// minted out of the reserved high band (MGPipeHandles.h:86-97, D-H7). One kind, because
// that is what the allocator has: the band is a second dense table inside the same
// kind, LiveCount counts both and HighWater is one past the highest slot handed out in
// either. The composite's leak case is a separate CASE rather than a separate kind for
// that reason - what makes it its own case is that a composite's slot has TWO
// independent release paths (the pipeline cache's LRU eviction and the composite
// ProgramObject's destructor), not that it is counted anywhere else.
// minted out of the reserved high band (MGPipeHandles.h:86-107, D-H7). One kind, because
// that is what the allocator has: the band is a second dense table inside the same kind
// and LiveCount counts both.
//
// THE TWO SPACES' HIGH-WATER MARKS ARE NOT ONE NUMBER, and the correction matters here
// more than anywhere else. c0b split them (contract-v2.md 4.3): HighWater(ShaderCso) is
// now the ORDINARY space only and the band's own mark is CompositeHighWater(), because
// a merged mark is pinned at ~983k from the first composite mint onward and every "the
// high-water mark did not move over N churn rounds" assertion about ordinary programs
// would be vacuously true for the rest of the process. The composite's leak case is a
// separate CASE and reads the BAND'S OWN counters below (PeekPipeCompositeSlot*) - a
// composite's slot has TWO independent release paths (the pipeline cache's LRU eviction
// and the composite ProgramObject's destructor), and a slot that never comes back to
// the band moves neither of the ordinary numbers.
ShaderCso,
};
@@ -64,4 +71,31 @@ namespace MGITest {
bool PeekPipeSlotLiveCount(PipeSlotKind kind, unsigned* outLive);
bool PeekPipeSlotHighWater(PipeSlotKind kind, unsigned* outHighWater);
// The ShaderCso COMPOSITE BAND's own three numbers, the seventh..ninth members
// contract-v2.md 4.3 asks this header for. There is no `kind` argument because the band is
// ShaderCso's alone - AllocateComposite is the one door into it and no other kind has one.
// All three return false on the same terms as the two above, and a caller that gets false
// must SKIP.
//
// PeekPipeCompositeSlotLiveCount = MGPipeSlotAllocator::CompositeLiveCount(), the band's
// share of LiveCount(ShaderCso).
// PeekPipeCompositeSlotHighWater = CompositeHighWater() VERBATIM, i.e. one past the
// highest band slot ever handed out. It is an ABSOLUTE
// slot number and therefore starts at the band's base,
// not at zero - "no composite was ever minted" reads as
// `high water == band base`, which is what the third
// member is for. It is not returned base-relative
// because a peek whose name says HighWater and whose
// value is a delta is exactly the kind of quietly
// redefined counter this member exists to correct.
// PeekPipeCompositeSlotBandBase = kMGPipeShaderCsoCompositeSlotBase, the floor the
// other two are read against. A constant, but it
// reaches a scenario only through this header: the
// MG_Pipe headers and the GL headers are not meant to
// meet in one translation unit, which is why this
// harness exists at all.
bool PeekPipeCompositeSlotLiveCount(unsigned* outLive);
bool PeekPipeCompositeSlotHighWater(unsigned* outHighWater);
bool PeekPipeCompositeSlotBandBase(unsigned* outBandBase);
} // namespace MGITest
@@ -173,6 +173,78 @@ namespace MGITest {
bool RunningInAHandleRecycleLane() { return std::getenv(kArmMarker) != nullptr; }
// Does THIS LANE run with a live client slot allocator behind it - i.e. did it pin a
// non-zero MOBILEGL_PIPE_PUSH?
//
// The leak cases below need this and not the arm (review F-m4). The arm says which key a
// pixel assertion is about; the leak assertion is not about a key at all, it is about the
// allocator, and "the allocator has slots to leak" is exactly "the mask is not zero". The
// two questions almost coincide - DirectGLES/DirectVulkan.HandleRecycle.Legacy. and
// DirectVulkan.HandleRecycle.AbaControl. do pin MOBILEGL_PIPE_PUSH=0 - but
// DirectVulkan.HandleRecycle.AbaControlHandles. pins the shipping 0x1fff WITH a live
// allocator, and gating on `arm == Handles` declined it while telling the reader the
// lane had no allocator, which was false. Reading the lane's own pin covers all three.
//
// The ENVIRONMENT is the right place to read it from and the library's config is not:
// MG_Config is inside the library, this module links the shipping .so on Android, and the
// pin is the LANE's statement about what it configured. An entry that pinned nothing (the
// ambient ones) is not a lane and answers false - the build default may well be non-zero
// there, but an ambient entry configured no arm, no allocator expectation and no private
// log, which is the reason the whole file declines them.
bool LanePinnedALiveAllocator() {
const char* mask = std::getenv("MOBILEGL_PIPE_PUSH");
if (mask == nullptr || mask[0] == '\0') return false;
// strtoull handles the 0x form every lane spells it in, and a value this module
// cannot parse is treated as "no pin" rather than as a non-zero mask.
char* end = nullptr;
const unsigned long long value = std::strtoull(mask, &end, 0);
return end != nullptr && *end == '\0' && value != 0ull;
}
// ---- which of the allocator's TWO spaces a leak case measures --------------------
//
// Every kind but ShaderCso has one space. ShaderCso has two: the ordinary program slots,
// and the reserved high band the program-pipeline COMPOSITES are minted out of through
// the allocator's one door, AllocateComposite (D-H7). c0b split their high-water marks
// (contract-v2.md 4.3) precisely so that a leak case can be written about either, and
// the composite's case has to read the BAND's - a band slot that never comes back moves
// neither of the ordinary numbers, which is the review's F-M4: the case would have
// reported green having never looked at the thing it exists for.
//
// Stated at every call site rather than defaulted, for PipeSlotPeek's `no default:`
// reason: a new leak case must say which space it is about, because the wrong answer is
// a green that asserts nothing rather than a compile error.
enum class SlotSpace {
Ordinary,
CompositeBand,
};
const char* SpaceSuffix(SlotSpace space) {
return space == SlotSpace::CompositeBand ? " [composite band]" : "";
}
bool ReadSpaceLiveCount(PipeSlotKind kind, SlotSpace space, unsigned* out) {
return space == SlotSpace::CompositeBand ? MGITest::PeekPipeCompositeSlotLiveCount(out)
: MGITest::PeekPipeSlotLiveCount(kind, out);
}
bool ReadSpaceHighWater(PipeSlotKind kind, SlotSpace space, unsigned* out) {
return space == SlotSpace::CompositeBand ? MGITest::PeekPipeCompositeSlotHighWater(out)
: MGITest::PeekPipeSlotHighWater(kind, out);
}
// The value a space's high-water mark has when NOTHING of it was ever handed out: 0 for
// the ordinary space, and the band's BASE for the band, because CompositeHighWater() is
// an absolute slot number. Reading this wrong is what would turn the band's "nothing was
// ever minted" skip into a silent pass on a tree that mints composites.
bool ReadSpaceHighWaterFloor(SlotSpace space, unsigned* out) {
if (space != SlotSpace::CompositeBand) {
*out = 0;
return true;
}
return MGITest::PeekPipeCompositeSlotBandBase(out);
}
const char* ArmName(Arm arm) {
switch (arm) {
case Arm::Handles: return "Handles";
@@ -622,28 +694,49 @@ void main() { oColor = texture(uTex, vUv); }
// vacuous - which is the one of the three that catches a death path that works but
// runs at the wrong time (a deferred queue, a frame-boundary sweep).
//
// `maxInFlight` is how many slots of the kind one round may legitimately hold at its
// peak: 1 where the round creates one object, more where the round creates several of
// the same kind (a pipeline composite's round also creates its two stage programs, and
// all three are ShaderCsos).
// `space` says WHICH of the allocator's two spaces the three assertions are about, and
// it is the difference between an assertion and a green that reads the wrong counter:
// only ShaderCso has two, and only the composite case is about the band.
//
// `maxInFlight` is how many slots of the kind IN THAT SPACE one round may legitimately
// hold at its peak: 1 where the round creates one object of it, more where the round
// creates several. The composite round creates three ShaderCsos - two stage programs
// and the composite they are flattened into - but only ONE of the three is a band
// slot, so the band's answer is 1 and the ordinary space's would have been 3.
using ChurnRound = std::function<void(bool checkPixels, const std::function<void()>& observe)>;
void AssertChurnReturnsEverySlot(PipeSlotKind kind, const char* kindName,
void AssertChurnReturnsEverySlot(PipeSlotKind kind, SlotSpace space, const char* kindName,
const char* owner, unsigned maxInFlight,
const ChurnRound& round) {
if (m_arm != Arm::Handles) {
GTEST_SKIP() << "the client mints a " << kindName
<< " slot only when its subsystem is on, and only the Handles arm "
"pins the shipping mask (0x1fff). The Legacy and AbaControl "
"lanes run MOBILEGL_PIPE_PUSH=0, where there is no allocator to "
"leak from.";
// THE LANE'S OWN PIN, not the arm (F-m4). The Legacy and AbaControl lanes run
// MOBILEGL_PIPE_PUSH=0 and really have no allocator to leak from; the Handles
// lanes and DirectVulkan.HandleRecycle.AbaControlHandles. all pin the shipping
// 0x1fff and do. The old gate declined the third of those while telling the
// reader it had no allocator, which was false, and left one lane's coverage on
// the table.
if (!LanePinnedALiveAllocator()) {
GTEST_SKIP() << "this entry pinned no non-zero MOBILEGL_PIPE_PUSH, so there is "
"no client slot allocator behind it to leak from: the "
"HandleRecycle.Legacy. and HandleRecycle.AbaControl. lanes pin "
"MOBILEGL_PIPE_PUSH=0 on purpose (they are about the pre-handle "
"guards), and the ambient entries configure no lane at all. The "
"lanes that carry this assertion are the two "
"HandleRecycle.Handles. ones and "
"DirectVulkan.HandleRecycle.AbaControlHandles., all of which pin "
"the shipping mask.";
}
unsigned probe = 0;
if (!MGITest::PeekPipeSlotLiveCount(kind, &probe)) {
if (!ReadSpaceLiveCount(kind, space, &probe)) {
GTEST_SKIP() << "the client slot allocator is out of reach from this module (a "
"pull build has none, and the Android link resolves no internal "
"symbol), so 'could not look' would be reported as 'did not "
"leak'";
}
unsigned highWaterFloor = 0;
if (!ReadSpaceHighWaterFloor(space, &highWaterFloor)) {
GTEST_SKIP() << "the composite band's base is out of reach from this module, so "
"'no composite was ever minted' cannot be told apart from 'the "
"band did not grow' and a green here would assert nothing";
}
// TWO WARM-UP ROUNDS BEFORE THE BASELINE IS TAKEN, so what is measured is growth
// WITH the churn and not the one-off cost of drawing at all. The first rounds in a
@@ -654,7 +747,7 @@ void main() { oColor = texture(uTex, vUv); }
unsigned peakLive = 0;
const std::function<void()> observe = [&]() {
unsigned live = 0;
if (MGITest::PeekPipeSlotLiveCount(kind, &live) && live > peakLive) peakLive = live;
if (ReadSpaceLiveCount(kind, space, &live) && live > peakLive) peakLive = live;
};
round(/*checkPixels=*/true, observe);
round(/*checkPixels=*/false, observe);
@@ -662,8 +755,8 @@ void main() { oColor = texture(uTex, vUv); }
unsigned liveBefore = 0;
unsigned highWaterBefore = 0;
ASSERT_TRUE(MGITest::PeekPipeSlotLiveCount(kind, &liveBefore));
ASSERT_TRUE(MGITest::PeekPipeSlotHighWater(kind, &highWaterBefore));
ASSERT_TRUE(ReadSpaceLiveCount(kind, space, &liveBefore));
ASSERT_TRUE(ReadSpaceHighWater(kind, space, &highWaterBefore));
constexpr int kChurn = 48;
for (int i = 0; i < kChurn; ++i) round(/*checkPixels=*/false, observe);
@@ -671,12 +764,13 @@ void main() { oColor = texture(uTex, vUv); }
unsigned liveAfter = 0;
unsigned highWaterAfter = 0;
ASSERT_TRUE(MGITest::PeekPipeSlotLiveCount(kind, &liveAfter));
ASSERT_TRUE(MGITest::PeekPipeSlotHighWater(kind, &highWaterAfter));
ASSERT_TRUE(ReadSpaceLiveCount(kind, space, &liveAfter));
ASSERT_TRUE(ReadSpaceHighWater(kind, space, &highWaterAfter));
std::cout << "[ HandleRecycle ] backend=" << Gl().BackendName() << " " << kindName
<< " live " << liveBefore << " -> " << liveAfter << " (peak " << peakLive
<< "), high water " << highWaterBefore << " -> " << highWaterAfter
<< " over " << kChurn << " create/draw/destroy rounds" << std::endl;
<< SpaceSuffix(space) << " live " << liveBefore << " -> " << liveAfter
<< " (peak " << peakLive << "), high water " << highWaterBefore << " -> "
<< highWaterAfter << " (floor " << highWaterFloor << ") over " << kChurn
<< " create/draw/destroy rounds" << std::endl;
// NOTHING WAS EVER MINTED, which is not "did not leak" and must not be reported as
// one. On the P4a contract tree the client emits nothing for any of these kinds -
@@ -685,9 +779,9 @@ void main() { oColor = texture(uTex, vUv); }
// a kind it never saw. This is the same rule as the peek returning false, applied
// to the other way of not being able to look, and it arms itself the moment the
// owning package's emitter lands.
if (highWaterAfter == 0 && peakLive == 0 && liveAfter == 0) {
if (highWaterAfter == highWaterFloor && peakLive == 0 && liveAfter == 0) {
GTEST_SKIP() << "subsystem not implemented on this tree: the client minted no "
<< kindName
<< kindName << SpaceSuffix(space)
<< " slot at all over " << (kChurn + 2)
<< " create/draw/destroy rounds, so there is nothing here that "
"could leak and a green would assert nothing. P4a package "
@@ -697,7 +791,7 @@ void main() { oColor = texture(uTex, vUv); }
}
EXPECT_EQ(liveAfter, liveBefore)
<< kChurn << " " << kindName
<< kChurn << " " << kindName << SpaceSuffix(space)
<< " objects were created, drawn with and destroyed and "
<< (liveAfter - liveBefore)
<< " slots never came back. Each one holds a SlotState, a lifetime-id map node "
@@ -708,12 +802,12 @@ void main() { oColor = texture(uTex, vUv); }
"(D-I1) rather than from a backend death table. Backend "
<< Gl().BackendName();
EXPECT_EQ(highWaterAfter, highWaterBefore)
<< "the " << kindName
<< "the " << kindName << SpaceSuffix(space)
<< " slot space grew with the churn instead of recycling the slot the warm-up "
"rounds already handed out; the frees are not reaching the allocator's free "
"list";
EXPECT_LE(peakLive > liveBefore ? peakLive - liveBefore : 0u, maxInFlight)
<< "more than " << maxInFlight << " churned " << kindName
<< "more than " << maxInFlight << " churned " << kindName << SpaceSuffix(space)
<< " object(s) were live at the allocator at once, so the deaths are arriving "
"late rather than at the destructor";
}
@@ -1235,11 +1329,20 @@ void main() { oColor = texture(uTex, vUv); }
// The corruption IS the assertion: with the identity half of the framebuffer memo
// key defeated, the replacement inherits the dead framebuffer's record and its
// clear lands on the dead one's attachment.
EXPECT_NE(deadIsNotRed, 0)
// sawStale, not `deadIsNotRed != 0` (review F-m12): the weaker form is satisfied
// by a dead attachment full of GARBAGE, which is not the observation this control
// claims. sawStale is the predicate the case already computes and prints - the
// dead attachment is now the REPLACEMENT'S green, i.e. the replacement's clear
// landed there - so the assertion and the printed line say the same thing.
EXPECT_TRUE(sawStale)
<< "[AbaControl expects the STALE framebuffer] the DEAD framebuffer's "
"attachment is still the red it was cleared to in the warm-up, so the "
"replacement's clear went to its own attachment after all and the ABA was "
"not reproduced - the control has stopped controlling anything.";
"attachment did not come back as the replacement's green, so the "
"replacement's clear did not land on it and the ABA was not reproduced - "
"the control has stopped controlling anything. Texels that are neither the "
"warm-up red nor the replacement green are a third answer and are not a "
"reproduction either ("
<< deadIsNotRed << " of " << (deadTexels.size() / 4)
<< " dead texels are not red).";
} else {
EXPECT_EQ(replacementIsNotGreen, 0)
<< "the replacement framebuffer's own attachment is not the colour it was "
@@ -1807,7 +1910,8 @@ void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
ConfigureQuadVao(vao, buffer);
AssertChurnReturnsEverySlot(
PipeSlotKind::Texture, "Texture", "B (clientfb)", /*maxInFlight=*/1u,
PipeSlotKind::Texture, SlotSpace::Ordinary, "Texture", "B (clientfb)",
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
const GLuint texture = MakeSolidTexture(0, 255, 0);
const Image image = DrawTexturedQuadAndRead(vao, texture);
@@ -1845,7 +1949,8 @@ void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
// DeleteSamplerState), which is precisely why it needs a case of its own: a helper
// that forgot one of its three frees leaks only that kind and nothing else moves.
AssertChurnReturnsEverySlot(
PipeSlotKind::SamplerViewCso, "SamplerViewCso", "C (clientsp)", /*maxInFlight=*/1u,
PipeSlotKind::SamplerViewCso, SlotSpace::Ordinary, "SamplerViewCso", "C (clientsp)",
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
const GLuint texture = MakeSolidTexture(0, 255, 0);
const Image image = DrawTexturedQuadAndRead(vao, texture);
@@ -1874,7 +1979,8 @@ void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
glGenFramebuffers(1, &fbo);
AssertChurnReturnsEverySlot(
PipeSlotKind::Renderbuffer, "Renderbuffer", "B (clientfb)", /*maxInFlight=*/1u,
PipeSlotKind::Renderbuffer, SlotSpace::Ordinary, "Renderbuffer", "B (clientfb)",
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
GLuint renderbuffer = 0;
glGenRenderbuffers(1, &renderbuffer);
@@ -1920,7 +2026,8 @@ void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
// allocator is therefore the only observable this kind has, which makes this case the
// whole of its lifetime coverage rather than a supplement to a wire assertion.
AssertChurnReturnsEverySlot(
PipeSlotKind::Framebuffer, "Framebuffer", "B (clientfb)", /*maxInFlight=*/1u,
PipeSlotKind::Framebuffer, SlotSpace::Ordinary, "Framebuffer", "B (clientfb)",
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
@@ -1959,7 +2066,8 @@ void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
// path. The LOD bias moves per round, which changes the hash and nothing else.
int round = 0;
AssertChurnReturnsEverySlot(
PipeSlotKind::SamplerCso, "SamplerCso", "C (clientsp)", /*maxInFlight=*/1u,
PipeSlotKind::SamplerCso, SlotSpace::Ordinary, "SamplerCso", "C (clientsp)",
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
GLuint sampler = 0;
glGenSamplers(1, &sampler);
@@ -2015,7 +2123,8 @@ void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
// shader moves per round.
int round = 0;
AssertChurnReturnsEverySlot(
PipeSlotKind::ShaderCso, "ShaderCso", "C (clientsp)", /*maxInFlight=*/1u,
PipeSlotKind::ShaderCso, SlotSpace::Ordinary, "ShaderCso", "C (clientsp)",
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
const std::string fs = "#version 330 core\nout vec4 oColor;\nvoid main() { "
"oColor = vec4(0.0, 1.0, 0.0, 1.0) + vec4(" +
@@ -2079,12 +2188,19 @@ void main() {
)";
int round = 0;
AssertChurnReturnsEverySlot(
PipeSlotKind::ShaderCso, "ShaderCso (pipeline composites)", "C (clientsp)",
// TWO in flight: a round creates two stage programs and the composite the pipeline
// flattens them into, and the two stage programs are ordinary ShaderCsos of the
// same kind. The composite is the third, and it is the one whose two release paths
// this case is about.
/*maxInFlight=*/3u,
PipeSlotKind::ShaderCso,
// THE BAND'S OWN COUNTERS, not the ordinary ShaderCso space's (review F-M4,
// contract-v2.md 4.3/7.6). A round creates THREE ShaderCsos - the two stage
// programs and the composite the pipeline flattens them into - and the two stage
// programs are ORDINARY slots. So the ordinary space moves in this case whether
// or not a composite ever comes back, the "nothing was ever minted" skip would
// not fire, and every assertion below would have been a statement about the two
// stage programs while the band - the double-free-refusal case the band exists to
// police - went unread.
SlotSpace::CompositeBand, "ShaderCso (pipeline composites)", "C (clientsp)",
// ONE in flight, because in the BAND a round holds exactly one slot: the
// composite. (Against the ordinary space the answer would have been three.)
/*maxInFlight=*/1u,
[&](bool checkPixels, const std::function<void()>& observe) {
// A DIFFERENT FRAGMENT STAGE PER ROUND: the composite is keyed on
// ProgramPipelineObject::ComputeDrawProgramSignature(), the per-stage
@@ -48,6 +48,20 @@
// that the refusal is NAMED and that the run then produces the same pixels as any other
// lane: a refusal that half-ran, or that aborted, would both be failures here.
//
// refused-texture (MOBILEGL_PIPE_PUSH=0x5ff = bits 0..8 plus bit 10, texture resources, WITHOUT
// bit 11)
// D-K2's FOURTH row (ID-15), and the direction the brief originally called harmless.
// MGPTextureParams::BuiltinSampler is a SamplerCso HANDLE and only bit 11 mints sampler
// CSOs, so with bit 10 alone every set_texture_params would carry a null there and the
// applier's Fatal{ProtocolCorruption} is the next thing that happens. Same two assertions
// as the lane above, with the two bits' roles swapped.
//
// both refusal lanes
// "NAMED" means ONE LINE of the library's log, at ERROR severity, that says it REFUSED and
// names both bits. Not a substring anywhere in the file: the word "sampler" appears in
// almost any log the sampler path writes to, and an assertion that cannot go red for its
// stated reason is worse than no assertion (review F-M6).
//
// every lane
// THE PIXELS MUST NOT MOVE. The workload draws one solid-colour quad through a texture, an
// explicit sampler object and a user framebuffer, and every lane must read back that colour.
@@ -72,6 +86,7 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>
@@ -97,6 +112,12 @@ namespace MGITest {
constexpr const char* kLaneOn = "on";
constexpr const char* kLaneOff = "off";
constexpr const char* kLaneRefused = "refused";
// D-K2's FOURTH row (ID-15): bit 10 without bit 11. 0x5ff is 0x1ff plus bit 10.
constexpr const char* kLaneRefusedTexture = "refused-texture";
bool LaneIsARefusalLane(const std::string& lane) {
return lane == kLaneRefused || lane == kLaneRefusedTexture;
}
constexpr int kInset = 2;
constexpr int kTextureSize = 4;
@@ -134,6 +155,70 @@ void main() { oColor = texture(uTex, vUv); }
return lane != nullptr ? std::string(lane) : std::string();
}
// ---- reading the refusal out of the library's own log ------------------------------
//
// THE UNIT IS A LINE, AND THE LINE HAS TO BE THE REFUSAL (review F-M6). The first cut of
// this asked whether the WHOLE FILE contained a lowercase "sampler" and whether it
// contained "texture resource", anywhere, in any order, at any severity. Both are true of
// almost any log the moment the sampler path says anything at all, so the assertion could
// not go red for the reason it claims and the one P4a control that is not vacuous before
// the emitters land would have been vacuous too.
//
// What is matched instead is one line that is ALL of:
// * at ERROR severity - the library writes "[<time>] [<os> <thread>/<TAG>]: <message>",
// one record per line (MG_Util/Debug/Log.cpp), and D-K2 asks for an MGLOG_E. A refusal
// that was demoted to a D or a W is a refusal an operator's log will not carry;
// * carrying the word REFUS(ING) - so a line that merely mentions the two bits (a future
// summary, a comment echoed into the log) is not mistaken for the decision;
// * naming the bit that was SET and the bit it NEEDED, on that same line.
//
// Espryt's text is one MGLOG_E from the helper the three dependent families share
// (Managers.cpp, PipeSubsystemDependencyMissing): "MGPipe: <A> (bit N) is set but <B>
// (bit M) is clear; <why> - REFUSING the dependent bit and running the legacy arm. Set
// both bits, or clear both". Three spellings are accepted per bit - the constant's name,
// "(bit N)", and the hexadecimal mask - so the assertion pins the DECISION and not the
// prose around it.
bool LineNamesTheBit(const std::string& line, const std::vector<std::string>& spellings) {
for (const std::string& spelling : spellings) {
if (line.find(spelling) != std::string::npos) return true;
}
return false;
}
// The matching line, or an empty string. Returned rather than a bool so the case can print
// what it found: a reader of a green refusal lane must be able to see the sentence.
std::string FindTheRefusalLine(const std::string& log,
const std::vector<std::string>& bitThatWasSet,
const std::vector<std::string>& bitThatWasNeeded) {
std::size_t pos = 0;
while (pos <= log.size()) {
const std::size_t newline = log.find('\n', pos);
const std::string line = log.substr(
pos, newline == std::string::npos ? std::string::npos : newline - pos);
const bool atErrorSeverity = line.find("/ERROR]") != std::string::npos;
const bool saysItRefused = line.find("REFUS") != std::string::npos ||
line.find("refus") != std::string::npos;
if (atErrorSeverity && saysItRefused && LineNamesTheBit(line, bitThatWasSet) &&
LineNamesTheBit(line, bitThatWasNeeded)) {
return line;
}
if (newline == std::string::npos) break;
pos = newline + 1;
}
return std::string();
}
// The three accepted spellings of each of the two P4a bits this file's two refusal lanes
// are about. MGPipe.h: bit 10 = kMGPipeSubsystemTextureResources = 0x400,
// bit 11 = kMGPipeSubsystemSamplers = 0x800.
std::vector<std::string> SamplerBitSpellings() {
return {"kMGPipeSubsystemSamplers", "(bit 11)", "0x800"};
}
std::vector<std::string> TextureResourceBitSpellings() {
return {"kMGPipeSubsystemTextureResources", "(bit 10)", "0x400"};
}
class ObjectSubsystemControlScenario : public ScenarioTest {
protected:
void SetUp() override {
@@ -305,12 +390,13 @@ void main() { oColor = texture(uTex, vUv); }
if (!Ready()) return;
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/true);
if (IsSkipped()) return;
if (m_lane == kLaneRefused) {
GTEST_SKIP() << "the refusal lane runs ASamplerBitWithoutTheTextureBitIsRefusedAndNamed "
"instead: at 0x9ff the sampler subsystem is refused at bring-up, so "
"the emission counts are neither the on-lane's nor the off-lane's and "
"asserting either would be reading a third arm as if it were one of "
"the two.";
if (LaneIsARefusalLane(m_lane)) {
GTEST_SKIP() << "the refusal lanes run their own case instead (0x9ff -> "
"ASamplerBitWithoutTheTextureBitIsRefusedAndNamed, 0x5ff -> "
"ATextureBitWithoutTheSamplerBitIsRefusedAndNamed): a refused "
"subsystem's emission counts are neither the on-lane's nor the "
"off-lane's, and asserting either would be reading a third arm as "
"if it were one of the two.";
}
BindDefaultFramebuffer();
@@ -379,9 +465,9 @@ void main() { oColor = texture(uTex, vUv); }
<< window.line;
} else {
FAIL() << "unknown " << kLaneMarker << " value '" << m_lane
<< "': the arms are on / off / refused. Reading an unrecognised name as "
"either would make this lane assert the other arm's expectation while "
"claiming to test this one.";
<< "': the arms are on / off / refused / refused-texture. Reading an "
"unrecognised name as any of them would make this lane assert another "
"arm's expectation while claiming to test this one.";
}
// ... and the picture is the same whichever arm ran.
@@ -469,23 +555,26 @@ void main() { oColor = texture(uTex, vUv); }
<< "the library wrote nothing to " << PipeStatsWindow::LibraryLogPath()
<< ", so the refusal cannot be read back. MOBILEGL_LOG_FILE_PATH is the only channel "
"this module has for the library's own report.";
// Named, not merely present: the refusal has to say which bit it refused AND which bit
// it needed, because "a sampler bit was ignored" without the dependency is a message an
// operator cannot act on. Both spellings are accepted - the constant's name and the
// hexadecimal mask - so the assertion does not pin the message's wording.
const bool namesTheSampler = log.find("Sampler") != std::string::npos ||
log.find("sampler") != std::string::npos ||
log.find("0x800") != std::string::npos;
const bool namesTheTexture = log.find("TextureResources") != std::string::npos ||
log.find("texture resource") != std::string::npos ||
log.find("0x400") != std::string::npos;
EXPECT_TRUE(namesTheSampler && namesTheTexture)
// ONE LINE, at ERROR severity, saying it refused and naming BOTH bits. See
// FindTheRefusalLine: a substring search over the whole file cannot go red for the
// reason this case claims (F-M6).
const std::string refusal =
FindTheRefusalLine(log, SamplerBitSpellings(), TextureResourceBitSpellings());
EXPECT_FALSE(refusal.empty())
<< "MOBILEGL_PIPE_PUSH=0x9ff sets the sampler subsystem (bit 11) without the texture "
"resource subsystem (bit 10) it depends on, and the library's log names neither "
"of them. D-K2 requires ONE MGLOG_E naming both bits and a fall back to the "
"legacy sampler arm; a mask that is silently half-honoured is the failure this "
"case exists to catch, and it is invisible in the pixels by construction. The log "
"was " << log.size() << " bytes.";
"resource subsystem (bit 10) it depends on, and no single ERROR line of the "
"library's log both says it REFUSED and names the two bits. D-K2 requires ONE "
"MGLOG_E naming both and a fall back to the legacy sampler arm; a mask that is "
"silently half-honoured is the failure this case exists to catch, and it is "
"invisible in the pixels by construction. Accepted spellings per bit are the "
"constant's name, '(bit 11)' / '(bit 10)', and '0x800' / '0x400'. The log was "
<< log.size() << " bytes and is at " << PipeStatsWindow::LibraryLogPath() << ".";
if (!refusal.empty()) {
// Printed on the pass as well: a reader of a green refusal lane must be able to
// see the sentence the lane went green on.
std::cout << "[ ObjectSubsystemControl ] refusal line: " << refusal << std::endl;
RecordProperty("refusal_line", refusal.c_str());
}
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
image.Height() - kInset, "green", 0.0,
@@ -497,5 +586,110 @@ void main() { oColor = texture(uTex, vUv); }
ReleaseTheWorkload();
}
// ------------------------------------------------------------------------------------
// D-K2's FOURTH dependency row, in the OTHER direction: bit 10 without bit 11 (ID-15).
//
// 0x5ff is bits 0..8 plus bit 10 (texture resources) and WITHOUT bit 11 (samplers).
//
// WHY THIS IS A REFUSAL AND NOT THE "FINE" MIRROR PAIR THE BRIEF ORIGINALLY CALLED IT.
// BRIEF-P4A.md's D-K2 says "bit 10 without bit 11 is fine", and that sentence is wrong for
// P4a AS BUILT: MGPTextureParams carries a BuiltinSampler, which is a SamplerCso HANDLE,
// and only bit 11 mints sampler CSOs - c0b's four unconditional mints deliberately exclude
// that kind (contract-v2.md), and package C content-addresses them through its own cache
// (ID-14). With bit 10 set and bit 11 clear every set_texture_params would therefore carry
// a NULL BuiltinSampler, which the applier treats as Fatal{ProtocolCorruption} (wire H1),
// and minting it client-side in the arm that exists to exclude samplers was rejected. So
// the dependency is real and it has to be refused at bring-up, exactly like bit 11 without
// bit 10 above and bit 8 without bit 7 one phase earlier. ID-15 puts the refusal in the
// texture family's Resolve*SubsystemArm - package D's Managers.cpp - and this case is the
// pin that says it is there.
//
// ON A TREE WHOSE BACKEND DOES NOT HONOUR P4a's MASK THIS SKIPS, NAMED, exactly as the
// 0x9ff case does and for the same reason: a backend that never reads the four constants
// cannot refuse a dependency between two of them, and reporting the absence of an
// unimplemented subsystem as a failure is what ID-2 forbids. Once the backend DOES name
// them the case is a hard pin, which is the point - if D's texture-family resolver honours
// the mask and does not carry this row, this entry is where that shows.
// ------------------------------------------------------------------------------------
TEST_F(ObjectSubsystemControlScenario, ATextureBitWithoutTheSamplerBitIsRefusedAndNamed) {
if (!Ready()) return;
// needsTheEmitters=false, for the 0x9ff case's reason: a bring-up decision made from
// the bitmask alone is assertable before any emitter exists.
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false);
if (IsSkipped()) return;
if (m_lane != kLaneRefusedTexture) {
GTEST_SKIP() << "runs only in the texture-side refusal lane "
"(MOBILEGL_PIPE_PUSH=0x5ff): every other lane configures a mask "
"whose dependencies are satisfied or a different refusal, so there "
"is nothing here to find and a search for one would report a "
"healthy lane as red.";
}
{
const std::string& backend = Gl().BackendName();
const std::string marker =
"MGITEST_HANDLE_REKEY_OBJECTS_" + (backend == "DirectVulkan"
? std::string("DirectVulkan")
: std::string("DirectGLES"));
if (!BuildMarkerIsSet(marker.c_str())) {
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
"MobileGL/MG_Backend/"
<< backend
<< " names any of kMGPipeSubsystem{Framebuffer, TextureResources, "
"Samplers, Programs}, so this backend does not honour P4a's mask "
"and cannot refuse a dependency inside it. D-K2's fourth row "
"(bit 10 requires bit 11, ID-15) lives in the texture family's "
"Resolve*SubsystemArm beside the bit-11-requires-bit-10 and "
"bit-8-requires-bit-7 refusals, which is P4a package D's file; "
"this control arms itself when that lands. The lane itself is "
"not wasted: the library came up under 0x5ff, which on a tree "
"with no P4a arm is P3a's mask plus one inert bit, and a mask "
"that aborted a bring-up would have failed this entry before "
"the skip.";
}
}
BindDefaultFramebuffer();
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
RunTheWorkload();
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR))
<< "the workload left a GL error behind on the refused lane, which would mean the "
"refusal did not fall back cleanly to the legacy arm";
const Image image = ReadPixels(Gl().Width(), Gl().Height());
Gl().EndFrame();
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
ASSERT_FALSE(log.empty())
<< "the library wrote nothing to " << PipeStatsWindow::LibraryLogPath()
<< ", so the refusal cannot be read back. MOBILEGL_LOG_FILE_PATH is the only channel "
"this module has for the library's own report.";
// The same line shape as the 0x9ff arm, with the two bits' roles swapped: the bit that
// was SET is the texture-resource one and the bit it NEEDED is the sampler one.
const std::string refusal =
FindTheRefusalLine(log, TextureResourceBitSpellings(), SamplerBitSpellings());
EXPECT_FALSE(refusal.empty())
<< "MOBILEGL_PIPE_PUSH=0x5ff sets the texture resource subsystem (bit 10) without "
"the sampler subsystem (bit 11) that MGPTextureParams::BuiltinSampler depends on, "
"and no single ERROR line of the library's log both says it REFUSED and names the "
"two bits. Only bit 11 mints sampler CSOs, so every set_texture_params emitted "
"under this mask would carry a null BuiltinSampler and the applier's Fatal is the "
"next thing that happens - which is why this pair is a refusal at bring-up and "
"not the harmless mirror of the 0x9ff one. Accepted spellings per bit are the "
"constant's name, '(bit 10)' / '(bit 11)', and '0x400' / '0x800'. The log was "
<< log.size() << " bytes and is at " << PipeStatsWindow::LibraryLogPath() << ".";
if (!refusal.empty()) {
std::cout << "[ ObjectSubsystemControl ] refusal line: " << refusal << std::endl;
RecordProperty("refusal_line", refusal.c_str());
}
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
image.Height() - kInset, "green", 0.0,
"the sampled draw [refused-texture]"))
<< "the refused configuration did not draw what every other lane draws. A refusal is "
"supposed to run the LEGACY arm, which is the arm that ships in a pull build - so "
"the pixels are the one thing it may not change.";
ReleaseTheWorkload();
}
} // namespace
} // namespace MGITest
@@ -324,11 +324,37 @@ void main() { oColor = texture(uTex, vUv); }
<< "tex[jobs=] must be at least tex[emit=]: a box emission is one driver upload job "
"and a rect-list emission is N. " << window.line;
// 3. the two sides agree, WHEN THERE ARE TWO SIDES. On the P4a contract tree the client
// emits nothing (the emit headers are the contract's stubs), so ctu= is zero and the
// honest reading is "one side only", recorded and not asserted - never a divergence
// reported against a client that has not been written yet.
if (clientEmissions > 0) {
// 3. the two sides agree, WHEN THERE ARE TWO SIDES - and "there are two sides" is
// answered by the BUILD, not by the number (review F-m7).
//
// ctu= IS ALWAYS PRESENT IN A PUSH BUILD: PipeStats.cpp writes the field whether or
// not anything ever incremented the counter, so `clientEmissions > 0` conflated
// three different trees - "no client emitter exists", "the emitter exists and
// emitted nothing", and "the counter was not published at all" - into one branch
// that asserts nothing and prints a sentence that is only true of the first. Once
// package B's texture emitter lands, an emitter that STOPPED emitting would read
// exactly like no emitter at all and this case would have gone green over it, which
// is the failure mode the whole scenario exists to make impossible.
//
// So the discriminator is MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT, the
// build's own content probe for a MG_Impl/Pipe source that emits
// CallClass::ClientTextureUploadEmissions - the same mechanism as every other arming
// decision in this package - and each side of it asserts something real.
const bool clientEmitterExists =
BuildMarkerIsSet("MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT");
ASSERT_GE(clientEmissions, 0)
<< "the summary line carries no ctu= field at all, in a push build, where PipeStats "
"publishes it unconditionally. The client half of the comparison cannot be read: "
<< window.line;
RecordProperty("client_emitter_present", clientEmitterExists ? 1 : 0);
if (clientEmitterExists) {
EXPECT_GT(clientEmissions, 0)
<< "a MG_Impl/Pipe source emits CallClass::ClientTextureUploadEmissions on this "
"tree, and the SERVER counted " << emissions
<< " texture upload emissions for this workload, but the client counted NONE. An "
"emitter that has stopped emitting reads exactly like no emitter at all in "
"this field, which is why this case asks the build rather than the number. "
<< window.line;
EXPECT_EQ(clientEmissions, emissions)
<< "the CLIENT counted " << clientEmissions
<< " texture upload records and the SERVER counted " << emissions
@@ -338,10 +364,20 @@ void main() { oColor = texture(uTex, vUv); }
"on Mali when it goes the wrong way. "
<< window.line;
} else {
std::cout << "[ TextureUploadShape ] the client side reads zero: no P4a client "
"emitter has landed on this tree, so this run records the SERVER shape "
"only. That is the expected reading on the contract tree and it is not "
"a divergence."
// Not merely "not asserted": on a tree with no client emitter the counter must be
// ZERO, and a non-zero one would mean the probe is looking for the wrong symbol -
// i.e. that the arming decision above is wrong and every future run of this case
// is mis-armed.
EXPECT_EQ(clientEmissions, 0)
<< "no MG_Impl/Pipe source emits CallClass::ClientTextureUploadEmissions on this "
"tree, yet the client counted " << clientEmissions
<< " of them. Something is incrementing that counter which this build's probe "
"cannot see, so the probe is looking for the wrong symbol and this case's "
"arming decision is unreliable in both directions. " << window.line;
std::cout << "[ TextureUploadShape ] no P4a client emitter has landed on this tree "
"(the build's ClientTextureUploadEmissions probe found none), so this "
"run records the SERVER shape only and pins ctu=0. That is the expected "
"reading on the contract tree and it is not a divergence."
<< std::endl;
RecordProperty("client_side", "absent");
}