- Fatal{PipeLegacyMemosDisabled} was raised from InitDisplayAndContext(), i.e. from inside
eglMakeCurrent. The integration harness pre-flights that exact sequence in a forked child
(MG_IntegrationTest/Harness/HeadlessGL.cpp) and reports a child that dies on a signal as
"no usable GPU/display/ICD", so every scenario SKIPPED and ctest called the lane 100%
passed while running nothing - on the very pair of env vars the D14/D18 A/B is driven
with. ROADMAP.md:7 forbids a gate that cannot go red for the reason it exists.
- The arm decision becomes a pure function of the two knobs, ClassifyEsprytSlotArm(), with
three verdicts. Bring-up now calls DiagnoseEsprytSlotArm(), which names both knobs at
ERROR and RETURNS; the stop stays in ResolveEsprytSlotTablesArm(), which the inline latch
reaches at the first twin lookup - a scenario body, where a crash is a test failure.
- A process that never looks a twin up never needs an arm and is no longer stopped by one
it would not have used. That is the only behaviour this moves.
- SanityTest gains an always-on case: the four knob combinations of the pure classifier,
that the diagnosis does not stop, and that the stop is SIGABRT whose log line names
PipeLegacyMemosDisabled, MOBILEGL_PIPE_PUSH, MOBILEGL_PIPE_LEGACY_MEMOS=0 and the bit -
the message and not merely the signal, because "Subprocess aborted" alone tells an
operator nothing. It skips visibly in the pull and no-legacy builds (G2 name parity).
- UnitBindingsSnapshot was split by #if MOBILEGL_PIPE_PUSH, so a push build ran P2's
lifetime-id debounce on the MOBILEGL_PIPE_PUSH=0 arm too. That arm has to reproduce P1
(ConfigLoader.cpp), or the integrator's A/B measures this slice's mechanism on both sides
and attributes it to neither - the same complaint g_fbSlotCache was already fixed for. The
snapshot now carries P1's WeakPtr fields beside the lifetime ids whenever the legacy arm
is compiled, and Capture/Unchanged pick by EsprytSlotTablesEnabled(). The two answers are
equivalent (OwnerEquals on two empty pointers is true and LifetimeIdOf(nullptr) == 0 == 0;
a live-versus-expired control block and two distinct lifetime ids both compare unequal),
so this is A/B fidelity, not a behaviour change, and a build with no legacy arm carries
neither the fields nor the branch.
- TwoTablesOfTheSameKindShareOneSlotAndKeepTheirOwnTwin deliberately never swept, so it left
a live MGPipeKind::Query slot behind for good, and ObjectChurnAloneDrivesTheSweep reads
HighWater/LiveCount of that same process-global kind. Deltas made them pass today, but
--gtest_shuffle or a third case on kind Query would have made them interact. The
two-holder case now has a kind to itself and returns its slot at the end.
- Its comment claimed two live tables of one kind "cannot arise outside this case". They
can and do: ScopedDirectGLESTextureBindings holds a second live table of kind Texture, and
package D's subsystem 4 re-keys VaoDrawMemo out of the same per-kind allocator. The
comment now states the real hazard (whichever holder frees first orphans the other's
entry; safe, because Free is generation-guarded and FindByHandle compares Gen, but not
free) and flags it for the integrator.
- P2 step e2, as far as the file-ownership table lets one package take it. New frontend
header MG_State/GLState/StateObjectDeathNotice.h carries BufferBackendOps' shape for the
other six kinds: an ops table the backend fills in, and one entry point that takes
{kind, lifetimeId} rather than the object, because by the time the last SharedPtr has
dropped there is no object left to pass and the lifetime id is exactly what the client
slot allocator resolves a handle from. Declared only under MOBILEGL_PIPE_PUSH, so the
pull build's symbol set is untouched.
- BackendSlotTable::DestroyByLifetimeId drops the twin and returns the slot at the moment
the object goes, instead of at the next sweep - which for a renderbuffer or a texture
atlas is the difference between freeing the driver allocation now and freeing it 64
creations from now. It returns the slot only when THIS table holds it: two holders of one
kind already exist (the ScopedDirectGLESTextureBindings fixture; Magma's subsystem-4
table shares the VertexElementsCso kind), and a table that never twinned the object must
not free a slot the other one still names. The legacy arm keys on the frontend heap
address, cannot answer a notice at all, and keeps the sweep - which is the announced-
versus-discovered half of the A/B the compile-time arm exists for.
- Managers.cpp registers one dispatcher for all six kinds from ResolveEsprytSlotTablesArm(),
i.e. exactly when the arm that can answer a notice is the arm that runs, and drops a
notice that arrives after exit() has begun.
- FIRING it needs a destructor per class, and the P2 ownership table gives
{Texture,Framebuffer,Sampler,VertexArray}State/* to other packages, so only ProgramObject
and RenderbufferObject raise it here. The other four still rely on the sweep; their four
one-line calls retire it entirely.
- Three cases pin the three halves: the slot comes back with no sweep and the notice is
idempotent and does not free another holder's slot; a program and a renderbuffer announce
their own death when the last SharedPtr drops and not before; and the handle arm actually
installs a consumer, rather than the two halves each being fine on their own.
- SanityTest never calls MG_ConfigLoader::Init, so MG_Config::Features.PipePush kept its
static default of 0 - the value a PULL build ships - and every case in the binary took
StateBackendObjectRegistry's legacy UnorderedMap arm. In build-linux, build-push AND
build-verify alike, the whole D13 "must not break" list (the scratch-FBO scrub, the three
context-generation guards on the texture/framebuffer/renderbuffer twins, the sampled-set
staleness walk and the whole-registry ScopedDirectGLESTextureBindings fixture) was
therefore evidence about code this package did not change; a gdb breakpoint on
MGPipeSlotAllocator::Acquire was the only way to see it.
- A gtest global Environment now seeds Features.PipePush with ConfigLoader's own push-build
default, so the binary runs the arm that SHIPS in the build it was compiled for: legacy in
build-linux (where the handle arm is not compiled and every DirectGLESSlotTable case skips
visibly), handles in build-push and build-verify. MOBILEGL_PIPE_PUSH in the environment
overrides it with ConfigLoader's decimal/0x contract, so the legacy-arm run of the same
binary is one env var. It is an Environment and not a static initializer because
MG_Config::Features has a String member and is dynamically initialised.
- TheTwinRegistryCasesInThisBinaryRunOnTheHandleArm is that gdb probe made falsifiable:
delete the environment and it goes red naming the arm rather than a symptom.
- ObjectChurnAloneDrivesTheSweep churns 256 objects through GetOrCreate and never calls
CollectGarbage{IfNeeded,Now}: nothing but the creation tick can collect them. Verified
red - it is the only case that fails - when the creation-driven sweep is neutered, and
green again on restore. The churn count is a fixed constant rather than a multiple of the
interval so that a negative control which pushes the interval out of reach makes the case
FAIL instead of running for 2^32 iterations.
- TwoTablesOfTheSameKindAgreeOnOneObjectsHandle could not fail: HandleOf never reads the
table, so any two tables agree for any implementation. Replaced by a case that asserts
what is actually load-bearing - ONE slot of the kind is consumed however many tables hold
a twin of the object (which is what makes Magma's subsystem-4 table resolve the same
handle for the same VAO), and the shared handle still addresses each table's own twin.
- GetOrCreateToleratesANullStateObject pins the release-build behaviour the map arm had.
- The three names are registered as visible GTEST_SKIPs in the pull build, like the five
before them, so the pull and push builds keep listing the same ctest entries.
- Five cases against BackendSlotTable directly, through a stand-in state object that carries
only GetLifetimeId(), so none of them needs a GLContext, a driver or ES entry points.
- The load-bearing one is the ABA: an object dies, the sweep returns its slot, the next
object takes the same slot with a moved generation, and the predecessor handle answers
null instead of the successor twin. That is the property the address key could only paper
over with a weak_ptr.
- Gen moves on reuse and only on reuse; Find never mutates the table (which is what lets
SyncTextureObjectToBackend drop its second Find); a whole table saves, resets with = {} and
restores, which is the shape ScopedDirectGLESTextureBindings needs; and two tables of one
kind agree on a single object handle, because the identity comes from the client allocator
rather than from either table.
- ScopedPipeVerb (MG_Test/ScopedPipeVerb.h): an RAII "as if we were inside verb X" object
that runs the real MGPipeFillForVerb for the verb it names, so the eleven unit entries
that construct a GLContext by hand and call a backend helper with no GL entry point in
between stop reading an empty, unstamped PipeInputs block
- it weakens nothing: it fills exactly that verb class's may-read mask, so a read outside
it is still Fatal{UnmigratedPipeInput} naming the field and the declared verb; leaving
the scope re-arms the poison with a one-field kQuery fill, so one case's declaration
cannot cover a later one when the binary runs as a single process
- placed the way MGP_FILL is placed in production: immediately before the backend call,
after every frontend mutation it is meant to see; a second call after the test moved
state is a second verb (Renew()), a helper of another class gets a nested scope
- no-op in the pull build, no test renamed, no test added, no production source touched
Field report: on Espryt with a BSL Iris pipeline built, every glyph in the
game died - HUD, menu labels, even the vanilla title screen after leaving
the world - while sprites kept rendering. Captured on-device (FCL apitrace
rig), reproduced headlessly on llvmpipe, and pinned with a three-way replay:
the same trace renders full text on raw Mesa desktop GL and on Magma, so
the stream was intact and the execution was Espryt's.
MECHANISM. WithTemporarilyBoundNamedTexture implements the by-name (DSA)
texture entry points by binding the named texture onto the active unit's
real slot, running the bound-texture code, and restoring - without moving
the texture bind generation on either edge. DirectGLES's per-draw texture
sync memo keys on that generation and BORROWS the slot pointer, so a memo
built for texture A kept passing every key while a by-name call had
texture B sitting in the slot: A's backend twin was driven with B's
frontend object, and SyncMipmapsToBackend re-specified A's storage with
B's shape. In the trace, a by-name upload to a BSL 2048x2048 map while
the 16x16 lightmap was bound re-specified the lightmap's GL texture
2048x2048-NULL and back 16x16-NULL. The lightmap exists only as render
output - no glTexSubImage2D ever touches it - so it stayed zero forever,
and rendertype_text (vertexColor = Color * texelFetch(lightmap, ...)),
alpha-discards every glyph. Background quads never sample the lightmap,
which is why only text died.
FIX, class-level, two layers:
- Frontend (shared, closes the same hole for DirectVulkan's generation-
keyed memos): the temporary bind and the restore each bump the texture
bind generation (only when the slot actually changed), and the restore
is an RAII scope guard so a throwing body can no longer leak the
temporary binding - a second latent bug of the same class. Deliberately
a generation bump and not a touched-unit note: the high-water mark must
not chase by-name calls, and a completed bind/restore pair leaves the
content epoch unchanged, so the cost is an owner-compare re-walk, not a
memo rebuild.
- DirectGLES defense in depth: both borrowed-pair memos
(g_unitTextureSyncList, g_fboTextureSyncList) record which frontend
texture each backend twin was paired with and re-check it before any
replay (last in the key conjunction, behind the context-id compare). A
stale pairing now costs a list rebuild instead of silent cross-texture
storage corruption.
Tests, both red with their own layer reverted:
TextureTest.NamedTextureCallKeepsUnitBindingAccountingCoherent (the
accounting contract) and DirectGLESTextureSync.UnitMemoRefusesToDriveA-
TwinFromAnotherTexture (the corrupting sequence shape against a mock GLES
table, asserting the resident texture's storage is never re-specified).
595/595 unit at default and with the async kill switch. Replay evidence:
the captured BSL ESC-menu trace renders all text through Espryt post-fix,
byte-comparable to the Mesa-direct and Magma replays; the no-shaderpack
control is unchanged. A trace fixture wiring this scene into CI follows
in a separate commit.
Both AdvertisesVoxyRequiredRenderingExtensions cases pinned TargetGLVersion at
3.3, which was the reported version until V_OpenGL40 joined the advertised
extension lists. The version assertion is incidental to what these cases are
for - Voxy needs the individual ARB extensions, not a version - so it just
tracks the new report instead of holding the old one.
Two unit tests asserted behaviour the conformance tests had since contradicted, so they
were testing MobileGL's old answer rather than GL's.
QueryTest expected glIsQuery to report a name straight out of glGenQueries as a query
object. It is not one: GenQueries reserves names, and they "acquire query state only when
they are first used by calling BeginQuery" (GL 4.6 core 4.2.1). The test now checks that a
reserved name reads FALSE, that BeginQuery is what turns it into an object, and that a
sibling name left untouched stays FALSE. A companion case covers the direct state access
half, where glCreateQueries does create the object outright - which is the whole reason the
two entry points both exist.
The DirectGLES binding test built its texture with glGenTextures and glBindTexture and
nothing else, then expected BindCurrentTextures to bind it natively. A texture with no
image is incomplete and samples as (0, 0, 0, 1), which DirectGLES expresses by leaving the
native target unbound, so the setup no longer produced the binding the test then went on to
clear. It now gives the texture a format and a 1x1 level 0 - one level is the entire mip
chain at that size, so it is complete under any filter - and asserts that directly, so a
future completeness change fails on the setup line instead of on the assertion three calls
later.
ctest -L unit had been failing 13 of its 418 cases, all of them tests left asserting
what the code did before a commit that changed it on purpose:
- "restore target GL version to 3.3" put the advertised target back after the
experimental 4.6 run, but the two Voxy sanity tests still demanded 4.6. The
extensions they really care about are all still advertised, so assert 3.3 and drop
the now-meaningless AtExperimentalCTSVersion from their names.
- "support rectangle textures where the emulation is exact" made every desktop-only
target supported - rectangle included, stored as a plain 2D - while the texture
test still expected rectangle to be rejected.
- "keep declared modern GLSL versions strict" changed two things at once: a
normalized legacy directive now carries a marker on its line, so the ten tests
matching "#version 330 core\n" whole no longer match; and a version the
application declared itself is no longer raised to 460, so the sources declaring
330/400 keep their own number and only MobileGL's own normalization is retargeted.
Test expectations follow, rather than the implementation being bent back: each of
the three changes is the intended behaviour and is argued for where it was made. The
retry test now drives the 460 escalation from a legacy "#version 130" source, which
is the only thing that is still rescued, and gained a case pinning the other half of
that contract - an application-declared "#version 330" stays at 330.
418/418 unit tests pass.
Store the primitive restart index as render state and report it through
glGetIntegerv(GL_PRIMITIVE_RESTART_INDEX), replacing the stub and the
hardcoded 0 in the getter.
- RenderState gains a PrimitiveRestartIndex field (default 0) with
set/get accessors and GLContext wrappers.
- glPrimitiveRestartIndex accepts any GLuint and generates no error.
- glGetIntegerv(GL_PRIMITIVE_RESTART_INDEX) now reads the stored value.
This is the state layer only. The backends do not yet honor an arbitrary
restart index at draw time -- Vulkan and GLES support only the fixed
all-ones restart value (GL_PRIMITIVE_RESTART_FIXED_INDEX) -- so a non-
default index is tracked and queryable but not yet applied to indexed
draws.
Tests: RenderStateSanity round-trip (default 0, mid value, and the full
32-bit range). Full SanityTest sweep green (31/31).
Adreno/Qualcomm report a huge maxPerStageDescriptorSampledImages, and the
per-stage texture-unit limits were clamped only to the combined array capacity
(TextureState::MAX_TEXTURE_IMAGE_UNITS = 192). glGetIntegerv thus advertised 192
for GL_MAX_TEXTURE_IMAGE_UNITS, but host code treats it as an array bound:
Minecraft's Blaze3D GlStateManager.TEXTURES[] holds 128 entries and Iris iterates
[0, GL_MAX_TEXTURE_IMAGE_UNITS) over it in CompositeRenderer.renderAll, throwing
ArrayIndexOutOfBoundsException: Index 128 out of bounds for length 128.
Introduce MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32 (desktop-driver value) and clamp
the per-stage sampler limits to it in both backends (DirectGLES previously did not
clamp at all), keeping the combined limit at the array capacity. Update SanityTest.
Promote the color writemask to per-draw-buffer state and implement the
indexed glColorMaski entry point (previously a stub), plus its read-back
through glGetBooleani_v.
- RenderState: replace the single BoolVec4 ColorMask with an array of
MAX_DRAW_BUFFERS masks, all initialized to true. SetColorMask now
broadcasts to every draw buffer (glColorMask semantics); GetColorMask
returns draw buffer 0. Add indexed set/get accessors + GLContext
wrappers.
- glColorMaski sets only the addressed draw buffer; out-of-range index
raises GL_INVALID_VALUE (buf is a GLuint, so no GL_INVALID_ENUM path),
mirroring the indexed blend entry points' MAX_DRAW_BUFFERS bound.
- glGetBooleani_v(GL_COLOR_WRITEMASK, i) reports draw buffer i's four
booleans; the non-indexed glGetBooleanv still reports draw buffer 0.
- Fix GLboolean coercion in the color-mask path: any nonzero value
enables the component (was == GL_TRUE, which wrongly rejected e.g. 2).
- DirectGLES sync reads ColorMasks[0] (GLES core has only non-indexed
glColorMask).
Tests: ColorMaskIndexedStoresAndReadsBack covers the per-buffer vs
broadcast semantics, buffer-0 read-back, out-of-range INVALID_VALUE, and
the GLboolean coercion (mutation-verified: == GL_TRUE fails it). Full
SanityTest sweep green (30/30).
Fill the two empty // TODO state handlers with GL 3.3 Core-conformant
behavior, backed by new RenderState fields and glGet* read-back.
glClampColor:
- Accept only GL_CLAMP_READ_COLOR (compat GL_CLAMP_VERTEX/FRAGMENT_COLOR
rejected); clamp is one of GL_TRUE / GL_FALSE / GL_FIXED_ONLY. Note the
Khronos man page wrongly omits GL_FIXED_ONLY from the accepted set, but
it is legal AND the default, so it is accepted here.
- Default GL_FIXED_ONLY; both error paths are GL_INVALID_ENUM with no
state change. glGetIntegerv returns the raw tri-state enum; GetFloatv/
GetDoublev widen it and GetBooleanv converts nonzero to GL_TRUE via the
existing fall-through, so one GetIntegerv case serves every getter.
glPolygonMode:
- Core accepts only face == GL_FRONT_AND_BACK (GL_FRONT/GL_BACK were
removed in 3.1 core); mode is GL_POINT / GL_LINE / GL_FILL. Both errors
are GL_INVALID_ENUM with no state change.
- Keep separate front/back slots so GL_POLYGON_MODE round-trips its two
values (identical under a core context). The raster effect (VkPolygonMode
+ fillModeNonSolid) remains a backend follow-up; this is the state layer.
Tests: two RenderStateSanity round-trips; the glClampColor GL_FIXED_ONLY
acceptance assertion is mutation-verified (rejecting it fails the test).
Full SanityTest sweep green (29/29).