mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38bdab4e2 | ||
|
|
6035c9d7f8 | ||
|
|
419f9941b5 | ||
|
|
72b4c91a45 | ||
|
|
ccb7b6b331 | ||
|
|
e4579ee80e | ||
|
|
0d01405cf8 | ||
|
|
dcc31e95ca | ||
|
|
4678519f99 | ||
|
|
4614abb9c6 | ||
|
|
7107d0f47c | ||
|
|
972dd811d7 | ||
|
|
f9c0e7ec43 | ||
|
|
b938c83b6f | ||
|
|
e62abff6b6 | ||
|
|
9a0a7f7608 | ||
|
|
993ce0fb76 | ||
|
|
ea38cfcb99 | ||
|
|
9c3e8ffd32 | ||
|
|
2f35014d73 | ||
|
|
8c8d460e14 | ||
|
|
b6e5c2bb39 | ||
|
|
246434bb35 | ||
|
|
41b7905000 | ||
|
|
2e03b82a67 | ||
|
|
2bebbbdcb7 | ||
|
|
500a8e462a | ||
|
|
be939e2cfe | ||
|
|
68d09bb585 | ||
|
|
5e7fb18c4a | ||
|
|
74d2daea1b | ||
|
|
7afa2f18d3 | ||
|
|
64332060fd | ||
|
|
96dca5a8ba | ||
|
|
e24e30bbec | ||
|
|
929d162594 | ||
|
|
0b12f89f3b | ||
|
|
31e6a7d6d8 | ||
|
|
29bd4d2c94 | ||
|
|
588b2277c5 | ||
|
|
1e8cdc5931 | ||
|
|
a632e26e47 | ||
|
|
ee31944bce | ||
|
|
245daca06a | ||
|
|
29d51ab98b | ||
|
|
17396216e0 | ||
|
|
a666201708 | ||
|
|
71a174a531 | ||
|
|
83a209bbd4 | ||
|
|
832620ddeb | ||
|
|
25e1a8483b | ||
|
|
2bd8f3c82d | ||
|
|
e021b4f62b | ||
|
|
a3bd7f0d7d | ||
|
|
5046b3fc89 | ||
|
|
5e5dc36e57 | ||
|
|
2bd19d5ee2 | ||
|
|
33f5e878e7 | ||
|
|
568d587ae7 | ||
|
|
602326f379 | ||
|
|
0338812cf3 | ||
|
|
09e0a83cee | ||
|
|
771e8e06a1 | ||
|
|
2d2090cf1c | ||
|
|
a11a5eb2af | ||
|
|
612dbac44f | ||
|
|
92dffb81b4 | ||
|
|
79b581984b | ||
|
|
d6e52f75c3 | ||
|
|
ac09e5b37a | ||
|
|
820d1a60b9 | ||
|
|
f0f8cab682 | ||
|
|
b2edca2728 | ||
|
|
ccde29064a | ||
|
|
bc2aad2176 | ||
|
|
5a3e9f0857 | ||
|
|
1534cf3784 |
@@ -601,7 +601,26 @@ jobs:
|
||||
# only in the pull `integration` job, where a MOBILEGL_PIPE_PUSH value steers nothing
|
||||
# (Config.h declares the field inside the push guard) and both arms are the same legacy path.
|
||||
# A lane that cannot go red where it is installed is not a gate (ROADMAP.md:7).
|
||||
- name: The handle-ABA, CSO and P3a subsystem controls (G8, G10, G12)
|
||||
# P4a ADDS THREE ALTERNATIVES, and each one is here because this is the only CI job that
|
||||
# unpacks a push build:
|
||||
# * `ObjectSubsystem` reaches the three DirectGLES.ObjectSubsystemControl. entries - the
|
||||
# 0x1fff-vs-0x1ff A/B and the 0x9ff dependency refusal (G12). `ResourceSubsystem` does
|
||||
# NOT match it: the two families are named apart on purpose, because they are different
|
||||
# phases' switches and a filter that merged them would hide one behind the other.
|
||||
# * `TextureParamsWithoutASamplerView` reaches G9's four cases, the scenario ROADMAP.md:20
|
||||
# names by hand. It runs in the ambient lanes, which the label already selects - but this
|
||||
# step is where those cases run against a PUSH library, and G9's whole claim is about the
|
||||
# push path. All four cases are green on the contract commit - including the one D-E3
|
||||
# expected to be red, for the reason the scenario's header records - so this row is green
|
||||
# from the day it lands and goes red only if a reachability path stops syncing texture
|
||||
# parameters at all, which is the coupling ARCHITECTURE.md:100 exists to remove.
|
||||
# * `TextureUploadShape` is RECORDED, not gated (D-D4): it asserts that the two upload-shape
|
||||
# counters could be read and that they agree, and prints the shape for MEASUREMENTS.md. It
|
||||
# is in the filter so that the number is actually collected on every run - an unmeasured
|
||||
# shape is not a recorded one - and because its own assertions can go red.
|
||||
# As with the four before them, each alternative is the SHORTEST string that selects only what
|
||||
# it means to.
|
||||
- name: The handle-ABA, CSO, subsystem and texture-parameter controls (G8, G8b, G9, G10, G12)
|
||||
working-directory: build-verify
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
@@ -612,7 +631,7 @@ jobs:
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
ctest --output-on-failure -L integration-gpu \
|
||||
-R 'HandleRecycle|CsoContentAddressing|ResourceSubsystem|MapPersistentRoundtrip' \
|
||||
-R 'HandleRecycle|CsoContentAddressing|ResourceSubsystem|MapPersistentRoundtrip|ObjectSubsystem|TextureParamsWithoutASamplerView|TextureUploadShape' \
|
||||
--no-tests=error -j 4
|
||||
|
||||
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
|
||||
@@ -771,7 +790,16 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y clang-20 libx11-dev
|
||||
|
||||
- name: Include-closure assertions and negative control
|
||||
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all
|
||||
# --expect-probes 4 (contract-v2.md 7.6): an exit code cannot tell four probes from none,
|
||||
# so a --probe typo or a manifest edit that selected nothing would run zero probes and
|
||||
# exit 0 - the second half of the finding that added the flag. The count is the length of
|
||||
# scripts/check_include_closure.py's PROBES list and changing one means changing the other.
|
||||
#
|
||||
# --compiler stays clang++-20, which is what the step above installs (Debian's clang-20
|
||||
# package ships /usr/bin/clang++-20). It is deliberately NOT the bare `clang++` the local
|
||||
# campaign gate spells: that spelling exists because the WSL box has no clang++-20, and
|
||||
# copying it here would trade a version-pinned compiler for whatever the runner has.
|
||||
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all --expect-probes 4
|
||||
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1557,12 +1585,18 @@ jobs:
|
||||
# Deliberately independent of build-linux: these are source-level gates, they take
|
||||
# seconds, and a broken build must not hide a drifted interface.
|
||||
env:
|
||||
# P3a's base ref, for the G5 region gate below. It is 44c2b5cf and NOT the workflow's
|
||||
# baseline_sha input: that input is the SYMBOL baseline (087685d1, P1's G1 reading) and it
|
||||
# is empty on a push, whereas this gate asks "did the buffer pool, the deferred-release
|
||||
# drain and the three rings move since the phase started". INTEGRATOR-DECISIONS ID-1 is
|
||||
# where the ref comes from.
|
||||
BASELINE: "44c2b5cf"
|
||||
# THE CURRENT PHASE's base ref, for the two G5 region gates below. It is 37da3c3a - P4a's
|
||||
# base ref, INTEGRATOR-DECISIONS ID-1 - and NOT the workflow's baseline_sha input: that input
|
||||
# is the SYMBOL baseline (087685d1, P1's G1 reading) and it is empty on a push, whereas these
|
||||
# gates ask "did the do-not-touch list move since the phase started".
|
||||
#
|
||||
# IT MOVED FROM P3a's 44c2b5cf TO P4a's 37da3c3a WITH THE PHASE, and that is a deliberate
|
||||
# narrowing rather than a loss: P3a's eleven functions were compared against 44c2b5cf at P3a's
|
||||
# own exit and were byte-identical there, so 37da3c3a carries the same bodies (measured: the
|
||||
# eleven shas at 37da3c3a are the eleven shas at 44c2b5cf, and FlushPendingRangesFrom's is
|
||||
# still the sha pinned in the script at 3e298c9a). What the two gates now both answer is "did
|
||||
# anything on the list move during P4a", which is the question this phase can act on.
|
||||
BASELINE: "37da3c3a"
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -1664,6 +1698,39 @@ jobs:
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p3a_untouched_regions.sh --self-test
|
||||
|
||||
# A GATE AS OF P4a (G5), and a SECOND script rather than an edit to the one above. P4a extends
|
||||
# the same claim to the rest of ARCHITECTURE.md:318's do-not-touch list - the unpack PBO ring's
|
||||
# staging repack and its two helpers, the attachment permutation, the D24S8 sampling-emulation
|
||||
# core and the format-caveat handler - which is SEVENTEEN regions across THREE files
|
||||
# (BRIEF-P4A.md D-N: P3a's eleven, which P4a must not touch either, plus P4a's six). The
|
||||
# parent's SOURCE_PATH is a single file, so the extension needed a per-region source path and
|
||||
# a region KIND (DepthStencilSamplingReadImpl is a namespace, not a function); everything else
|
||||
# about the extraction is its parent's, verbatim.
|
||||
#
|
||||
# Both scripts run. The parent keeps answering its own question against its own eleven, so a
|
||||
# regression in either half names itself, and neither gate can be silenced by editing the
|
||||
# other's list.
|
||||
#
|
||||
# Same feat/disaggregated-or-dispatch guard as the P3a step, for the same reason: the question
|
||||
# is "did these move since the phase started", and on dev - where unrelated buffer and texture
|
||||
# fixes land on their own schedule - it would be a question nobody posed. It belongs with the
|
||||
# TEMPORARY trigger lines at the top of this file and retires with them.
|
||||
#
|
||||
# --self-test is the half that keeps it honest and is not optional: a comparison that silently
|
||||
# stopped comparing produces exactly the same green as seventeen untouched regions. It runs
|
||||
# three positive controls (seventeen regions extracted, an untouched copy compared equal, an
|
||||
# edit OUTSIDE them invisible in all three files) and FOUR negative ones - ClearBufferPool,
|
||||
# FlushPendingRangesNow, RecomputeBackendColorSlots and StageBlocksIntoUnpackRing, each
|
||||
# perturbed on its own and each required to be named BY NAME - and fails if any of them does
|
||||
# not answer.
|
||||
- name: The unpack ring, the attachment permutation, the D24S8 core and the format caveat did not move (G5)
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p4a_untouched_regions.sh "${BASELINE}" HEAD
|
||||
|
||||
- name: The P4a untouched-region gate can still fail (G5)
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p4a_untouched_regions.sh --self-test
|
||||
|
||||
# Warning only for now: the disaggregation documents are still being written, and a
|
||||
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
|
||||
# --strict when the documents settle.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,11 @@
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P3a: the vertex-input payload views the handle arm of the VAO twin consumes.
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
// P4a: the RECORDS the five re-keyed twins read instead of the frontend object. The readers
|
||||
// below hand back pointers to them, and MGPipeResourceRecord::PendingUpload is a nested type,
|
||||
// so a forward declaration would not do. Push-only, like everything else P4a adds to this
|
||||
// header, so the pull build's include graph is unchanged (D-P).
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -428,6 +433,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// P4a (D-B1): resolve-or-create BY THE HANDLE THE CALL CARRIED. This is the shape P3a
|
||||
// already runs for the buffer family through BackendBufferResourceTable, lifted onto
|
||||
// the five registries that still mint their own handles off a frontend lifetime id -
|
||||
// the debt SlotTables.h records against itself at the top of that file.
|
||||
//
|
||||
// POINTER, not the reference GetOrCreate(StatePtr) returns, and that is deliberate:
|
||||
// this call has THREE ways to decline and every one of them has to be visible to the
|
||||
// caller rather than answered with a parked twin.
|
||||
// * the legacy arm is running, so there is no slot table to index;
|
||||
// * the slot is past the table's sanity bound (a corrupt 32-bit slot must not decide
|
||||
// a vector resize);
|
||||
// * the generation is BEHIND the live entry's. SlotTables.h:301-321 is the whole
|
||||
// argument: forward is a recycle and resets the twin, BACKWARD is refused, because
|
||||
// adopting it would destroy the incumbent LIVE twin's driver ids and then stamp the
|
||||
// slot back to the dead object's generation - the shape commit d7655247 fixed.
|
||||
// The refusal is SILENT here and gets its release-build voice at the per-kind resolver
|
||||
// in Managers.cpp, exactly as GetOrCreateBufferResourceForHandle gives P3a's.
|
||||
BackendPtr* GetOrCreateByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (!EsprytSlotTablesEnabled()) return nullptr;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
if (handle.Slot >= SlotTable::kMaxHandleSlot) return nullptr;
|
||||
const Uint32 liveGen = m_slotTable.LiveGenAt(handle.Slot);
|
||||
if (liveGen != 0 && liveGen > handle.Gen) return nullptr;
|
||||
return &m_slotTable.GetOrCreate(handle);
|
||||
}
|
||||
|
||||
// The generation of the LIVE entry at this slot, or 0. It exists so a caller can
|
||||
// DIAGNOSE, in a release build where MOBILEGL_ASSERT is inert, the refusal above
|
||||
// performs silently.
|
||||
Uint32 LiveGenAt(Uint32 slot) const {
|
||||
if (!EsprytSlotTablesEnabled()) return 0;
|
||||
return m_slotTable.LiveGenAt(slot);
|
||||
}
|
||||
|
||||
// NO ReleaseByHandle HERE, AND THAT IS A DECISION (review M-4). The death half of
|
||||
// GetOrCreateByHandle exists for a kind whose announcement is its own destroy CALL
|
||||
// rather than the shared death notice - which is the BUFFER family
|
||||
// (BackendBufferResourceTable::ReleaseByHandle, SlotTables.h, called from
|
||||
// resource_destroy) and none of the five kinds this registry serves: every one of them
|
||||
// dies through DestroyByLifetimeId below, because P4a adds no server-side destroy arm
|
||||
// for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader CSO. v1
|
||||
// declared one here anyway and it had no caller on either arm, which made its bound and
|
||||
// its wording things nobody would exercise until P5. The one-line wrapper comes back in
|
||||
// the commit that gives it a caller; SlotTable::ReleaseByHandle underneath is untouched
|
||||
// and is what SanityTest drives directly.
|
||||
|
||||
// P2 step e2. STATIC, because a death notice is about an object and not about a
|
||||
// registry instance: it is answered by EVERY table of this kind that exists - this
|
||||
// registry's own, and any by-value copy of it a fixture or a context reset is holding
|
||||
@@ -552,6 +603,121 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
using TwinRegistry = StateBackendObjectRegistry<StateObject, BackendObject>;
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P4a (D-K3): one arm resolver per family, beside BufferImpl's two ----
|
||||
//
|
||||
// Four bits and therefore four resolvers, for P3a's reason one level out: a framebuffer
|
||||
// path that regressed, a texture path that regressed, a sampler path that regressed and a
|
||||
// program path that regressed are four different findings, and clearing one must not
|
||||
// disarm the other three.
|
||||
//
|
||||
// THE RESOLUTION IS LAZY, at the first use, and never at bring-up. Backend context creation
|
||||
// runs inside eglMakeCurrent and the integration harness pre-flights exactly that sequence
|
||||
// in a FORKED CHILD; a child that dies on a signal is reported as "no usable GPU" and every
|
||||
// scenario in the lane is SKIPPED - the lane goes green having run nothing, on the very
|
||||
// pair of env vars the A/B is driven with, which is what ROADMAP.md:7 forbids. So a stop
|
||||
// has to land in a test body, i.e. at the first lookup. That is what the inline latches
|
||||
// below give: a guard-variable load and a perfectly-predicted branch per consult, and the
|
||||
// arm dispatch folds into the caller (SlotTables.h's EsprytSlotTablesEnabled argument
|
||||
// verbatim - every one of these is consulted on the per-draw path).
|
||||
//
|
||||
// ALL FOUR CAN REACH NoArm and all four STOP there rather than skipping green, because
|
||||
// every one of the four legacy arms is compiled under MOBILEGL_PIPE_LEGACY_MEMOS:
|
||||
// framebuffer - the four g_fboSynced* arrays and StampSyncedFBO;
|
||||
// texture - the twin's m_prevTextureInfo / m_syncedContentVersion cheap-gate trio;
|
||||
// samplers - UnitSamplerLookupMemo's WeakPtr arm and SamplerPassMemo's raw
|
||||
// BackendSamplerObject* rows;
|
||||
// programs - g_programTwinLookupMemo.
|
||||
//
|
||||
// THREE OF THEM CARRY A DEPENDENCY (MGPipe.h, D-K2) and it is diagnosed and REFUSED here
|
||||
// rather than half-run, the bit-8-requires-bit-7 shape ResolveVertexInputSubsystemArm
|
||||
// already ships: bit 11 requires bit 10, bit 9 requires bit 10, bit 10 requires bit 7. The
|
||||
// mirror pairs (10 without 11, 10 without 9, 7 without 10) are all FINE and are said so out
|
||||
// loud, because an unreachable branch that says something different is how the reachable
|
||||
// one drifts. Bit 12 depends on nothing: a ShaderCso handle names no texture and no buffer.
|
||||
//
|
||||
// ResolveFramebufferSubsystemArm additionally carries D-C3's bring-up refusal: the wire
|
||||
// array is MGPFramebufferState::Color[8] and GetDynamicParameters().MaxColorAttachments is
|
||||
// the driver's RAW ES cap, which is not clamped to 8 on this path. A driver reporting more
|
||||
// would silently truncate the record, so the bit is refused with one MGLOG_E naming the cap
|
||||
// and the legacy arm runs. Widening the payload is a wire change nobody has evidence for;
|
||||
// truncating silently is the bug class this phase is closing.
|
||||
Bool ResolveFramebufferSubsystemArm();
|
||||
Bool ResolveTextureResourceSubsystemArm();
|
||||
Bool ResolveSamplerSubsystemArm();
|
||||
Bool ResolveProgramSubsystemArm();
|
||||
|
||||
inline Bool FramebufferSubsystemEnabled() {
|
||||
static const Bool enabled = ResolveFramebufferSubsystemArm();
|
||||
return enabled;
|
||||
}
|
||||
inline Bool TextureResourceSubsystemEnabled() {
|
||||
static const Bool enabled = ResolveTextureResourceSubsystemArm();
|
||||
return enabled;
|
||||
}
|
||||
inline Bool SamplerSubsystemEnabled() {
|
||||
static const Bool enabled = ResolveSamplerSubsystemArm();
|
||||
return enabled;
|
||||
}
|
||||
inline Bool ProgramSubsystemEnabled() {
|
||||
static const Bool enabled = ResolveProgramSubsystemArm();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// ---- P4a: what the twins read INSTEAD of the frontend object ----
|
||||
//
|
||||
// One reader per record kind, all const, all null-on-miss, and all bounds-checked against
|
||||
// the applier's own dense table rather than against a constant: a slot at or above the
|
||||
// table's size simply has no record, which is the same answer as "not live" and is not a
|
||||
// protocol error on THIS side (the applier already refused and counted the call that would
|
||||
// have created it - PipeApply.h's RefusedObjectCalls).
|
||||
//
|
||||
// A NULL ANSWER IS NOT A FALL-BACK TO THE FRONTEND. On a family's handle arm, quietly
|
||||
// reaching into the frontend object again would hide a missing record behind a picture that
|
||||
// still looks right, which is exactly what the subsystem A/B exists to expose
|
||||
// (MarkBufferGpuWritten's note, P3a). Every caller below either declines the work with a
|
||||
// named MGLOG_E_ONCE or runs its family's LEGACY arm, decided by the family latch and
|
||||
// never per record.
|
||||
//
|
||||
// The returned pointer is into a Vector the applier may grow, so it is valid only until the
|
||||
// next applier call - the same rule the legacy arm's map-into pointers carried, and every
|
||||
// caller here reads what it needs and lets go.
|
||||
const MG_Pipe::MGPipeResourceRecord* PipeTextureRecordForHandle(MG_Pipe::MGPipeHandle res);
|
||||
const MG_Pipe::MGPipeResourceRecord* PipeRenderbufferRecordForHandle(MG_Pipe::MGPipeHandle res);
|
||||
const MG_Pipe::MGPipeSamplerCsoRecord* PipeSamplerCsoRecordForHandle(MG_Pipe::MGPipeHandle cso);
|
||||
const MG_Pipe::MGPipeSamplerViewRecord* PipeSamplerViewRecordForHandle(MG_Pipe::MGPipeHandle view);
|
||||
// ShaderCso is the one kind whose slot space is split in two on the CLIENT side - the
|
||||
// composite band lives in its own dense table so a single program-pipeline composite does
|
||||
// not grow a 983040-entry vector (contract D13). The server never learns a handle is a
|
||||
// composite: this reader hides the split behind one lookup, exactly as the wire does.
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* PipeShaderCsoRecordForHandle(MG_Pipe::MGPipeHandle cso);
|
||||
|
||||
// The pending-upload entry the applier accumulated for this (uploadTarget, level) of this
|
||||
// texture record, or null (D-D5). SERVER-SIDE STATE, and that is the whole point: the
|
||||
// client clears its own dirty flags at EMISSION for the levels the applier accepted, while
|
||||
// Espryt's upload loop has bail arms - an incomplete texture returns early, a multisample
|
||||
// target refreshes and skips - that today leave the frontend flag set. A naive move of the
|
||||
// clear to the client would lose exactly those texels. The set survives any number of
|
||||
// bails; ConsumePipeTextureUpload below is called ONLY where the level actually uploaded.
|
||||
//
|
||||
// `uploadTarget` is static_cast<Uint16>(MobileGL::TextureUploadTarget) - the HALF, not the
|
||||
// packed field. The stored key is MGPSubData::Target whole (low byte MGPipeResourceTarget,
|
||||
// high byte TextureUploadTarget, ID-12) and both functions decode it with
|
||||
// MGPipeSubDataUploadTargetOf; they are the only two places this package compares it.
|
||||
const MG_Pipe::MGPipeResourceRecord::PendingUpload* FindPipeTextureUpload(
|
||||
const MG_Pipe::MGPipeResourceRecord& record, Uint16 uploadTarget, Uint16 level);
|
||||
void ConsumePipeTextureUpload(MG_Pipe::MGPipeHandle res, Uint16 uploadTarget, Uint16 level);
|
||||
|
||||
// THE SERVER'S OWN RE-DIRTY, armed in the applier's set instead of in the frontend's model
|
||||
// (esprytobj review M-1). `packedTarget` is a full MGPSubData::Target built with
|
||||
// MGPipePackSubDataTarget, because the entry this writes has to be indistinguishable from
|
||||
// one the client emitted. Whole-level, no regions, merged with any entry already there;
|
||||
// false (and one named log line) when the applier's pending set is at its bound. The three
|
||||
// server-side MarkStorageDirty sites are enumerated at the definition.
|
||||
Bool RearmPipeTextureLevelUpload(MG_Pipe::MGPipeHandle res, Uint16 packedTarget, Uint16 level,
|
||||
const MG_Pipe::MGPBox& wholeLevel);
|
||||
#endif
|
||||
|
||||
namespace BufferImpl {
|
||||
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
|
||||
|
||||
@@ -1514,6 +1680,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// single-level texture with a mipmapping filter), and an incomplete texture samples
|
||||
// (0, 0, 0, 1) rather than its contents.
|
||||
Bool m_forceSamplerResync = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P4a's handle arm: the two prologues that decide WHETHER there is work and
|
||||
// WHERE the values come from. Both answer null for "nothing to do", which covers
|
||||
// three cases the caller treats identically and the callee names individually in
|
||||
// the log: the record's serial has not moved, this texture has no record at all,
|
||||
// or its params name a sampler CSO the applier does not hold.
|
||||
//
|
||||
// NEITHER FALLS BACK TO THE FRONTEND. On this arm the texture family is switched
|
||||
// over, and quietly re-reading the object would hide a missing record behind a
|
||||
// picture that still looks right - which is precisely what the subsystem A/B exists
|
||||
// to expose (MarkBufferGpuWritten's note, P3a).
|
||||
const SamplerParameters* ResolvePushedBuiltinSampler(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// Hands back the whole RECORD rather than its Params, because the parameter push
|
||||
// reads two things from beside them: Desc.InternalFormat, which decides the two
|
||||
// channel-widening swizzle compositions, and Params.BuiltinSampler, which is where
|
||||
// the border colour lives (it is sampler state, GL 4.6 table 23.18, and P4a does
|
||||
// not duplicate it onto MGPTextureParams).
|
||||
const MG_Pipe::MGPipeResourceRecord* ResolvePushedTextureParams(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
|
||||
// ---- P4a's handle arm (D-B3). Three server-owned serials that REPLACE, on their
|
||||
// own arm, the six frontend-version memos above; the legacy members stay beside
|
||||
// them under MOBILEGL_PIPE_LEGACY_MEMOS because ARCHITECTURE.md:369 keeps the
|
||||
// pre-handle arm compiled through P3a/P4a, and because clearing the family's bit
|
||||
// has to run the pre-handle arm rather than a half-migrated one.
|
||||
//
|
||||
// Inside the guard, so the PULL build's BackendTextureObject is byte-for-byte the
|
||||
// pre-P4a object and G1's admitted-resize set stays empty (D-P).
|
||||
//
|
||||
// 0 is never a real serial - the applier's counters start at 1 and only ever
|
||||
// advance, including across a make-current (PipeApply.cpp's three-way argument) -
|
||||
// so a zeroed memo is a guaranteed miss and a fresh twin owes a full sync.
|
||||
|
||||
// The resource record's Serial at the last completed mipmap sync. It replaces the
|
||||
// whole cheap-gate trio (m_syncedShapeContextId / m_syncedShapeGeneration /
|
||||
// m_syncedShapeParamsVersion) AND m_syncedContentVersion: the applier bumps it on
|
||||
// every respecify and every sub-data it applies to this resource, which is exactly
|
||||
// the union those four covered, without the coarse "any texture's churn re-opens
|
||||
// every gate" behaviour the sampling-resolution generation had.
|
||||
Uint64 m_syncedResourceSerial = 0;
|
||||
// The record's ParamsSerial at the last SyncTextureParamsToBackend. Replaces
|
||||
// m_syncedTextureParamsVersion; MGPTextureParams::ForceResync replaces
|
||||
// m_forceTextureParamsResync and is consumed the same way - read, acted on, and
|
||||
// NOT written back, because the client never clears a server flag and the server
|
||||
// never clears the client's (D-E2, the D-D5 inversion applied to two bits).
|
||||
Uint64 m_syncedParamsSerial = 0;
|
||||
// The BuiltinSampler CSO record's Serial at the last SyncBuiltinSamplerToBackend,
|
||||
// plus the handle it was read through - a texture whose params name a DIFFERENT
|
||||
// CSO than last time has had its sampling state replaced wholesale even if the new
|
||||
// CSO's serial happens to match, which is a real sequence under content addressing
|
||||
// (two textures sharing one CSO, then one of them diverging).
|
||||
Uint64 m_syncedBuiltinSamplerSerial = 0;
|
||||
MG_Pipe::MGPipeHandle m_syncedBuiltinSampler = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
};
|
||||
|
||||
void ActivateTextureUnit(Uint unit);
|
||||
@@ -1606,11 +1827,64 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// mismatch means some backend texture id was re-minted since, and any of this
|
||||
// twin's attachment points may still hold the dead id even though the frontend
|
||||
// attachment versions match - so the walk re-attaches everything first.
|
||||
//
|
||||
// SERVER-OWNED AND IT SURVIVES P4a (D-B3). It answers "did *I* re-mint a driver
|
||||
// texture id", which no client-side version can answer; dropping it would
|
||||
// reintroduce exactly the class of bug commit d7655247 fixed on the buffer side.
|
||||
Uint64 m_syncedBackendIdGeneration = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-C4): MGPFramebufferState::ContentHash as of this twin's last sync, PER
|
||||
// TARGET IT WAS SYNCED AS, and it is the second of the hash's two jobs - "the server's
|
||||
// render-pass memo key, and the CLIENT's emission suppressor". It replaces
|
||||
// m_syncedFrontendAttachmentVersions AS A KEY (the array stays: it is what the
|
||||
// legacy arm compares, and it is the mechanism the handle arm re-arms through).
|
||||
//
|
||||
// The hash covers every field the record carries - Fbo included, so a recycled
|
||||
// framebuffer handle whose successor happens to carry an identical attachment set
|
||||
// can never be suppressed against its predecessor, and DrawBuffers[8] included, so
|
||||
// a suppressed record provably means the draw-buffer array did not move, which
|
||||
// provably means the fragColor broadcast count did not move.
|
||||
//
|
||||
// PER TARGET rather than one, and it stays that way under ID-19's per-OBJECT record:
|
||||
// there is now ONE record for this framebuffer, but syncing it as Draw and syncing it
|
||||
// as Read do different work (glDrawBuffers and the four cross-object masks are
|
||||
// Draw-only, glReadBuffer is Read-only), so "I have already applied this record" is a
|
||||
// per-target claim and one memo would let the second target skip work the first never
|
||||
// did. 0 is never a live hash (a computed 0 is remapped to 1 by the client's
|
||||
// suppressor), so a zeroed memo is a guaranteed miss.
|
||||
Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> m_syncedRecordHashes = {0};
|
||||
#endif
|
||||
};
|
||||
|
||||
extern TwinRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject, MG_Pipe::MGPipeKind::Framebuffer>
|
||||
g_backendFramebufferObjects;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-C2 as corrected by ID-19): the applier's record for THE FRAMEBUFFER OBJECT this
|
||||
// handle names, or null.
|
||||
//
|
||||
// v1 asked the applier for its two BOUND-target working records and answered null unless
|
||||
// one of them happened to name this twin - which meant every DSA entry point
|
||||
// (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) drove a framebuffer that is
|
||||
// bound to neither target, found no record, declined, and then had the clear or blit
|
||||
// issued against a driver FBO that never got its attachments. The record is now keyed by
|
||||
// the framebuffer HANDLE (MGPipeApplierState::FramebufferRecords, wire v3), so a record
|
||||
// that comes back is this framebuffer's by construction and it comes back whether the
|
||||
// object is bound to Draw, to Read, to both or to neither. A null here means "no emission
|
||||
// has ever described this framebuffer, or the handle's generation is stale" - both of
|
||||
// them seam defects on an integrated tree, never a binding question.
|
||||
//
|
||||
// `fbo` is the handle the caller resolved for this twin; passing it in rather than
|
||||
// resolving it here keeps the monolith-glue lookup at one site per sync.
|
||||
const MG_Pipe::MGPFramebufferState* PushedFramebufferRecord(MG_Pipe::MGPipeHandle fbo);
|
||||
|
||||
// THE BINDING QUESTION, WHICH IS NOW A DIFFERENT QUESTION FROM THE DESCRIPTION (ID-19(d)):
|
||||
// "is the framebuffer this handle names the one bound to `target`". One array compare
|
||||
// against MGPipeApplierState::BoundFramebuffer, never a record lookup - a Named record
|
||||
// describes an object without claiming any binding for it, so asking the record would
|
||||
// give the wrong answer by construction.
|
||||
Bool PushedFramebufferIsBoundTo(FramebufferTarget target, MG_Pipe::MGPipeHandle fbo);
|
||||
#endif
|
||||
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
|
||||
// backend actually stores in a floating-point format. GL clamps a read from a
|
||||
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
|
||||
@@ -1704,6 +1978,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the driver FBO would keep the deleted texture name attached forever. The
|
||||
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
|
||||
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
|
||||
//
|
||||
// AND WHENEVER AN ATTACHABLE OBJECT'S DRIVER STORAGE IS REDEFINED IN PLACE (P4a fable
|
||||
// seam F-3): a mutable texture regenerated on the same id, a renderbuffer re-storaged
|
||||
// on the same id. The id did not move, but the four cross-object masks SyncToBackend
|
||||
// computes from the attachment's format did, and nothing else the FBO memo reads sees
|
||||
// a respecify of an attached object. So "did I change something under an attachment
|
||||
// point that no frontend version can tell the framebuffer about" is what this counts,
|
||||
// and the re-mint is one case of it. The in-place bumps are compiled under
|
||||
// MOBILEGL_PIPE_PUSH: G1 keeps the pull library byte-identical to the P4a baseline,
|
||||
// so the pull build keeps the pre-P4a hole until they land on dev on their own.
|
||||
extern Uint64 g_attachmentBackendIdGeneration;
|
||||
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
|
||||
// target; part of the synced tuple above.
|
||||
@@ -1718,6 +2002,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GL_FRAMEBUFFER binds both targets.
|
||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// THE HANDLE ARM'S OWN FRAMEBUFFER MEMOS, AND THEY ARE PACKAGE E's STORAGE
|
||||
// (DirectGLES.cpp: g_fboSyncedSerials, g_fboRecordsTrusted). E's review MAJOR-4 handed
|
||||
// this to D because InvalidateFramebufferBindingCache is in THIS file and has three
|
||||
// callers E cannot reach - MG_Test/SanityTest.cpp's ScopedStateGuardMocks::ResetShadows
|
||||
// and ScopedBackendTwinMocks' constructor and destructor - which clear the pre-handle
|
||||
// trio and would leave the handle-arm memos claiming a target is synced across a GLES
|
||||
// function-table swap. Calling it from INSIDE InvalidateFramebufferBindingCache is what
|
||||
// makes forgetting impossible, and that call is written below.
|
||||
//
|
||||
// IT IS GATED, AND HERE IS THE HANDSHAKE, because the definition is `static` in E's file
|
||||
// on the tree this package was built against (esprytdraw v2, DirectGLES.cpp:2789) and an
|
||||
// internal-linkage function cannot be called from Managers.cpp. E's verification round
|
||||
// drops that one keyword; D's verification round flips this constant to 1, in this file,
|
||||
// one line. Neither side can do it silently: the flip has no other reader and the
|
||||
// declaration below has no other definition.
|
||||
#define MOBILEGL_ESPRYT_FBO_HANDLE_ARM_MEMOS_LINKED 0
|
||||
#if MOBILEGL_ESPRYT_FBO_HANDLE_ARM_MEMOS_LINKED
|
||||
void InvalidateFramebufferHandleArmMemos();
|
||||
#endif
|
||||
#endif
|
||||
void InvalidateFramebufferBindingCache();
|
||||
// A driver framebuffer id is about to be deleted: ES reverts every target that
|
||||
// currently binds it to 0, so the binding shadow has to follow or the next
|
||||
@@ -2009,6 +2314,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stale as one built before a relink - while the sampler half, which really is
|
||||
// re-issued per draw, needs nothing of the sort.
|
||||
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-B3, D-H5): the ShaderCso record's Serial this backend program was built
|
||||
// from. It is what the draw path's nine-clause rebuild condition reads on the handle
|
||||
// arm INSTEAD OF the two frontend versions above - one server-owned counter that
|
||||
// moves on every create_shader_state the applier applies to this handle, including a
|
||||
// RE-create on the same handle, which is how a relink travels (Gen moves only on slot
|
||||
// reuse, never on a respecify).
|
||||
//
|
||||
// THE CLAUSE COUNT DOES NOT SHRINK, and a brief that treated create_shader_state as
|
||||
// self-contained would produce a per-draw rebuild: the other eight inputs - the draw
|
||||
// FBO's snorm/unorm clamp masks, the fragColor broadcast count, the storage-block
|
||||
// binding signature, the atomic-counter set, the live image formats and the patch
|
||||
// parameters - are all still specialised at the verb, from state this backend holds.
|
||||
// 0 means "never stamped", which is a guaranteed miss (applier serials start at 1).
|
||||
Uint64 GetSyncedShaderCsoSerial() const { return m_syncedShaderCsoSerial; }
|
||||
#endif
|
||||
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
||||
// resolve to are still the ones its ESSL was generated against.
|
||||
//
|
||||
@@ -2108,6 +2429,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||
Uint32 m_syncedLinkVersion = ~0u;
|
||||
Uint32 m_syncedImageUnitVersion = ~0u;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a's replacement for the two above on the handle arm; see GetSyncedShaderCsoSerial.
|
||||
// Push-only, so the pull build's object is byte-for-byte the pre-P4a one (D-P).
|
||||
Uint64 m_syncedShaderCsoSerial = 0;
|
||||
#endif
|
||||
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
||||
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
||||
// that declares a format on all of its images, which is the overwhelming majority -
|
||||
@@ -2211,7 +2537,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
~BackendSamplerObject();
|
||||
BackendSamplerObject(const BackendSamplerObject&) = delete;
|
||||
BackendSamplerObject& operator=(const BackendSamplerObject&) = delete;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// THE SAMPLER CSO HANDLE IS CARRIED BY THE CALLER, and it has to be, because a
|
||||
// SamplerCso is CONTENT-ADDRESSED on the client (D-F1) while this twin is keyed on
|
||||
// the frontend OBJECT. g_backendSamplerObjects mints a SamplerCso slot off the
|
||||
// SamplerObject's lifetime id - that handle is this twin's identity and is what
|
||||
// FindByHandle memos index - but the client's cache allocates its handles by
|
||||
// CONTENT (MGPipeSlots().Allocate, SamplerEmit.h), so no create_sampler_state ever
|
||||
// lands at the identity handle and looking a record up by it can only ever miss.
|
||||
// The carried fact that DOES name the right record is the applier's own
|
||||
// MGPipeApplier().BoundSamplerStates[unit], which the client writes per unit at
|
||||
// bind_sampler_states; the caller that knows the unit passes it here.
|
||||
//
|
||||
// Defaulted so a caller that has no unit - the backend's OWN raw-depth-fetch
|
||||
// sampler (DirectGLES.cpp:217), a SamplerObject the client has never seen and for
|
||||
// which no record can exist - keeps working: that arm reads the object, which is
|
||||
// the authority for server-owned state. An APPLICATION sampler reaching here
|
||||
// without a handle is the E-side call-site gap and says so once.
|
||||
//
|
||||
// Push-only spelling on purpose: a defaulted parameter is still part of the
|
||||
// signature, so widening it unconditionally would rename this symbol in the PULL
|
||||
// build and P4a's admitted-change set is EMPTY (D-P/G1).
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject,
|
||||
MG_Pipe::MGPipeHandle pushedCso = MG_Pipe::kMGPipeNullHandle);
|
||||
#else
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
||||
#endif
|
||||
void Bind(Uint unit);
|
||||
Uint GetBackendSamplerId() const;
|
||||
|
||||
@@ -2221,6 +2572,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool m_isInitialized = false;
|
||||
SamplerParameters m_cacheSamplerParameters;
|
||||
Uint16 m_syncedSamplerVersion = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-B3): the SamplerCso record's Serial at the last completed sync. It replaces
|
||||
// m_syncedSamplerVersion, which stays beside it because the pre-handle arm compiles
|
||||
// under MOBILEGL_PIPE_LEGACY_MEMOS through P3a/P4a (ARCHITECTURE.md:369).
|
||||
//
|
||||
// The two are not interchangeable and that is the point: the frontend version is per
|
||||
// OBJECT, while the serial is per CONTENT-ADDRESSED CSO, and two frontend samplers
|
||||
// with identical parameters share one CSO and therefore one serial - so under the
|
||||
// handle arm the second of them costs no driver call at all.
|
||||
//
|
||||
// Push-only, so the pull build's object is byte-for-byte the pre-P4a one (D-P).
|
||||
Uint64 m_syncedSamplerSerial = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
void UnbindSampler(Uint unit);
|
||||
@@ -2229,8 +2593,108 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_boundSamplersCache;
|
||||
extern TwinRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject, MG_Pipe::MGPipeKind::SamplerCso>
|
||||
g_backendSamplerObjects;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a FABLE SEAM F-4: THE TWIN FOR A CONTENT-ADDRESSED SamplerCso HANDLE.
|
||||
//
|
||||
// bind_sampler_states carries, per unit, the handle of a CSO the client allocated BY
|
||||
// CONTENT (SamplerEmit.h: MGPipeSlots().Allocate with no lifetime id), while every twin
|
||||
// in g_backendSamplerObjects was minted off a SamplerObject's lifetime id - two disjoint
|
||||
// slot families out of one allocator. So `g_backendSamplerObjects.FindByHandle(
|
||||
// BoundSamplerStates[unit])` (the record arm of BindCurrentUnitSamplers, E's S4) could
|
||||
// never find a twin, the record arm bound nothing on every draw, and every glBindSampler
|
||||
// reached the driver only through the pre-handle program pass - S-1's confusion one
|
||||
// loop over, and exactly what SamplerEmit.h:201-205 forbids ("a backend must NOT key a
|
||||
// sampler twin on a SamplerObject's lifetime id; the twin's life is
|
||||
// create_sampler_state -> delete_sampler_state").
|
||||
//
|
||||
// This is the twin keyed the way the record is: resolved-or-created AT THE CSO HANDLE
|
||||
// (GetOrCreateByHandle, the same slot table, a slot the identity family can never hold)
|
||||
// and synced from the record it names, serial-gated. Two callers bind it - the record
|
||||
// arm of BindCurrentUnitSamplers and the program pass's sampler override - so the two
|
||||
// cannot ping-pong the unit between an identity twin and a CSO twin. Its death is the
|
||||
// slot's recycle: the client's LRU eviction drops the record and frees the slot, and
|
||||
// the next handout at that slot arrives with a moved generation, which GetOrCreate(
|
||||
// handle) answers by resetting the twin (the driver sampler goes with it). A twin for
|
||||
// an evicted CSO therefore lives until its slot is reused - bounded by the cache's
|
||||
// capacity, never by draw count - and there is no delete_sampler_state hook to retire it
|
||||
// earlier; the ops table carries none for this kind.
|
||||
//
|
||||
// Null, loudly, when the handle names no record (a client seam) or cannot be adopted
|
||||
// (a generation behind the slot's live entry); null silently for the null handle. The
|
||||
// pre-handle arm - a twin keyed on the frontend object - is untouched and still serves
|
||||
// the raw-depth-fetch sampler and every caller that carries no handle.
|
||||
BackendSamplerObject* ResolveSamplerCsoTwin(MG_Pipe::MGPipeHandle cso);
|
||||
#endif
|
||||
} // namespace SamplerImpl
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace SamplerViewImpl {
|
||||
// P4a (D-F2/D-F3): the SIXTH Espryt twin table, and the only one of the six whose kind
|
||||
// has no frontend object at all. MobileGL has no sampler-view class: GL binds a texture
|
||||
// to a unit and the sampler uniform's type, the mipmap-completeness predicates and
|
||||
// IsUndefinedDefaultTexture decide what the shader sees. Gallium's one-view-per-slot IS
|
||||
// that resolved form, the resolution moves to the CLIENT (ARCHITECTURE.md:206), and
|
||||
// create_sampler_view carries the restrictions the resolution had to read.
|
||||
//
|
||||
// So this twin owns NO DRIVER ID. There is nothing in ES to create for a view; the id
|
||||
// the unit binds is the texture's, and it lives on BackendTextureObject. What this twin
|
||||
// is, is the server's MEMO of one resolved view: the record it was built from, keyed on
|
||||
// that record's serial, plus the two BACKEND-SPECIFIC POST-PROCESSINGS
|
||||
// ARCHITECTURE.md:206 keeps on the server and which act on the already-resolved set.
|
||||
// Espryt's is the raw-depth-fetch sampler substitution; Magma's feedback-loop detection
|
||||
// is its own and is not here.
|
||||
//
|
||||
// A twin with no driver id still earns a table: it is what turns "re-derive the
|
||||
// substitution decision for every sampled unit of every draw" into one serial compare,
|
||||
// and it is the slot space the client's per-texture SamplerViewCso handle indexes.
|
||||
// There is deliberately no destructor: nothing here owns a GPU object, so the teardown
|
||||
// sentinel's whole reason (a twin destructor must not call into an unloaded driver)
|
||||
// does not apply and the default one is correct in every teardown order.
|
||||
struct BackendSamplerViewObject {
|
||||
// The view record as last synced, verbatim. Reading it here rather than re-asking
|
||||
// the applier is what lets a caller hold the twin across another applier call.
|
||||
MG_Pipe::MGPSamplerView View{};
|
||||
// The applier record's Serial this memo was built from. 0 = never synced, and 0 is
|
||||
// never a real serial (the applier's counters start at 1), so a zeroed memo is a
|
||||
// guaranteed miss.
|
||||
Uint64 SyncedSerial = 0;
|
||||
// Espryt's post-processing, decided from the RESOLVED set: the view's
|
||||
// InternalFormat answers IsDepthFormatInternalFormat and the sampler CSO record's
|
||||
// SamplerParameters answer compareMode / minFilter / mipmapMode / magFilter. The
|
||||
// decision is re-derived when either serial moves; the sampler serial is kept
|
||||
// beside it so a sampler mutation alone re-derives it.
|
||||
Uint64 SyncedSamplerSerial = 0;
|
||||
Bool NeedsRawDepthFetchSampler = false;
|
||||
};
|
||||
|
||||
// Handle-keyed ONLY, exactly like P3a's BackendBufferResourceTable: the StateObject
|
||||
// parameter names ITextureObject because the template names one and because the view is
|
||||
// minted off the TEXTURE's lifetime id (D-F2: one view per ITextureObject), which is
|
||||
// what makes HandleOf below resolve at all. Not one member that would dereference it is
|
||||
// instantiated - no Find(StateObject*), no ForEachLive - and the handle overloads never
|
||||
// look at it.
|
||||
using BackendSamplerViewTable = BackendSlotTable<MG_State::GLState::ITextureObject,
|
||||
BackendSamplerViewObject,
|
||||
MG_Pipe::MGPipeKind::SamplerViewCso>;
|
||||
extern BackendSamplerViewTable g_backendSamplerViews;
|
||||
|
||||
// Resolve-or-create / resolve-only by the handle the call carried. Neither touches
|
||||
// MGPipeSlots(): the handle ARRIVED, already minted by the side that owns minting.
|
||||
BackendSamplerViewObject* GetOrCreateSamplerViewForHandle(MG_Pipe::MGPipeHandle view);
|
||||
BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view);
|
||||
|
||||
// MONOLITH GLUE, and named as such, the HandleOfBuffer shape: the SamplerViewCso handle
|
||||
// of a texture this backend is looking at through a frontend object. Legal only because
|
||||
// the view is minted off the texture's own lifetime id; under a real split neither the
|
||||
// object nor its lifetime id exists on this side and the handle has to arrive in the
|
||||
// payload (which, for every path P4a switches over, it does - this is for the paths
|
||||
// P3b/P4b still owns).
|
||||
MG_Pipe::MGPipeHandle HandleOfSamplerViewForTexture(
|
||||
const MG_State::GLState::ITextureObject* textureObject);
|
||||
} // namespace SamplerViewImpl
|
||||
#endif
|
||||
|
||||
namespace RenderbufferImpl {
|
||||
class BackendRenderbufferObject {
|
||||
public:
|
||||
@@ -2252,6 +2716,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int m_cacheWidth = 0;
|
||||
Int m_cacheHeight = 0;
|
||||
Int m_cacheSamples = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-D2/D-B3): the resource record's Serial at the last completed allocation.
|
||||
// It replaces the four-field cache above AS A GATE - the four members stay, because
|
||||
// they are also what the legacy arm compares and what the twin reports about the
|
||||
// storage it actually holds - and it closes the publication hole D-D2 names:
|
||||
// RenderbufferObject::{SetInternalFormat, AllocateStorage, SetSamples} bump no
|
||||
// version and raise no notice, so `glBindRenderbuffer; glRenderbufferStorage(new)`
|
||||
// on an ALREADY-ATTACHED renderbuffer moved nothing the framebuffer bit could see.
|
||||
// The client now emits resource_respecify straight from those three mutators, the
|
||||
// applier bumps this serial, and one compare here sees it.
|
||||
//
|
||||
// Push-only, so the pull build's object is byte-for-byte the pre-P4a one (D-P).
|
||||
Uint64 m_syncedResourceSerial = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
extern TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
|
||||
|
||||
@@ -194,10 +194,115 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
//
|
||||
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||
//
|
||||
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||
// RETIRED says nothing about the key that phase SHIPS.
|
||||
//
|
||||
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||
// conclude the knob covers its kind:
|
||||
//
|
||||
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||
// on this tree can produce, which would be a permanently red always-on lane.
|
||||
//
|
||||
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||
return MG_Config::Features.PipeHandleAbaControl;
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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 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");
|
||||
|
||||
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||
//
|
||||
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||
|
||||
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c). This file is the ONLY place every DSA framebuffer entry point lives, and the
|
||||
// emitter it reaches is this package's own header rather than a declaration in one of the
|
||||
// contract's: MG_Pipe/PipeMutation.h is the door MG_State has into the client and carries no
|
||||
// framebuffer row, and MG_Impl/GLImpl and MG_Impl/Pipe are the same layer (this file already
|
||||
// includes MG_Impl/Pipe/PipeFill.h for MGP_FILL).
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#endif
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
@@ -613,6 +621,35 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c): ANY FRAMEBUFFER THE SERVER IS ABOUT TO RECEIVE BY NAME HAS A RECORD.
|
||||
//
|
||||
// The applier keeps framebuffer records PER OBJECT, keyed by the handle - but before
|
||||
// ID-19 it held only the two BOUND-target records, and the emitter only ever built them
|
||||
// at the validate point out of the two bindings. So glClearNamedFramebufferfv(fbo) or
|
||||
// glBlitNamedFramebuffer(..., fbo, ...) on an fbo bound to NEITHER binding reached a
|
||||
// backend that minted a fresh driver framebuffer with no attachments, found no record
|
||||
// for it, declined, and issued the clear against it anyway: GL_INVALID_FRAMEBUFFER_-
|
||||
// OPERATION and nothing cleared, where the legacy arm cleared correctly.
|
||||
//
|
||||
// TWO CLASSES OF SITE call this, and both are "the point at which the object is final
|
||||
// for this call": the five CONSUMERS (blit and the four clears) publish immediately
|
||||
// before MGP_FILL, so the record precedes the verb that hands the object over and a
|
||||
// later bound-target record for the same object still wins; the ten MUTATORS (the DSA
|
||||
// attachment, draw-buffer and read-buffer setters) publish immediately after the
|
||||
// frontend mutation, because they have no validate point at all - FillPoints.def has no
|
||||
// verb for any of them, so there is no MGP_FILL to sit in front of.
|
||||
//
|
||||
// A CALL THAT MOVED NOTHING IS FREE: the record's ContentHash is the emitter's own
|
||||
// suppressor and it is keyed per framebuffer object, so a redundant publish emits zero
|
||||
// bytes. EmitFramebufferByName picks Draw/Read/Both over Named when the object IS
|
||||
// bound, so a Named record can never overwrite a bound record's Target underneath the
|
||||
// binding that resolves through it.
|
||||
void PipePublishFramebufferByName(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo) {
|
||||
if (!fbo) return;
|
||||
MG_Pipe::MGPipeFramebufferEmitterInstance().EmitFramebufferByName(*fbo);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||
@@ -631,6 +668,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(readFramebuffer);
|
||||
PipePublishFramebufferByName(drawFramebuffer);
|
||||
#endif
|
||||
MGP_FILL(BlitNamedFramebuffer);
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
dstY1, mask, filter);
|
||||
@@ -643,6 +684,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfv);
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
@@ -654,6 +698,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfi);
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
@@ -665,6 +712,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferiv);
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
@@ -676,6 +726,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferuiv);
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
@@ -1404,6 +1457,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1430,6 +1486,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
|
||||
@@ -1453,6 +1512,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1480,6 +1542,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
|
||||
@@ -1534,6 +1599,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1636,6 +1704,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||
/*layered=*/false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
|
||||
@@ -1705,6 +1776,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (renderbuffer == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1714,6 +1788,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!renderbufferObject) return;
|
||||
|
||||
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
|
||||
@@ -1913,6 +1990,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
|
||||
if (!framebufferObject) return;
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
|
||||
@@ -1927,6 +2007,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLenum bufs[] = {buf};
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
|
||||
@@ -1936,6 +2019,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!framebufferObject) return;
|
||||
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
|
||||
"NamedFramebufferReadBuffer_State");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// P4a, ID-18 M2. The ONE door MG_State and MG_Impl have into the client's emitters; the three
|
||||
// call sites below are declarations only, exactly as the frontend's mutators are.
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
@@ -1337,6 +1340,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||
// Wrong pixels, not a lost optimisation.
|
||||
//
|
||||
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||
//
|
||||
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
||||
@@ -1415,6 +1442,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||
// Wrong pixels, not a lost optimisation.
|
||||
//
|
||||
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||
//
|
||||
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
@@ -2105,6 +2156,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||
// Wrong pixels, not a lost optimisation.
|
||||
//
|
||||
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||
//
|
||||
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/CompositeResolver.h
|
||||
// Copyright (c) 2025-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
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// P4a's PROGRAM-PIPELINE COMPOSITE, on the client side.
|
||||
//
|
||||
// GLContext::GetProgramForDraw() already flattens a bound pipeline into one hidden composite
|
||||
// ProgramObject entirely in the frontend - it joins every graphics stage, computes the
|
||||
// pipeline's draw-program signature, looks it up in the pipeline's own cache and, on a miss,
|
||||
// attaches each stage's LINKED SNAPSHOT into a fresh ProgramObject and links it. All of that
|
||||
// is frontend work and none of it moves. What this file adds is the one thing the wire needs:
|
||||
// the composite gets ONE handle, out of the ShaderCso reserved high band, and
|
||||
// create_shader_state goes out for it exactly as for an ordinary program. THE SERVER NEVER
|
||||
// LEARNS IT IS A COMPOSITE and needs no "resolved draw program" hook at all.
|
||||
//
|
||||
// WHY A BAND RATHER THAN A FLAG ON THE HANDLE: a flag would have to be carried, honoured and
|
||||
// masked off by every consumer of a ShaderCso handle, on both sides; a reserved slot range is
|
||||
// a property of the allocator instead, so "an ordinary program can never be handed a composite
|
||||
// slot" is true by construction. MGPipeSlotAllocator::Allocate refuses the band outright and
|
||||
// AllocateComposite is the only door in.
|
||||
//
|
||||
// WHAT THIS FILE IS ACTUALLY FOR: the composite's slot has TWO INDEPENDENT RELEASE PATHS and
|
||||
// either order has to free it exactly once.
|
||||
// * the pipeline cache drops the composite when the draw-program signature moves. In the
|
||||
// frontend that overwrite drops the last SharedPtr, so the composite's own destructor
|
||||
// usually runs first; the resolver still speaks the release, because "usually" is not a
|
||||
// contract and a client that only reacted to destructors would leak a slot the moment the
|
||||
// frontend started holding a second reference.
|
||||
// * the composite ProgramObject's own ~ProgramObject, which is an ordinary program's death
|
||||
// path and takes the same helper.
|
||||
// Both go through MGPipeEmitShaderCsoDestroyAndFree, and whichever runs second is a PROVEN
|
||||
// no-op: MGPipeSlotAllocator::Free refuses a slot that is not live at that generation and
|
||||
// bumps no generation of its own, so a double release cannot skip a generation either.
|
||||
//
|
||||
// THE MEMO's KEY IS (CONTEXT ID, PIPELINE GL NAME) AND THE CONTEXT HALF IS NOT OPTIONAL.
|
||||
// This resolver is a PROCESS singleton while a pipeline's GL name is per context: GLContext
|
||||
// owns m_programPipelines AND its own name generator m_programPipelineNames (Core.h), so name
|
||||
// N names two different ProgramPipelineObjects in two contexts, each with its own composite
|
||||
// and its own handle. Keyed on the name alone, the first emission after a make-current found
|
||||
// the OTHER context's entry, matched nothing - two composites are two ProgramObjects with two
|
||||
// lifetime ids, so the handles differ even when the stage set and the signature are identical
|
||||
// - and released it: a delete_shader_state and a cleared publication latch for a composite
|
||||
// whose frontend ProgramObject is alive, its band slot handed back and re-issued at gen + 1,
|
||||
// and the server rebuilding that program (glslang + SPIR-V + spirv-opt, the very cost the
|
||||
// signature below exists to avoid) once per context switch.
|
||||
//
|
||||
// THE CONTEXT ID IS GLContext::GetTextureContextId() AND NOTHING ELSE - the tree's existing
|
||||
// never-reused per-context id (TextureState::AllocateContextId; PipeInputs carries it as
|
||||
// m_textureContextId at seven fill points and the backends' own per-context memos key on it).
|
||||
// Deliberately NOT the GLContext ADDRESS that MGB_CTX_IDENTITY and MGPipeTracker::m_context
|
||||
// compare, because Core.h states the reason that id exists at all: a context freed and remade
|
||||
// lands on the old heap address, which would put this same defect back one context recreation
|
||||
// later.
|
||||
//
|
||||
// WHAT RELEASES A DESTROYED CONTEXT's ENTRIES: nothing in this file, and that is the correct
|
||||
// answer rather than an omission. Destroying a context drops m_programPipelines, which drops
|
||||
// each ProgramPipelineObject, which drops the composite it cached; ~ProgramObject then runs
|
||||
// MGPipeEmitShaderCsoDestroyAndFree - the composite's OWN release path, the second of the two
|
||||
// above - and the slot goes back exactly once. The entries those composites leave behind can
|
||||
// never be found again (no future Observe can carry a dead context id) and could not release
|
||||
// anything if they were (the allocator erases the lifetime-id mapping on Free), so Reset()
|
||||
// DROPS them instead of releasing them. That is also what bounds the vector; see Reset().
|
||||
//
|
||||
// THE SIGNATURE IS ComputeDrawProgramSignature(), the per-graphics-stage {lifetimeId,
|
||||
// GetLinkVersion()} array - and DELIBERATELY NOT GetBackendStateVersion(), which is what made
|
||||
// the SSO conformance loop rebuild the composite (glslang + SPIR-V + spirv-opt) on every draw,
|
||||
// because a glUniform1i to a sampler moves it.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h states: a new .cpp would need the root
|
||||
// CMakeLists.txt, which is the contract package's.
|
||||
//
|
||||
// IT IS INCLUDED BY ProgramEmit.h AND NOT THE OTHER WAY ROUND, deliberately: the composite is
|
||||
// a special case of the program family's own emission, so the family header depends on this
|
||||
// one and this one depends on nothing of the family's. The reverse arrangement would make the
|
||||
// resolver reachable only from a translation unit that had already decided to use it, i.e.
|
||||
// dead in the build that matters and live only in the tests.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramPipelineObject.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// IS THIS PROGRAM A PIPELINE COMPOSITE? A composite is the one ProgramObject in the system
|
||||
// constructed with external index 0 (Core.cpp's MakeShared<ProgramObject>(0u)), and that is
|
||||
// not an accident of implementation: it is deliberately not a named program, so it must not
|
||||
// answer glIsProgram and must not consume a GL name, and glCreateProgram never returns 0.
|
||||
//
|
||||
// ASKED THIS WAY RATHER THAN CARRIED ON THE OBJECT because a Bool member on ProgramObject
|
||||
// would resize the pull build's object and break G1 outright - the phase's admitted-resize
|
||||
// set is empty - and a hook in Core.cpp would have to be maintained on a path that already
|
||||
// states the invariant in its own comment.
|
||||
inline Bool MGPipeProgramIsPipelineComposite(const MG_State::GLState::ProgramObject& program) {
|
||||
return program.GetExternalIndex() == 0;
|
||||
}
|
||||
|
||||
class MGPipeCompositeResolver {
|
||||
public:
|
||||
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||
using ProgramPipelineObject = MG_State::GLState::ProgramPipelineObject;
|
||||
using DrawProgramSignature = ProgramPipelineObject::DrawProgramSignature;
|
||||
|
||||
struct Counters {
|
||||
Uint64 Mints = 0; // signatures this resolver has seen minted
|
||||
Uint64 Reuses = 0; // a signature that had not moved
|
||||
Uint64 Releases = 0; // signature-move releases, i.e. the pipeline-cache path
|
||||
// Entries dropped by Reset() because the composite's slot was already gone - the
|
||||
// shape every entry of a DESTROYED CONTEXT ends in. A dropped entry is not a
|
||||
// release: nothing is emitted and nothing is freed, the obligation having been
|
||||
// discharged by the composite's own ~ProgramObject.
|
||||
Uint64 Sweeps = 0;
|
||||
};
|
||||
|
||||
// Told, at every emission, which composite the frontend handed out for which pipeline.
|
||||
// Returns the handle the emitter should use, which is always the one already minted off
|
||||
// the composite's own lifetime id - the resolver never mints a second identity for an
|
||||
// object that has one.
|
||||
//
|
||||
// WHEN THE SIGNATURE MOVES the previous composite's slot is released here, through the
|
||||
// one death helper and in its fixed order. That is the pipeline-cache release path; the
|
||||
// composite's own destructor is the other one and the second of the two is the proven
|
||||
// no-op.
|
||||
MGPipeHandle Observe(Uint64 contextId, const ProgramPipelineObject& pipeline,
|
||||
const ProgramObject& composite, MGPipeHandle handle) {
|
||||
const DrawProgramSignature signature = pipeline.ComputeDrawProgramSignature();
|
||||
const Uint pipelineName = pipeline.GetExternalIndex();
|
||||
Entry* entry = Find(contextId, pipelineName);
|
||||
if (entry != nullptr) {
|
||||
if (entry->Signature == signature && entry->Handle == handle) {
|
||||
// THE SAME COMPOSITE. Not merely "the same signature": the handle is minted
|
||||
// off the composite ProgramObject's own lifetime id, so an identical handle
|
||||
// IS an identical object and there is nothing to release. Live is
|
||||
// deliberately NOT touched - it is the release obligation and it is still
|
||||
// owed for exactly this handle.
|
||||
++m_counters.Reuses;
|
||||
return handle;
|
||||
}
|
||||
// A MOVED SIGNATURE ON THIS CONTEXT's OWN ENTRY, which is the only thing that
|
||||
// can reach here now: another context's pipeline of the same name is not found
|
||||
// above and therefore not released, its obligation staying owed to the context
|
||||
// that took it.
|
||||
ReleaseEntry(*entry);
|
||||
} else {
|
||||
m_entries.push_back(Entry{});
|
||||
entry = &m_entries.back();
|
||||
entry->ContextId = contextId;
|
||||
entry->PipelineName = pipelineName;
|
||||
}
|
||||
entry->Signature = signature;
|
||||
entry->Handle = handle;
|
||||
entry->CompositeLifetimeId = composite.GetLifetimeId();
|
||||
entry->Live = true;
|
||||
++m_counters.Mints;
|
||||
return handle;
|
||||
}
|
||||
|
||||
// A make-current, and it RELEASES NOTHING. The entries name composites that belong to
|
||||
// the frontend objects of the context being left, those objects outlive the switch, and
|
||||
// releasing them would emit a delete for a live program.
|
||||
//
|
||||
// NOR IS ANY MEMO INVALIDATED, and that is what the context key bought. This used to
|
||||
// clear a per-entry `Fresh` flag beside `Live`, because with a name-only key an entry
|
||||
// could not say whether it described "my own pipeline before the switch" or "another
|
||||
// context's pipeline of the same name" - and exactly one of those two properties could
|
||||
// hold at a time. The key answers the question directly now, so the freshness flag and
|
||||
// its one reader (a HandleFor() accessor that had no caller anywhere in the tree) are
|
||||
// both gone rather than left as scaffolding: `Live`, the release obligation, is the
|
||||
// entry's only state and nothing but ReleaseEntry may clear it.
|
||||
//
|
||||
// WHAT IS LEFT TO DO HERE IS RECLAMATION, and this is the one moment the client is told
|
||||
// that a context boundary was crossed. An entry whose composite slot is no longer live
|
||||
// has had its obligation discharged elsewhere - by that composite's own ~ProgramObject,
|
||||
// which is precisely what happened to EVERY entry of a context that has just been
|
||||
// destroyed - so it is DROPPED rather than released: a release would resolve nothing
|
||||
// anyway (the allocator erases the lifetime-id mapping on Free) and no reader is left.
|
||||
// Without this the vector would grow by one per (context, pipeline name) pair the
|
||||
// process ever used, where the name-only key bounded it by the highest pipeline name;
|
||||
// with it, it is bounded by the pairs whose composite slot is actually live.
|
||||
void Reset() {
|
||||
SizeT kept = 0;
|
||||
for (SizeT i = 0; i < m_entries.size(); ++i) {
|
||||
if (!m_entries[i].Live || !MGPipeSlots().IsLive(MGPipeKind::ShaderCso, m_entries[i].Handle)) {
|
||||
++m_counters.Sweeps;
|
||||
continue;
|
||||
}
|
||||
if (kept != i) m_entries[kept] = m_entries[i];
|
||||
++kept;
|
||||
}
|
||||
m_entries.resize(kept);
|
||||
}
|
||||
|
||||
void ResetCounters() { m_counters = Counters{}; }
|
||||
|
||||
// Diagnostics and unit cases only; nothing on the emission path asks. There is no
|
||||
// HandleFor(name) accessor and there must not be one: the emitter takes the handle from
|
||||
// the composite ProgramObject it already holds, so a lookup by name would be a second
|
||||
// authority on an identity the allocator already owns.
|
||||
SizeT Size() const { return m_entries.size(); }
|
||||
const Counters& GetCounters() const { return m_counters; }
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
// NO FRONTEND SharedPtr, and that is the exit-order rule rather than a style
|
||||
// choice: a static that held one would put a frontend destructor on an exit
|
||||
// handler's path into a torn-down pipe. A GL name, a signature of plain integers,
|
||||
// a handle and a lifetime id are all this needs.
|
||||
// KEYED ON (CONTEXT ID, GL NAME), and the name half is the GL name because a
|
||||
// ProgramPipelineObject has no lifetime id - ComputeDrawProgramSignature reads the
|
||||
// STAGE programs' ids and the pipeline itself carries none. The context half is
|
||||
// GLContext::GetTextureContextId(); see the file header for why the name alone was
|
||||
// wrong and why the context ADDRESS would be too.
|
||||
//
|
||||
// WITHIN ONE CONTEXT glGenProgramPipelines recycles names, so a deleted-and-
|
||||
// recreated pipeline can still inherit its predecessor's entry; that is bounded and
|
||||
// self-correcting rather than a hazard. The first Observe on the new object finds a
|
||||
// signature and a handle that do not match and releases the old entry, and that
|
||||
// release resolves NOTHING - the allocator erases the lifetime-id mapping on Free,
|
||||
// so a stale CompositeLifetimeId emits no delete and frees no slot; all it costs is
|
||||
// one redundant, idempotent death notice, which is the same shape the composite's
|
||||
// own second release path already has.
|
||||
Uint64 ContextId = 0;
|
||||
Uint PipelineName = 0;
|
||||
DrawProgramSignature Signature{};
|
||||
MGPipeHandle Handle = kMGPipeNullHandle;
|
||||
Uint64 CompositeLifetimeId = 0;
|
||||
// THE RELEASE OBLIGATION. Set when this entry takes responsibility for a composite's
|
||||
// slot, cleared ONLY by ReleaseEntry when that responsibility is discharged.
|
||||
Bool Live = false;
|
||||
};
|
||||
|
||||
// BOTH HALVES OF THE KEY, always. An entry of another context is not this pipeline's
|
||||
// entry: not found, not matched, not released.
|
||||
Entry* Find(Uint64 contextId, Uint pipelineName) {
|
||||
for (Entry& entry : m_entries) {
|
||||
if (entry.ContextId == contextId && entry.PipelineName == pipelineName) return &entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ReleaseEntry(Entry& entry) {
|
||||
if (!entry.Live || entry.CompositeLifetimeId == 0) return;
|
||||
entry.Live = false;
|
||||
MGPipeEmitShaderCsoDestroyAndFree(entry.CompositeLifetimeId);
|
||||
entry.Handle = kMGPipeNullHandle;
|
||||
entry.CompositeLifetimeId = 0;
|
||||
++m_counters.Releases;
|
||||
}
|
||||
|
||||
Vector<Entry> m_entries;
|
||||
Counters m_counters;
|
||||
};
|
||||
|
||||
inline MGPipeCompositeResolver& MGPipeCompositeResolverInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason, and named in the phase's risk
|
||||
// list beside the other three new client singletons: heap-constructed and intentionally
|
||||
// leaked at exit, holding no frontend SharedPtr.
|
||||
static MGPipeCompositeResolver* resolver = new MGPipeCompositeResolver();
|
||||
return *resolver;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -26,36 +26,606 @@
|
||||
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
|
||||
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <xxhash.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR, and it is 0 until the emitter below
|
||||
// has a body. PipeFill.cpp ORs the four per-family constants into kMGPipeWiredSubsystems,
|
||||
// so the bit is added by the commit that gives the emitters their bodies, with no file
|
||||
// touched twice - and a Coverage.def row can never silently drop a field on the floor
|
||||
// before the call that carries it exists.
|
||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = 0;
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the four per-family
|
||||
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives the
|
||||
// emitters their bodies, with no file touched twice - and a Coverage.def row can never
|
||||
// silently drop a field on the floor before the call that carries it exists.
|
||||
//
|
||||
// TURNING IT ON RETIRES NO PULL. GetFramebufferBindingSlot is the family's one
|
||||
// Coverage.def emitted row and PipeFill.cpp's EmittedCallSuppliesTheWholeField answers
|
||||
// FALSE for it, with the reason: the field's storage is a BindingSlot<FramebufferObject> -
|
||||
// a frontend heap reference - and the call that supplies it carries eight-byte {slot, gen}
|
||||
// handles and a fully resolved descriptor. So this bit switches the EMISSION on and the
|
||||
// residual fill keeps writing the mirror, which is what keeps the verify lane at zero
|
||||
// divergence.
|
||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = kMGPipeSubsystemFramebuffer;
|
||||
|
||||
inline Bool MGPipeFramebufferSubsystemEnabled() {
|
||||
return (kMGPipeWiredFramebufferSubsystem & kMGPipeSubsystemFramebuffer) != 0 &&
|
||||
(MG_Config::Features.PipePush & kMGPipeSubsystemFramebuffer) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-C1: the MGPSurface builder, one pure function, one statement per field
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// MGPSurface::Kind's three constants ARE THE CONTRACT'S (ID-12 DV-4, c0c):
|
||||
// kMGPipeSurfaceKindNone / ...Texture / ...Renderbuffer live in MG_Pipe/MGPipeTypes.h under
|
||||
// exactly these names with the same MGPipeKind derivation and the same static_assert. This
|
||||
// package's copies were a redefinition in the same namespace and are deleted.
|
||||
|
||||
// The upload target an attachment names, RESOLVED: an attachment made through an entry
|
||||
// point that carries no face token stores TextureUploadTarget::Unknown, and the record goes
|
||||
// out fully resolved - nothing in it may require a lookup on the far side.
|
||||
//
|
||||
// THE FALLBACK IS ONLY LEGAL FOR A SINGLE-TARGET TEXTURE (m1), and v1's was not. The
|
||||
// precedent it copied - FramebufferAttachmentObject::GetSize - needs an EXTENT, which is
|
||||
// identical across a cube map's six faces; face IDENTITY is not, so
|
||||
// `glFramebufferTexture(GL_COLOR_ATTACHMENT0, cube, 0)` resolved to targets[0] and the
|
||||
// record ASSERTED CubeMapPositiveX for a layered attachment that names all six. A texture
|
||||
// with exactly one upload target has a [0] that IS the truth; anything else keeps Unknown,
|
||||
// which is the value the field already carries for "this attachment names no single face"
|
||||
// and which Layered = 1 tells the reader to ignore.
|
||||
inline MobileGL::TextureUploadTarget MGPipeResolveAttachmentUploadTarget(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
MobileGL::TextureUploadTarget resolved = attachment.GetTextureUploadTarget();
|
||||
if (resolved != MobileGL::TextureUploadTarget::Unknown) return resolved;
|
||||
const auto& texture = attachment.GetTexture();
|
||||
if (!texture) return MobileGL::TextureUploadTarget::Unknown;
|
||||
const auto& targets = texture->GetUploadTargets();
|
||||
return targets.size() == 1 ? targets[0] : MobileGL::TextureUploadTarget::Unknown;
|
||||
}
|
||||
|
||||
// ONE PURE FUNCTION, ONE STATEMENT PER FIELD, and that shape is a gate requirement rather
|
||||
// than taste: G7's scripted control stops this conversion copying exactly one member
|
||||
// (MGPSurface::Layered) and expects the framebuffer suite to go red NAMING that field. A
|
||||
// loop or a memcpy would make the control unanswerable.
|
||||
//
|
||||
// `res` is handed in because resolving it needs the slot allocator and this function stays
|
||||
// pure; `internalFormat` is INLINE in the record on purpose, so the four cross-object masks
|
||||
// fall out at push time with no lookup on the far side.
|
||||
// THE EMPTY POINT IS THE ZERO-INITIALISED RECORD EXCEPT FOR ITS TWO TARGET FIELDS. Both
|
||||
// are Uint16 enumerations whose zero is a REAL value - TextureTarget::Texture1D and
|
||||
// TextureUploadTarget::Texture1D - so a reader that forgot to gate on Kind would read a
|
||||
// plausible wrong answer rather than a nonsense one. Unknown (0xFFFF) is what the contract
|
||||
// spells for TextureTarget (kMGPipeSurfaceNoTextureTarget) and m6 applies the same rule to
|
||||
// UploadTarget, which shares the collision ID-12 DV-3 ruled on for MGPSubData::Target.
|
||||
inline MGPSurface MGPipeEmptySurface() {
|
||||
MGPSurface surface{};
|
||||
surface.UploadTarget = static_cast<Uint16>(MobileGL::TextureUploadTarget::Unknown);
|
||||
surface.TextureTarget = kMGPipeSurfaceNoTextureTarget;
|
||||
return surface;
|
||||
}
|
||||
|
||||
inline MGPSurface MGPipeBuildSurface(const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
MGPipeHandle res) {
|
||||
MGPSurface surface = MGPipeEmptySurface();
|
||||
if (attachment.IsEmpty()) return surface;
|
||||
surface.Res = res;
|
||||
if (attachment.IsTexture()) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
surface.Kind = kMGPipeSurfaceKindTexture;
|
||||
surface.InternalFormat = static_cast<Uint32>(texture->GetFormat());
|
||||
surface.Layered = attachment.IsLayered() ? 1 : 0;
|
||||
surface.Level = static_cast<Uint16>(std::max<Int>(attachment.GetTextureLevel(), 0));
|
||||
surface.Layer = static_cast<Uint32>(std::max<Int>(attachment.GetTextureLayer(), 0));
|
||||
surface.UploadTarget = static_cast<Uint16>(MGPipeResolveAttachmentUploadTarget(attachment));
|
||||
// ID-12 DV-5: the field that WAS Pad0, and the size did not move. The four
|
||||
// cross-object masks all reduce to (format, TEXTURE TARGET) -
|
||||
// ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha - and no
|
||||
// TextureUploadTarget -> TextureTarget inverse exists anywhere in the tree, so
|
||||
// without this the inline InternalFormat cannot make them fall out at push time and
|
||||
// the backend keeps reading the frontend attachment objects.
|
||||
surface.TextureTarget = static_cast<Uint16>(texture->GetTarget());
|
||||
return surface;
|
||||
}
|
||||
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||
surface.Kind = kMGPipeSurfaceKindRenderbuffer;
|
||||
surface.InternalFormat = static_cast<Uint32>(renderbuffer->GetInternalFormat());
|
||||
surface.Layered = 0;
|
||||
surface.Level = 0;
|
||||
surface.Layer = 0;
|
||||
return surface;
|
||||
}
|
||||
|
||||
// MGPFramebufferState::DrawBuffers[i]: an index INTO THIS RECORD'S OWN Color[] array, and
|
||||
// -1 for NONE, which is the field's documented convention read literally.
|
||||
//
|
||||
// THE FOUR DEFAULT-FRAMEBUFFER TOKENS map to 0, and that is a deliberate narrowing rather
|
||||
// than an oversight: a default framebuffer has one colour surface, this record carries it
|
||||
// in Color[0] (see MGPipeBuildFramebufferState), and IsDefault is what tells the server
|
||||
// which framebuffer it is looking at. The distinction the narrowing loses is FRONT versus
|
||||
// BACK and LEFT versus RIGHT, which MobileGL's frontend never gives a default framebuffer
|
||||
// in the first place - FramebufferObject's constructor seeds BackLeft and nothing writes
|
||||
// another. A phase that needs stereo has to widen the field, not re-encode this one.
|
||||
inline Int8 MGPipeDrawBufferIndex(MobileGL::FramebufferAttachmentType buffer) {
|
||||
using MobileGL::FramebufferAttachmentType;
|
||||
if (buffer == FramebufferAttachmentType::None) return -1;
|
||||
if (buffer >= FramebufferAttachmentType::Color0 && buffer <= FramebufferAttachmentType::ColorMax) {
|
||||
return static_cast<Int8>(static_cast<Int>(buffer) - static_cast<Int>(FramebufferAttachmentType::Color0));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// m2: A DRAW-BUFFER TOKEN CAN NAME A COLOUR POINT THE RECORD CANNOT CARRY, and D-C3's
|
||||
// refusal loop only ever scanned ATTACHMENTS. `glDrawBuffers(1, {GL_COLOR_ATTACHMENT10})`
|
||||
// with nothing attached at 10 is legal state - draw-incomplete, but legal - and the index
|
||||
// above would have written 10 into a record whose Color[] is 8 wide, so the server would
|
||||
// index out of its own storage or invent a bound the record does not carry. Truncating
|
||||
// silently is the bug class this phase is closing, so the record is refused exactly as an
|
||||
// over-wide attachment is.
|
||||
inline Bool MGPipeDrawBufferIsInsideTheWireWidth(MobileGL::FramebufferAttachmentType buffer) {
|
||||
using MobileGL::FramebufferAttachmentType;
|
||||
if (buffer < FramebufferAttachmentType::Color0 || buffer > FramebufferAttachmentType::ColorMax) {
|
||||
return true; // None and the four default-framebuffer tokens; neither indexes Color[]
|
||||
}
|
||||
return static_cast<Int>(buffer) - static_cast<Int>(FramebufferAttachmentType::Color0) <
|
||||
static_cast<Int>(kMGPipeMaxColorAttachments);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-C4: ContentHash, and the one input it must not swallow
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// XXH64 over the WHOLE record with ContentHash itself zeroed, computed field-wise into a
|
||||
// zero-initialised staging copy so that no padding byte can enter the hash. Two jobs: the
|
||||
// server's render-pass memo key, and this client's emission suppressor.
|
||||
//
|
||||
// IT MUST COVER Fbo. A recycled framebuffer handle whose successor happens to carry an
|
||||
// identical attachment set would otherwise be suppressed against its predecessor; Fbo
|
||||
// carries Gen, so it cannot be.
|
||||
//
|
||||
// IT MUST COVER DrawBuffers[8], and this is the trap worth naming. The backend derives the
|
||||
// fragColor BROADCAST COUNT from the draw-buffer array, and it does that at the verb, from
|
||||
// the framebuffer state it then holds, precisely so a program can relink inside the same
|
||||
// draw. A hash that did not cover the array would let a suppressed set_framebuffer_state
|
||||
// mean "the draw buffers did not move" when they had, and the shader would be specialised
|
||||
// for the previous output shape. With the array in the hash, a suppression provably means
|
||||
// the array did not move, which provably means the broadcast count did not move.
|
||||
inline void MGPipeCopySurfaceForHash(MGPSurface& dst, const MGPSurface& src) {
|
||||
dst.Res = src.Res;
|
||||
dst.InternalFormat = src.InternalFormat;
|
||||
dst.Kind = src.Kind;
|
||||
dst.Layered = src.Layered;
|
||||
dst.Level = src.Level;
|
||||
dst.Layer = src.Layer;
|
||||
dst.UploadTarget = src.UploadTarget;
|
||||
// MANDATORY, not optional: TextureTarget is a PipeFields.def row now, so a
|
||||
// field-wise copy that skipped it would suppress a record whose only moved field is
|
||||
// the attachment's texture target - and that field decides three of the four
|
||||
// cross-object masks.
|
||||
dst.TextureTarget = src.TextureTarget;
|
||||
}
|
||||
|
||||
inline Uint64 MGPipeFramebufferStateContentHash(const MGPFramebufferState& state) {
|
||||
MGPFramebufferState staging{};
|
||||
staging.Fbo = state.Fbo;
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
MGPipeCopySurfaceForHash(staging.Color[i], state.Color[i]);
|
||||
}
|
||||
MGPipeCopySurfaceForHash(staging.Depth, state.Depth);
|
||||
MGPipeCopySurfaceForHash(staging.Stencil, state.Stencil);
|
||||
MGPipeCopySurfaceForHash(staging.ReadSurface, state.ReadSurface);
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
staging.DrawBuffers[i] = state.DrawBuffers[i];
|
||||
}
|
||||
staging.Width = state.Width;
|
||||
staging.Height = state.Height;
|
||||
staging.Layers = state.Layers;
|
||||
staging.Samples = state.Samples;
|
||||
staging.FixedSampleLocations = state.FixedSampleLocations;
|
||||
staging.IsDefault = state.IsDefault;
|
||||
staging.Complete = state.Complete;
|
||||
staging.Target = state.Target;
|
||||
// staging.ContentHash stays 0 - that is the whole point.
|
||||
return XXH64(&staging, sizeof(staging), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The emitter
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// set_framebuffer_state. STUB AT THE CONTRACT COMMIT: it emits nothing and returns 0
|
||||
// payload bytes, so the validate point's ladder has its final shape and the package that
|
||||
// fills this in never edits PipeFill.cpp.
|
||||
class MGPipeFramebufferEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||
using FramebufferAttachmentType = MobileGL::FramebufferAttachmentType;
|
||||
|
||||
// The handle for `fbo`. kMGPipeDefaultFramebuffer ({0,1}) for the default framebuffer,
|
||||
// which is what retires the four pDefaultFramebufferInfo->defaultFBO identity
|
||||
// comparisons into an ordinary handle compare; a client-minted {slot, gen} otherwise.
|
||||
//
|
||||
// Minted, never gated: a framebuffer handle is CLIENT state and costs one free-list pop.
|
||||
static MGPipeHandle HandleFor(const FramebufferObject& fbo) {
|
||||
if (fbo.IsDefaultFramebuffer()) return kMGPipeDefaultFramebuffer;
|
||||
return MGPipeSlots().Acquire(MGPipeKind::Framebuffer, fbo.GetLifetimeId());
|
||||
}
|
||||
|
||||
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
|
||||
const auto& drawFbo = ctx.GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).GetBoundObject();
|
||||
const auto& readFbo = ctx.GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).GetBoundObject();
|
||||
if (!drawFbo && !readFbo) return 0;
|
||||
|
||||
// ONE OBJECT BOUND TO BOTH TARGETS IS ONE RECORD WITH Target = Both, and that is
|
||||
// not an optimisation: Espryt's "same FBO as draw" skip is the habitat of the
|
||||
// read-buffer defect class, and a record that says which target it describes turns
|
||||
// "apply the draw buffers only for the draw target" from call-site discipline into
|
||||
// a one-line test on the far side.
|
||||
const Bool shared = drawFbo && readFbo && drawFbo.get() == readFbo.get();
|
||||
|
||||
MGPFramebufferState drawState{};
|
||||
MGPFramebufferState readState{};
|
||||
Bool drawOk = false;
|
||||
Bool readOk = false;
|
||||
if (shared) {
|
||||
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
||||
} else {
|
||||
if (drawFbo) {
|
||||
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
|
||||
}
|
||||
if (readFbo) {
|
||||
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
|
||||
}
|
||||
}
|
||||
if (!drawOk && !readOk) return 0;
|
||||
|
||||
// THE SUPPRESSOR SLOT IS FED THE COMBINED ANSWER and the per-target latches decide
|
||||
// which of the two records actually goes out. The slot exists so that
|
||||
// InvalidateAll() on a fresh context reaches this family like every other, and so
|
||||
// that "nothing moved" costs one compare rather than two.
|
||||
const Uint64 drawHash = drawOk ? drawState.ContentHash : 0;
|
||||
const Uint64 readHash = readOk ? readState.ContentHash : 0;
|
||||
const Uint64 combined =
|
||||
MGPipeMixShutter(MGPipeMixShutter(drawHash, readHash), shared ? 1u : 0u);
|
||||
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetFramebufferState,
|
||||
combined)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint64 bytes = 0;
|
||||
if (shared) {
|
||||
if (drawOk && (drawHash != m_lastEmitted[kDraw] || drawHash != m_lastEmitted[kRead])) {
|
||||
bytes += Emit(drawState);
|
||||
m_lastEmitted[kDraw] = drawHash;
|
||||
m_lastEmitted[kRead] = drawHash;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
if (drawOk && drawHash != m_lastEmitted[kDraw]) {
|
||||
bytes += Emit(drawState);
|
||||
m_lastEmitted[kDraw] = drawHash;
|
||||
}
|
||||
if (readOk && readHash != m_lastEmitted[kRead]) {
|
||||
bytes += Emit(readState);
|
||||
m_lastEmitted[kRead] = readHash;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// ID-19(c): EVERY DSA ENTRY POINT THAT HANDS A FRAMEBUFFER TO THE SERVER BY NAME IS
|
||||
// PRECEDED BY A RECORD FOR IT, and that is the phase's main correction rather than a
|
||||
// nicety. With only the two BOUND-target records, glClearNamedFramebufferfv(fbo) on an
|
||||
// unbound fbo made the backend mint a fresh driver framebuffer with NO ATTACHMENTS,
|
||||
// find no record for it, decline, and issue the clear against it anyway -
|
||||
// GL_INVALID_FRAMEBUFFER_OPERATION and nothing cleared, where the legacy arm cleared
|
||||
// correctly (esprytobj C-1).
|
||||
//
|
||||
// THE TARGET IS Named ONLY WHEN THE OBJECT IS BOUND TO NEITHER BINDING. A record always
|
||||
// writes FramebufferRecords[Fbo.Slot]; Draw/Read/Both ADDITIONALLY set the bound
|
||||
// handle(s). So handing a currently-bound framebuffer a Named record would overwrite
|
||||
// the bound record's Target with one that says "no binding" while BoundFramebuffer
|
||||
// still names it, and the server would read a record whose Target contradicts the
|
||||
// binding it is resolved through. Re-asserting the binding the object already has is
|
||||
// free (the content hash suppresses it) and keeps the two consistent.
|
||||
//
|
||||
// Returns the bytes that went on the wire.
|
||||
Uint64 EmitFramebufferByName(const FramebufferObject& fbo) {
|
||||
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
|
||||
MGPipeFramebufferTarget target = MGPipeFramebufferTarget::Named;
|
||||
const Bool boundToDraw = IsBoundTo(fbo, MobileGL::FramebufferTarget::Draw);
|
||||
const Bool boundToRead = IsBoundTo(fbo, MobileGL::FramebufferTarget::Read);
|
||||
if (boundToDraw && boundToRead) {
|
||||
target = MGPipeFramebufferTarget::Both;
|
||||
} else if (boundToDraw) {
|
||||
target = MGPipeFramebufferTarget::Draw;
|
||||
} else if (boundToRead) {
|
||||
target = MGPipeFramebufferTarget::Read;
|
||||
}
|
||||
|
||||
MGPFramebufferState state{};
|
||||
if (!BuildFramebufferState(fbo, target, state)) return 0;
|
||||
|
||||
// THE SUPPRESSOR IS KEYED BY THE FRAMEBUFFER THE RECORD NAMES, never by one global
|
||||
// slot (MGPipeTypes.h states the rule): two different objects' Named records in a
|
||||
// row must both go out, and a Named record must never be suppressed against the
|
||||
// same object's bound record or the reverse. Target is a ContentHash input, so the
|
||||
// second half holds by construction; the per-object table is what buys the first.
|
||||
// The two BOUND latches stay what they are - "does the server's draw/read binding
|
||||
// already hold this record" - and a bound-target emission from here consults them,
|
||||
// because a rebind of an unchanged object must still move the binding.
|
||||
if (target == MGPipeFramebufferTarget::Named) {
|
||||
NamedEntry& entry = NamedEntryFor(state.Fbo);
|
||||
if (entry.Has && entry.Gen == state.Fbo.Gen && entry.LastHash == state.ContentHash) {
|
||||
return 0;
|
||||
}
|
||||
const Uint64 bytes = Emit(state);
|
||||
entry.Has = true;
|
||||
entry.Gen = state.Fbo.Gen;
|
||||
entry.LastHash = state.ContentHash;
|
||||
return bytes;
|
||||
}
|
||||
if (target == MGPipeFramebufferTarget::Both) {
|
||||
if (state.ContentHash == m_lastEmitted[kDraw] && state.ContentHash == m_lastEmitted[kRead]) {
|
||||
return 0;
|
||||
}
|
||||
const Uint64 bytes = Emit(state);
|
||||
m_lastEmitted[kDraw] = state.ContentHash;
|
||||
m_lastEmitted[kRead] = state.ContentHash;
|
||||
return bytes;
|
||||
}
|
||||
const SizeT slot = target == MGPipeFramebufferTarget::Read ? kRead : kDraw;
|
||||
if (state.ContentHash == m_lastEmitted[slot]) return 0;
|
||||
const Uint64 bytes = Emit(state);
|
||||
m_lastEmitted[slot] = state.ContentHash;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
|
||||
// same objects, so "what was emitted" costs no copy. ----
|
||||
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
|
||||
const MGPFramebufferState& LastRead() const { return m_lastRead; }
|
||||
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
||||
Uint64 EmissionCount() const { return m_emissions; }
|
||||
Uint64 RefusedCount() const { return m_refusals; }
|
||||
|
||||
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||
// LATCHES reset here - the applier's object records survive a make-current and
|
||||
// re-publishing them would move their serials for nothing.
|
||||
void Reset() {}
|
||||
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
|
||||
// ReadFramebuffer working state, so these mirrors have to go with them or the first
|
||||
// emission after a make-current would be suppressed as unchanged and the server would
|
||||
// draw into the previous context's framebuffer. The suppressor slot is invalidated by
|
||||
// the validate point's own InvalidateAll(), beside this call.
|
||||
void Reset() {
|
||||
m_lastEmitted[kDraw] = 0;
|
||||
m_lastEmitted[kRead] = 0;
|
||||
// The per-object latch goes too, and the safe direction is why: MGPipeApplierReset
|
||||
// keeps FramebufferRecords standing (they are object state, ID-19(b)) but
|
||||
// ReleaseObjectRecords clears the whole table, and this emitter cannot tell the two
|
||||
// scopes apart from here. Keeping a latch across a table that may have been dropped
|
||||
// would suppress the one record that had to go out; dropping it costs one extra
|
||||
// 304-byte record per named framebuffer after a context switch.
|
||||
m_named.clear();
|
||||
}
|
||||
|
||||
void ResetCounters() { m_emissions = m_refusals = 0; }
|
||||
|
||||
void ResetForTest() {
|
||||
Reset();
|
||||
ResetCounters();
|
||||
m_lastDraw = MGPFramebufferState{};
|
||||
m_lastRead = MGPFramebufferState{};
|
||||
m_lastNamed = MGPFramebufferState{};
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr SizeT kDraw = 0;
|
||||
static constexpr SizeT kRead = 1;
|
||||
|
||||
Uint64 Emit(const MGPFramebufferState& state) {
|
||||
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Named)) {
|
||||
m_lastNamed = state;
|
||||
} else if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read)) {
|
||||
m_lastRead = state;
|
||||
} else {
|
||||
m_lastDraw = state;
|
||||
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state;
|
||||
}
|
||||
MGPipeApplySetFramebufferState(state);
|
||||
++m_emissions;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1);
|
||||
}
|
||||
return sizeof(MGPFramebufferState);
|
||||
}
|
||||
|
||||
// ONE RECORD DESCRIBES ONE FRAMEBUFFER OBJECT - the one named by `fbo` - and every
|
||||
// field in it is a property of THAT object. Target is the only binding-specific one.
|
||||
//
|
||||
// ReadSurface IS RESOLVED FROM THIS FRAMEBUFFER'S OWN READ BUFFER UNDER EVERY TARGET,
|
||||
// Named included (c0e / MGPipeTypes.h). v1 resolved a Draw record's ReadSurface from
|
||||
// the READ-bound object, which was D-C2's letter and muddled in substance: the record
|
||||
// then described a surface that is not part of the framebuffer its own Fbo names, and a
|
||||
// glReadBuffer on the read FBO moved the DRAW record's ContentHash and forced a
|
||||
// redundant draw emission. Resolving it per object is what makes the
|
||||
// read-buffer-shared-FBO defect class unrepresentable rather than merely fixed - the
|
||||
// record carries a surface, not an index, and no field of it refers to "whatever is
|
||||
// bound".
|
||||
Bool BuildFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
|
||||
MGPFramebufferState& out) {
|
||||
// D-C3, THE CLIENT HALF OF THE BRING-UP REFUSAL. The wire array is 8 wide and
|
||||
// GetDynamicParameters().MaxColorAttachments is the driver's raw ES cap, not
|
||||
// clamped to 8 on the GLES path. An attachment point at or above the wire width
|
||||
// cannot be carried at all, so the record is REFUSED and the legacy arm runs -
|
||||
// truncating it silently is exactly the bug class this phase is closing. The
|
||||
// backend half of the same refusal (bit 9 declined at its first lookup, with one
|
||||
// ERROR naming the cap) rides ResolveFramebufferSubsystemArm.
|
||||
for (Int point = static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||
static_cast<Int>(kMGPipeMaxColorAttachments);
|
||||
point <= static_cast<Int>(FramebufferAttachmentType::ColorMax); ++point) {
|
||||
if (fbo.GetAttachment(static_cast<FramebufferAttachmentType>(point)).IsEmpty()) continue;
|
||||
MGLOG_E_ONCE("MGPipe: framebuffer %u has an attachment at colour point %d, which is at or "
|
||||
"above the wire width of %u - set_framebuffer_state is refused rather than "
|
||||
"truncated and the legacy arm runs",
|
||||
fbo.GetExternalIndex(),
|
||||
point - static_cast<Int>(FramebufferAttachmentType::Color0),
|
||||
static_cast<Uint>(kMGPipeMaxColorAttachments));
|
||||
++m_refusals;
|
||||
return false;
|
||||
}
|
||||
|
||||
// m2, THE SAME REFUSAL ONE FIELD OVER. A draw-buffer token may name a colour point
|
||||
// at or above the wire width with nothing attached there, which the loop above
|
||||
// cannot see; MGPipeDrawBufferIndex would then write 8..31 into an 8-wide array.
|
||||
{
|
||||
const auto& tokens = fbo.GetDrawBuffers();
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
if (MGPipeDrawBufferIsInsideTheWireWidth(tokens[i])) continue;
|
||||
MGLOG_E_ONCE("MGPipe: framebuffer %u names colour point %d in draw buffer %u, which is "
|
||||
"at or above the wire width of %u - set_framebuffer_state is refused "
|
||||
"rather than truncated and the legacy arm runs",
|
||||
fbo.GetExternalIndex(),
|
||||
static_cast<Int>(tokens[i]) -
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0),
|
||||
static_cast<Uint>(i), static_cast<Uint>(kMGPipeMaxColorAttachments));
|
||||
++m_refusals;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
out = MGPFramebufferState{};
|
||||
out.Fbo = HandleFor(fbo);
|
||||
out.Target = static_cast<Uint8>(target);
|
||||
out.IsDefault = fbo.IsDefaultFramebuffer() ? 1 : 0;
|
||||
|
||||
// THE COLOUR POINTS. A default framebuffer keeps its one colour surface under
|
||||
// BackLeft rather than under Color0, and the record has exactly one place to put
|
||||
// it: Color[0], which is also the index MGPipeDrawBufferIndex maps that token to,
|
||||
// so the array and the draw-buffer indices agree by construction.
|
||||
if (out.IsDefault != 0) {
|
||||
out.Color[0] = SurfaceOf(fbo, FramebufferAttachmentType::BackLeft);
|
||||
} else {
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
out.Color[i] = SurfaceOf(fbo, static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||
static_cast<Int>(i)));
|
||||
}
|
||||
}
|
||||
out.Depth = SurfaceOf(fbo, FramebufferAttachmentType::Depth);
|
||||
out.Stencil = SurfaceOf(fbo, FramebufferAttachmentType::Stencil);
|
||||
out.ReadSurface = SurfaceOf(fbo, fbo.GetReadBuffer());
|
||||
|
||||
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
out.DrawBuffers[i] = MGPipeDrawBufferIndex(drawBuffers[i]);
|
||||
}
|
||||
|
||||
FillGeometry(fbo, out);
|
||||
// Complete is FramebufferObject::CheckCompleteness(), the FRONTEND-ONLY answer, and
|
||||
// never glCheckFramebufferStatus's: that entry point additionally consults the
|
||||
// backend's probed format-capability cache, and a client emitting it would be
|
||||
// reading the backend from the client side - the exact coupling this boundary
|
||||
// exists to remove. glCheckFramebufferStatus keeps answering from the frontend
|
||||
// exactly as it does today.
|
||||
out.Complete = fbo.CheckCompleteness() ? 1 : 0;
|
||||
out.ContentHash = MGPipeFramebufferStateContentHash(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
|
||||
if (MG_State::pGLContext == nullptr) return false;
|
||||
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
|
||||
return bound && bound.get() == &fbo;
|
||||
}
|
||||
|
||||
struct NamedEntry {
|
||||
Uint32 Gen = 0;
|
||||
Uint64 LastHash = 0;
|
||||
Bool Has = false;
|
||||
};
|
||||
|
||||
NamedEntry& NamedEntryFor(MGPipeHandle fbo) {
|
||||
const SizeT slot = fbo.Slot;
|
||||
if (slot >= m_named.size()) m_named.resize(slot + 1);
|
||||
return m_named[slot];
|
||||
}
|
||||
|
||||
MGPSurface SurfaceOf(const FramebufferObject& fbo, FramebufferAttachmentType type) {
|
||||
if (type == FramebufferAttachmentType::None || type == FramebufferAttachmentType::Unknown) {
|
||||
return MGPipeEmptySurface();
|
||||
}
|
||||
const auto& attachment = fbo.GetAttachment(type);
|
||||
if (attachment.IsEmpty()) return MGPipeEmptySurface();
|
||||
MGPipeTextureEmitter& textures = MGPipeTextureEmitterInstance();
|
||||
// D-A4's two producers: an attachment point is what sets RENDER_TARGET and
|
||||
// DEPTH_STENCIL, the two sticky bind bits nothing set before P4a. Sticky and ORed,
|
||||
// so a texture that was ever a colour attachment keeps saying so, and the mask is
|
||||
// republished on the resource's next respecify.
|
||||
const Uint16 bit = (type == FramebufferAttachmentType::Depth ||
|
||||
type == FramebufferAttachmentType::Stencil)
|
||||
? static_cast<Uint16>(kMGPipeBindDepthStencil)
|
||||
: static_cast<Uint16>(kMGPipeBindRenderTarget);
|
||||
MGPipeHandle res = kMGPipeNullHandle;
|
||||
if (attachment.IsTexture()) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
res = textures.AcquireTexture(texture->GetLifetimeId(), texture.get());
|
||||
textures.NoteTextureBoundAs(res, bit);
|
||||
} else if (attachment.IsRenderbuffer()) {
|
||||
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||
res = textures.AcquireRenderbuffer(renderbuffer->GetLifetimeId());
|
||||
textures.NoteRenderbufferBoundAs(res, bit);
|
||||
}
|
||||
return MGPipeBuildSurface(attachment, res);
|
||||
}
|
||||
|
||||
// The attachments' common extent, and the ARB_framebuffer_no_attachments defaults when
|
||||
// there is no attachment at all (GL 4.6 core table 23.24 - the shape a framebuffer with
|
||||
// no attachments rasterizes at).
|
||||
static void FillGeometry(const FramebufferObject& fbo, MGPFramebufferState& out) {
|
||||
Bool found = false;
|
||||
for (const auto& attachment : fbo.GetAllAttachmentObjects()) {
|
||||
if (attachment.IsEmpty()) continue;
|
||||
const IntVec3 size = attachment.GetSize();
|
||||
if (!found) {
|
||||
out.Width = static_cast<Uint16>(std::clamp<Int>(size.x(), 0, 0xFFFF));
|
||||
out.Height = static_cast<Uint16>(std::clamp<Int>(size.y(), 0, 0xFFFF));
|
||||
out.Layers = static_cast<Uint16>(
|
||||
attachment.IsLayered() ? std::clamp<Int>(size.z(), 1, 0xFFFF) : 1);
|
||||
if (attachment.IsTexture()) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
out.Samples = static_cast<Uint16>(std::max<Int>(texture->GetSamples(), 0));
|
||||
out.FixedSampleLocations = texture->HasFixedSampleLocations() ? 1 : 0;
|
||||
} else {
|
||||
out.Samples = static_cast<Uint16>(
|
||||
std::max<Int>(attachment.GetRenderbuffer()->GetSamples(), 0));
|
||||
out.FixedSampleLocations = 1;
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found) return;
|
||||
out.Width = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultWidth(), 0, 0xFFFF));
|
||||
out.Height = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultHeight(), 0, 0xFFFF));
|
||||
out.Layers = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultLayers(), 0, 0xFFFF));
|
||||
out.Samples = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultSamples(), 0, 0xFFFF));
|
||||
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
|
||||
}
|
||||
|
||||
Array<Uint64, 2> m_lastEmitted{};
|
||||
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
|
||||
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
|
||||
// (D-I2), so a successor simply overwrites its predecessor's entry.
|
||||
Vector<NamedEntry> m_named;
|
||||
MGPFramebufferState m_lastDraw{};
|
||||
MGPFramebufferState m_lastRead{};
|
||||
MGPFramebufferState m_lastNamed{};
|
||||
Uint64 m_emissions = 0;
|
||||
Uint64 m_refusals = 0;
|
||||
};
|
||||
|
||||
inline MGPipeFramebufferEmitter& MGPipeFramebufferEmitterInstance() {
|
||||
|
||||
@@ -33,23 +33,135 @@
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <xxhash.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
// D-G3. Over the tail with Start and Count mixed in, the same shape the two sampler sets
|
||||
// use - and it covers InternalFormat and Access because those are live glBindImageTexture
|
||||
// state that the format-less image bake keys on, not decoration.
|
||||
inline Uint64 MGPipeShaderImageSetContentHash(const MGPImageView* entries, Uint32 start, Uint32 count) {
|
||||
Uint64 hash = XXH64(entries, static_cast<SizeT>(count) * sizeof(MGPImageView), 0);
|
||||
hash = MGPipeMixShutter(hash, start);
|
||||
hash = MGPipeMixShutter(hash, count);
|
||||
return hash;
|
||||
}
|
||||
|
||||
class MGPipeImageEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// set_shader_images. Start is 0 and Count is the image-unit window described below.
|
||||
//
|
||||
// WHERE THE HIGH-WATER MARK COMES FROM, because the frontend has none and this is the
|
||||
// one place a reader will look for it. DirectGLES keeps g_imageUnitHighWaterMark, but
|
||||
// that is written from inside its own per-unit sync and lives on the far side of the
|
||||
// boundary; TextureState::NoteUnitTouched is the TEXTURE-unit path and
|
||||
// glBindImageTexture does not reach it. Adding a counter to TextureState would edit
|
||||
// another package's file and resize the pull build's object, which G1 forbids outright.
|
||||
//
|
||||
// So the window is derived instead, from the one thing that decides whether an image
|
||||
// unit can matter at all: the highest image unit the CURRENT PROGRAM names, memoised
|
||||
// per program state in SamplerEmit.h's shared inversion, UNIONED with a sticky mark of
|
||||
// every unit this emitter has already described. A program with no image uniforms
|
||||
// gives MaxImageUnit == -1 and, with nothing sticky yet, a window of 0 - which is the
|
||||
// zero early-out, taken BEFORE any hash and before any 192-entry walk, exactly as
|
||||
// property 1 requires. The mark is sticky so that a program which stops naming a unit
|
||||
// does not silently stop describing it: the window only grows, and shrinking it is how
|
||||
// a stale binding would become invisible to the server.
|
||||
Uint64 EmitShaderImages(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
const auto& program = ctx.GetProgramForDraw();
|
||||
const auto& resolution = MGPipeProgramOpaqueUnitsShared().For(program.get());
|
||||
const Uint32 programWindow =
|
||||
resolution.MaxImageUnit < 0 ? 0u : static_cast<Uint32>(resolution.MaxImageUnit) + 1u;
|
||||
if (programWindow > m_window) m_window = programWindow;
|
||||
const Uint32 count = m_window < kMGPipeMaxImageUnits ? m_window : kMGPipeMaxImageUnits;
|
||||
// PROPERTY 1, and it is one integer test on every draw of every application that
|
||||
// never binds an image.
|
||||
if (count == 0) return 0;
|
||||
|
||||
for (Uint32 unit = 0; unit < count; ++unit) {
|
||||
const auto& binding = ctx.GetImageTextureBinding(static_cast<Int>(unit));
|
||||
MGPImageView& entry = m_entries[unit];
|
||||
entry = MGPImageView{};
|
||||
entry.Unit = unit;
|
||||
entry.Res = binding.Texture ? MGPipeSlots().Acquire(MGPipeKind::Texture,
|
||||
binding.Texture->GetLifetimeId())
|
||||
: kMGPipeNullHandle;
|
||||
// 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
|
||||
// MapToBackendTextureTarget and forces layer to 0 for a non-layerable one -
|
||||
// Adreno took a stray layer index literally. A client that pre-applied any of
|
||||
// that would be answering a driver question from the wrong side.
|
||||
entry.InternalFormat = static_cast<Uint32>(binding.Format);
|
||||
entry.Layer = static_cast<Uint32>(binding.Layer);
|
||||
entry.Level = static_cast<Uint16>(binding.Level);
|
||||
entry.Layered = binding.Layered != GL_FALSE ? 1 : 0;
|
||||
entry.Access = static_cast<Uint8>(MGPipeEncodeImageAccess(binding.Access));
|
||||
}
|
||||
|
||||
const Uint64 hash = MGPipeShaderImageSetContentHash(m_entries.data(), 0, count);
|
||||
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetShaderImages, hash)) {
|
||||
return 0;
|
||||
}
|
||||
m_lastImages = MGPShaderImages{};
|
||||
m_lastImages.Start = 0;
|
||||
m_lastImages.Count = count;
|
||||
m_lastImages.ContentHash = hash;
|
||||
MGPipeApplySetShaderImages(m_lastImages, m_entries.data());
|
||||
++m_imageSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ShaderImageEmissions, 1);
|
||||
}
|
||||
return sizeof(MGPShaderImages) + static_cast<Uint64>(count) * sizeof(MGPImageView);
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
// The validate point's FreshlyPrimed arm. A fresh context is a fresh set of image
|
||||
// bindings, so the sticky window starts over; the suppressor slot this set latches is
|
||||
// invalidated beside this call. There is no record half here at all - set_shader_images
|
||||
// is pure working state and mints no object of its own.
|
||||
void Reset() { m_window = 0; }
|
||||
|
||||
void ResetCounters() { m_imageSets = 0; }
|
||||
|
||||
const MGPShaderImages& LastShaderImages() const { return m_lastImages; }
|
||||
const Array<MGPImageView, kMGPipeMaxImageUnits>& LastImageViews() const { return m_entries; }
|
||||
Uint64 ImageSetCount() const { return m_imageSets; }
|
||||
Uint32 Window() const { return m_window; }
|
||||
|
||||
private:
|
||||
// GL_READ_ONLY / GL_WRITE_ONLY / GL_READ_WRITE folded into the one byte the wire
|
||||
// carries. A value the enum does not name would otherwise truncate silently into a
|
||||
// Uint8, which is the class of bug the descriptors exist to close.
|
||||
static Uint32 MGPipeEncodeImageAccess(GLenum access) {
|
||||
switch (access) {
|
||||
case GL_READ_ONLY:
|
||||
return 0;
|
||||
case GL_WRITE_ONLY:
|
||||
return 1;
|
||||
case GL_READ_WRITE:
|
||||
return 2;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "glBindImageTexture access 0x%x is not one of the three GL names",
|
||||
static_cast<Uint>(access));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Array<MGPImageView, kMGPipeMaxImageUnits> m_entries{};
|
||||
MGPShaderImages m_lastImages{};
|
||||
Uint32 m_window = 0;
|
||||
Uint64 m_imageSets = 0;
|
||||
};
|
||||
|
||||
inline MGPipeImageEmitter& MGPipeImageEmitterInstance() {
|
||||
|
||||
@@ -875,15 +875,197 @@ namespace MobileGL::MG_Pipe {
|
||||
using MG_State::GLState::RenderbufferObject;
|
||||
using MG_State::GLState::SamplerObject;
|
||||
|
||||
// THE SAME PAIR `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is
|
||||
// THE FOUR FAMILIES P4a MIGRATES, as one mask, so the consumer rule below is stated
|
||||
// once instead of four times. It is deliberately NOT kMGPipeSubsystemsMigratedAtP4a
|
||||
// (which is 0x1fff, every bit through P4a): the rule belongs to the families this
|
||||
// phase adds and to no earlier one.
|
||||
inline constexpr Uint64 kMGPipeP4aFamilySubsystems =
|
||||
kMGPipeSubsystemFramebuffer | kMGPipeSubsystemTextureResources |
|
||||
kMGPipeSubsystemSamplers | kMGPipeSubsystemPrograms;
|
||||
|
||||
// AND THE THIRD HALF, WHICH IS P3a's SECOND ONE: HAS A BACKEND REGISTERED THE CONSUMER?
|
||||
//
|
||||
// `MGPipeResourceSubsystemEnabled()` (above, ~:612) is bit 7 AND
|
||||
// `MGPipeGetResourceOps() != nullptr`, and the second conjunct is not decoration - it is
|
||||
// what keeps P3a's buffers on the legacy pull path under a backend that registers no
|
||||
// table. DirectVulkan (Magma) is exactly that backend: it registers no
|
||||
// MGPipeResourceOps and has none of P4a's twins. Without this conjunct the four P4a
|
||||
// families emitted there anyway, the applier ACCEPTED every record, the emitters cleared
|
||||
// their per-level dirty flags on that acceptance (D-D5 as amended by ID-18 M3), and
|
||||
// Magma's legacy upload path then found nothing left to upload: 66 texture-upload-shaped
|
||||
// DirectVulkan integration-gpu cases red on the push build at the default mask, with the
|
||||
// pull build 966/966 green (ID-39).
|
||||
//
|
||||
// ALL FOUR FAMILIES RIDE THE ONE SIGNAL, and the reason is D-D1: a texture and a
|
||||
// renderbuffer are RESOURCE rows - they travel on P3a's own resource_create /
|
||||
// resource_respecify / resource_subdata catalogue, whose consumer IS this table - so the
|
||||
// texture family's gate is P3a's gate by construction. The other three name texture
|
||||
// handles and cannot be live without it (MGPSurface::Res is a texture or renderbuffer
|
||||
// handle, MGPBoundView::Texture and MGPImageView::Res are texture handles, and
|
||||
// MGPTextureParams is addressed by one), so they follow. There is no fifth signal to
|
||||
// invent and no per-family registration to add: a backend that consumes P4a records
|
||||
// consumes resource rows first.
|
||||
//
|
||||
// A BACKEND THAT REGISTERS ONE IS UNAFFECTED. DirectGLES (Espryt) registers the table
|
||||
// at RegisterBufferBackendOps, unconditionally and at bring-up, so every predicate
|
||||
// below answers exactly what it answered before this commit.
|
||||
//
|
||||
// THE REGISTER/UNREGISTER WINDOW IS THE SAME ONE P3a LIVES WITH, and it is closed the
|
||||
// same way: UnregisterBufferBackendOps nulls the table at context teardown and the
|
||||
// re-register happens at the next MakeCurrent, so an object born in that window never
|
||||
// publishes a create and latches Published = false - after which the family's own
|
||||
// self-healing create on the next respecify (TextureEmit.h ~:576 / ~:709, the shape
|
||||
// MGPipeEmitResourceRespecify above uses for buffers) publishes it. Nothing here needs
|
||||
// to remember the window.
|
||||
Bool P4aFamilyHasItsConsumer(Uint64 subsystem) {
|
||||
return (subsystem & kMGPipeP4aFamilySubsystems) == 0 ||
|
||||
MGPipeGetResourceOps() != nullptr;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// AND THE FOURTH HALF: D-K2's DEPENDENCY TABLE, ON THE CLIENT (S-3, ID-41)
|
||||
// ================================================================================
|
||||
//
|
||||
// THE DEFECT THIS CLOSES. Espryt's four `Resolve<Family>SubsystemArm()` functions
|
||||
// (Managers.cpp ~:3595-3745) REFUSE a family whose D-K2 dependency bit is clear and run
|
||||
// the legacy arm instead - the shape ResolveVertexInputSubsystemArm's bit-8-requires-
|
||||
// bit-7 refusal set as the precedent. That refusal is a BACKSTOP and it cannot restore a
|
||||
// correct picture on its own, because the client's emission was gated on the operator's
|
||||
// mask ALONE: at 0x7ff (bit 10 set, bit 11 clear) the client emitted the whole texture
|
||||
// family, the applier accepted it, the emitter cleared each level's per-level dirty flag
|
||||
// on that acceptance (D-D5 as amended by ID-18 M3) - and then the server refused bit 10
|
||||
// and ran the legacy path, which found nothing left to upload. 438/491 on the DirectGLES
|
||||
// integration lane, the same 47 texture-upload failures ID-39 saw on Magma for the
|
||||
// consumer-less version of exactly this mistake.
|
||||
//
|
||||
// So the rule is the SAME "nothing at all, not less" rule as the consumer conjunct
|
||||
// above: with a dependency unmet the client emits NOTHING for that family and the legacy
|
||||
// pull path runs untouched, on both sides of the boundary.
|
||||
//
|
||||
// THE TABLE IS WRITTEN ONCE, HERE, and every one of its rows is the client mirror of the
|
||||
// refusal Espryt already implements, bit for bit and non-transitively - the two must say
|
||||
// the SAME thing, because a client that withheld more than the server refuses would
|
||||
// leave the server's handle arm live with no records to read, and a client that withheld
|
||||
// less is the defect above.
|
||||
struct P4aFamilyDependencyRow {
|
||||
Uint64 Family; // exactly one bit, and it is one of kMGPipeP4aFamilySubsystems
|
||||
Uint64 Requires; // the bits MOBILEGL_PIPE_PUSH must ALSO carry for it to be live
|
||||
};
|
||||
|
||||
inline constexpr P4aFamilyDependencyRow kMGPipeP4aFamilyDependencies[] = {
|
||||
// BIT 9 REQUIRES BIT 10. Every MGPSurface::Res in a set_framebuffer_state record
|
||||
// names a Texture or a Renderbuffer handle, and only bit 10 populates those two slot
|
||||
// tables (Managers.cpp ResolveFramebufferSubsystemArm).
|
||||
{kMGPipeSubsystemFramebuffer, kMGPipeSubsystemTextureResources},
|
||||
|
||||
// BIT 10 REQUIRES BIT 7 - a buffer texture's MGPResourceDesc::BufferForTexBuffer
|
||||
// names a Buffer handle and only bit 7 puts twins in the resource slot table (D-D1,
|
||||
// ResolveTextureResourceSubsystemArm's first row) - AND BIT 11, which is D-K2's
|
||||
// FOURTH row (ID-14/ID-15): MGPTextureParams::BuiltinSampler is a SamplerCso HANDLE,
|
||||
// only bit 11 mints sampler CSOs (c0b's four unconditional mints deliberately
|
||||
// exclude it), and the applier's verdict for a null one is Fatal{ProtocolCorruption}
|
||||
// rather than a decline. The brief's original "bit 10 without 11 is fine" is
|
||||
// WITHDRAWN for P4a as built.
|
||||
{kMGPipeSubsystemTextureResources,
|
||||
kMGPipeSubsystemResources | kMGPipeSubsystemSamplers},
|
||||
|
||||
// BIT 11 REQUIRES BIT 10. Every MGPBoundView::Texture and every MGPImageView::Res
|
||||
// names a Texture handle and only bit 10 populates that slot table; without it every
|
||||
// per-unit lookup would miss and the walk would `continue` WITHOUT unbinding
|
||||
// (ResolveSamplerSubsystemArm). With the row above this is SYMMETRIC: bits 10 and 11
|
||||
// are one arm with two switches, and the only two masks that reach either handle arm
|
||||
// are "both set" and "neither set".
|
||||
{kMGPipeSubsystemSamplers, kMGPipeSubsystemTextureResources},
|
||||
|
||||
// BIT 12 DEPENDS ON NOTHING, and that is a ROW rather than an absence so the table
|
||||
// covers the four families exhaustively (the static_assert below): a ShaderCso handle
|
||||
// names no texture and no buffer, the archive rides beside the record as a companion
|
||||
// pointer, and the extra inputs the server specialises on are read from state the
|
||||
// backend already holds (ResolveProgramSubsystemArm).
|
||||
{kMGPipeSubsystemPrograms, 0},
|
||||
};
|
||||
|
||||
// THE MIRROR PAIRS THAT STAY FINE, said out loud rather than left as an absence, because
|
||||
// an unreachable branch that says something different is how the reachable one drifts
|
||||
// (Managers.cpp's own words at :2377-2381) - and because the table is only trustworthy if
|
||||
// what it does NOT contain was decided rather than forgotten:
|
||||
// - bit 10 set, bit 9 clear: FINE. The legacy FBO sync reaches the texture twin through
|
||||
// SyncTextureObjectToBackend, which dispatches to the handle arm by itself.
|
||||
// - bit 11 set, bit 9 clear: FINE, for the same reason - a sampler view names a texture,
|
||||
// never a framebuffer.
|
||||
// - bit 7 set, bit 10 clear: FINE, and it is P3a's shipped configuration.
|
||||
// - bit 12 set with any or none of 9/10/11: FINE, per the last row.
|
||||
// - bit 10 set, bit 11 clear (and its mirror) is NOT fine and is the row above; this is
|
||||
// the one sentence in the brief that P4a as built withdrew.
|
||||
constexpr Uint64 P4aFamilyDependencyBits(Uint64 subsystem) {
|
||||
Uint64 required = 0;
|
||||
for (const P4aFamilyDependencyRow& row : kMGPipeP4aFamilyDependencies) {
|
||||
if ((subsystem & row.Family) != 0) required |= row.Requires;
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
// The table covers the four families this phase migrates and nothing else, so a fifth
|
||||
// family added to kMGPipeP4aFamilySubsystems without a row here does not silently inherit
|
||||
// "depends on nothing".
|
||||
constexpr Uint64 P4aFamilyDependencyTableCoverage() {
|
||||
Uint64 covered = 0;
|
||||
for (const P4aFamilyDependencyRow& row : kMGPipeP4aFamilyDependencies) covered |= row.Family;
|
||||
return covered;
|
||||
}
|
||||
static_assert(P4aFamilyDependencyTableCoverage() == kMGPipeP4aFamilySubsystems,
|
||||
"every P4a family needs a D-K2 dependency row, even an empty one");
|
||||
static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemFramebuffer) ==
|
||||
kMGPipeSubsystemTextureResources,
|
||||
"bit 9 requires bit 10");
|
||||
static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemTextureResources) ==
|
||||
(kMGPipeSubsystemResources | kMGPipeSubsystemSamplers),
|
||||
"bit 10 requires bit 7 and bit 11");
|
||||
static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemSamplers) ==
|
||||
kMGPipeSubsystemTextureResources,
|
||||
"bit 11 requires bit 10");
|
||||
static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemPrograms) == 0, "bit 12 depends on nothing");
|
||||
// No family may depend on itself: a row that did would be unfalsifiable (its own bit is
|
||||
// set by the time the conjunct is evaluated) and would read as a dependency nobody has.
|
||||
static_assert((P4aFamilyDependencyBits(kMGPipeSubsystemFramebuffer) &
|
||||
kMGPipeSubsystemFramebuffer) == 0 &&
|
||||
(P4aFamilyDependencyBits(kMGPipeSubsystemTextureResources) &
|
||||
kMGPipeSubsystemTextureResources) == 0 &&
|
||||
(P4aFamilyDependencyBits(kMGPipeSubsystemSamplers) &
|
||||
kMGPipeSubsystemSamplers) == 0,
|
||||
"a D-K2 row must not name its own family");
|
||||
// The default mask carries every dependency, so the shipped arm is unchanged by all of
|
||||
// this - the table only ever narrows a HAND-PICKED A/B mask.
|
||||
static_assert((kMGPipeSubsystemsMigratedAtP4a &
|
||||
P4aFamilyDependencyBits(kMGPipeP4aFamilySubsystems)) ==
|
||||
P4aFamilyDependencyBits(kMGPipeP4aFamilySubsystems),
|
||||
"the P4a phase mask must satisfy every dependency it declares");
|
||||
|
||||
// IT IS THE RUNTIME BIT THAT IS TESTED, NOT THE OTHER FAMILY'S LIVENESS, and that is
|
||||
// deliberate: Espryt's resolvers classify their arms from MOBILEGL_PIPE_PUSH alone, so
|
||||
// testing anything else here would make the two sides disagree at some mask - which is
|
||||
// the failure this whole commit is about, one level up. The mask is passed in rather than
|
||||
// read, so the walk's single read of MG_Config::Features.PipePush stays the one read a
|
||||
// whole validate point resolves against.
|
||||
Bool P4aFamilyDependenciesAreSet(Uint64 subsystem, Uint64 pushMask) {
|
||||
const Uint64 required = P4aFamilyDependencyBits(subsystem);
|
||||
return (pushMask & required) == required;
|
||||
}
|
||||
|
||||
// THE SAME QUADRUPLE `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is
|
||||
// deliberately the same predicate rather than a second copy of it: the operator's
|
||||
// per-subsystem A/B bit in MOBILEGL_PIPE_PUSH, AND this build having WIRED the family
|
||||
// at all. The second half is the family's own kMGPipeWired*Subsystem constant, which
|
||||
// lives in the family's emit header and is 0 until the commit that gives the emitter
|
||||
// its body - so a client path that lands before its emitter does is inert by
|
||||
// construction rather than by everyone remembering to check.
|
||||
// per-subsystem A/B bit in MOBILEGL_PIPE_PUSH, this build having WIRED the family
|
||||
// at all, AND - for a P4a family - a backend having registered the consumer and every
|
||||
// D-K2 dependency bit of the family being set. The second half is the family's own
|
||||
// kMGPipeWired*Subsystem constant, which lives in the family's emit header and is 0 until
|
||||
// the commit that gives the emitter its body - so a client path that lands before its
|
||||
// emitter does is inert by construction rather than by everyone remembering to check; the
|
||||
// third is P4aFamilyHasItsConsumer above and the fourth is P4aFamilyDependenciesAreSet.
|
||||
Bool FamilyIsLive(Uint64 subsystem, Uint64 wired) {
|
||||
return (MG_Config::Features.PipePush & subsystem) != 0 && (wired & subsystem) != 0;
|
||||
const Uint64 pushMask = MG_Config::Features.PipePush;
|
||||
return (pushMask & subsystem) != 0 && (wired & subsystem) != 0 &&
|
||||
P4aFamilyHasItsConsumer(subsystem) &&
|
||||
P4aFamilyDependenciesAreSet(subsystem, pushMask);
|
||||
}
|
||||
|
||||
// ---- THE FAMILY SEAM ----
|
||||
@@ -1039,6 +1221,18 @@ namespace MobileGL::MG_Pipe {
|
||||
PublicationLatch().NoteUnpublished(kind, handle);
|
||||
}
|
||||
|
||||
// THE GATE ITSELF, AS AN OBSERVABLE (ID-39, widened by S-3 / ID-41). Every P4a birth hook
|
||||
// below and every `wants()` row in the walk resolve through FamilyIsLive /
|
||||
// P4aFamilyHasItsConsumer / P4aFamilyDependenciesAreSet, and none of the three is reachable
|
||||
// from a test - so this is the one door a unit case has onto the answer, and it is the SAME
|
||||
// expression rather than a second copy of it. A subsystem outside kMGPipeP4aFamilySubsystems
|
||||
// answers the pair the P2/P3a families have always answered (its consumer conjunct is
|
||||
// vacuous and its dependency set is empty), which is what makes "nothing that emits today
|
||||
// changes" checkable instead of asserted.
|
||||
Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired) {
|
||||
return FamilyIsLive(subsystem, wired);
|
||||
}
|
||||
|
||||
void MGPipeMintTextureHandle(ITextureObject& texture) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Texture, texture.GetLifetimeId());
|
||||
}
|
||||
@@ -2171,7 +2365,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// would be a second copy of that map in the only path that runs, and mis-gating a bit
|
||||
// in it would pass every test the map has.
|
||||
//
|
||||
// FOUR CONDITIONS, AND THE WIRED MASK IS ONE OF THEM. `kMGPipeWiredSubsystems` is the
|
||||
// FIVE CONDITIONS, AND THE WIRED MASK IS ONE OF THEM. `kMGPipeWiredSubsystems` is the
|
||||
// OR of the per-family constants each emit header defines, and the whole ownership
|
||||
// design rests on it MEANING what the headers, this file and the result files all say
|
||||
// it means: an emitter runs only once the commit that gave it a body set its family's
|
||||
@@ -2181,11 +2375,28 @@ namespace MobileGL::MG_Pipe {
|
||||
// measure an arm nobody thinks is on - and the mirror error is worse: a family that
|
||||
// lands its body and forgets the constant would emit nothing and look broken. The
|
||||
// P2/P3a bits are all in the mask, so nothing that emits today changes.
|
||||
//
|
||||
// AND THE FIFTH IS P4aFamilyHasItsConsumer (ID-39), the same conjunct FamilyIsLive
|
||||
// applies to every birth hook: a P4a family whose records nothing on this backend
|
||||
// consumes emits NOTHING, so the legacy pull path runs exactly as it does on the pull
|
||||
// build. It is written here rather than folded into kMGPipeWiredSubsystems because the
|
||||
// wired mask is a property of the BUILD - a constexpr an emit header sets - and this is
|
||||
// a property of the RUNNING BACKEND, and collapsing the two would make a bisect that
|
||||
// lands between them unreadable. The P2/P3a bits are outside kMGPipeP4aFamilySubsystems,
|
||||
// so the conjunct is true for every one of them and nothing that emits today changes.
|
||||
//
|
||||
// AND THE SIXTH IS P4aFamilyDependenciesAreSet (S-3 / ID-41), the client half of D-K2:
|
||||
// a family one of whose dependency bits the operator left clear emits NOTHING here for
|
||||
// the same reason - the server REFUSES that family and runs its legacy arm, and an
|
||||
// emission the server refuses is an emission whose acceptance already cleared a frontend
|
||||
// dirty flag the legacy arm still owed. Same table, same four families, one place.
|
||||
const Uint64 pushMask = MG_Config::Features.PipePush;
|
||||
const auto wants = [&](MGPipeDirty bit) {
|
||||
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
|
||||
return subsystem != 0 && (pushMask & subsystem) != 0 &&
|
||||
(kMGPipeWiredSubsystems & subsystem) != 0 &&
|
||||
P4aFamilyHasItsConsumer(subsystem) &&
|
||||
P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
|
||||
(dirty & MGPipeDirtyBit(bit)) != 0;
|
||||
};
|
||||
Uint64 payloadBytes = 0;
|
||||
@@ -2250,10 +2461,17 @@ namespace MobileGL::MG_Pipe {
|
||||
payloadBytes += EmitShaderState(*ctx);
|
||||
}
|
||||
// The texture drain has no dirty bit over it (see its definition); it is gated on the
|
||||
// subsystem bit and on this build having wired the family at all, which is the same
|
||||
// pair `wants()` applies to every other emission.
|
||||
// subsystem bit, on this build having wired the family at all, on a backend having
|
||||
// registered the consumer and on D-K2's dependency bits for the family being set, which
|
||||
// is the same quadruple `wants()` applies to every other emission. The last two are the
|
||||
// whole of ID-39 and of S-3 on the path where they mattered most: the drain is what
|
||||
// clears a level's dirty flags on acceptance, so a drain that ran against an applier no
|
||||
// backend reads is exactly how Magma lost its texel uploads, and a drain that ran at a
|
||||
// mask whose bit 11 or bit 7 is clear is how Espryt lost them at 0x7ff and 0x5ff.
|
||||
if ((pushMask & kMGPipeSubsystemTextureResources) != 0 &&
|
||||
(kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0) {
|
||||
(kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0 &&
|
||||
P4aFamilyHasItsConsumer(kMGPipeSubsystemTextureResources) &&
|
||||
P4aFamilyDependenciesAreSet(kMGPipeSubsystemTextureResources, pushMask)) {
|
||||
payloadBytes += DrainTextureSubData(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewSamplerViews)) {
|
||||
@@ -2326,8 +2544,15 @@ namespace MobileGL::MG_Pipe {
|
||||
// the very fields the migration just took over.
|
||||
const MGPipeFieldEmitter emitter = kMGPipeFieldEmittedBy[i];
|
||||
const Uint64 subsystem = SubsystemForEmitter(emitter);
|
||||
// P4aFamilyHasItsConsumer and P4aFamilyDependenciesAreSet are in this conjunction for
|
||||
// the reason they are in `wants()`: "supplied" means A CALL WENT OUT CARRYING THIS
|
||||
// FIELD, and on a backend with no consumer - or at a mask that leaves one of the
|
||||
// family's D-K2 dependency bits clear - no P4a call went out at all, so withholding
|
||||
// the pull here would leave the field unfilled at the very verb that reads it.
|
||||
const Bool supplied = subsystem != 0 && (subsystem & kMGPipeWiredSubsystems) != 0 &&
|
||||
(pushMask & subsystem) != 0 &&
|
||||
P4aFamilyHasItsConsumer(subsystem) &&
|
||||
P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
|
||||
EmittedCallSuppliesTheWholeField(field) &&
|
||||
(applierDerives || AppliedWithoutDerivation(field));
|
||||
if (!supplied) MGPipeFillAccess::CopyField(inputs, *ctx, field);
|
||||
|
||||
@@ -43,6 +43,33 @@ namespace MobileGL::MG_Pipe {
|
||||
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
|
||||
void MGPipeLeaveVerb();
|
||||
|
||||
// PipeFill.cpp. DOES THIS BUILD, ON THIS BACKEND, AT THIS MASK, EMIT FOR THIS P4a FAMILY?
|
||||
// (ID-39, widened by S-3 / ID-41.) The four conjuncts are the operator's per-subsystem bit
|
||||
// in MOBILEGL_PIPE_PUSH, the family's own kMGPipeWired*Subsystem constant (`wired`, which
|
||||
// the caller passes because it lives in the family's emit header and this header may not
|
||||
// include one), and - for the four families P4a migrates - a backend having registered
|
||||
// MGPipeResourceOps (the same per-backend signal `MGPipeResourceSubsystemEnabled()` has
|
||||
// applied to P3a's buffers since the phase began) and every D-K2 dependency bit of the
|
||||
// family being set in the same mask.
|
||||
//
|
||||
// THE LAST TWO CONJUNCTS ARE THE ONES THIS DECLARATION EXISTS FOR, and they are the same
|
||||
// defect twice. Magma (DirectVulkan) registers no table and has no P4a twins; at a mask like
|
||||
// 0x7ff Espryt REFUSES the texture family server-side because D-K2's fourth row says bit 10
|
||||
// requires bit 11. In both cases the client emitted anyway, the applier accepted, the
|
||||
// emitters cleared their per-level dirty flags on that acceptance, and the legacy upload
|
||||
// path that still owed those texels found nothing to upload (66 DirectVulkan cases at ID-39,
|
||||
// 47 DirectGLES cases at ID-41). With them the four families emit NOTHING in that state and
|
||||
// the legacy pull path runs exactly as it does on a pull build.
|
||||
//
|
||||
// D-K2's TABLE IS IN PipeFill.cpp, ONCE: bit 9 requires bit 10, bit 10 requires bits 7 and
|
||||
// 11, bit 11 requires bit 10, bit 12 depends on nothing - the client mirror, bit for bit, of
|
||||
// the four `Resolve<Family>SubsystemArm()` refusals in DirectGLES/Managers.cpp.
|
||||
//
|
||||
// It is exported for the unit gate and for no other caller: the gate itself is
|
||||
// FamilyIsLive() inside PipeFill.cpp, every birth hook and every `wants()` row resolves
|
||||
// through it, and this returns that same expression rather than a second copy of it.
|
||||
Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired);
|
||||
|
||||
// PipeFill.cpp. P3a D-H2.1: the DRAW's raw vertex-fetch base instance, which
|
||||
// set_vertex_buffers now carries as an explicit field.
|
||||
//
|
||||
|
||||
@@ -36,38 +36,442 @@
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/CompositeResolver.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = 0;
|
||||
// WIRED. create/bind/delete_shader_state, set_draw_program, set_dispatch_program and
|
||||
// set_global_constants all have bodies, so this family contributes its bit to
|
||||
// kMGPipeWiredSubsystems.
|
||||
//
|
||||
// AND SINCE c0b THAT CONSTANT REALLY IS PART OF THE EMISSION GATE, so the note that used to
|
||||
// say otherwise here was true only against the contract commit: the validate point's
|
||||
// `wants()` asks the subsystem mapping, the operator's MOBILEGL_PIPE_PUSH mask, THIS
|
||||
// CONSTANT and the dirty bit, and the birth hooks' `FamilyIsLive` asks the same pair one
|
||||
// level in. It is also a compile-time contract - while it is non-zero PipeFill.cpp's
|
||||
// `if constexpr` seam instantiates the forward to EmitShaderCso below, so a missing entry
|
||||
// point is a build error here rather than at the merge. The RUNTIME A/B that switches the
|
||||
// family off is still the mask. See SamplerEmit.h's twin note.
|
||||
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = kMGPipeSubsystemPrograms;
|
||||
|
||||
// D-H6. ~0u is the BACKENDS' "never uploaded" sentinel for a global-constants version, and
|
||||
// ProgramObject::MarkUBOContentDirty skips it on the wrap for exactly that reason. The
|
||||
// client must never put it on the wire either: a server that received it would read its own
|
||||
// record as "nothing has ever been uploaded here" and re-upload for ever.
|
||||
inline constexpr Uint32 kMGPipeGlobalConstantsNeverUploaded = ~Uint32{0};
|
||||
|
||||
inline constexpr Bool MGPipeGlobalConstantsVersionIsEmittable(Uint32 version) {
|
||||
return version != kMGPipeGlobalConstantsNeverUploaded;
|
||||
}
|
||||
|
||||
// A program's identity for the wire, out of the SNAPSHOT the last link consumed and never
|
||||
// out of the live attach list: glAttachShader and glCompileShader take effect only at the
|
||||
// NEXT link and neither moves m_linkVersion, so a stage mask built from GetAttachedShaders
|
||||
// would describe a program that does not exist yet. GetLinkedShaderStages() is also what
|
||||
// indexes GetGeneratedSpirv(), so the two halves of this descriptor are guaranteed to agree
|
||||
// by construction rather than by care.
|
||||
inline Uint32 MGPipeStageMaskOf(const MG_State::GLState::ProgramObject& program) {
|
||||
Uint32 mask = 0;
|
||||
for (const ShaderStage stage : program.GetLinkedShaderStages()) {
|
||||
if (stage == ShaderStage::Unknown) continue;
|
||||
mask |= Uint32{1} << static_cast<Uint32>(stage);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeProgramEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||
|
||||
// create_shader_state (re-issued on the SAME handle whenever the link version moves -
|
||||
// Gen moves only on slot reuse), then bind_shader_state and set_draw_program /
|
||||
// set_dispatch_program. Two program calls because the frontend has two joins and two
|
||||
// PipeInputs slots.
|
||||
//
|
||||
// BOTH JOINS HAPPEN HERE and both are the verb's own: GetProgramForDraw flattens a
|
||||
// bound pipeline into its composite and GetProgramForDispatch answers the compute
|
||||
// question, and with a plain glUseProgram they are the same object, so the ordinary
|
||||
// frame pays one join it was going to pay anyway.
|
||||
Uint64 EmitShaderState(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
Uint64 bytes = 0;
|
||||
const auto& drawProgram = ctx.GetProgramForDraw();
|
||||
const auto& dispatchProgram = ctx.GetProgramForDispatch();
|
||||
|
||||
const MGPipeHandle drawCso =
|
||||
drawProgram ? AcquireShaderCso(*drawProgram, bytes) : kMGPipeNullHandle;
|
||||
// THE COMPOSITE'S SECOND RELEASE PATH is spoken here, not in a destructor: when the
|
||||
// bound pipeline's draw-program signature moves, the resolver releases the slot the
|
||||
// previous composite held. Whichever of the two paths runs second - this one or the
|
||||
// composite ProgramObject's own ~ProgramObject - is a proven no-op, because the slot
|
||||
// allocator refuses a slot that is not live at that generation.
|
||||
if (drawProgram && MGPipeProgramIsPipelineComposite(*drawProgram)) {
|
||||
if (const auto& pipeline = ctx.GetBoundProgramPipeline()) {
|
||||
// THE CONTEXT IS PART OF THE RESOLVER's KEY and this is the only place that
|
||||
// supplies it: the resolver is a process singleton and a pipeline's GL name
|
||||
// is per context, so without it a make-current between two contexts holding
|
||||
// one pipeline name released the other context's LIVE composite.
|
||||
// GetTextureContextId() is the tree's never-reused per-context id, the same
|
||||
// one PipeInputs carries and the backends' per-context memos key on.
|
||||
MGPipeCompositeResolverInstance().Observe(ctx.GetTextureContextId(), *pipeline,
|
||||
*drawProgram, drawCso);
|
||||
}
|
||||
}
|
||||
const MGPipeHandle dispatchCso =
|
||||
dispatchProgram ? (dispatchProgram == drawProgram ? drawCso
|
||||
: AcquireShaderCso(*dispatchProgram, bytes))
|
||||
: kMGPipeNullHandle;
|
||||
|
||||
// THE BOUND CSO IS THE DRAW ONE WHEN THERE IS ONE. bind_shader_state names what
|
||||
// glUseProgram selected, and when a program pipeline is bound instead that is the
|
||||
// composite; a compute-only pipeline has no draw program at all, and then the
|
||||
// dispatch program is the only thing bound. A null handle is legal here and means
|
||||
// exactly "nothing bound".
|
||||
const MGPipeHandle boundCso = !MGPipeHandleIsNull(drawCso) ? drawCso : dispatchCso;
|
||||
if (boundCso != m_boundCso) {
|
||||
MGPipeApplyBindShaderState(HandleOnly(boundCso));
|
||||
m_boundCso = boundCso;
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
if (drawCso != m_drawCso) {
|
||||
MGPipeApplySetDrawProgram(HandleOnly(drawCso));
|
||||
m_drawCso = drawCso;
|
||||
++m_drawSets;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
if (dispatchCso != m_dispatchCso) {
|
||||
MGPipeApplySetDispatchProgram(HandleOnly(dispatchCso));
|
||||
m_dispatchCso = dispatchCso;
|
||||
++m_dispatchSets;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// set_global_constants: the DEFAULT UNIFORM BLOCK only, keyed (ShaderCso, Version) and
|
||||
// at most once per program per frame. Version is GetUBOContentVersion() and must never
|
||||
// be ~0u, which is the backends' "never uploaded" sentinel - the wrap skips it.
|
||||
//
|
||||
// NAMED uniform blocks are NOT this call's: set_shader_buffers(Uniform) is a later
|
||||
// phase's and BindCurrentProgramWithResources' named-UBO block is untouched. What
|
||||
// travels here is globalUboScratch, the link phase's CPU array, which has no GL name
|
||||
// and no BufferObject behind it.
|
||||
Uint64 EmitGlobalConstants(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
const auto& program = ctx.GetProgramForDraw();
|
||||
if (!program) return 0;
|
||||
const Uint32 version = program->GetUBOContentVersion();
|
||||
// THE SENTINEL IS NEVER EMITTED. A server that received ~0u would read its own
|
||||
// record as "never uploaded" and re-upload every frame for ever.
|
||||
if (!MGPipeGlobalConstantsVersionIsEmittable(version)) return 0;
|
||||
const Uint size = program->GetUBOSize();
|
||||
if (size == 0) return 0;
|
||||
|
||||
Uint64 bytes = 0;
|
||||
const MGPipeHandle cso = AcquireShaderCso(*program, bytes);
|
||||
// (ShaderCso, Version) IS the key, so the latch is the key: an unchanged pair means
|
||||
// the server already holds these bytes and re-sending them would move the record's
|
||||
// serial for nothing.
|
||||
if (cso == m_constantsCso && version == m_constantsVersion) return bytes;
|
||||
|
||||
m_lastConstants = MGPGlobalConstants{};
|
||||
m_lastConstants.ShaderCso = cso;
|
||||
m_lastConstants.Version = version;
|
||||
// THE ONE BLOB RULE: Size 0 means "this record does not declare its blob" - which
|
||||
// is what a monolith emission is - and the bytes ride beside it as a companion
|
||||
// pointer. Offset carries the staging address for diagnostics only; nothing reads
|
||||
// it as a length.
|
||||
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
|
||||
m_lastConstants.Blob.Size = 0;
|
||||
MGPipeApplySetGlobalConstants(m_lastConstants, program->GetUBOData());
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
++m_constantSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
|
||||
}
|
||||
return bytes + sizeof(MGPGlobalConstants) + size;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
// D-H4's re-issue rule, and it is the CreateVertexElements shape one for one: the
|
||||
// record goes out again on the SAME handle whenever the link version moves, which is
|
||||
// legal because MGPipeHandle::Gen increments only on slot reuse and never on a
|
||||
// respecify. A program that relinks is the same GL object and the server's twin table
|
||||
// must not be asked to mint a second one.
|
||||
MGPipeHandle AcquireShaderCso(const ProgramObject& program, Uint64& payloadBytes) {
|
||||
const MGPipeHandle handle = AcquireShaderCsoHandle(program);
|
||||
if (MGPipeHandleIsNull(handle)) return handle;
|
||||
Latch& latch = LatchFor(handle);
|
||||
|
||||
const Uint32 linkVersion = program.GetLinkVersion();
|
||||
if (latch.RecordLive && latch.RecordGen == handle.Gen && latch.LinkVersion == linkVersion) {
|
||||
return handle;
|
||||
}
|
||||
|
||||
const auto& link = program.GetLinkReflection();
|
||||
const auto& spirv = program.GetSpirvReflection();
|
||||
|
||||
m_lastDesc = MGPProgramDesc{};
|
||||
m_lastDesc.Cso = handle;
|
||||
m_lastDesc.StageMask = MGPipeStageMaskOf(program);
|
||||
m_lastDesc.GlobalUboSize = static_cast<Uint32>(program.GetUBOSize());
|
||||
m_lastDesc.ReservedNumSamplesOffset = static_cast<Uint32>(spirv.reservedNumSamplesOffset);
|
||||
m_lastDesc.SpirvStatus = spirv.spirvStatus ? 1 : 0;
|
||||
m_lastDesc.NativeFloat64 = spirv.nativeFloat64 ? 1 : 0;
|
||||
m_lastDesc.PointSizeDemoted = spirv.pointSizeDemoted ? 1 : 0;
|
||||
m_lastDesc.EnableSpirvValidation = spirv.enableSpirvValidation ? 1 : 0;
|
||||
|
||||
// ONE BLOB REF PER MODULE, IN THE LINKED-SHADER-SNAPSHOT'S ORDER, which is the
|
||||
// order GetGeneratedSpirv() is indexed in - so Spirv[i] and StageMask agree because
|
||||
// they came out of the same snapshot. Every one of them declares Size 0 (the one
|
||||
// Blob rule); Offset carries the module's staging address so a reader can see which
|
||||
// slots are occupied without the record pretending to declare a length it does not
|
||||
// own.
|
||||
//
|
||||
// A COUNTED REFUSAL AND NOT AN ASSERTION (D-J3). MOBILEGL_ASSERT compiles out at
|
||||
// INFO, which is all three gate builds and every shipped build, so an assert here
|
||||
// would leave the truncation below completely silent in exactly the builds that
|
||||
// run - which is the idiom D-J3 exists to forbid. generatedSpirv cannot exceed six
|
||||
// stages today, so this is a guard against a seventh; truncation is the safe
|
||||
// direction and the counter is what makes it visible.
|
||||
const SizeT moduleCount = spirv.generatedSpirv.size();
|
||||
if (moduleCount > 6) ++m_moduleTruncations;
|
||||
for (SizeT i = 0; i < moduleCount && i < 6; ++i) {
|
||||
m_lastDesc.Spirv[i].Seg = kMGHostSpanSegNone;
|
||||
m_lastDesc.Spirv[i].Offset = reinterpret_cast<Uint64>(spirv.generatedSpirv[i].data());
|
||||
m_lastDesc.Spirv[i].Size = 0;
|
||||
}
|
||||
m_lastDesc.Reflection.Seg = kMGHostSpanSegNone;
|
||||
m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link);
|
||||
m_lastDesc.Reflection.Size = 0;
|
||||
|
||||
MGPipeApplyCreateShaderState(m_lastDesc, &link, &spirv);
|
||||
// THE CREATE WENT OUT, so the publication latch is taken here and nowhere else
|
||||
// (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it
|
||||
// delete_shader_state can never go out - for an ordinary program or for a
|
||||
// composite, both of which take that one helper.
|
||||
MGPipeNoteHandlePublished(MGPipeKind::ShaderCso, handle);
|
||||
++m_creates;
|
||||
payloadBytes += sizeof(MGPProgramDesc);
|
||||
|
||||
// A RE-ISSUED create_shader_state CLEARS THE APPLIER's DEFAULT UNIFORM BLOCK (wire
|
||||
// W6), so the (Cso, Version) latch that suppresses set_global_constants has to go
|
||||
// with it or the block is never re-sent. The case the design worries about is a
|
||||
// FAILED relink of a bound program - GL keeps the previous executable and its
|
||||
// uniforms running - and the general one is any future re-issue trigger that does
|
||||
// not happen to move the content version, of which a recycled slot is one.
|
||||
// Invalidated rather than re-emitted here, because this function has no business
|
||||
// deciding when the constants go out: the next EmitGlobalConstants sees an
|
||||
// unlatched key and sends them.
|
||||
if (m_constantsCso == handle) {
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
}
|
||||
|
||||
latch.RecordLive = true;
|
||||
latch.RecordGen = handle.Gen;
|
||||
latch.LinkVersion = linkVersion;
|
||||
return handle;
|
||||
}
|
||||
|
||||
// ---- THE CONTRACT ENTRY POINT THIS FAMILY OWES (contract-v2 §3.4) ----
|
||||
//
|
||||
// PipeFill.cpp's MGPipeEmitShaderCsoCreate forwards here through the `if constexpr`
|
||||
// seam keyed on kMGPipeWiredProgramSubsystem, so while that constant is non-zero this
|
||||
// must exist and be spelled exactly like this. A thin wrapper on purpose:
|
||||
// AcquireShaderCso above IS this family's handle rule - identity-addressed per
|
||||
// ProgramObject, the composite band entered through the one door, the re-issue on the
|
||||
// same handle and the publication - and a second copy of any of it here would be a
|
||||
// second authority.
|
||||
//
|
||||
// THE HOOK HAS ALREADY APPLIED BOTH GATES (the operator's mask and the wired constant),
|
||||
// so this body applies none of its own. The byte count is discarded: a birth is not a
|
||||
// validate-point emission and has no payload budget to report into.
|
||||
void EmitShaderCso(ProgramObject& program) {
|
||||
Uint64 bytes = 0;
|
||||
AcquireShaderCso(program, bytes);
|
||||
}
|
||||
|
||||
// The emitter's OWN record memo - "have I already published a create_shader_state at
|
||||
// this slot, for this generation, at this link version".
|
||||
//
|
||||
// IT IS NOT WHAT THE DEATH PATH ASKS, and that changed at c0b (contract-v2 §3.1/D17):
|
||||
// MGPipeEmitShaderCsoDestroyAndFree reads A's publication latch, which is one answer
|
||||
// per {kind, slot, gen} that all six death helpers share. This stays because the
|
||||
// VERSION-FIRST SKIP needs it - it is the same latch AcquireShaderCso consults before
|
||||
// it builds a descriptor - and because a unit case reads it.
|
||||
//
|
||||
// THE COMPOSITE BAND IS INDEXED SEPARATELY, for the allocator's own reason: the band
|
||||
// base is 983040, so a slot-indexed vector would allocate ~983k latches for one program
|
||||
// pipeline. Both spaces stay dense against their own high-water mark.
|
||||
Bool RecordIsPublished(MGPipeHandle handle) const {
|
||||
if (MGPipeHandleIsNull(handle)) return false;
|
||||
const Vector<Latch>& table = TableOf(handle);
|
||||
const SizeT slot = SlotIndexOf(handle);
|
||||
if (slot >= table.size()) return false;
|
||||
const Latch& latch = table[slot];
|
||||
return latch.RecordLive && latch.RecordGen == handle.Gen;
|
||||
}
|
||||
|
||||
// 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.
|
||||
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
Vector<Latch>& table = TableOf(handle);
|
||||
const SizeT slot = SlotIndexOf(handle);
|
||||
if (slot < table.size() && table[slot].RecordGen == handle.Gen) {
|
||||
table[slot] = Latch{};
|
||||
}
|
||||
if (m_boundCso == handle) m_boundCso = kMGPipeNullHandle;
|
||||
if (m_drawCso == handle) m_drawCso = kMGPipeNullHandle;
|
||||
if (m_dispatchCso == handle) m_dispatchCso = kMGPipeNullHandle;
|
||||
if (m_constantsCso == handle) {
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
}
|
||||
}
|
||||
|
||||
// The validate point's FreshlyPrimed arm. MGPipeApplierReset clears DrawProgram,
|
||||
// DispatchProgram and BoundShaderCso - all three are per-context WORKING STATE - so
|
||||
// the three mirrors here go with them, or the first emission after a make-current
|
||||
// would be suppressed as unchanged and the server would draw with the previous
|
||||
// context's program bound.
|
||||
//
|
||||
// The RECORD half stays, and that is the rule rather than an oversight: the applier
|
||||
// keeps its shader-CSO records across a make-current because a program lives in a share
|
||||
// group, and re-publishing one would move its Serial for nothing. The global-constants
|
||||
// key goes with the working state because its record's bytes are per (Cso, Version) and
|
||||
// a fresh server has not been told them.
|
||||
void Reset() {
|
||||
m_boundCso = kMGPipeNullHandle;
|
||||
m_drawCso = kMGPipeNullHandle;
|
||||
m_dispatchCso = kMGPipeNullHandle;
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
// The composite memo's freshness goes with them - and only its freshness. Its
|
||||
// ENTRIES name composites whose frontend objects outlive the context switch, so
|
||||
// releasing them here would emit a delete for a live program.
|
||||
MGPipeCompositeResolverInstance().Reset();
|
||||
}
|
||||
|
||||
void ResetCounters() {
|
||||
m_creates = m_binds = m_drawSets = m_dispatchSets = m_constantSets = 0;
|
||||
m_moduleTruncations = 0;
|
||||
}
|
||||
|
||||
// ---- what a unit case reads ----
|
||||
const MGPProgramDesc& LastProgramDesc() const { return m_lastDesc; }
|
||||
const MGPGlobalConstants& LastGlobalConstants() const { return m_lastConstants; }
|
||||
// THE (Cso, Version) KEY set_global_constants is suppressed against. Exposed so a case
|
||||
// can pin that a re-issued create_shader_state invalidates it - the applier clears the
|
||||
// block on the re-issue (wire W6), so a latch that survived it would never re-send.
|
||||
MGPipeHandle GlobalConstantsCso() const { return m_constantsCso; }
|
||||
Uint32 GlobalConstantsVersion() const { return m_constantsVersion; }
|
||||
// D-J3's counted refusal: programs whose linked snapshot carried more modules than
|
||||
// MGPProgramDesc::Spirv[] can name, and whose tail was therefore dropped.
|
||||
Uint64 TruncatedModuleCount() const { return m_moduleTruncations; }
|
||||
MGPipeHandle BoundCso() const { return m_boundCso; }
|
||||
MGPipeHandle DrawCso() const { return m_drawCso; }
|
||||
MGPipeHandle DispatchCso() const { return m_dispatchCso; }
|
||||
Uint64 CreateCount() const { return m_creates; }
|
||||
Uint64 BindCount() const { return m_binds; }
|
||||
Uint64 DrawProgramSetCount() const { return m_drawSets; }
|
||||
Uint64 DispatchProgramSetCount() const { return m_dispatchSets; }
|
||||
Uint64 GlobalConstantsSetCount() const { return m_constantSets; }
|
||||
|
||||
private:
|
||||
// THE ONE PLACE THE BAND CAN ENTER. An ordinary program's slot comes from the ordinary
|
||||
// allocator door keyed on its lifetime id. CompositeResolver.h widens this to send a
|
||||
// pipeline composite through MGPipeSlotAllocator::AllocateComposite instead, and
|
||||
// nothing else about the emission changes - the server never learns a composite is a
|
||||
// composite.
|
||||
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
|
||||
const Uint64 lifetimeId = program.GetLifetimeId();
|
||||
const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
if (!MGPipeHandleIsNull(existing)) return existing;
|
||||
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
|
||||
// an ordinary ShaderCso handle in every other respect - the same kind, the same
|
||||
// {slot, gen} rules, the same Free, the same death helper. Keying it on its own
|
||||
// lifetime id rather than on the pipeline's signature is what makes ~ProgramObject
|
||||
// able to release it at all, and it is why two pipelines that happen to have the
|
||||
// same signature keep their own composite: sharing one handle between two frontend
|
||||
// objects would let the first one's death free a slot the second still names.
|
||||
return MGPipeProgramIsPipelineComposite(program)
|
||||
? MGPipeSlots().AllocateComposite(lifetimeId)
|
||||
: MGPipeSlots().AllocateFor(MGPipeKind::ShaderCso, lifetimeId);
|
||||
}
|
||||
|
||||
struct Latch {
|
||||
Bool RecordLive = false;
|
||||
Uint32 RecordGen = 0;
|
||||
Uint32 LinkVersion = 0;
|
||||
};
|
||||
|
||||
// TWO TABLES, NOT A WIDER ONE, and it is the allocator's own reason repeated where it
|
||||
// bites a second time: the composite band starts at slot 983040, so folding a composite
|
||||
// into the ordinary slot-indexed vector would allocate ~983k latches - and grow them
|
||||
// again on every future push_back - for a single program pipeline. Both spaces stay
|
||||
// dense against their own high-water mark, which is exactly what the allocator does one
|
||||
// level down.
|
||||
Vector<Latch>& TableOf(MGPipeHandle handle) {
|
||||
return MGPipeIsCompositeShaderSlot(handle.Slot) ? m_compositeLatch : m_latch;
|
||||
}
|
||||
const Vector<Latch>& TableOf(MGPipeHandle handle) const {
|
||||
return MGPipeIsCompositeShaderSlot(handle.Slot) ? m_compositeLatch : m_latch;
|
||||
}
|
||||
static SizeT SlotIndexOf(MGPipeHandle handle) {
|
||||
return MGPipeIsCompositeShaderSlot(handle.Slot)
|
||||
? static_cast<SizeT>(handle.Slot - kMGPipeShaderCsoCompositeSlotBase)
|
||||
: static_cast<SizeT>(handle.Slot);
|
||||
}
|
||||
Latch& LatchFor(MGPipeHandle handle) {
|
||||
Vector<Latch>& table = TableOf(handle);
|
||||
const SizeT slot = SlotIndexOf(handle);
|
||||
if (slot >= table.size()) table.resize(slot + 1);
|
||||
return table[slot];
|
||||
}
|
||||
|
||||
static MGPHandleOnly HandleOnly(MGPipeHandle handle) {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::ShaderCso);
|
||||
return only;
|
||||
}
|
||||
|
||||
MGPProgramDesc m_lastDesc{};
|
||||
MGPGlobalConstants m_lastConstants{};
|
||||
|
||||
Vector<Latch> m_latch;
|
||||
Vector<Latch> m_compositeLatch;
|
||||
MGPipeHandle m_boundCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_drawCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_constantsCso = kMGPipeNullHandle;
|
||||
Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
|
||||
Uint64 m_creates = 0;
|
||||
Uint64 m_binds = 0;
|
||||
Uint64 m_drawSets = 0;
|
||||
Uint64 m_dispatchSets = 0;
|
||||
Uint64 m_constantSets = 0;
|
||||
Uint64 m_moduleTruncations = 0;
|
||||
};
|
||||
|
||||
inline MGPipeProgramEmitter& MGPipeProgramEmitterInstance() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1103
-14
File diff suppressed because it is too large
Load Diff
+128
-12
@@ -349,6 +349,18 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 bindings = 0;
|
||||
Uint64 constants = 0;
|
||||
Uint64 programImages = 0;
|
||||
// THE PROGRAM INPUT OF THE PROGRAM-RESOLVED VIEW SET (P4a fable seam F-1).
|
||||
// set_sampler_views is resolved for the program in use (SamplerEmit.h: the sampler
|
||||
// uniform's TYPE picks which of a unit's targets is the view) and the emitter
|
||||
// memoises that resolution on (lifetime id, link version, backend state version). A
|
||||
// shutter that read only the texture generations therefore missed a glUseProgram:
|
||||
// `glBindTexture x N; glUseProgram(P1); draw; glUseProgram(P2); draw` moved nothing
|
||||
// bit 12 read, so the view set stayed P1's - and E's record epoch, keyed on the two
|
||||
// set serials, then never rebuilt the texture sync list for P2 either. This value is
|
||||
// that memo key, and bit 12 mixes it in below: over-firing costs one re-resolution
|
||||
// the set-hash suppressor absorbs, under-firing left the record describing the
|
||||
// previous program's units.
|
||||
Uint64 opaqueUnits = 0;
|
||||
if (program) {
|
||||
shader = MGPipeMixShutter(program->GetLifetimeId(), program->GetLinkVersion());
|
||||
bindings = MGPipeMixShutter(
|
||||
@@ -357,7 +369,16 @@ namespace MobileGL::MG_Pipe {
|
||||
program->GetBlockBindingVersion()),
|
||||
program->GetUniformWriteSetVersion());
|
||||
constants = MGPipeMixShutter(program->GetLifetimeId(), program->GetUBOContentVersion());
|
||||
programImages = program->GetImageUnitVersion();
|
||||
// THE IDENTITY IS MIXED IN (P4a fable seam F-2), exactly as the pipeline arm
|
||||
// below mixes stageLinks into its half: the counter alone is a per-program
|
||||
// number two programs routinely share - 0 == 0 for any pair that never moved an
|
||||
// image unit through glUniform1i, and 0 == 0 against no program at all - so a
|
||||
// glUseProgram between them fired nothing, set_shader_images' window stayed the
|
||||
// previous program's, and a program whose only image is a BUFFER image (E's
|
||||
// SD-4: nothing else moves between the bind and the dispatch) never reached the
|
||||
// record at all.
|
||||
programImages = MGPipeMixShutter(shader, program->GetImageUnitVersion());
|
||||
opaqueUnits = MGPipeMixShutter(shader, program->GetBackendStateVersion());
|
||||
} else if (const auto& pipeline = ctx.GetBoundProgramPipeline(); pipeline) {
|
||||
using Pipeline = MG_State::GLState::ProgramPipelineObject;
|
||||
// THE FIELDS ARE READ DIRECTLY RATHER THAN THROUGH THE TWO FUNCTIONS THAT
|
||||
@@ -401,6 +422,10 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 stageLinks = static_cast<Uint64>(ctx.GetBoundProgramPipelineName());
|
||||
Uint64 stageState = 0;
|
||||
Uint64 stageImages = 0;
|
||||
// The per-stage sampler/image unit assignments alone (glUniform1i on a stage
|
||||
// program's sampler moves its backend state version and reaches the composite
|
||||
// through the uniform mirror), for bit 12's program input below.
|
||||
Uint64 stageOpaque = 0;
|
||||
for (SizeT stage = 0; stage < Pipeline::kGraphicsStageCount; ++stage) {
|
||||
const auto& staged = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!staged) continue;
|
||||
@@ -412,17 +437,88 @@ namespace MobileGL::MG_Pipe {
|
||||
staged->GetBlockBindingVersion())),
|
||||
staged->GetUniformWriteSetVersion());
|
||||
stageImages = MGPipeMixShutter(stageImages, staged->GetImageUnitVersion());
|
||||
stageOpaque = MGPipeMixShutter(stageOpaque, staged->GetBackendStateVersion());
|
||||
}
|
||||
shader = stageLinks;
|
||||
stageState = MGPipeMixShutter(stageLinks, stageState);
|
||||
bindings = MGPipeMixShutter(stageState, stageImages);
|
||||
constants = stageState;
|
||||
programImages = MGPipeMixShutter(stageLinks, stageImages);
|
||||
opaqueUnits = MGPipeMixShutter(stageLinks, stageOpaque);
|
||||
}
|
||||
now[Index(MGPipeDirty::NewShader)] = shader;
|
||||
now[Index(MGPipeDirty::NewShaderBindings)] = bindings;
|
||||
now[Index(MGPipeDirty::NewGlobalConstants)] = constants;
|
||||
|
||||
// ===========================================================================
|
||||
// THE RECORD-FIELD -> SETTER -> SHUTTER TABLE FOR THE SEVEN P4a BITS.
|
||||
//
|
||||
// THE RULE (P4a fable seam audit, section C.1): every field of every emitted
|
||||
// record names the frontend setter that changes it, and that setter moves a
|
||||
// counter the emitting bit's shutter reads - or the emission is unconditional at
|
||||
// the setter (the resource_* family, set_texture_params). A record field whose
|
||||
// setter moves no shutter input is a stale record with nothing to refuse: c0d
|
||||
// (bit 13 without the bind generation), SD-0 (an image re-bind), F-1 (the
|
||||
// program behind the view set), F-2 (the program behind the image window) and
|
||||
// F-3 (an attached object's storage) were all this one class. DirtySurface.def
|
||||
// cannot catch it - it maps MUTATORS to bits and cannot see that a DERIVED field
|
||||
// depends on a mutator whose row is another family's - so the table lives here,
|
||||
// beside the shutters, and a row is added whenever a record gains a field.
|
||||
//
|
||||
// bit 6 create/bind_shader_state, set_draw/dispatch_program (ProgramEmit.h)
|
||||
// fields: Cso, StageMask, GlobalUboSize, the artefact blob refs, the two
|
||||
// bound handles
|
||||
// setters: glUseProgram (m_currentProgram), glLinkProgram (link version),
|
||||
// glBindProgramPipeline / glUseProgramStages (pipeline name +
|
||||
// per-stage {lifetime id, link version})
|
||||
// shutter: lifetime id x link version, or stageLinks under a pipeline
|
||||
// bit 7 the program's bindings (image units, block bindings, uniform write set)
|
||||
// setters: glUniform1i on an opaque uniform (backend state version, image
|
||||
// unit version), glUniformBlockBinding / glShaderStorageBlockBinding
|
||||
// (block binding version), any glUniform* (uniform write set)
|
||||
// shutter: the four per-program counters, x stageLinks under a pipeline
|
||||
// bit 8 set_global_constants: ShaderCso, Version, the default-block image
|
||||
// setters: any glUniform* on the default block (UBO content version),
|
||||
// glUseProgram (lifetime id)
|
||||
// shutter: lifetime id x UBO content version, or stageState
|
||||
// bit 11 set_framebuffer_state: Fbo, Color[8]/Depth/Stencil/ReadSurface
|
||||
// (Res, Kind, InternalFormat, TextureTarget, Layered, Level, Layer,
|
||||
// UploadTarget), DrawBuffers[8], Width/Height/Layers/Samples/
|
||||
// FixedSampleLocations, IsDefault, Complete, Target
|
||||
// setters: glFramebufferTexture*/glFramebufferRenderbuffer, glDrawBuffer(s),
|
||||
// glReadBuffer, glFramebufferParameteri (the attachment
|
||||
// aggregate); glBindFramebuffer (the two binding slot versions);
|
||||
// AND a storage redefinition of an ATTACHED texture or
|
||||
// renderbuffer - glTexImage*/glTexStorage*/glTexBuffer/
|
||||
// glTextureView/glRenderbufferStorage* - because InternalFormat,
|
||||
// TextureTarget, the extent, Samples and Complete are INLINED at
|
||||
// emission (D-C1): those bump the attachment aggregate from the
|
||||
// object's PipePublishDescriptor (F-3)
|
||||
// shutter: attachment aggregate x draw bind version x read bind version
|
||||
// bit 12 set_sampler_views: per unit {View, Texture}
|
||||
// setters: glBindTexture / glActiveTexture (bind generation), a texture's
|
||||
// or a sampler object's parameters (SamplesAsIncompleteTexture -
|
||||
// the params aggregate), an upload that defines a level (content
|
||||
// aggregate), the default texture's image appearing (bind
|
||||
// generation, TextureObject.cpp); AND the program in use -
|
||||
// glUseProgram, a relink, glUniform1i on a sampler uniform (which
|
||||
// unit a uniform's TYPE resolves) - F-1
|
||||
// shutter: content x params x bind generation x opaqueUnits
|
||||
// bit 13 bind_sampler_states: per unit the sampler CSO handle
|
||||
// setters: glBindSampler (bind generation, c0d), glSamplerParameter* /
|
||||
// glTexParameter* (params aggregate + sampling resolution),
|
||||
// glDeleteSamplers (bind generation)
|
||||
// shutter: params x sampling resolution x bind generation
|
||||
// bit 14 set_shader_images: per unit {Res, InternalFormat, Layer, Level,
|
||||
// Layered, Access} over the program's image-unit window
|
||||
// setters: glBindImageTexture (bind generation, SD-0), a texture's
|
||||
// content/params, glUniform1i on an image uniform (image unit
|
||||
// version); AND the program in use - glUseProgram, a relink -
|
||||
// F-2
|
||||
// shutter: content x params x bind generation x programImages
|
||||
// (lifetime id x link version x image unit version)
|
||||
// ===========================================================================
|
||||
|
||||
// ---- the object-class bits 9..17 ----
|
||||
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
|
||||
const Uint64 textureParams = ctx.GetAnyTextureParamsGeneration();
|
||||
@@ -470,13 +566,24 @@ namespace MobileGL::MG_Pipe {
|
||||
// firing costs one extra push; under-firing renders stale, and this file's own
|
||||
// rule is that under-firing is the dangerous direction.
|
||||
//
|
||||
// A RENDERBUFFER RESPECIFY IS STILL INVISIBLE HERE, and deliberately so:
|
||||
// RenderbufferObject's SetInternalFormat / AllocateStorage / SetSamples bump no
|
||||
// version and raise no notice, so re-storaging an ALREADY-ATTACHED renderbuffer
|
||||
// moves neither half of this shutter. That hole is closed by emitting
|
||||
// resource_respecify straight from the storage entry point - not by widening this
|
||||
// shutter and not by adding a version counter to RenderbufferObject, which would
|
||||
// resize the pull build's object and break G1.
|
||||
// A STORAGE REDEFINITION OF AN ATTACHED OBJECT MOVES THIS SHUTTER (P4a fable seam
|
||||
// F-3), and the sentence that stood here - "a renderbuffer respecify is still
|
||||
// invisible here, and deliberately so ... closed by emitting resource_respecify
|
||||
// straight from the storage entry point" - was true of the RESOURCE record only.
|
||||
// set_framebuffer_state inlines each attachment's InternalFormat, TextureTarget,
|
||||
// extent, Samples and Complete (D-C1: "so the four cross-object masks fall out at
|
||||
// push time with no lookup"), so `glTexImage2D(tex, RGB8); attach; draw;
|
||||
// glTexImage2D(tex, RGBA8); draw` left the FRAMEBUFFER record saying RGB8 while the
|
||||
// resource record said RGBA8, and the handle arm answered its alpha-widening,
|
||||
// snorm-clamp and integer masks from the stale copy where the legacy arm re-read
|
||||
// the frontend at the same re-sync - a proven arm divergence on a public-GL
|
||||
// sequence. The fix is at the SETTER, not here: TextureObjectBase::PipePublish
|
||||
// Descriptor and RenderbufferObject::PipePublishDescriptor - the one funnel every
|
||||
// storage-defining entry point of either object takes, push-only - bump the
|
||||
// attachment aggregate this shutter already reads. No counter is added to either
|
||||
// object (G1), nothing widens this shutter onto the texture-content aggregate (which
|
||||
// would fire the 304-byte record build on every glTexSubImage2D), and a storage
|
||||
// definition of an UNATTACHED object over-fires it exactly once at load time.
|
||||
//
|
||||
// AND A TRAP THE NEXT NARROWING WOULD WALK INTO, recorded here because it is
|
||||
// invisible from the shutter: FramebufferObject::SetDrawBuffer versions the VALUE
|
||||
@@ -491,8 +598,16 @@ namespace MobileGL::MG_Pipe {
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion())),
|
||||
m_readFramebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Read).GetVersion()));
|
||||
now[Index(MGPipeDirty::NewSamplerViews)] =
|
||||
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
|
||||
// Bit 12 reads FOUR things (F-1): the two texture aggregates, the bind generation
|
||||
// and the program input computed above. The params aggregate is here because
|
||||
// SamplerEmit.h drops a unit's view to null when SamplesAsIncompleteTexture says so,
|
||||
// and that predicate reads the effective sampler's filters - a glTexParameteri(
|
||||
// MIN_FILTER) that completes a texture fired bit 13 and not this one, so the entry
|
||||
// stayed null. The program input is here because the set is resolved FOR THE
|
||||
// PROGRAM IN USE, and a glUseProgram alone moved nothing this shutter read.
|
||||
now[Index(MGPipeDirty::NewSamplerViews)] = MGPipeMixShutter(
|
||||
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), ctx.GetTextureBindGeneration()),
|
||||
opaqueUnits);
|
||||
// Bit 13, WIDENED AT P4a FOR BIT 11's REASON and found the same way. glBindSampler
|
||||
// moves NEITHER half of what this used to read: GL_Sampler.cpp's BindSampler_State
|
||||
// goes through NoteTextureUnitTouched and TextureUnit::SetSamplerObject, and both
|
||||
@@ -515,8 +630,9 @@ namespace MobileGL::MG_Pipe {
|
||||
now[Index(MGPipeDirty::NewSamplers)] = MGPipeMixShutter(
|
||||
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration()),
|
||||
ctx.GetTextureBindGeneration());
|
||||
now[Index(MGPipeDirty::NewShaderImages)] =
|
||||
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages);
|
||||
now[Index(MGPipeDirty::NewShaderImages)] = MGPipeMixShutter(
|
||||
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages),
|
||||
ctx.GetTextureBindGeneration());
|
||||
now[Index(MGPipeDirty::NewConstBuffers)] = buffers;
|
||||
now[Index(MGPipeDirty::NewShaderBuffers)] = buffers;
|
||||
now[Index(MGPipeDirty::NewSoTargets)] =
|
||||
|
||||
@@ -53,6 +53,8 @@ add_executable(MobileGLIntegrationTest
|
||||
Harness/HeadlessGL.cpp
|
||||
Harness/BackendCapsPeek.cpp
|
||||
Harness/PipeSlotPeek.cpp
|
||||
Harness/PipeApplyPeek.cpp
|
||||
Harness/P4aSeamPeek.cpp
|
||||
Scenarios/OrientationScenario.cpp
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
@@ -133,6 +135,10 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/HandleRecycleScenario.cpp
|
||||
Scenarios/CsoContentAddressingScenario.cpp
|
||||
Scenarios/ResourceSubsystemControlScenario.cpp
|
||||
Scenarios/TextureParamsWithoutASamplerViewScenario.cpp
|
||||
Scenarios/TextureUploadShapeScenario.cpp
|
||||
Scenarios/ObjectSubsystemControlScenario.cpp
|
||||
Scenarios/P4aSeamAuditScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -405,6 +411,60 @@ function(mgl_itest_probe_for_symbol outVar directory symbolRegex)
|
||||
set(${outVar} "${mglItestProbeHit}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# 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
|
||||
# not answered by "some source reads Features.PipeHandleAbaControl" - MagmaPipeArms.h does, and its
|
||||
# consumers are Magma's VERTEX-INPUT keys, so a single-regex probe would arm the six P4a ABA
|
||||
# controls on a backend where the knob cannot reach a texture, a framebuffer, a sampler, a view or a
|
||||
# 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 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(mglItestProbeHitA "")
|
||||
set(mglItestProbeHitB "")
|
||||
foreach(mglItestProbeSource IN LISTS mglItestProbeSources)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${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()
|
||||
if (mglItestProbeHitA AND mglItestProbeHitB)
|
||||
set(${outVar} "${mglItestProbeHitA} + ${mglItestProbeHitB}" PARENT_SCOPE)
|
||||
else()
|
||||
set(${outVar} "" PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
# DirectGLES' Track H arm, probed by the subsystem bit it is gated on rather than by
|
||||
# SlotTables.h existing: the bit is declared in the contract (MG_Pipe/MGPipe.h:77) and the
|
||||
@@ -490,6 +550,91 @@ if (MOBILEGL_PIPE_PUSH)
|
||||
"mpr entries will SKIP")
|
||||
endif()
|
||||
|
||||
# P4a's THIRD re-key question, per backend, and it is not either of the two above. Bits 5/6
|
||||
# re-keyed each backend's VERTEX-INPUT memos and bit 7/8 the BUFFER's; P4a re-keys six OBJECT
|
||||
# classes - texture, renderbuffer, framebuffer, sampler CSO, sampler view, shader CSO - behind
|
||||
# four new subsystem bits, and a backend has to NAME one of those constants to honour
|
||||
# MOBILEGL_PIPE_PUSH's default mask. A P4a case that read either older marker would arm its
|
||||
# Handles arm on a tree where nothing about a texture is keyed on a handle: green for a re-key
|
||||
# that does not exist. Probed by the constants rather than by a file, for the reason the block
|
||||
# above gives - packages D and E choose their own file layout.
|
||||
foreach(mglItestObjectBackend DirectGLES DirectVulkan)
|
||||
mgl_itest_probe_for_symbol(MGL_ITEST_OBJECT_REKEY
|
||||
"${MGL_ITEST_ROOT}/MobileGL/MG_Backend/${mglItestObjectBackend}"
|
||||
"kMGPipeSubsystem(Framebuffer|TextureResources|Samplers|Programs)")
|
||||
if (MGL_ITEST_OBJECT_REKEY)
|
||||
message(STATUS "Integration tests: ${mglItestObjectBackend} is keyed on {slot, gen} for "
|
||||
"P4a's object families (${MGL_ITEST_OBJECT_REKEY})")
|
||||
list(APPEND MGL_ITEST_CAPABILITY_ENV
|
||||
"MGITEST_HANDLE_REKEY_OBJECTS_${mglItestObjectBackend}=1")
|
||||
else()
|
||||
message(STATUS "Integration tests: no ${mglItestObjectBackend} source names "
|
||||
"kMGPipeSubsystem{Framebuffer,TextureResources,Samplers,Programs} - "
|
||||
"HandleRecycle's six P4a cases will SKIP their Handles arm on it")
|
||||
endif()
|
||||
|
||||
# ...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"
|
||||
"kMGPipeSubsystem(Framebuffer|TextureResources|Samplers|Programs)")
|
||||
if (MGL_ITEST_OBJECT_ABA)
|
||||
message(STATUS "Integration tests: MOBILEGL_PIPE_HANDLE_ABA_CONTROL steers "
|
||||
"${mglItestObjectBackend}'s P4a object keys (${MGL_ITEST_OBJECT_ABA})")
|
||||
list(APPEND MGL_ITEST_CAPABILITY_ENV
|
||||
"MGITEST_HANDLE_ABA_OBJECTS_${mglItestObjectBackend}=1")
|
||||
else()
|
||||
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()
|
||||
|
||||
# The client-side emitter of P4a's four suppressor-visible emission counters, which is what
|
||||
# ObjectSubsystemControl reads. Same shape and same reason as the CSO and map-persistent
|
||||
# emitter probes above: the emitters are minted in MG_Impl/Pipe (packages B and C), header-only
|
||||
# by design (D-P), so the question is "does any source there emit it", not "does a named file
|
||||
# exist". Until one does, every counter in the emit[] bracket is structurally zero in BOTH arms
|
||||
# of the A/B and an assertion about their difference would be a statement about nothing.
|
||||
mgl_itest_probe_for_symbol(MGL_ITEST_OBJECT_EMITTER
|
||||
"${MGL_ITEST_ROOT}/MobileGL/MG_Impl/Pipe" "FramebufferEmissions")
|
||||
if (MGL_ITEST_OBJECT_EMITTER)
|
||||
message(STATUS "Integration tests: P4a's object emissions have an emitter "
|
||||
"(${MGL_ITEST_OBJECT_EMITTER})")
|
||||
list(APPEND MGL_ITEST_CAPABILITY_ENV "MGITEST_PIPE_OBJECT_EMITTER_PRESENT=1")
|
||||
else()
|
||||
message(STATUS "Integration tests: no MG_Impl/Pipe source emits FramebufferEmissions - "
|
||||
"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)
|
||||
@@ -502,8 +647,9 @@ if (MOBILEGL_PIPE_PUSH)
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Integration tests: pull build - HandleRecycle.{Handles,AbaControl}, "
|
||||
"CsoContentAddressing and ResourceSubsystemControl stay registered (G2) and SKIP: "
|
||||
"every arm they assert is compiled only under MOBILEGL_PIPE_PUSH")
|
||||
"CsoContentAddressing, ResourceSubsystemControl, ObjectSubsystemControl and "
|
||||
"TextureUploadShape stay registered (G2) and SKIP: every arm they assert is "
|
||||
"compiled only under MOBILEGL_PIPE_PUSH")
|
||||
endif()
|
||||
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ENVIRONMENT
|
||||
@@ -939,14 +1085,15 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
# explicit bitmask below that leaked PUSH=0 turned this lane's LEGACY_MEMOS=0 into D14's armless
|
||||
# combination: the bring-up aborted, on purpose, and the lane went red for a reason that was never
|
||||
# about handles. The mask is the PHASE default, not a hand-picked bit, so the lane keeps measuring
|
||||
# the shape that ships: it was kMGPipeSubsystemsMigratedAtP2 (0x7f) and is now
|
||||
# kMGPipeSubsystemsMigratedAtP3a (0x1ff, MG_Pipe/MGPipe.h), which adds bit 7 (resources) and bit 8
|
||||
# (vertex input). Pinning it at 0x7f after P3a would leave the Handles arm asserting the P2 shape
|
||||
# while the buffer and vertex-input handles it is supposed to be about stayed switched off - a lane
|
||||
# that still passes and no longer measures the key that ships. Each phase's constant survives as the
|
||||
# NEXT phase's A/B control, which is what ResourceSubsystemControl's Off lane uses 0x7f for.
|
||||
# the shape that ships: it was kMGPipeSubsystemsMigratedAtP2 (0x7f), then
|
||||
# kMGPipeSubsystemsMigratedAtP3a (0x1ff), and is now kMGPipeSubsystemsMigratedAtP4a (0x1fff,
|
||||
# MG_Pipe/MGPipe.h), which adds bits 9-12 - framebuffer, texture resources, samplers and programs.
|
||||
# Pinning it at 0x1ff after P4a would leave the Handles arm asserting the P3a shape while the six
|
||||
# OBJECT handles it is now also about stayed switched off - a lane that still passes and no longer
|
||||
# measures the key that ships. Each phase's constant survives as the NEXT phase's A/B control, which
|
||||
# is what ResourceSubsystemControl's Off lane uses 0x7f for and ObjectSubsystemControl's uses 0x1ff.
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
set(MGL_ITEST_HANDLES_ARM_KNOBS "MOBILEGL_PIPE_LEGACY_MEMOS=0" "MOBILEGL_PIPE_PUSH=0x1ff")
|
||||
set(MGL_ITEST_HANDLES_ARM_KNOBS "MOBILEGL_PIPE_LEGACY_MEMOS=0" "MOBILEGL_PIPE_PUSH=0x1fff")
|
||||
set(MGL_ITEST_ABA_ARM_KNOBS "MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1")
|
||||
else()
|
||||
set(MGL_ITEST_HANDLES_ARM_KNOBS "")
|
||||
@@ -1051,33 +1198,36 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
# only thing these four lanes are an A/B about is `no CSO content addressing` (bit 63,
|
||||
# kMGPipeBehaviourNoCsoContentAddressing), which is what separates the On lane from the Off lane.
|
||||
# Every
|
||||
# other bit is the build's shipping mask, so it moves with the phase: it was 0x7f
|
||||
# (kMGPipeSubsystemsMigratedAtP2) and is now 0x1ff (kMGPipeSubsystemsMigratedAtP3a) for the same
|
||||
# reason MGL_ITEST_HANDLES_ARM_KNOBS above moved. Left pinned at 0x7f after P3a these lanes would
|
||||
# keep passing while running on a configuration nothing ships - bits 7 (resources) and 8 (vertex
|
||||
# input) cleared - which is the "still green, no longer measuring the shape that ships" failure
|
||||
# the Handles-arm comment above rejects. The counters they read (csom / csob) are render-state and
|
||||
# other bit is the build's shipping mask, so it moves with the phase: 0x7f
|
||||
# (kMGPipeSubsystemsMigratedAtP2), then 0x1ff (kMGPipeSubsystemsMigratedAtP3a), now 0x1fff
|
||||
# (kMGPipeSubsystemsMigratedAtP4a) for the same reason MGL_ITEST_HANDLES_ARM_KNOBS above moved. Left
|
||||
# pinned at 0x1ff after P4a these lanes would keep passing while running on a configuration nothing
|
||||
# ships - bits 9-12 (framebuffer, texture resources, samplers, programs) cleared - which is the
|
||||
# "still green, no longer measuring the shape that ships" failure the Handles-arm comment above
|
||||
# rejects. NOTE THAT THE OFF LANE'S MASK MOVED TOO, from 0x80000000000001ff to
|
||||
# 0x8000000000001fff: the mask moves with the phase, the control is bit 63, and the two must not be
|
||||
# confused. The counters they read (csom / csob) are render-state and
|
||||
# are steered by neither bit, so raising the mask is behaviour-preserving for what they assert;
|
||||
# what it buys is that a CSO regression that only shows up with the P3a subsystems on can reach
|
||||
# them. (contract-review-v1.md item 11, closed here for all three lane families.)
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_CSO_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_CSO_LANE=content-addressed"
|
||||
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/cso-content-addressed-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_CSO_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_CSO_LANE=no-content-addressing"
|
||||
"MOBILEGL_PIPE_PUSH=0x80000000000001ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x8000000000001fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/cso-no-content-addressing-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_CSO_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MGITEST_CSO_LANE=content-addressed"
|
||||
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/cso-content-addressed-DirectVulkan.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_CSO_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MGITEST_CSO_LANE=no-content-addressing"
|
||||
"MOBILEGL_PIPE_PUSH=0x80000000000001ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x8000000000001fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/cso-no-content-addressing-DirectVulkan.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
@@ -1120,12 +1270,15 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
|
||||
# --- G12: the P3a subsystem A/B, and G10's map-persistent-roundtrips lanes ------------
|
||||
#
|
||||
# THE MASKS ARE THE TWO PHASE CONSTANTS, not hand-picked bits: 0x1ff is
|
||||
# kMGPipeSubsystemsMigratedAtP3a (the push build's default) and 0x7f is
|
||||
# kMGPipeSubsystemsMigratedAtP2, which is exactly that default with bits 7 (resources) and 8
|
||||
# (vertex input) cleared. MGPipe.h:79 keeps each phase's constant alive as the next phase's A/B
|
||||
# control for this reason, and a lane that spelled its own bit pattern would stop being the shape
|
||||
# that ships the first time the default moved.
|
||||
# THE MASKS ARE PHASE CONSTANTS, not hand-picked bits: 0x1fff is
|
||||
# kMGPipeSubsystemsMigratedAtP4a (the push build's default as of P4a) and 0x7f is
|
||||
# kMGPipeSubsystemsMigratedAtP2, which is that default with bits 7 (resources), 8 (vertex input)
|
||||
# and 9-12 (P4a's four object families) cleared. MGPipe.h keeps each phase's constant alive as the
|
||||
# next phase's A/B control for this reason, and a lane that spelled its own bit pattern would stop
|
||||
# being the shape that ships the first time the default moved. THE ON LANE MOVED TO 0x1fff WITH THE
|
||||
# PHASE and the OFF LANE'S 0x7f DID NOT: this A/B is about bit 7, and pinning its On arm at 0x1ff
|
||||
# after P4a would measure the resource emitter on a configuration nothing ships. P4a's own A/B -
|
||||
# 0x1fff against 0x1ff - is ObjectSubsystemControl's, further down.
|
||||
#
|
||||
# DirectGLES only. P3a migrates Espryt's buffer and VAO paths; Magma's buffer path is P7 and
|
||||
# registers no MGPipeResourceOps, so a DirectVulkan arm would be measuring the client emitter
|
||||
@@ -1146,7 +1299,7 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
# the field inside the push guard), exactly as the HandleRecycle Legacy lanes already rely on.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_RESOURCE_SUBSYSTEM_LANE=on"
|
||||
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/resource-subsystem-on-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_OFF_ENVIRONMENT
|
||||
@@ -1180,12 +1333,12 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
# TEST_FILTER, for the private-log reason above.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_MPR_REGROW_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_MPR_LANE=storage-buffer-regrow"
|
||||
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/mpr-storage-buffer-regrow-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_MPR_ARENA_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_MPR_LANE=large-arena-adoption"
|
||||
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/mpr-large-arena-adoption-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
@@ -1216,7 +1369,7 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
# four processes to truncate. The fourth case skips in both lanes for exactly that reason, saying
|
||||
# so.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ARENA_SUBSYSTEM_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_PUSH=0x1ff"
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_PUSH=0x1fff"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_PUSH=0x7f"
|
||||
@@ -1241,6 +1394,191 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- G12: the P4a subsystem A/B, and its dependency refusal ---------------------------
|
||||
#
|
||||
# THE MASKS ARE THE TWO PHASE CONSTANTS, exactly as the P3a block above: 0x1fff is
|
||||
# kMGPipeSubsystemsMigratedAtP4a (the push build's default) and 0x1ff is
|
||||
# kMGPipeSubsystemsMigratedAtP3a, which is that default with bits 9 (framebuffer), 10 (texture
|
||||
# resources), 11 (samplers) and 12 (programs) cleared. THIS is the phase's "new 0x1ff off-lane": the
|
||||
# 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 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"),
|
||||
# so every process in a lane truncates it). Two entries in one lane race under `ctest -j` with a
|
||||
# failure indistinguishable from "the counter was never emitted". Same rule as the CSO lanes, the
|
||||
# map-persistent lanes and the verify arming lane.
|
||||
#
|
||||
# THE FIRST FOUR LANES ARE DirectGLES ONLY. P4a migrates Espryt's framebuffer, texture, sampler
|
||||
# and program paths; Magma's are P7 and register nothing here, so a DirectVulkan lane that measured
|
||||
# the emit[] bracket would be measuring the client emitters against a backend nobody asked to
|
||||
# change.
|
||||
#
|
||||
# THE FIFTH AND SIXTH LANES INVERT THAT, and the inversion is the point (c0f, ID-39/ID-40). "Magma
|
||||
# registers no consumer" stopped being a reason to have no lane the moment it became a THING THE
|
||||
# CLIENT MUST CHECK: P4a's families were wired without P3a's "a backend registered
|
||||
# MGPipeResourceOps" conjunct, so on Magma the client emitted, the applier ACCEPTED, the client
|
||||
# cleared its dirty flags on that acceptance and Magma's legacy path found nothing to upload - 66
|
||||
# DirectVulkan cases red, every one of them texture-upload shaped, every one of them green at
|
||||
# 0x1ff. The fix's belt (the applier refusing and counting RefusedNoConsumer) is invisible in
|
||||
# pixels and invisible in the emit[] bracket, so it needs its own entry, and it needs it on the
|
||||
# backend WITHOUT the consumer. The DirectGLES twin is the control that keeps the assertion from
|
||||
# being vacuously true of a tree where nothing emits anywhere.
|
||||
#
|
||||
# Both run at the PHASE DEFAULT (0x1fff) rather than a hand-picked mask, for the reason the whole
|
||||
# block gives: the shape that ships is the shape worth measuring.
|
||||
#
|
||||
# Registered in EVERY build, including the pull build where none of the counters exists, so that
|
||||
# `ctest -L integration-gpu` stays name-for-name identical between pull and push (gate G2). In a
|
||||
# pull build MGITEST_PIPE_PUSH_BUILD is absent and every one of these cases skips saying so.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=on"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-on-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=off"
|
||||
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-off-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=refused"
|
||||
"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})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_CONSUMER_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=consumer"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-consumer-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_OBJECT_SUBSYSTEM_NO_CONSUMER_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MGITEST_OBJECT_SUBSYSTEM_LANE=no-consumer"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-no-consumer-DirectVulkan.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.On."
|
||||
TEST_FILTER "ObjectSubsystemControlScenario.ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_ON_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.Off."
|
||||
TEST_FILTER "ObjectSubsystemControlScenario.ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_OFF_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.Refused."
|
||||
TEST_FILTER "ObjectSubsystemControlScenario.ASamplerBitWithoutTheTextureBitIsRefusedAndNamed"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
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}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.Consumer."
|
||||
TEST_FILTER "ObjectSubsystemControlScenario.TheAppliersNoConsumerBeltNeverFiresBehindTheClientsGate"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_CONSUMER_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.ObjectSubsystemControl.NoConsumer."
|
||||
TEST_FILTER "ObjectSubsystemControlScenario.TheAppliersNoConsumerBeltNeverFiresBehindTheClientsGate"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_OBJECT_SUBSYSTEM_NO_CONSUMER_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- The texture upload shape: RECORDED, NOT GATED in P4a (D-D4) ----------------------
|
||||
#
|
||||
# SSIM is blind to the box-versus-rect upload shape and the Mali cliff it hides is ~+6 ms/frame, so
|
||||
# the shape needs a number - two numbers, in fact, the server's tex[emit= box= rect= jobs=] and the
|
||||
# client's emit[ctu=], which agreeing is the whole reason both are published (D-L). What this lane
|
||||
# asserts is that the numbers could be READ, that the server bracket's own arithmetic holds and that
|
||||
# the two sides agree when both are non-zero; WHICH shape each texture took is RecordProperty'd and
|
||||
# printed for MEASUREMENTS.md. P3b/P4b turns it into a gate against a gold standard, with the Mali
|
||||
# frame-time delta published beside it - gating a shape this phase has not finished deciding would
|
||||
# either pin today's shape as the answer or fail on the change that is the next phase's point.
|
||||
#
|
||||
# Its own log path and its own single-case filter, for the per-lane-log reason above.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_TEXTURE_UPLOAD_SHAPE_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_TEXTURE_UPLOAD_SHAPE_LANE=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/texture-upload-shape-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.TextureUploadShape."
|
||||
TEST_FILTER "TextureUploadShapeScenario.TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_TEXTURE_UPLOAD_SHAPE_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- G9: the mandatory red-before scenario -------------------------------------------
|
||||
#
|
||||
# TextureParamsWithoutASamplerViewScenario needs NO lane of its own, and that is deliberate rather
|
||||
# than an omission. It reads no counter and no log - its observable is a sampled colour - and its
|
||||
# claim is about the SHIPPING configuration, so the two ambient registrations at the top of this
|
||||
# file (which run at the build's default mask) are exactly the arms it wants. A lane here would pin
|
||||
# a mask and make the entry stop describing what ships the next time the default moved. The
|
||||
# DirectVulkan ambient entries skip in the scenario's SetUp, naming the backend: the gap it is about
|
||||
# is Espryt's SyncAttachmentObject / SyncNeccessaryTextures pair and P4a touches no DirectVulkan
|
||||
# source but MagmaPipeArms.h (D-Q).
|
||||
#
|
||||
# D-E3 expects its second case, AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver, to be
|
||||
# RED until package esprytobj lands. MEASURED ON THE CONTRACT COMMIT IT IS GREEN, and the scenario's
|
||||
# header carries the mechanism: a texture parameter's only public-GL observable is a sample, and the
|
||||
# sample repairs the state it was meant to catch (the unit sync list re-syncs whenever the params
|
||||
# version moved). Nothing about any of that is expressed in this file - a registration that
|
||||
# "expected" a red would be a gate that could never go green - and the ruling on the missing
|
||||
# artefact is the integrator's, recorded in the gates result document.
|
||||
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
# 900s, not the ambient 120: the comparator re-reads every field of the fill mask at the verb
|
||||
# boundary and again at every accessor read, which the design budgets at 5-10x.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.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 "P4aSeamPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||
#define MGITEST_P4A_SEAM_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_P4A_SEAM_PEEK_LIVE)
|
||||
namespace {
|
||||
namespace MGP = MobileGL::MG_Pipe;
|
||||
namespace MGB = MobileGL::MG_Backend::DirectGLES;
|
||||
|
||||
// "Is Espryt the backend running" - the same test PipeApplyPeek.cpp makes through a twin:
|
||||
// on Magma no ES entry point was ever resolved and every member of g_GLESFuncs is null.
|
||||
// It is asked BEFORE SamplerSubsystemEnabled(), which is Espryt's own latch and must not
|
||||
// be resolved on a process whose backend is not Espryt.
|
||||
bool EsprytIsRunning() { return MGB::g_GLESFuncs.glBindSampler != nullptr; }
|
||||
} // namespace
|
||||
|
||||
bool PeekEsprytSamplerHandleArmIsLive(bool* outLive) {
|
||||
if (outLive == nullptr) return false;
|
||||
if (!EsprytIsRunning()) return false;
|
||||
*outLive = MGB::SamplerSubsystemEnabled();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekEsprytFramebufferHandleArmIsLive(bool* outLive) {
|
||||
if (outLive == nullptr) return false;
|
||||
if (!EsprytIsRunning()) return false;
|
||||
*outLive = MGB::FramebufferSubsystemEnabled();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||
out->Start = static_cast<unsigned>(applier.ShaderImageStart);
|
||||
out->Count = static_cast<unsigned>(applier.ShaderImageCount);
|
||||
out->Serial = static_cast<unsigned long long>(applier.ShaderImagesSerial);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekEsprytUnitSampler(unsigned unit, unsigned glSamplerName, EsprytUnitSamplerPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
if (!EsprytIsRunning()) return false;
|
||||
if (!MobileGL::MG_State::pGLContext) return false;
|
||||
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||
if (unit >= applier.BoundSamplerStates.size() || unit >= MGB::SamplerImpl::g_boundSamplersCache.size()) {
|
||||
return false;
|
||||
}
|
||||
*out = EsprytUnitSamplerPeek{};
|
||||
|
||||
// Espryt's own binding shadow: every glBindSampler this backend issues routes through it
|
||||
// (BackendSamplerObject::Bind / UnbindSampler), so it IS what the driver holds.
|
||||
if (MGB::SamplerImpl::BackendSamplerObject* const bound = MGB::SamplerImpl::g_boundSamplersCache[unit]) {
|
||||
out->BoundSamplerId = static_cast<unsigned>(bound->GetBackendSamplerId());
|
||||
}
|
||||
|
||||
const MGP::MGPipeHandle cso = applier.BoundSamplerStates[unit];
|
||||
out->CsoHandleSlot = static_cast<unsigned>(cso.Slot);
|
||||
out->CsoHandleGen = static_cast<unsigned>(cso.Gen);
|
||||
out->UnitInsideWindow = unit >= applier.SamplerStateStart &&
|
||||
unit - applier.SamplerStateStart < applier.SamplerStateCount;
|
||||
// The twin AT THE CSO HANDLE, asked of the same table Espryt asks (FindByHandle): a null
|
||||
// here with a live handle is the F-4 shape - a content-addressed handle looked up in a
|
||||
// table that only ever held identity-minted slots.
|
||||
if (!MGP::MGPipeHandleIsNull(cso)) {
|
||||
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.FindByHandle(cso); slot && *slot) {
|
||||
out->CsoTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
|
||||
}
|
||||
}
|
||||
|
||||
// And the twin keyed on the frontend OBJECT, which is what the pre-handle program pass
|
||||
// used to mint and bind, so a scenario can say which of the two the driver holds.
|
||||
const auto& object = MobileGL::MG_State::pGLContext->GetSamplerObject(
|
||||
static_cast<MobileGL::Uint>(glSamplerName));
|
||||
if (object) {
|
||||
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.Find(object.get()); slot && *slot) {
|
||||
out->IdentityTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool PeekEsprytSamplerHandleArmIsLive(bool*) { return false; }
|
||||
bool PeekEsprytFramebufferHandleArmIsLive(bool*) { return false; }
|
||||
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek*) { return false; }
|
||||
bool PeekEsprytUnitSampler(unsigned, unsigned, EsprytUnitSamplerPeek*) { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,78 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.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 three readings P4aSeamAuditScenario.cpp takes from the inside, for the two seams the fable
|
||||
// seam audit proved that PUBLIC GL CANNOT SEE: F-4 (the record arm's sampler bind is a permanent
|
||||
// no-op, hidden by the pre-handle program pass binding the same values) and F-2 / SD-4 (the
|
||||
// shader-image window does not follow a program switch, hidden by the server's window/high-water
|
||||
// union taking the pre-handle bind for the units outside it). Both are correct pictures over a
|
||||
// permanent silent fallback, which is precisely the class ROADMAP.md:20 says a gate has to be
|
||||
// able to make red - and the only place the difference exists is inside.
|
||||
//
|
||||
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek.h's reason, verbatim: this file includes
|
||||
// Espryt's own Managers.h, which may not meet a scenario's GL headers in one file. It is NOT
|
||||
// PipeApplyPeek.cpp because that file is package F's (gates v3) and this round may not edit it.
|
||||
//
|
||||
// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK - a pull build, Android,
|
||||
// a backend that is not Espryt - and a caller that gets false has learned NOTHING: "could not
|
||||
// look" is not "was bound". The scenario declines the reading BY NAME and keeps its public-GL
|
||||
// half, which is the shape TextureParamsWithoutASamplerViewScenario.cpp argues for.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// ---- is Espryt's sampler family on its HANDLE arm in this process? -------------------
|
||||
//
|
||||
// The gate for every other reading here. True only on DirectGLES, in a push build, with
|
||||
// Espryt's own resolver answering "handle" for kMGPipeSubsystemSamplers (bit 11 set and its
|
||||
// dependency satisfied) - i.e. exactly when bind_sampler_states / set_shader_images are
|
||||
// consumed, so a white-box assertion about them can be red for its own reason and for no
|
||||
// other. Written only on true.
|
||||
bool PeekEsprytSamplerHandleArmIsLive(bool* outLive);
|
||||
|
||||
// The same question for the FRAMEBUFFER family (bit 9): true when Espryt consumes
|
||||
// set_framebuffer_state in this process. The renderbuffer half of the F-3 case asserts only
|
||||
// there - on the pre-handle arm a renderbuffer re-storaged while attached moves nothing the
|
||||
// FBO memo reads (D-D2's documented hole, pre-P4a code), and the record is what closes it.
|
||||
bool PeekEsprytFramebufferHandleArmIsLive(bool* outLive);
|
||||
|
||||
// ---- the applier's shader-image window, as last received ------------------------------
|
||||
//
|
||||
// MGPipeApplierState::ShaderImageStart / ShaderImageCount / ShaderImagesSerial. Count is
|
||||
// "how many units set_shader_images last described" - 0 means the set has NEVER arrived
|
||||
// (MGPipeApplierReset advances the serial whether or not anything was emitted, so the serial
|
||||
// is not that test). Push build only.
|
||||
struct PipeShaderImageWindowPeek {
|
||||
unsigned Start;
|
||||
unsigned Count;
|
||||
unsigned long long Serial;
|
||||
};
|
||||
|
||||
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out);
|
||||
|
||||
// ---- which driver sampler a texture unit is bound to, and whose twin it is -------------
|
||||
//
|
||||
// For F-4. `BoundSamplerId` is the ES sampler name Espryt's own binding shadow says unit
|
||||
// `unit` carries (0 = none). `CsoHandleSlot/Gen` is bind_sampler_states' handle for the unit,
|
||||
// `CsoTwinSamplerId` the ES name of the twin Espryt holds AT THAT HANDLE (0 = no twin at the
|
||||
// content-addressed slot - the F-4 shape), and `IdentityTwinSamplerId` the ES name of a twin
|
||||
// keyed on the frontend SamplerObject named `glSamplerName` (0 = none). On a correct handle
|
||||
// arm the unit's driver sampler IS the CSO twin. Push build, DirectGLES only.
|
||||
struct EsprytUnitSamplerPeek {
|
||||
unsigned BoundSamplerId;
|
||||
unsigned CsoHandleSlot;
|
||||
unsigned CsoHandleGen;
|
||||
bool UnitInsideWindow;
|
||||
unsigned CsoTwinSamplerId;
|
||||
unsigned IdentityTwinSamplerId;
|
||||
};
|
||||
|
||||
bool PeekEsprytUnitSampler(unsigned unit, unsigned glSamplerName, EsprytUnitSamplerPeek* out);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,188 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.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 "PipeApplyPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||
#define MGITEST_PIPE_APPLY_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_PIPE_APPLY_PEEK_LIVE)
|
||||
namespace {
|
||||
namespace MGP = MobileGL::MG_Pipe;
|
||||
namespace MGB = MobileGL::MG_Backend::DirectGLES;
|
||||
|
||||
// The frontend texture object a GL name denotes in the CURRENT context, or null. This is
|
||||
// a LOOKUP KEY and nothing else: every value this file reports comes from the applier or
|
||||
// from Espryt, never from the object found here. (Reading the frontend's own parameter
|
||||
// state would answer the question the scenario is asking with the input to it.)
|
||||
MobileGL::MG_State::GLState::ITextureObject* FrontendTexture(unsigned glTextureName) {
|
||||
if (!MobileGL::MG_State::pGLContext) return nullptr;
|
||||
const auto& object = MobileGL::MG_State::pGLContext->GetTextureObject(
|
||||
static_cast<MobileGL::Uint>(glTextureName));
|
||||
return object ? object.get() : nullptr;
|
||||
}
|
||||
|
||||
// Espryt's twin for that texture, or null - which is also this file's "is Espryt even the
|
||||
// backend running" answer. On Magma no Espryt twin was ever built, so every entry point
|
||||
// below stops here rather than reaching for g_GLESFuncs, whose members are null there.
|
||||
MGB::TextureImpl::BackendTextureObject* EsprytTwin(unsigned glTextureName) {
|
||||
MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName);
|
||||
if (object == nullptr) return nullptr;
|
||||
auto* const found = MGB::TextureImpl::g_backendTextureObjects.Find(object);
|
||||
if (found == nullptr || !*found) return nullptr;
|
||||
return found->get();
|
||||
}
|
||||
|
||||
int SwizzleToGLEnum(MobileGL::Uint8 encoded) {
|
||||
return static_cast<int>(MobileGL::MG_Util::ConvertTextureSwizzleParamToGLEnum(
|
||||
static_cast<MobileGL::TextureSwizzleParam>(encoded)));
|
||||
}
|
||||
|
||||
// MGPipeTypes.h owns the two numbers and says why depth is 0 (a zeroed record must decode
|
||||
// to what an untouched texture already has). This is that decode, and nothing else in
|
||||
// this module may open-code it.
|
||||
int DepthStencilModeToGLEnum(MobileGL::Uint8 encoded) {
|
||||
return encoded == MGP::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX
|
||||
: GL_DEPTH_COMPONENT;
|
||||
}
|
||||
|
||||
// The GL_TEXTURE_BINDING_* query for a target, or 0 where this file has no answer. A
|
||||
// guess would be worse than a refusal: the binding is what gets RESTORED, so a wrong
|
||||
// pname would leave the driver bound to this test's texture.
|
||||
int BindingQueryFor(unsigned glTarget) {
|
||||
switch (glTarget) {
|
||||
case GL_TEXTURE_2D: return GL_TEXTURE_BINDING_2D;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool PeekPipeTextureParamsRecord(unsigned glTextureName, PipeTextureParamsRecordPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||
// Slot 0 is the reserved null handle and is never live (MGPipeHandles.h), so the scan
|
||||
// starts at 1 and a match at 0 is impossible rather than merely unlikely.
|
||||
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->ParamsSerial = static_cast<unsigned long long>(record.ParamsSerial);
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
out->Swizzle[channel] = SwizzleToGLEnum(record.Params.Swizzle[channel]);
|
||||
}
|
||||
out->DepthStencilMode = DepthStencilModeToGLEnum(record.Params.DepthStencilMode);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PeekEsprytAppliedTextureParams(unsigned glTextureName, unsigned glTarget,
|
||||
EsprytAppliedTextureParamsPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
const int bindingQuery = BindingQueryFor(glTarget);
|
||||
if (bindingQuery == 0) return false;
|
||||
MGB::TextureImpl::BackendTextureObject* const twin = EsprytTwin(glTextureName);
|
||||
if (twin == nullptr) return false;
|
||||
const MobileGL::Uint backendId = twin->GetBackendTextureId();
|
||||
if (backendId == 0) return false;
|
||||
if (MGB::g_GLESFuncs.glGetTexParameteriv == nullptr ||
|
||||
MGB::g_GLESFuncs.glBindTexture == nullptr || MGB::g_GLESFuncs.glGetIntegerv == nullptr ||
|
||||
MGB::g_GLESFuncs.glGetError == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAVE / QUERY / RESTORE ON THE UNIT THAT IS ALREADY ACTIVE. No glActiveTexture, so the
|
||||
// only driver state this touches is one unit's binding, and it is put back byte for byte
|
||||
// - which is what keeps Espryt's own g_boundTexturesCache true rather than merely
|
||||
// consistent. (Binding through the twin's own Bind() would update that shadow and would
|
||||
// therefore CHANGE what the scenario measures next; this does not.)
|
||||
GLint previousBinding = 0;
|
||||
MGB::g_GLESFuncs.glGetIntegerv(static_cast<GLenum>(bindingQuery), &previousBinding);
|
||||
MGB::g_GLESFuncs.glBindTexture(static_cast<GLenum>(glTarget), backendId);
|
||||
|
||||
out->BackendTextureId = static_cast<unsigned>(backendId);
|
||||
static const GLenum kSwizzlePnames[4] = {GL_TEXTURE_SWIZZLE_R, GL_TEXTURE_SWIZZLE_G,
|
||||
GL_TEXTURE_SWIZZLE_B, GL_TEXTURE_SWIZZLE_A};
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
GLint value = 0;
|
||||
MGB::g_GLESFuncs.glGetTexParameteriv(static_cast<GLenum>(glTarget),
|
||||
kSwizzlePnames[channel], &value);
|
||||
out->Swizzle[channel] = static_cast<int>(value);
|
||||
}
|
||||
|
||||
// The depth/stencil aspect mode is ES 3.1 and is INVALID_ENUM on a driver without it, so
|
||||
// it is asked for last and its own error decides whether the answer is usable. The queue
|
||||
// is drained first because a stale error from anywhere else would be indistinguishable
|
||||
// from this call's - Espryt drains it the same way at every one of its own sync sites
|
||||
// (DebugImpl::ErrorLopper), and this module's own GL errors are read from the FRONTEND
|
||||
// state (ScenarioTest::FirstGLError), which none of this touches.
|
||||
while (MGB::g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
GLint mode = 0;
|
||||
MGB::g_GLESFuncs.glGetTexParameteriv(static_cast<GLenum>(glTarget),
|
||||
GL_DEPTH_STENCIL_TEXTURE_MODE, &mode);
|
||||
out->DepthStencilModeIsReadable = MGB::g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
out->DepthStencilMode = static_cast<int>(mode);
|
||||
|
||||
MGB::g_GLESFuncs.glBindTexture(static_cast<GLenum>(glTarget),
|
||||
static_cast<GLuint>(previousBinding));
|
||||
while (MGB::g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekEsprytHasSamplerViewForTexture(unsigned glTextureName, bool* outExists) {
|
||||
if (outExists == nullptr) return false;
|
||||
MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName);
|
||||
if (object == nullptr) return false;
|
||||
// Espryt must be the backend running, or "no view" would be true of every texture on
|
||||
// every other backend and the assertion would be vacuous where it is loudest.
|
||||
if (EsprytTwin(glTextureName) == nullptr) return false;
|
||||
// HandleOfSamplerViewForTexture is the monolith glue that derives the view's handle from
|
||||
// the TEXTURE's lifetime id (D-F2: one view per ITextureObject), so this asks Espryt's
|
||||
// own table the same way Espryt asks it - it does not consult the applier record's
|
||||
// ViewCso, which is the client's statement about the same fact and would make one side
|
||||
// of the seam vouch for the other.
|
||||
const MGP::MGPipeHandle view = MGB::SamplerViewImpl::HandleOfSamplerViewForTexture(object);
|
||||
if (MGP::MGPipeHandleIsNull(view)) {
|
||||
*outExists = false;
|
||||
return true;
|
||||
}
|
||||
*outExists = MGB::SamplerViewImpl::FindSamplerViewForHandle(view) != nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekPipeApplierRefusedNoConsumer(unsigned long long* outCount) {
|
||||
if (outCount == nullptr) return false;
|
||||
*outCount = static_cast<unsigned long long>(MGP::MGPipeApplier().RefusedNoConsumer);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool PeekPipeTextureParamsRecord(unsigned, PipeTextureParamsRecordPeek*) { return false; }
|
||||
bool PeekEsprytAppliedTextureParams(unsigned, unsigned, EsprytAppliedTextureParamsPeek*) {
|
||||
return false;
|
||||
}
|
||||
bool PeekEsprytHasSamplerViewForTexture(unsigned, bool*) { return false; }
|
||||
bool PeekPipeApplierRefusedNoConsumer(unsigned long long*) { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,110 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.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 APPLIER's texture-parameter record, ESPRYT's applied value for the same texture, and
|
||||
// whether that texture has a sampler view yet. Three readings taken from a scenario, for gate
|
||||
// G9's WHITE-BOX half.
|
||||
//
|
||||
// WHY A WHITE-BOX HALF EXISTS AT ALL (ID-19, brief section F, gates review R1). G9's public-GL
|
||||
// cases in TextureParamsWithoutASamplerViewScenario.cpp catch "the parameter never reached the
|
||||
// driver". They CANNOT catch "the parameter reached the driver LATE", because a texture
|
||||
// parameter's only public-GL observable is a SAMPLE and the sample is itself what repairs an
|
||||
// unsynced parameter: it puts the texture on the unit list, and that walk pushes the parameters
|
||||
// for anything whose params serial moved. A backend that deferred every attachment-only
|
||||
// texture's parameters to the first sampler view would be green on all four of those cases,
|
||||
// forever, on every tree. The distinction only exists on the inside, so the reading has to be
|
||||
// taken there - while the texture is still attachment-only, before any sample.
|
||||
//
|
||||
// A SEPARATE TRANSLATION UNIT for PipeSlotPeek.h's reason, verbatim: the scenario sources
|
||||
// include the GL headers with prototypes and MobileGL's umbrella header is not meant to meet
|
||||
// them in one file. This one goes further than PipeSlotPeek and includes Espryt's own
|
||||
// Managers.h, which is exactly why it may not be anywhere near a scenario's GL headers.
|
||||
//
|
||||
// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK, and a caller that
|
||||
// gets false has learned NOTHING - "could not look" is not "was applied". Out of reach means:
|
||||
// a PULL build (there is no applier: it is `#if MOBILEGL_PIPE_PUSH`); Android, where this module
|
||||
// links the shipping libMobileGL.so built -fvisibility=hidden and no internal symbol resolves;
|
||||
// a backend that is not DirectGLES (Espryt is the subject; Magma answers the same GL question
|
||||
// through P7's own paths); and, for the record peek, a mask whose texture-resource bit is off,
|
||||
// where no record exists to find because nothing was ever emitted.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// ---- the applier's set_texture_params record for one GL texture name ------------------
|
||||
//
|
||||
// ADDRESSED BY GL NAME, and the search key is MGPResourceDesc::GlNameForDiag. That field is
|
||||
// diagnostics-only by contract - never an identity, never a memo key (MGPipeTypes.h) - and
|
||||
// this is a diagnostic: a test harness looking for the record a named GL object produced.
|
||||
// The alternative would be to ask the CLIENT emitter for the texture's handle, and the
|
||||
// review is explicit that this probe must arm on package D's applier/backend state and not
|
||||
// on the emitter markers B and C set: they are different questions, and a shared marker
|
||||
// would re-create the shape review F-M5 was raised about.
|
||||
struct PipeTextureParamsRecordPeek {
|
||||
// The handle the record sits at, so a caller can print it.
|
||||
unsigned Slot;
|
||||
unsigned Gen;
|
||||
// set_texture_params' own serial. 0 means the record exists (the resource was created)
|
||||
// but NO set_texture_params has ever been applied to it - which is a different finding
|
||||
// from "no record", and the two must not be merged.
|
||||
unsigned long long ParamsSerial;
|
||||
// MGPTextureParams::Swizzle[4], translated to the GL enums the application passed to
|
||||
// glTextureParameteri (GL_ZERO / GL_ONE / GL_RED / GL_GREEN / GL_BLUE / GL_ALPHA), so
|
||||
// the scenario compares what it set against what the record carries in ONE vocabulary
|
||||
// and neither side has to know the other's encoding.
|
||||
int Swizzle[4];
|
||||
// MGPTextureParams::DepthStencilMode, translated the same way: GL_DEPTH_COMPONENT or
|
||||
// GL_STENCIL_INDEX.
|
||||
int DepthStencilMode;
|
||||
};
|
||||
|
||||
bool PeekPipeTextureParamsRecord(unsigned glTextureName, PipeTextureParamsRecordPeek* out);
|
||||
|
||||
// ---- Espryt's APPLIED value for the same texture --------------------------------------
|
||||
//
|
||||
// Read from the DRIVER, through the twin's own ES name, because "applied" means the driver
|
||||
// was told - the same thing package D's white-box unit probe asserts against its mocked
|
||||
// driver (esprytobj-v2 (9)). The current binding on the ACTIVE unit is saved and restored
|
||||
// around the query and no unit is switched, so Espryt's binding shadow still describes
|
||||
// reality afterwards: nothing is perturbed for it to be stale about.
|
||||
//
|
||||
// `glTarget` is the texture's GL target (only GL_TEXTURE_2D is supported today; any other
|
||||
// target returns false rather than guessing a binding query).
|
||||
struct EsprytAppliedTextureParamsPeek {
|
||||
// The driver name Espryt minted for this texture, for the caller's message.
|
||||
unsigned BackendTextureId;
|
||||
int Swizzle[4];
|
||||
int DepthStencilMode;
|
||||
// False when the driver rejected the depth/stencil query - a non-depth texture, or an ES
|
||||
// level without GL_DEPTH_STENCIL_TEXTURE_MODE. The swizzle half is still valid.
|
||||
bool DepthStencilModeIsReadable;
|
||||
};
|
||||
|
||||
bool PeekEsprytAppliedTextureParams(unsigned glTextureName, unsigned glTarget,
|
||||
EsprytAppliedTextureParamsPeek* out);
|
||||
|
||||
// ---- and the claim that makes the two above mean anything ------------------------------
|
||||
//
|
||||
// Whether Espryt holds a SAMPLER VIEW twin for this texture. This is the assertion the
|
||||
// public-GL cases cannot make, because making it there would create the view. `*outExists`
|
||||
// is written only on true.
|
||||
bool PeekEsprytHasSamplerViewForTexture(unsigned glTextureName, bool* outExists);
|
||||
|
||||
// ---- c0f's belt, for the ObjectSubsystemControl arms -----------------------------------
|
||||
//
|
||||
// MGPipeApplierState::RefusedNoConsumer: the number of P4a-family entry points that were
|
||||
// refused because no backend had registered MGPipeResourceOps. On a backend WITH a consumer
|
||||
// it must never move; on one without (Magma, ID-39/ID-40) the client's own gate is supposed
|
||||
// to stop the emission before the belt is reached, so it must never move there either. A
|
||||
// non-zero delta says the gate and the belt disagreed, which is the whole point of having
|
||||
// both. Reset by MGPipeApplierReset, so a caller reads it as a DELTA and treats a value that
|
||||
// went DOWN as "the applier was reset, count everything since as `after`".
|
||||
bool PeekPipeApplierRefusedNoConsumer(unsigned long long* outCount);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -20,11 +20,24 @@ namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_PIPE_SLOT_PEEK_LIVE)
|
||||
namespace {
|
||||
// One arm per member, and NO `default:` on purpose: adding a PipeSlotKind without
|
||||
// deciding which MGPipeKind it names is a compiler warning here (-Wswitch) rather than
|
||||
// a row that silently counts VertexElementsCso and reports "did not leak" about a kind
|
||||
// it never looked at. The trailing return is the unreachable one the compiler needs.
|
||||
MobileGL::MG_Pipe::MGPipeKind Translate(PipeSlotKind kind) {
|
||||
switch (kind) {
|
||||
case PipeSlotKind::Buffer: return MobileGL::MG_Pipe::MGPipeKind::Buffer;
|
||||
default: return MobileGL::MG_Pipe::MGPipeKind::VertexElementsCso;
|
||||
case PipeSlotKind::VertexElementsCso:
|
||||
return MobileGL::MG_Pipe::MGPipeKind::VertexElementsCso;
|
||||
case PipeSlotKind::Texture: return MobileGL::MG_Pipe::MGPipeKind::Texture;
|
||||
case PipeSlotKind::Renderbuffer: return MobileGL::MG_Pipe::MGPipeKind::Renderbuffer;
|
||||
case PipeSlotKind::Framebuffer: return MobileGL::MG_Pipe::MGPipeKind::Framebuffer;
|
||||
case PipeSlotKind::SamplerCso: return MobileGL::MG_Pipe::MGPipeKind::SamplerCso;
|
||||
case PipeSlotKind::SamplerViewCso:
|
||||
return MobileGL::MG_Pipe::MGPipeKind::SamplerViewCso;
|
||||
case PipeSlotKind::ShaderCso: return MobileGL::MG_Pipe::MGPipeKind::ShaderCso;
|
||||
}
|
||||
return MobileGL::MG_Pipe::MGPipeKind::None;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -36,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
|
||||
|
||||
@@ -30,6 +30,36 @@ namespace MGITest {
|
||||
enum class PipeSlotKind {
|
||||
Buffer,
|
||||
VertexElementsCso,
|
||||
// P4a's six (G8b). Every one of them is a kind the CLIENT mints and the client alone
|
||||
// frees (BRIEF-P4A.md D-I1: one death helper per kind, called from the frontend
|
||||
// object's own destructor, whatever backend is running), so every one of them can leak
|
||||
// the P3a C-1 way - and the leak is invisible in pixels, in GL names and in
|
||||
// glGetError, exactly as the VertexElementsCso one was.
|
||||
Texture,
|
||||
Renderbuffer,
|
||||
// Framebuffer has a HANDLE but no wire lifetime (D-I2): no create_*, no destroy row in
|
||||
// the catalogue, and its death helper does the notice and the free and emits nothing.
|
||||
// That makes the allocator the ONLY observable of its lifetime, so this row matters
|
||||
// more here than the others rather than less.
|
||||
Framebuffer,
|
||||
SamplerCso,
|
||||
SamplerViewCso,
|
||||
// ShaderCso covers BOTH the ordinary program slots and the program-pipeline COMPOSITES
|
||||
// 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,
|
||||
};
|
||||
|
||||
// Live slots of this kind right now, and one past the highest slot ever handed out.
|
||||
@@ -41,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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,878 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.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 P4a SUBSYSTEM A/B IS REAL, AND ITS DEPENDENCY REFUSALS ARE EXERCISED (gate G12).
|
||||
//
|
||||
// P4a migrates FOUR subsystems (D-K1, MG_Pipe/MGPipe.h):
|
||||
//
|
||||
// bit 9 kMGPipeSubsystemFramebuffer set_framebuffer_state
|
||||
// bit 10 kMGPipeSubsystemTextureResources texture + renderbuffer resource_*, set_texture_params
|
||||
// bit 11 kMGPipeSubsystemSamplers sampler CSO, sampler view, the three unit sets
|
||||
// bit 12 kMGPipeSubsystemPrograms shader CSO, draw/dispatch program, global constants
|
||||
//
|
||||
// so the push build's default mask becomes kMGPipeSubsystemsMigratedAtP4a = 0x1fff, and P3a's
|
||||
// 0x1ff survives as the control that clears exactly those four - MGPipe.h's rule that every phase's
|
||||
// constant keeps meaning what it meant, so an operator's recorded mask is still readable a phase
|
||||
// later. THE OFF LANE IS 0x1ff AND NOT A HAND-PICKED PATTERN, for that reason.
|
||||
//
|
||||
// That A/B is what every "push vs pull" number in MEASUREMENTS.md is taken against, and it has one
|
||||
// characteristic failure mode: the bits stop steering anything, both arms run the same code, and
|
||||
// every later comparison is quietly taken against a switch that does nothing. This file is the
|
||||
// entry that cannot let that happen. It is the P4a analogue of ResourceSubsystemControlScenario and
|
||||
// deliberately its twin in shape.
|
||||
//
|
||||
// WHAT IT ASSERTS, per lane:
|
||||
//
|
||||
// on (MOBILEGL_PIPE_PUSH=0x1fff)
|
||||
// The client emits P4a's records for the workload: a framebuffer state per bound target that
|
||||
// moved, the three unit sets, and the client-side texture upload record. The window's
|
||||
// emit[fbe= sve= sse= sie= ctu=] bracket therefore carries a NON-ZERO total.
|
||||
//
|
||||
// off (MOBILEGL_PIPE_PUSH=0x1ff, P3a's default = P4a's four subsystems cleared)
|
||||
// The frontend dispatch falls through to the legacy MGB_CTX-reading arms, nothing is emitted
|
||||
// through any of the four families, and every one of those five counters must read ZERO.
|
||||
// This is the reading a dead switch fails: with the bits ignored, this lane would report the
|
||||
// same non-zero counts as the other one.
|
||||
//
|
||||
// refused (MOBILEGL_PIPE_PUSH=0x9ff = bits 0..8 plus bit 11, samplers, WITHOUT bit 10)
|
||||
// D-K2's dependency refusal. Every MGPBoundView::Texture and MGPImageView::Res names a
|
||||
// Texture handle and only bit 10 populates the texture slot table, so a sampler subsystem
|
||||
// without it would miss every lookup and walk on without unbinding. The bring-up logs ONE
|
||||
// error naming BOTH bits, refuses bit 11 and runs the legacy sampler arm - modelled on the
|
||||
// bit-8-requires-bit-7 refusal that already ships (Managers.cpp:2393-2410). The assertion is
|
||||
// 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.
|
||||
// "The counters moved and the picture did not" is the whole claim - a switch that changed
|
||||
// what is drawn would not be an A/B, it would be a bug.
|
||||
//
|
||||
// WHY IT CAN SKIP. The counters are emitted by the client-side emitters P4a packages B and C own,
|
||||
// and this file is written against the P4a contract commit, before either lands. Until then nothing
|
||||
// emits, the five counters are structurally zero in BOTH lanes, and an assertion about the
|
||||
// difference would be a statement about nothing. The build answers the question rather than a
|
||||
// hand-maintained list: MG_IntegrationTest/CMakeLists.txt greps every source under MG_Impl/Pipe/
|
||||
// for the counters' names and passes the answer in as MGITEST_PIPE_OBJECT_EMITTER_PRESENT, with a
|
||||
// CONFIGURE_DEPENDS on that directory and on each file it finds so the answer cannot go stale. It
|
||||
// is a CONTENT probe, not a filename probe, so the owning packages keep control of their own file
|
||||
// layout - P4a's new client files are headers (D-P), and a glob for a named .cpp would have kept
|
||||
// this control skipping forever with a reason that had become false.
|
||||
//
|
||||
// DIRECTGLES ONLY, and that is the honest scope: P4a migrates Espryt's framebuffer, texture,
|
||||
// sampler and program paths. Magma's are P7 (D-Q) and register nothing here, so a DirectVulkan lane
|
||||
// would be measuring the client emitters against a backend nobody asked to change.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeApplyPeek.h"
|
||||
#include "../Harness/PipeStatsWindow.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 {
|
||||
|
||||
// Set by the three ObjectSubsystemControl. ctest entries and by nothing else; a harness
|
||||
// marker, never read by the library. Its absence means an ambient entry, where neither the
|
||||
// stats channel nor a private log path is configured.
|
||||
constexpr const char* kLaneMarker = "MGITEST_OBJECT_SUBSYSTEM_LANE";
|
||||
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";
|
||||
// c0f's two halves (ID-39/ID-40), run at the phase default on BOTH backends: the client
|
||||
// GATE (a P4a family emits only where a backend registered MGPipeResourceOps) and the
|
||||
// applier's BELT (every P4a entry point refuses and counts RefusedNoConsumer when none
|
||||
// did). One lane per backend, because the interesting one is the backend with NO
|
||||
// consumer - Magma - and the other is the control that says the assertion is not
|
||||
// vacuously true of a tree where nothing emits at all.
|
||||
constexpr const char* kLaneConsumer = "consumer";
|
||||
constexpr const char* kLaneNoConsumer = "no-consumer";
|
||||
|
||||
bool LaneIsARefusalLane(const std::string& lane) {
|
||||
return lane == kLaneRefused || lane == kLaneRefusedTexture;
|
||||
}
|
||||
|
||||
bool LaneIsAConsumerLane(const std::string& lane) {
|
||||
return lane == kLaneConsumer || lane == kLaneNoConsumer;
|
||||
}
|
||||
|
||||
constexpr int kInset = 2;
|
||||
constexpr int kTextureSize = 4;
|
||||
// Enough frames that a per-frame emitter and a per-draw emitter read differently, and few
|
||||
// enough that one summary window covers exactly this.
|
||||
constexpr int kDrawsInTheWindow = 4;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
bool BuildMarkerIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||
}
|
||||
|
||||
std::string LaneName() {
|
||||
const char* lane = std::getenv(kLaneMarker);
|
||||
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 helper's own decision clause, verbatim - 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, AND IN THAT
|
||||
// ORDER - see the direction check below.
|
||||
//
|
||||
// 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 the
|
||||
// DIRECTION, and not the family-specific prose in <why>.
|
||||
//
|
||||
// THE DIRECTION IS THE HALF THIS FILE USED TO BE MISSING (review F-v2-m1). The first form
|
||||
// of the matcher asked "does the line name bit A?" AND "does the line name bit B?", which
|
||||
// is a SYMMETRIC conjunction: swapping the two arguments - exactly what separates the
|
||||
// 0x5ff case from the 0x9ff one below, and what each of their comments claims to be
|
||||
// doing - could not change the answer, and both cases went green on either line. A
|
||||
// resolver that refused correctly but printed the MIRROR sentence would have been green
|
||||
// on a refusal that told the operator the wrong dependency, which is the same class of
|
||||
// "the log says something plausible" defect that made the whole-file substring search
|
||||
// (F-M6) worthless one level up. The two resolvers are forty lines apart in one file,
|
||||
// share this helper and differ only in the `what` string, so the copy-paste is one edit
|
||||
// away at all times.
|
||||
//
|
||||
// What makes the direction readable is the sentence's own shape: the SET bit is named
|
||||
// before " is set but " and the NEEDED bit between that and " is clear". So the check is
|
||||
// four offsets in strictly increasing order, and it is the sentence Espryt emits rather
|
||||
// than a re-statement of it.
|
||||
constexpr const char* kSaysItRefused = "REFUSING the dependent bit and running the legacy arm";
|
||||
constexpr const char* kSaysWhichIsSet = " is set but ";
|
||||
constexpr const char* kSaysWhichIsClear = " is clear";
|
||||
|
||||
// The earliest offset at which any accepted spelling of one bit appears, or npos. The
|
||||
// EARLIEST rather than any: a spelling that also occurs later in <why> (Espryt's
|
||||
// bit-10-requires-bit-11 sentence says "only bit 11 mints sampler CSOs" in its reason)
|
||||
// must not be able to satisfy an ordering the first occurrence does not.
|
||||
std::size_t EarliestSpellingOffset(const std::string& line,
|
||||
const std::vector<std::string>& spellings) {
|
||||
std::size_t earliest = std::string::npos;
|
||||
for (const std::string& spelling : spellings) {
|
||||
const std::size_t at = line.find(spelling);
|
||||
if (at != std::string::npos && (earliest == std::string::npos || at < earliest)) {
|
||||
earliest = at;
|
||||
}
|
||||
}
|
||||
return earliest;
|
||||
}
|
||||
|
||||
// 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 std::size_t refusedAt = line.find(kSaysItRefused);
|
||||
const std::size_t setAt = EarliestSpellingOffset(line, bitThatWasSet);
|
||||
const std::size_t setClauseAt = line.find(kSaysWhichIsSet);
|
||||
const std::size_t neededAt = EarliestSpellingOffset(line, bitThatWasNeeded);
|
||||
const std::size_t clearClauseAt = line.find(kSaysWhichIsClear);
|
||||
const bool everyPartIsThere =
|
||||
refusedAt != std::string::npos && setAt != std::string::npos &&
|
||||
setClauseAt != std::string::npos && neededAt != std::string::npos &&
|
||||
clearClauseAt != std::string::npos;
|
||||
// "<set bit> ... is set but ... <needed bit> ... is clear", strictly in that
|
||||
// order. Swapping the caller's two arguments breaks the chain, which is the
|
||||
// whole of F-v2-m1.
|
||||
const bool inTheRightDirection =
|
||||
everyPartIsThere && setAt < setClauseAt && setClauseAt < neededAt &&
|
||||
neededAt < clearClauseAt;
|
||||
if (atErrorSeverity && inTheRightDirection) {
|
||||
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 {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_lane = LaneName();
|
||||
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);
|
||||
RecordProperty("lane", m_lane.empty() ? "ambient" : m_lane.c_str());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindSampler(0, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
// GTEST_SKIP() returns from the function it is written in, so this cannot report
|
||||
// through a return value; every caller pairs it with `if (IsSkipped()) return;`.
|
||||
void SkipUnlessTheLaneIsAssertableHere(bool needsTheEmitters) {
|
||||
if (m_lane.empty()) {
|
||||
GTEST_SKIP() << "runs only in its own lane: the three ObjectSubsystemControl. "
|
||||
"ctest entries set " << kLaneMarker
|
||||
<< " together with the MOBILEGL_PIPE_PUSH bitmask that arm means, "
|
||||
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a "
|
||||
"private MOBILEGL_LOG_FILE_PATH. None of that is configured in "
|
||||
"the ambient entries, and the ambient log is shared, so a read "
|
||||
"here would race.";
|
||||
return;
|
||||
}
|
||||
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: there are no "
|
||||
"subsystem bits to clear, P4a's five CallClass members do not "
|
||||
"exist and the summary line carries no emit[...] bracket. The "
|
||||
"entry is registered here anyway so that `ctest -L "
|
||||
"integration-gpu` names the same tests in the pull build and the "
|
||||
"push build (gate G2).";
|
||||
return;
|
||||
}
|
||||
if (needsTheEmitters && !BuildMarkerIsSet("MGITEST_PIPE_OBJECT_EMITTER_PRESENT")) {
|
||||
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||
"MobileGL/MG_Impl/Pipe/ emits FramebufferEmissions, so nothing "
|
||||
"sends a P4a record, every counter in the emit[] bracket is "
|
||||
"structurally zero in BOTH lanes and the difference between them "
|
||||
"is not observable yet. P4a packages B (framebuffer, texture) "
|
||||
"and C (sampler, image, program) own those emitters; this "
|
||||
"control arms itself when they land, whatever files they use.";
|
||||
return;
|
||||
}
|
||||
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||
"own log is the only channel this module has for reading "
|
||||
"PipeStats and the bring-up's refusal line";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The workload, and every one of P4a's four families is in it exactly once per draw:
|
||||
// a USER FRAMEBUFFER with a texture attachment (bit 9), a TEXTURE with parameters and
|
||||
// an upload (bit 10), an explicit SAMPLER OBJECT on the unit (bit 11) and a PROGRAM
|
||||
// with a default-uniform-block write (bit 12). A lane that steered only one of the four
|
||||
// would move only its own counter, which is why they are counted separately.
|
||||
void RunTheWorkload() {
|
||||
std::vector<std::uint8_t> texels(kTextureSize * kTextureSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i] = 0;
|
||||
texels[i + 1] = 255;
|
||||
texels[i + 2] = 0;
|
||||
texels[i + 3] = 255;
|
||||
}
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureSize, kTextureSize, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_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, 0);
|
||||
|
||||
glGenSamplers(1, &m_sampler);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
// The user framebuffer, drawn into once per iteration so that the framebuffer
|
||||
// record has a reason to move: the binding alternates between it and the default
|
||||
// framebuffer, which is exactly what a per-target set_framebuffer_state counts.
|
||||
glGenTextures(1, &m_attachment);
|
||||
glBindTexture(GL_TEXTURE_2D, m_attachment);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureSize, kTextureSize, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
m_attachment, 0);
|
||||
BindDefaultFramebuffer();
|
||||
|
||||
for (int draw = 0; draw < kDrawsInTheWindow; ++draw) {
|
||||
// Into the user framebuffer...
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kTextureSize, kTextureSize);
|
||||
glUseProgram(m_program);
|
||||
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glBindSampler(0, m_sampler);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
// ...and into the default one, which is what the case reads back.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
// One sub-region upload per iteration, so the client-side texture upload
|
||||
// counter (ctu) has something to count and the server's tex[emit=] has the
|
||||
// same something.
|
||||
const std::uint8_t green[4] = {0, 255, 0, 255};
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, draw % kTextureSize, 0, 1, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, green);
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseTheWorkload() {
|
||||
glBindSampler(0, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
BindDefaultFramebuffer();
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_sampler != 0) glDeleteSamplers(1, &m_sampler);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
if (m_attachment != 0) glDeleteTextures(1, &m_attachment);
|
||||
m_fbo = m_sampler = m_texture = m_attachment = 0;
|
||||
}
|
||||
|
||||
std::string m_lane;
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_quadBuffer = 0;
|
||||
GLuint m_texture = 0;
|
||||
GLuint m_attachment = 0;
|
||||
GLuint m_sampler = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// ONE case per lane, and it is a constraint rather than a preference: this case READS the
|
||||
// library log, the log is a per-LANE resource (the library opens it fopen(path, "w"), so
|
||||
// every process in a lane truncates it), and a second case in the same lane would race this
|
||||
// one under `ctest -j` with a failure indistinguishable from "the counter was never
|
||||
// emitted". The CMake registration gives each lane a TEST_FILTER naming one case.
|
||||
TEST_F(ObjectSubsystemControlScenario, ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/true);
|
||||
if (IsSkipped()) return;
|
||||
if (LaneIsAConsumerLane(m_lane)) {
|
||||
GTEST_SKIP() << "the two consumer lanes run their own case instead "
|
||||
"(TheAppliersNoConsumerBeltNeverFiresBehindTheClientsGate). They "
|
||||
"are at the phase default on both backends and their subject is "
|
||||
"c0f's gate/belt pair, not the on/off A/B: on the backend with no "
|
||||
"consumer the emit[] bracket is structurally zero AT the default "
|
||||
"mask, which is neither the on-lane's expectation nor the "
|
||||
"off-lane's.";
|
||||
}
|
||||
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();
|
||||
Gl().EndFrame(); // close the setup window: everything below is one window
|
||||
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
RunTheWorkload();
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the workload left a GL error behind";
|
||||
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||
|
||||
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||
ASSERT_TRUE(window.found)
|
||||
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD before getting "
|
||||
"here), so either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not reach "
|
||||
"the process, or no summary line was emitted at all because nothing reached "
|
||||
"PipeStats::OnPresent.";
|
||||
RecordProperty("stats_line", window.line.c_str());
|
||||
|
||||
// The five counters of the emit[] bracket, read individually so that a lane which
|
||||
// steered one family and not another says WHICH.
|
||||
const long long framebuffer = PipeStatsWindow::CounterOrAbsent(window, "fbe");
|
||||
const long long samplerViews = PipeStatsWindow::CounterOrAbsent(window, "sve");
|
||||
const long long samplerStates = PipeStatsWindow::CounterOrAbsent(window, "sse");
|
||||
const long long shaderImages = PipeStatsWindow::CounterOrAbsent(window, "sie");
|
||||
const long long clientUploads = PipeStatsWindow::CounterOrAbsent(window, "ctu");
|
||||
ASSERT_GE(framebuffer, 0)
|
||||
<< "the summary line carries no fbe= field, so this build's PipeStats has no P4a "
|
||||
"emission counters to read: "
|
||||
<< window.line;
|
||||
ASSERT_GE(samplerViews, 0) << "no sve= field: " << window.line;
|
||||
ASSERT_GE(samplerStates, 0) << "no sse= field: " << window.line;
|
||||
ASSERT_GE(shaderImages, 0) << "no sie= field: " << window.line;
|
||||
ASSERT_GE(clientUploads, 0) << "no ctu= field: " << window.line;
|
||||
const long long total = framebuffer + samplerViews + samplerStates + shaderImages +
|
||||
clientUploads;
|
||||
|
||||
if (m_lane == kLaneOn) {
|
||||
EXPECT_GT(total, 0)
|
||||
<< "with bits 9|10|11|12 SET the four P4a families are the path this workload "
|
||||
"takes - a user framebuffer bound and unbound "
|
||||
<< kDrawsInTheWindow
|
||||
<< " times, a texture with parameters and a sub-region upload per iteration, an "
|
||||
"explicit sampler object on the unit and a program with a default-uniform "
|
||||
"write - so the window's emit[] bracket must carry something. All five "
|
||||
"reading zero means the emitters never ran on the arm that is supposed to run "
|
||||
"them. It reported: "
|
||||
<< window.line;
|
||||
// The framebuffer family on its own, because it is the one that would be hidden by
|
||||
// a large upload count: a suppressor that stopped suppressing shows up as fbe
|
||||
// tracking the DRAW count, and a family that never emitted shows up as zero.
|
||||
EXPECT_GT(framebuffer, 0)
|
||||
<< "fbe= is zero on the ON lane: set_framebuffer_state never went out even "
|
||||
"though the workload bound a user framebuffer and the default framebuffer "
|
||||
<< kDrawsInTheWindow << " times each. " << window.line;
|
||||
} else if (m_lane == kLaneOff) {
|
||||
EXPECT_EQ(total, 0)
|
||||
<< "with bits 9|10|11|12 CLEARED (MOBILEGL_PIPE_PUSH=0x1ff, P3a's default) the "
|
||||
"frontend dispatch must fall through to the legacy MGB_CTX-reading arms and "
|
||||
"emit nothing through any of the four P4a families, so every counter in the "
|
||||
"emit[] bracket must be zero. A non-zero count here is the dead-switch "
|
||||
"reading: the bits are being ignored, both arms run the same code, and every "
|
||||
"push-vs-pull number taken against this A/B is measuring one arm twice. It "
|
||||
"reported: "
|
||||
<< window.line;
|
||||
} else {
|
||||
FAIL() << "unknown " << kLaneMarker << " value '" << m_lane
|
||||
<< "': the arms are on / off / refused / refused-texture / consumer / "
|
||||
"no-consumer. 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.
|
||||
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||
image.Height() - kInset, "green", 0.0,
|
||||
"the sampled draw [" + m_lane + "]"))
|
||||
<< "the subsystem bits changed what is DRAWN, which is not an A/B - the handle path "
|
||||
"and the legacy path must produce the same pixels from the same texture, sampler "
|
||||
"and framebuffer.";
|
||||
|
||||
ReleaseTheWorkload();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// D-K2's dependency refusal, in the direction that has to be refused.
|
||||
//
|
||||
// 0x9ff is bits 0..8 (everything P3a shipped) plus bit 11 (samplers) and WITHOUT bit 10
|
||||
// (texture resources). Every MGPBoundView::Texture and every MGPImageView::Res names a
|
||||
// Texture handle, and only bit 10 populates the texture slot table, so with bit 11 alone
|
||||
// every lookup would miss and the unit walk would `continue` without unbinding - a
|
||||
// half-run subsystem, which ROADMAP.md:7 forbids as loudly as a dead switch. The bring-up
|
||||
// logs ONE error naming both bits, refuses bit 11, and runs the legacy sampler arm.
|
||||
//
|
||||
// TWO ASSERTIONS, and the second is the one that stops this from being a log-scraping test:
|
||||
// the refusal is NAMED in the library's own log, and the run then draws the same picture as
|
||||
// every other lane. A refusal that aborted the process, and a refusal that silently let the
|
||||
// half-configured arm run, are both failures - and they look completely different here.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(ObjectSubsystemControlScenario, ASamplerBitWithoutTheTextureBitIsRefusedAndNamed) {
|
||||
if (!Ready()) return;
|
||||
// needsTheEmitters=false: the refusal is a BRING-UP decision made from the bitmask
|
||||
// alone, so it is assertable before any emitter exists - which is exactly what makes it
|
||||
// the one P4a control that is not vacuous on the contract tree.
|
||||
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false);
|
||||
if (IsSkipped()) return;
|
||||
if (m_lane != kLaneRefused) {
|
||||
GTEST_SKIP() << "runs only in the refusal lane (MOBILEGL_PIPE_PUSH=0x9ff): the "
|
||||
"on/off lanes configure a mask whose dependencies are all satisfied, "
|
||||
"so there is no refusal there to find and a search for one would "
|
||||
"report a healthy lane as red.";
|
||||
}
|
||||
// The refusal is decided from the bitmask, but it is a BACKEND's decision: D-K2 puts it
|
||||
// in ResolveSamplersSubsystemArm(), beside the bit-8-requires-bit-7 refusal that
|
||||
// already ships, and that function is package D's (Managers.cpp). A backend that does
|
||||
// not yet honour P4a's mask at all cannot refuse a dependency inside it, so on such a
|
||||
// tree there is nothing here to find and this case SKIPS rather than reporting the
|
||||
// absence of an unimplemented subsystem as a failure. The marker is the same one
|
||||
// HandleRecycle's P4a cases read - "does any source under this backend name one of the
|
||||
// four P4a subsystem constants" - because naming the constant is exactly what honouring
|
||||
// the mask means.
|
||||
{
|
||||
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 refusal lives "
|
||||
"in ResolveSamplersSubsystemArm() beside the bit-8-requires-bit-7 "
|
||||
"one that already ships (Managers.cpp:2393-2410), 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 0x9ff, "
|
||||
"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.";
|
||||
// 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 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,
|
||||
"the sampled draw [refused]"))
|
||||
<< "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();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// 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.
|
||||
//
|
||||
// WHAT THIS ARM DOES ON THE INTEGRATED TREE, corrected (review F-v2-m2). An earlier
|
||||
// round's report told the integrator to expect this lane to go RED between esprytobj's
|
||||
// integration and package D's rework, and to read that red as expected. That window does
|
||||
// not exist: esprytobj v2 already carries D-K2's fourth row - Managers.cpp's
|
||||
// ResolveTextureResourceSubsystemArm refuses bit 10 without bit 11 with the sentence this
|
||||
// case matches - so the arm ARMS AND PASSES. A red here is therefore a real finding about
|
||||
// that resolver (it stopped refusing, refused for the wrong reason, or printed the mirror
|
||||
// sentence, which the direction check above is what catches) and must not be waved
|
||||
// through as a sequencing artefact.
|
||||
// ------------------------------------------------------------------------------------
|
||||
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. The
|
||||
// swap is now a REAL difference between the two cases: FindTheRefusalLine requires the
|
||||
// set bit to be named before " is set but " and the needed bit after it (F-v2-m1), so
|
||||
// this call and the 0x9ff one above accept disjoint sentences. Espryt's is
|
||||
// "kMGPipeSubsystemTextureResources (bit 10) is set but kMGPipeSubsystemSamplers
|
||||
// (bit 11) is clear; MGPTextureParams::BuiltinSampler is a SamplerCso handle, only
|
||||
// bit 11 mints sampler CSOs, and the applier's verdict for a null one is
|
||||
// Fatal{ProtocolCorruption} - REFUSING the dependent bit and running the legacy arm.
|
||||
// Set both bits, or clear both" (Managers.cpp, ResolveTextureResourceSubsystemArm).
|
||||
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();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// c0f's GATE AND BELT, MEASURED TOGETHER (ID-39, ID-40).
|
||||
//
|
||||
// WHAT WENT WRONG AND WHY IT NEEDS A LANE. P4a's four families were wired without the
|
||||
// gate P3a's buffers have had since PipeFill.cpp ~656: emission required the family's bit
|
||||
// and nothing else. On Magma, which registers no MGPipeResourceOps, the client therefore
|
||||
// emitted, THE APPLIER ACCEPTED, the client cleared its dirty flags on that acceptance -
|
||||
// and Magma's legacy path then found nothing to upload. Sixty-six DirectVulkan cases went
|
||||
// red at once, all texture-upload-shaped, and every one of them was green at 0x1ff. The
|
||||
// fix has two halves that are deliberately independent: the client's gate (bit N AND
|
||||
// wired AND a backend registered the ops) and the applier's belt (every P4a entry point
|
||||
// returns accepted = false and counts RefusedNoConsumer when none did).
|
||||
//
|
||||
// THE ASSERTION IS THAT THE BELT NEVER FIRES, and it is the same assertion on both
|
||||
// backends, which is what makes it worth having twice:
|
||||
//
|
||||
// no-consumer (DirectVulkan, 0x1fff): the belt is the SAFETY NET. A non-zero count here
|
||||
// means a record reached the applier on a backend with no consumer - i.e. the client
|
||||
// gate leaked and only the belt stopped the dirty flag from being cleared. That is
|
||||
// ID-39's bug caught one layer later, and it is invisible in these pixels because the
|
||||
// belt does its job; the 66 red cases were in another suite entirely.
|
||||
// consumer (DirectGLES, 0x1fff): the CONTROL. Espryt registers the ops, so no entry
|
||||
// point may take the no-consumer arm at all. Without this lane a green above could
|
||||
// also mean "nothing is ever emitted anywhere", which is exactly what a gate that was
|
||||
// accidentally always-false would look like.
|
||||
//
|
||||
// A DELTA, not an absolute: the counter is process-global and other cases in this binary
|
||||
// run before this one. MGPipeApplierReset also zeroes it, so a count that went DOWN is
|
||||
// read as "the applier was reset and everything since is `after`" rather than as an
|
||||
// underflow.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(ObjectSubsystemControlScenario, TheAppliersNoConsumerBeltNeverFiresBehindTheClientsGate) {
|
||||
if (!Ready()) return;
|
||||
// needsTheEmitters=false: the assertion is that a counter did NOT move, which is
|
||||
// meaningful before the emitters land as well as after - and on the no-consumer lane
|
||||
// it is meaningful precisely BECAUSE nothing may be emitted there.
|
||||
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false);
|
||||
if (IsSkipped()) return;
|
||||
if (!LaneIsAConsumerLane(m_lane)) {
|
||||
GTEST_SKIP() << "runs only in the two consumer lanes (MGITEST_OBJECT_SUBSYSTEM_LANE="
|
||||
<< kLaneConsumer << " / " << kLaneNoConsumer
|
||||
<< "), which pin MOBILEGL_PIPE_PUSH at the phase default on the two "
|
||||
"backends. Every other lane configures a mask or a backend whose "
|
||||
"emission shape is a different question.";
|
||||
}
|
||||
|
||||
unsigned long long before = 0;
|
||||
if (!PeekPipeApplierRefusedNoConsumer(&before)) {
|
||||
GTEST_SKIP() << "MGPipeApplierState::RefusedNoConsumer is out of reach here: there "
|
||||
"is no applier in a PULL build (it is #if MOBILEGL_PIPE_PUSH), and "
|
||||
"on Android this module links the shipping libMobileGL.so built "
|
||||
"-fvisibility=hidden. 'Could not look' is not 'did not fire'.";
|
||||
}
|
||||
|
||||
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 " << m_lane << " lane";
|
||||
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||
Gl().EndFrame();
|
||||
|
||||
unsigned long long after = 0;
|
||||
ASSERT_TRUE(PeekPipeApplierRefusedNoConsumer(&after))
|
||||
<< "the counter could be read before the workload and not after it";
|
||||
// Down means MGPipeApplierReset ran inside the window, so everything still counted is
|
||||
// what happened since - which is the number this case is about either way.
|
||||
const unsigned long long fired = after >= before ? after - before : after;
|
||||
|
||||
std::cout << "[ ObjectSubsystemControl ] " << m_lane
|
||||
<< " lane: applier RefusedNoConsumer " << before << " -> " << after
|
||||
<< " over the workload (delta " << fired << ")" << std::endl;
|
||||
RecordProperty("refused_no_consumer_delta", static_cast<int>(fired));
|
||||
|
||||
EXPECT_EQ(fired, 0u)
|
||||
<< "the applier's no-consumer BELT fired " << fired
|
||||
<< " time(s) during this workload on the " << m_lane
|
||||
<< " lane. The belt exists so that a P4a record arriving on a backend that "
|
||||
"registered no MGPipeResourceOps is refused rather than accepted - and an "
|
||||
"accepted record is what makes the client clear the dirty flags whose texels "
|
||||
"nobody then uploads (ID-39: sixty-six DirectVulkan cases, all texture-upload "
|
||||
"shaped). A non-zero count means the CLIENT'S GATE let an emission through and "
|
||||
"only the belt caught it: the two are supposed to agree, and PipeFill's "
|
||||
"P4aFamilyHasItsConsumer() is where they stopped.";
|
||||
|
||||
// The pixels, on both lanes, for the reason every arm of this file asserts them: a
|
||||
// backend running its legacy path because no consumer is registered must draw exactly
|
||||
// what a backend running the handle arm draws.
|
||||
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||
image.Height() - kInset, "green", 0.0,
|
||||
"the sampled draw [" + m_lane + "]"))
|
||||
<< "the " << m_lane
|
||||
<< " lane did not draw what every other lane draws, so whatever the counter says, "
|
||||
"this configuration is not running the workload correctly.";
|
||||
|
||||
ReleaseTheWorkload();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,803 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.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 FOUR SEAMS THE P4a FABLE SEAM AUDIT PROVED, each pinned by the public-GL sequence
|
||||
// (or the white-box reading) that was red on the tree the audit read and is green with its fix.
|
||||
//
|
||||
// The audit's rule, which every case here is an instance of: EVERY FIELD OF EVERY EMITTED RECORD
|
||||
// NAMES THE FRONTEND SETTER THAT CHANGES IT, AND THAT SETTER MOVES A COUNTER THE EMITTING BIT'S
|
||||
// SHUTTER READS. A record whose field has a setter no shutter sees is a stale record with nothing
|
||||
// to refuse - no census line, no Fatal, a wrong picture or a permanent silent fallback - which is
|
||||
// why none of the 80 scenarios before this file caught any of the four (Tracker.h carries the
|
||||
// record-field -> setter -> shutter table this file is the gate for).
|
||||
//
|
||||
// F-3 set_framebuffer_state INLINES an attachment's format (D-C1) and a storage redefinition of
|
||||
// an ATTACHED texture or renderbuffer moved nothing bit 11 read: Espryt's handle arm then
|
||||
// answered its alpha-widening / snorm-clamp / integer masks from the stale copy. AND THE
|
||||
// PRE-HANDLE ARM WAS NOT FRESH EITHER, which these cases found on the 0x1ff / 0 / pull
|
||||
// lanes: a redefinition that keeps the driver id (mutable texture storage regenerated in
|
||||
// place, a renderbuffer re-storaged in place) moves neither the framebuffer's frontend
|
||||
// versions nor the backend-id generation the FBO memo reads, so SyncToBackend never
|
||||
// re-ran and the masks stayed on both arms. The texture half is fixed on both arms OF A
|
||||
// PUSH BUILD (an in-place regeneration now takes the same generation a re-mint takes -
|
||||
// compiled under MOBILEGL_PIPE_PUSH because G1 keeps the pull library byte-identical to
|
||||
// the P4a baseline, so the pull build keeps the pre-P4a hole until the fix lands on dev on
|
||||
// its own and the texture cases decline by name there); the renderbuffer half only on the
|
||||
// handle arm, where the resource record carries the re-storage - on the pre-handle arm a
|
||||
// renderbuffer's twin is only ever reached from inside the FBO walk the memo skips (D-D2's
|
||||
// documented hole, pre-P4a code), so that case asserts on the handle arm and declines by
|
||||
// name elsewhere. Three cases, both directions, texture and renderbuffer. DirectGLES only:
|
||||
// the masks are Espryt's substitution machinery.
|
||||
// F-1 set_sampler_views is resolved for the PROGRAM IN USE and bit 12's shutter read no program
|
||||
// input, so a glUseProgram alone never re-emitted it; E's record epoch (the two set serials)
|
||||
// then kept the program-independent texture sync list from ever rebuilding, and a texture
|
||||
// bound to an EMPTY slot under one program was never synced for the next. One case, both
|
||||
// backends, red as a black quad.
|
||||
// F-2 bit 14's plain-program arm mixed a per-program COUNTER two programs routinely share, so a
|
||||
// program switch never re-emitted set_shader_images and the window stayed the previous
|
||||
// program's - and E's SD-4 (a buffer image never reaching the record at all) is the same
|
||||
// bit through the null -> program transition. One case, white-box, both backends run it.
|
||||
// F-4 BindCurrentUnitSamplers' record arm looked a CONTENT-addressed CSO handle up in the
|
||||
// IDENTITY-keyed twin registry: a miss on every draw, hidden because the pre-handle program
|
||||
// pass bound the same values. One case, white-box: the unit's driver sampler must be the
|
||||
// CSO's own twin.
|
||||
//
|
||||
// A WHITE-BOX READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES with its
|
||||
// public-GL half (the shape TextureParamsWithoutASamplerViewScenario.cpp argues for): a pull
|
||||
// build, Magma, or a lane whose mask leaves Espryt's sampler family on its legacy arm has no
|
||||
// record arm to assert about, and skipping the whole case there would delete the public-GL
|
||||
// verdict those lanes carry. Every decline is printed and RecordProperty'd.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/P4aSeamPeek.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 kSize = 16;
|
||||
constexpr int kInset = 2;
|
||||
|
||||
// No attributes: the quad's corners come from gl_VertexID, so a bare VAO is all a draw
|
||||
// needs and no vertex-input state can enter any of the sequences below.
|
||||
constexpr const char* kQuadVS = R"(#version 330 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kColorFS = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = uColor; }
|
||||
)";
|
||||
|
||||
// texelFetch, so WHICH image the unit holds is the whole answer and no filter, wrap or
|
||||
// completeness rule can explain a colour away.
|
||||
constexpr const char* kFetchFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = texelFetch(uTex, ivec2(0, 0), 0); }
|
||||
)";
|
||||
|
||||
// texture() at (1.5, 1.5): outside the image on both axes, so the WRAP mode of whichever
|
||||
// sampler applies - the unit's sampler object or the texture's built-in one - decides
|
||||
// whether the texel or the border colour comes back.
|
||||
constexpr const char* kOutsideSampleFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = texture(uTex, vec2(1.5, 1.5)); }
|
||||
)";
|
||||
|
||||
// F-2 / SD-4: two compute programs over BUFFER images (the SD-4 shape - the kind E's I2
|
||||
// flip found never reached the record at all), the second naming one unit more than the
|
||||
// first, and both image-unit counters equal (layout(binding) assigns the unit at link, so
|
||||
// neither program ever moves it through glUniform1i).
|
||||
constexpr const char* kOneBufferImageCS = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, r32ui) writeonly uniform uimageBuffer i0;
|
||||
void main() { imageStore(i0, 0, uvec4(7u, 0u, 0u, 0u)); }
|
||||
)";
|
||||
|
||||
constexpr const char* kTwoBufferImagesCS = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, r32ui) readonly uniform uimageBuffer i0;
|
||||
layout(binding = 1, r32ui) writeonly uniform uimageBuffer i1;
|
||||
void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); }
|
||||
)";
|
||||
|
||||
class P4aSeamAuditScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// The F-3 cases are about Espryt's four cross-object masks, which are its own
|
||||
// substitution machinery (three-channel widening, SNORM/UNORM clamp, integer outputs);
|
||||
// Magma answers the same GL questions on its own terms, so a verdict there would pin
|
||||
// a coincidence - the same reason SnormAttachment and ThreeChannelAttachment skip.
|
||||
// Marks the case skipped; the caller tests IsSkipped() and returns (GTEST_SKIP is a
|
||||
// void statement, so it cannot return the verdict itself).
|
||||
void SkipUnlessEspryt(const char* what) {
|
||||
if (Gl().BackendName() == "DirectGLES") return;
|
||||
GTEST_SKIP() << what << " is a DirectGLES handle-arm seam; backend is " << Gl().BackendName();
|
||||
}
|
||||
|
||||
static void DrawQuad() { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); }
|
||||
|
||||
// One pixel's RGBA as floats, from the currently bound READ framebuffer.
|
||||
static void ReadPixelFloat(int x, int y, float out[4]) {
|
||||
out[0] = out[1] = out[2] = out[3] = -1.0f;
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_FLOAT, out);
|
||||
}
|
||||
|
||||
// The white-box gate shared by F-2 and F-4: true when Espryt's sampler family is on
|
||||
// its handle arm in this process, so the applier's unit sets are consumed and an
|
||||
// assertion about them can only be red for its own reason. Prints the decline.
|
||||
bool SamplerHandleArmIsLive(const char* what) {
|
||||
bool live = false;
|
||||
std::string why;
|
||||
if (!PeekEsprytSamplerHandleArmIsLive(&live)) {
|
||||
why = "the reading cannot be taken here (a pull build, Android, or a backend that "
|
||||
"is not DirectGLES)";
|
||||
} else if (!live) {
|
||||
why = "Espryt's sampler family runs its legacy arm in this process "
|
||||
"(MOBILEGL_PIPE_PUSH leaves bit 11 clear or refuses it)";
|
||||
}
|
||||
if (why.empty()) return true;
|
||||
std::cout << "[ P4aSeamAudit ] white-box reading DECLINED for " << what << ": " << why
|
||||
<< "; the public-GL half of the case still runs" << std::endl;
|
||||
RecordProperty("p4a_seam_white_box", "declined");
|
||||
RecordProperty("p4a_seam_white_box_reason", why);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A 2x2 RGBA8 texture filled with one colour, NEAREST, single level - complete under
|
||||
// every rule, so nothing about completeness can enter the F-1 and F-4 sequences.
|
||||
static GLuint MakeSolidTexture2D(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
std::uint8_t texels[2 * 2 * 4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
texels[i * 4 + 0] = r;
|
||||
texels[i * 4 + 1] = g;
|
||||
texels[i * 4 + 2] = b;
|
||||
texels[i * 4 + 3] = 255;
|
||||
}
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return texture;
|
||||
}
|
||||
|
||||
bool ComputeImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
GLint maxBufferSize = 0;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxBufferSize);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits >= 2 && maxComputeImageUniforms >= 2 && maxBufferSize >= 4;
|
||||
}
|
||||
|
||||
static GLuint MakeComputeProgram(const char* source, std::string* outError) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
*outError = std::string("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
*outError = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// An R32UI buffer texture over a fresh 4-texel buffer, every texel `fill`.
|
||||
static GLuint MakeBufferTexture(GLuint* outBuffer, GLuint fill) {
|
||||
const GLuint texels[4] = {fill, fill, fill, fill};
|
||||
glGenBuffers(1, outBuffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, *outBuffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, sizeof(texels), texels, GL_DYNAMIC_COPY);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, *outBuffer);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static GLuint ReadBufferTexel0(GLuint buffer) {
|
||||
GLuint value = 0xFFFFFFFFu;
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, sizeof(value), &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// F-3: a storage redefinition WHILE ATTACHED reaches the framebuffer record
|
||||
// -----------------------------------------------------------------------------------
|
||||
//
|
||||
// GL_SRGB8 is a format Espryt can only render into through its three-channel widening
|
||||
// (llvmpipe reports INCOMPLETE_ATTACHMENT for it natively - ThreeChannelAttachmentScenario
|
||||
// measured the table), so its draw buffer carries the alpha-widened mask: every draw has its
|
||||
// alpha masked off so the stored alpha stays at the 1.0 a three-channel format implies.
|
||||
// Redefine the same attached texture as GL_SRGB8_ALPHA8 and the application owns alpha
|
||||
// again - the mask must clear. On the tree the audit read the record still said SRGB8, the
|
||||
// handle arm kept masking, and the 0.25 this case draws never reached the storage; on the
|
||||
// pre-handle arm the twin regenerated the (mutable) storage on the same driver id, nothing
|
||||
// the FBO memo reads moved, and the masks stayed the same way.
|
||||
|
||||
TEST_F(P4aSeamAuditScenario, ATextureRespecifiedWhileAttachedReachesTheFramebufferRecord) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("F-3");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||
ASSERT_GE(colorLocation, 0);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, kSize, kSize, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8 texture was refused";
|
||||
|
||||
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), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "an SRGB8 colour attachment must be complete (natively or through the widening)";
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glUseProgram(program);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
|
||||
// Phase 1: the three-channel format. Whatever the draw writes, GL reports alpha 1.0.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
float pixel[4];
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[3], 1.0f, 0.02f) << "a three-channel attachment reports alpha 1.0";
|
||||
|
||||
// Phase 2: THE RESPECIFY, while attached, with no re-attach and no rebind of the FBO.
|
||||
// The only thing that moves between the two draws is the texture's storage.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the respecify to SRGB8_ALPHA8 was refused";
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all";
|
||||
// PUSH BUILDS ONLY, EVERY ARM OF THEM. The pre-handle half of the fix (an in-place
|
||||
// regeneration takes the backend-id generation a re-mint takes) is Espryt code the
|
||||
// pull build would share, and G1 keeps the pull library byte-identical to the P4a
|
||||
// baseline - so it is compiled under MOBILEGL_PIPE_PUSH and the pull build keeps the
|
||||
// pre-P4a hole until the same lines land on dev on their own. The peek returns true
|
||||
// exactly where it could look, which for a case that already skipped off Espryt means
|
||||
// "a push build"; what it writes (is the handle arm live) does not matter here.
|
||||
bool framebufferArmLive = false;
|
||||
if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive)) {
|
||||
EXPECT_NEAR(pixel[3], 0.25f, 0.02f)
|
||||
<< "the draw's alpha never reached a four-channel attachment: the framebuffer record "
|
||||
"(handle arm) or the FBO twin's memo (pre-handle arm) still describes the "
|
||||
"three-channel storage the texture was attached with, so alpha stayed masked off (F-3)";
|
||||
} else {
|
||||
std::cout << "[ P4aSeamAudit ] texture respecify verdict DECLINED on the pull build (the "
|
||||
"in-place regeneration bump is push-only by G1); alpha read "
|
||||
<< pixel[3] << std::endl;
|
||||
RecordProperty("p4a_seam_white_box", "declined");
|
||||
RecordProperty("p4a_seam_white_box_reason", "texture respecify: pull build (G1)");
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// The renderbuffer twin. A renderbuffer's three storage setters bump no version at all:
|
||||
// D-D2 closed the RESOURCE record by emitting from the entry point and left the framebuffer
|
||||
// record - and with it the masks - describing the storage it was attached with.
|
||||
TEST_F(P4aSeamAuditScenario, ARenderbufferRestoragedWhileAttachedReachesTheFramebufferRecord) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("F-3");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||
ASSERT_GE(colorLocation, 0);
|
||||
|
||||
GLuint renderbuffer = 0;
|
||||
glGenRenderbuffers(1, &renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_SRGB8, kSize, kSize);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8 renderbuffer was refused";
|
||||
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "an SRGB8 renderbuffer attachment must be complete (natively or through the widening)";
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glUseProgram(program);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
float pixel[4];
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[3], 1.0f, 0.02f) << "a three-channel attachment reports alpha 1.0";
|
||||
|
||||
// THE RE-STORAGE, while attached.
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_SRGB8_ALPHA8, kSize, kSize);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the re-storage to SRGB8_ALPHA8 was refused";
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all";
|
||||
// THE HANDLE ARM ONLY. On the pre-handle arm a renderbuffer's twin is reached only from
|
||||
// inside the FBO walk, and nothing that walk's memo reads moves on glRenderbufferStorage
|
||||
// - the frontend setters bump no version (D-D2), no framebuffer version sees them, and
|
||||
// the twin that would bump the backend generation is exactly what the memo skips. That
|
||||
// is pre-P4a code and D-D2's documented hole; the resource record is what closes it,
|
||||
// so the verdict is taken where the record is consumed and declined by name elsewhere
|
||||
// (measured: alpha 1.0 on the pull build and at 0x1ff / 0, the mask of the storage the
|
||||
// renderbuffer was attached with).
|
||||
bool framebufferArmLive = false;
|
||||
if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive) && framebufferArmLive) {
|
||||
EXPECT_NEAR(pixel[3], 0.25f, 0.02f)
|
||||
<< "the draw's alpha never reached the four-channel renderbuffer: the framebuffer "
|
||||
"record still describes the storage it was attached with (F-3)";
|
||||
} else {
|
||||
std::cout << "[ P4aSeamAudit ] renderbuffer re-storage verdict DECLINED on the pre-handle arm "
|
||||
"(D-D2's documented hole: no frontend version and no backend generation moves on a "
|
||||
"renderbuffer re-storage until the FBO walk the memo skips); alpha read "
|
||||
<< pixel[3] << std::endl;
|
||||
RecordProperty("p4a_seam_white_box", "declined");
|
||||
RecordProperty("p4a_seam_white_box_reason", "renderbuffer re-storage: pre-handle arm (D-D2)");
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &renderbuffer);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// The mirror direction, four channels -> three, and it needs the driver to READ the stored
|
||||
// alpha because the readback fixup (which consults the frontend) would hide it: after the
|
||||
// respecify to SRGB8 the widening discipline has to hold - the clear puts 1.0 into the
|
||||
// carrier's alpha and the draw is masked away from it - so a GL_DST_ALPHA blend of white sees
|
||||
// 1.0. On a stale record the draw wrote its 0.25 into the carrier and the blend saw that.
|
||||
TEST_F(P4aSeamAuditScenario, ATextureRespecifiedToThreeChannelsWhileAttachedReachesTheFramebufferRecord) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("F-3");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||
ASSERT_GE(colorLocation, 0);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8_ALPHA8 texture was refused";
|
||||
|
||||
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), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glUseProgram(program);
|
||||
|
||||
// Phase 1: four channels, the application owns alpha.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
DrawQuad();
|
||||
float pixel[4];
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[3], 0.25f, 0.02f) << "a four-channel attachment stores the draw's alpha";
|
||||
|
||||
// Phase 2: THE RESPECIFY to three channels, while attached.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, kSize, kSize, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the respecify to SRGB8 was refused";
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
DrawQuad();
|
||||
// dst = stored alpha; src factor GL_DST_ALPHA, dst factor GL_ZERO, source white =>
|
||||
// the colour becomes (storedAlpha, storedAlpha, storedAlpha) - ThreeChannelAttachment's
|
||||
// own probe, which nothing on the readback path can doctor.
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_DST_ALPHA, GL_ZERO);
|
||||
glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
DrawQuad();
|
||||
glDisable(GL_BLEND);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
// PUSH BUILDS ONLY, EVERY ARM OF THEM (the mirror). The pre-handle half of the fix (an in-place
|
||||
// regeneration takes the backend-id generation a re-mint takes) is Espryt code the
|
||||
// pull build would share, and G1 keeps the pull library byte-identical to the P4a
|
||||
// baseline - so it is compiled under MOBILEGL_PIPE_PUSH and the pull build keeps the
|
||||
// pre-P4a hole until the same lines land on dev on their own. The peek returns true
|
||||
// exactly where it could look, which for a case that already skipped off Espryt means
|
||||
// "a push build"; what it writes (is the handle arm live) does not matter here.
|
||||
bool framebufferArmLive = false;
|
||||
if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive)) {
|
||||
EXPECT_NEAR(pixel[0], 1.0f, 0.05f)
|
||||
<< "GL_DST_ALPHA read the stored alpha of a three-channel attachment and it was not "
|
||||
"1.0: the framebuffer record (handle arm) or the FBO twin's memo (pre-handle arm) "
|
||||
"still describes the four-channel storage the texture was attached with, so the "
|
||||
"draw was let write alpha (F-3, mirror)";
|
||||
} else {
|
||||
std::cout << "[ P4aSeamAudit ] three-channel respecify verdict DECLINED on the pull build (the "
|
||||
"in-place regeneration bump is push-only by G1); red read "
|
||||
<< pixel[0] << std::endl;
|
||||
RecordProperty("p4a_seam_white_box", "declined");
|
||||
RecordProperty("p4a_seam_white_box_reason", "three-channel respecify: pull build (G1)");
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// F-1 / F-1b: a program switch re-resolves the view set, and the texture sync list with it
|
||||
// -----------------------------------------------------------------------------------
|
||||
//
|
||||
// The sequence the audit named, and every step of it is ordinary: two programs sampling two
|
||||
// different units, a texture bound to a unit's EMPTY 2D slot - the unit was already touched
|
||||
// through another target, so the high-water mark does not move - while a program that does
|
||||
// not sample it is in use, then the switch to the one that does. Nothing between the two
|
||||
// draws touches a parameter, a level or a populated slot, which is exactly what leaves the
|
||||
// record epoch - and the program-independent texture sync list keyed on it - unmoved on
|
||||
// the tree the audit read: the second program sampled an unbound unit and drew black.
|
||||
TEST_F(P4aSeamAuditScenario, ATextureBoundToAnEmptySlotUnderOneProgramIsSampledByTheNext) {
|
||||
if (!Ready()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint first = CompileProgram(kQuadVS, kFetchFS, &error);
|
||||
ASSERT_NE(first, 0u) << error;
|
||||
const GLuint second = CompileProgram(kQuadVS, kFetchFS, &error);
|
||||
ASSERT_NE(second, 0u) << error;
|
||||
glUseProgram(first);
|
||||
glUniform1i(glGetUniformLocation(first, "uTex"), 0);
|
||||
glUseProgram(second);
|
||||
glUniform1i(glGetUniformLocation(second, "uTex"), 1);
|
||||
glUseProgram(0);
|
||||
|
||||
// Every texture exists, complete, with its parameters set, BEFORE the first draw: a
|
||||
// parameter or a level defined between the two draws would move the sampling-resolution
|
||||
// generation and rescue the list by accident.
|
||||
const GLuint red = MakeSolidTexture2D(255, 0, 0);
|
||||
const GLuint green = MakeSolidTexture2D(0, 255, 0);
|
||||
GLuint touch3D = 0;
|
||||
glGenTextures(1, &touch3D);
|
||||
glBindTexture(GL_TEXTURE_3D, touch3D);
|
||||
const std::uint8_t blue[2 * 2 * 2 * 4] = {0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255,
|
||||
0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255};
|
||||
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, blue);
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_3D, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup left a GL error behind";
|
||||
|
||||
ColorFbo target = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(target.fbo, 0u);
|
||||
BindFbo(target);
|
||||
glBindVertexArray(m_vao);
|
||||
|
||||
// Unit 1 is TOUCHED through its 3D slot; its 2D slot stays empty. Unit 0 holds red.
|
||||
// The first program is in use BEFORE the first verb (the clear), so the very first
|
||||
// view set that goes out is already resolved for it - measured: with no program in
|
||||
// use at the clear the first set is [null, null], and the bind below then re-resolves
|
||||
// to [red, null], a DIFFERENT set that moves the serial and rescues the case by
|
||||
// accident.
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_3D, touch3D);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, red);
|
||||
glUseProgram(first);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawQuad();
|
||||
|
||||
// THE BIND ONTO THE EMPTY SLOT, under a program that does not sample unit 1, and a
|
||||
// draw with THAT program so the bind's own re-resolution of the view set happens under
|
||||
// it (the bind generation fires bit 12 at the next verb; a switch inside the same verb
|
||||
// gap would let that fire resolve under the second program by accident) ...
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, green);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
DrawQuad();
|
||||
// ... and THE SWITCH to the one that does sample it. No other state moves.
|
||||
glUseProgram(second);
|
||||
DrawQuad();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the two draws left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kSize, kSize);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
EXPECT_TRUE(RegionIsMostly(image, kInset, kSize - 1 - kInset, kInset, kSize - 1 - kInset, "green", 0.0,
|
||||
"the draw after the program switch"))
|
||||
<< "black means the second program sampled an unbound unit: the texture bound to the "
|
||||
"empty slot was never synced because the view set - and E's record epoch with it - "
|
||||
"did not move on the program switch (F-1 / F-1b); red means the first program's "
|
||||
"set was still in force";
|
||||
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindTexture(GL_TEXTURE_3D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
DestroyColorFbo(target);
|
||||
glDeleteTextures(1, &red);
|
||||
glDeleteTextures(1, &green);
|
||||
glDeleteTextures(1, &touch3D);
|
||||
glDeleteProgram(first);
|
||||
glDeleteProgram(second);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// F-2 / SD-4: the image window follows the program, through buffer images
|
||||
// -----------------------------------------------------------------------------------
|
||||
//
|
||||
// Public-GL half: both dispatches store what they should (every arm passes this - the
|
||||
// server's window/high-water union takes the pre-handle bind for a unit the record does
|
||||
// not cover, which is exactly why the seam was silent). White-box half, on Espryt's handle
|
||||
// arm: after the first dispatch set_shader_images must have arrived with a window of ONE
|
||||
// unit (SD-4: on the tree the audit read a buffer image never reached the record at all -
|
||||
// the null -> program transition moved nothing bit 14 read), and after the switch to the
|
||||
// program naming two units the window must be TWO (F-2: the two programs' image-unit
|
||||
// counters are equal, so the switch alone moved nothing either).
|
||||
TEST_F(P4aSeamAuditScenario, AProgramSwitchWithEqualImageUnitCountersMovesTheImageWindow) {
|
||||
if (!Ready()) return;
|
||||
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units / buffer textures on this host";
|
||||
|
||||
std::string error;
|
||||
const GLuint one = MakeComputeProgram(kOneBufferImageCS, &error);
|
||||
ASSERT_NE(one, 0u) << error;
|
||||
const GLuint two = MakeComputeProgram(kTwoBufferImagesCS, &error);
|
||||
ASSERT_NE(two, 0u) << error;
|
||||
|
||||
GLuint buffer0 = 0;
|
||||
GLuint buffer1 = 0;
|
||||
const GLuint image0 = MakeBufferTexture(&buffer0, 0u);
|
||||
const GLuint image1 = MakeBufferTexture(&buffer1, 0u);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "buffer texture setup left a GL error behind";
|
||||
|
||||
// Both units bound BEFORE any dispatch, so the bind generation does not move between
|
||||
// the two dispatches and the only thing that changes is the program in use.
|
||||
glBindImageTexture(0, image0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI);
|
||||
glBindImageTexture(1, image1, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the buffer images left a GL error behind";
|
||||
|
||||
const bool whiteBox = SamplerHandleArmIsLive("F-2 / SD-4");
|
||||
|
||||
glUseProgram(one);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the first dispatch leaked a GL error";
|
||||
if (whiteBox) {
|
||||
PipeShaderImageWindowPeek window{};
|
||||
ASSERT_TRUE(PeekPipeShaderImageWindow(&window));
|
||||
EXPECT_EQ(window.Start, 0u);
|
||||
EXPECT_EQ(window.Count, 1u)
|
||||
<< "set_shader_images never arrived for a program whose only image is a BUFFER "
|
||||
"image (SD-4): the null -> program transition moved nothing bit 14 read";
|
||||
}
|
||||
|
||||
glUseProgram(two);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the second dispatch leaked a GL error";
|
||||
if (whiteBox) {
|
||||
PipeShaderImageWindowPeek window{};
|
||||
ASSERT_TRUE(PeekPipeShaderImageWindow(&window));
|
||||
EXPECT_EQ(window.Start, 0u);
|
||||
EXPECT_EQ(window.Count, 2u)
|
||||
<< "the image window did not follow the program switch: two programs with equal "
|
||||
"image-unit counters, and bit 14 mixed only the counter (F-2)";
|
||||
}
|
||||
|
||||
EXPECT_EQ(ReadBufferTexel0(buffer0), 7u) << "the first program's store did not land";
|
||||
EXPECT_EQ(ReadBufferTexel0(buffer1), 9u) << "the second program's store did not land";
|
||||
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(one);
|
||||
glDeleteProgram(two);
|
||||
glDeleteTextures(1, &image0);
|
||||
glDeleteTextures(1, &image1);
|
||||
glDeleteBuffers(1, &buffer0);
|
||||
glDeleteBuffers(1, &buffer1);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// F-4: the unit's driver sampler is the CSO's own twin on the handle arm
|
||||
// -----------------------------------------------------------------------------------
|
||||
//
|
||||
// Public-GL half: a glBindSampler'd object whose wrap differs from the texture's built-in
|
||||
// sampler wins (GL 4.6 core 8.10) - every arm passes this, because the pre-handle program
|
||||
// pass bound the object through its identity twin. White-box half, on Espryt's handle arm:
|
||||
// the sampler the unit carries on the driver must be the twin Espryt holds AT THE CSO
|
||||
// HANDLE bind_sampler_states named for the unit. On the tree the audit read that twin did
|
||||
// not exist - the handle is content-addressed, the registry's twins were minted off
|
||||
// lifetime ids - so the record arm bound nothing on every draw.
|
||||
TEST_F(P4aSeamAuditScenario, ABoundSamplerObjectIsDrivenThroughItsCsoTwinOnTheHandleArm) {
|
||||
if (!Ready()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kOutsideSampleFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// The texture's built-in sampler REPEATS, so (1.5, 1.5) reads the red texel through it;
|
||||
// the sampler object CLAMPS TO A WHITE BORDER, so the same coordinate reads white
|
||||
// through it. White is a Vulkan palette border colour, so Magma needs no extension.
|
||||
const GLuint red = MakeSolidTexture2D(255, 0, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
GLuint sampler = 0;
|
||||
glGenSamplers(1, &sampler);
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
const GLfloat white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
glSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR, white);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "sampler setup left a GL error behind";
|
||||
|
||||
ColorFbo target = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(target.fbo, 0u);
|
||||
BindFbo(target);
|
||||
glBindVertexArray(m_vao);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, red);
|
||||
glBindSampler(0, sampler);
|
||||
glUseProgram(program);
|
||||
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawQuad();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the draw left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kSize, kSize);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
EXPECT_TRUE(RegionIsMostly(image, kInset, kSize - 1 - kInset, kInset, kSize - 1 - kInset, "white", 0.0,
|
||||
"the draw through the bound sampler object"))
|
||||
<< "red means the texture's own REPEAT sampler applied instead of the bound object's "
|
||||
"CLAMP_TO_BORDER";
|
||||
|
||||
if (SamplerHandleArmIsLive("F-4")) {
|
||||
EsprytUnitSamplerPeek peek{};
|
||||
ASSERT_TRUE(PeekEsprytUnitSampler(0, sampler, &peek));
|
||||
std::cout << "[ P4aSeamAudit ] white-box: unit 0 driver sampler " << peek.BoundSamplerId
|
||||
<< ", bind_sampler_states handle {" << peek.CsoHandleSlot << ", " << peek.CsoHandleGen
|
||||
<< "} inside window " << (peek.UnitInsideWindow ? "yes" : "no") << ", CSO twin "
|
||||
<< peek.CsoTwinSamplerId << ", identity twin " << peek.IdentityTwinSamplerId << std::endl;
|
||||
EXPECT_TRUE(peek.UnitInsideWindow) << "bind_sampler_states did not describe unit 0";
|
||||
EXPECT_NE(peek.CsoHandleSlot, 0u) << "bind_sampler_states names no CSO for a unit that carries "
|
||||
"a sampler object";
|
||||
EXPECT_NE(peek.CsoTwinSamplerId, 0u)
|
||||
<< "Espryt holds no twin at the CSO handle bind_sampler_states named: the record arm's "
|
||||
"lookup went to the identity-keyed registry with a content-addressed handle and "
|
||||
"could never hit (F-4)";
|
||||
EXPECT_EQ(peek.BoundSamplerId, peek.CsoTwinSamplerId)
|
||||
<< "the driver sampler on unit 0 is not the CSO's twin, so it was put there by the "
|
||||
"pre-handle program pass and not by the record arm (F-4)";
|
||||
EXPECT_EQ(peek.IdentityTwinSamplerId, 0u)
|
||||
<< "an identity-keyed twin was minted for the sampler object on the handle arm: the "
|
||||
"pre-handle pass is still the one doing the binding";
|
||||
}
|
||||
|
||||
glBindSampler(0, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
DestroyColorFbo(target);
|
||||
glDeleteSamplers(1, &sampler);
|
||||
glDeleteTextures(1, &red);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,775 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.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 - A TEXTURE'S PARAMETERS TAKE EFFECT EVEN WHEN IT HAS NO SAMPLER VIEW (gate G9), the
|
||||
// one scenario ROADMAP.md:20 names by hand and requires to be RED before P4a lands.
|
||||
//
|
||||
// THE DESIGN STATEMENT IT TESTS, ARCHITECTURE.md:100 (D10), verbatim: "SetTextureParams 按资源寻址、
|
||||
// 与 sampler view 分开(D10):只作 FBO attachment / image 单元 / glCopyImageSubData 端点的纹理没有
|
||||
// sampler view,但 Espryt 对 attachment 也同步纹理参数,且 RequireImageBindableStorage 需要在前端参数
|
||||
// 版本不动时强制重同步." A texture that is only an attachment, only an image-unit binding or only a
|
||||
// copy endpoint has NO sampler view at all - so a design that carried texture parameters on the
|
||||
// view would silently drop them for exactly those textures. P4a addresses set_texture_params by
|
||||
// RESOURCE, independently of any binding, which is what makes the record exist for every texture
|
||||
// the moment its parameters move.
|
||||
//
|
||||
// THE FOUR CASES, and BRIEF-P4A.md D-E3 is where their expected verdicts come from:
|
||||
//
|
||||
// AnAttachmentOnlyTexturesSwizzleReachesTheDriver green today, green after
|
||||
// AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver D-E3 says RED today
|
||||
// AnImageUnitOnlyTexturesSwizzleSurvivesARequireImageBindableStorageRemint green today, green after
|
||||
// ACopyImageEndpointOnlyTexturesParamsReachTheDriver green today, green after
|
||||
//
|
||||
// [MEASURED, AND IT DOES NOT MATCH D-E3] All four are GREEN on the P4a contract commit (08192d72),
|
||||
// on llvmpipe, in the push build. The second one is green FOR A REASON THAT IS ITSELF THE FINDING,
|
||||
// and it is written here rather than in a review comment because the next person to try to make it
|
||||
// red needs to know why they cannot:
|
||||
//
|
||||
// A texture parameter's only public-GL observable is a SAMPLE - nothing about an attachment, an
|
||||
// image binding or a copy endpoint reads a swizzle or an aspect mode - and a sample puts the
|
||||
// texture on SyncNeccessaryTextures' UNIT list. That walk calls SyncTextureParamsToBackend for
|
||||
// every entry whose IsDrawSyncClean is false (DirectGLES.cpp:1896-1898), and IsDrawSyncClean is
|
||||
// false whenever the frontend's parameter version has moved since the last sync
|
||||
// (Managers.h:1399-1416, `m_syncedTextureParamsVersion != paramsVersion`). So the very act of
|
||||
// observing the parameter repairs the state the observation was meant to catch: the gap between
|
||||
// "the parameter moved on a read-attachment-only texture" and "the driver was told" is REAL, and
|
||||
// it is closed by the next sampler binding, which is also the only thing that can see it.
|
||||
//
|
||||
// What that means for the gate, stated plainly so nobody reads a green here as evidence of
|
||||
// anything it is not:
|
||||
//
|
||||
// * the four cases are a REGRESSION NET around D10, not the evidence for the change. They pin
|
||||
// the design statement: a texture's parameters take effect however the texture is reached, and
|
||||
// in particular they would go RED if any of the four reachability paths were ever made to
|
||||
// depend on the texture having a sampler VIEW - which is exactly the coupling P4a's
|
||||
// resource-addressed set_texture_params removes and the thing a later phase could reintroduce;
|
||||
// * the "落地前必须红" artefact ROADMAP.md:20 asks for is NOT produced by the public-GL half
|
||||
// of this file, and no public-GL integration scenario on a monolith tree can produce it.
|
||||
// Producing it needs an observation of the DRIVER's texture object taken while the texture is
|
||||
// still read-attachment-only. ID-19 rules that G9 is therefore a WHITE-BOX assertion, and this
|
||||
// file now carries the SCENARIO half of it (the unit half is package D's,
|
||||
// MG_Test/SanityTest.cpp's DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachThe
|
||||
// DriverWithNoSamplerView).
|
||||
//
|
||||
// THE WHITE-BOX HALF, and what it adds to the four cases below. Each case, at the point where its
|
||||
// texture is reachable ONLY its own way and BEFORE the observing sample, takes three readings
|
||||
// through MG_IntegrationTest/Harness/PipeApplyPeek.h and asserts all three:
|
||||
//
|
||||
// (a) the APPLIER holds a set_texture_params record for this texture, at a non-zero ParamsSerial,
|
||||
// carrying the field the case moved;
|
||||
// (b) ESPRYT's applied value for the same texture - read back from the DRIVER, through the twin's
|
||||
// own ES name - is that value ALREADY, not after the first sampler view;
|
||||
// (c) Espryt holds NO SAMPLER VIEW for this texture yet, which is what turns (b) from "applied"
|
||||
// into "applied WITHOUT one" and is the whole claim D10 makes.
|
||||
//
|
||||
// (c) is the assertion the public-GL half structurally cannot make: making it there would create
|
||||
// the view. (b) is the half that goes red on a backend that DEFERS - a tree where the parameter
|
||||
// push is gated on a sampler view existing is green on all four public-GL cases forever, because
|
||||
// the sample that observes the parameter is also what mints the view and repairs the state.
|
||||
//
|
||||
// WHAT THE THREE READINGS DO **NOT** COVER, so the next reader does not over-trust them
|
||||
// (esprytobj re-review N-9, carried here by request). D's unit probe drives
|
||||
// SyncTextureParamsToBackend directly, so the only deferral shape IT can see is one INSIDE that
|
||||
// function. These three run through the real per-frame paths and therefore also see a deferral
|
||||
// introduced ABOVE it - in SyncNeccessaryTextures, in the attachment walk, or in E's per-unit walk.
|
||||
// Between them the two halves cover both, and neither covers both alone.
|
||||
//
|
||||
// A READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES - it is not a
|
||||
// GTEST_SKIP, and that is a deliberate departure from the shape the review sketched. These four
|
||||
// cases are dual-purpose: they are also the END-TO-END regression net around D10, and that net is
|
||||
// the ONLY thing measuring D10 on exactly the arms where the peek cannot look (the pull build,
|
||||
// which has no applier at all; the 0x1ff and 0 lanes, where the texture family is switched off;
|
||||
// Magma, which has no Espryt twin). Skipping the case there would delete the one verdict those
|
||||
// lanes carry in order to report the absence of a second one. The decline is printed, recorded as
|
||||
// a test property and named, so a lane that silently stopped taking the reading is visible in the
|
||||
// log rather than in a count.
|
||||
//
|
||||
// WHY THE SECOND ONE IS THE RED, mechanically (scout-espryt-framebuffer.md 2.6, re-opened at the
|
||||
// base ref). Today a texture's parameters ride on the UNIT BINDING and on the DRAW attachment set:
|
||||
//
|
||||
// bound to a sampler unit <= the high-water mark SyncNeccessaryTextures' unit list -> synced
|
||||
// attachment of the DRAW framebuffer SyncNeccessaryTextures' FBO list -> synced
|
||||
// attachment of the READ framebuffer ONLY SyncCurrentFBO -> SyncToBackend ->
|
||||
// SyncAttachmentObject, which calls
|
||||
// SyncMipmapsToBackend at Managers.cpp:7161
|
||||
// and NOTHING ELSE -> NOT synced
|
||||
// bound to an image unit SyncImageTextureBinding ->
|
||||
// SyncTextureObjectToBackend, and
|
||||
// RequireImageBindableStorage additionally
|
||||
// forces m_forceTextureParamsResync -> synced
|
||||
// a glCopyImageSubData endpoint MakeGLESCopyImageEndpoint ->
|
||||
// SyncTextureObjectToBackend -> synced
|
||||
//
|
||||
// SyncNeccessaryTextures' attachment list reads GetFramebufferBindingSlotChecked(Draw) only
|
||||
// (DirectGLES.cpp:1944), so a texture that is exclusively a READ attachment gets its STORAGE synced
|
||||
// and its PARAMETERS never. P4a closes that gap deliberately (D-E3): the record is addressed by
|
||||
// resource, and Espryt's SyncAttachmentObject applies parameters for ANY attachment, draw or read.
|
||||
// It is a behaviour change and it is the deliverable, not a drive-by dev fix (ROADMAP.md:98).
|
||||
//
|
||||
// HOW EACH CASE OBSERVES "REACHED THE DRIVER", and why the observation is always a LATER SAMPLE.
|
||||
// A texture parameter is by definition a sampling parameter: nothing about an attachment, an image
|
||||
// binding or a copy endpoint reads a swizzle or a depth/stencil aspect mode, so the only thing that
|
||||
// can see one is a sample. Each case therefore does the same three things -
|
||||
//
|
||||
// 1. put the texture through ONE of the five reachability paths above, and only that one,
|
||||
// 2. move a parameter while it is reachable ONLY that way (through the DSA entry points
|
||||
// glTextureParameteri / glTextureSubImage2D, so no step of the setup ever binds the texture to
|
||||
// a sampler unit - a bind would put it on the unit list and answer the question by accident),
|
||||
// 3. sample it once, at the end, and read the colour back.
|
||||
//
|
||||
// - and the difference between them is step 1 alone. A case that is red says: the parameter set
|
||||
// while the texture was reachable only that way did not survive to the sample.
|
||||
//
|
||||
// DIRECTGLES ONLY. The gap is Espryt's - it is a statement about SyncAttachmentObject and
|
||||
// SyncNeccessaryTextures - and P4a does not touch MG_Backend/DirectVulkan (D-Q). Magma answers the
|
||||
// same GL question through an entirely different path, so a red or a green there would be evidence
|
||||
// about P7's work rather than about this gate; the cases SKIP on any other backend, naming that.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeApplyPeek.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 int kTextureSize = 4;
|
||||
|
||||
constexpr const char* kQuadVS = 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);
|
||||
}
|
||||
)";
|
||||
|
||||
// Samples texel (0,0) with an explicit fetch: no filtering, no derivatives, no wrap - so
|
||||
// the colour that comes back is the texel as the driver's swizzle presents it and nothing
|
||||
// else can move it.
|
||||
constexpr const char* kFetchFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = texelFetch(uTex, ivec2(0, 0), 0); }
|
||||
)";
|
||||
|
||||
// The stencil aspect of a packed depth/stencil texture is an UNSIGNED INTEGER texture, so
|
||||
// it needs a usampler2D. The case that uses it turns "the stencil value is what was
|
||||
// cleared" into a colour, because a colour is the only thing this harness can read back.
|
||||
constexpr const char* kStencilFetchFS = R"(#version 330 core
|
||||
uniform usampler2D uTex;
|
||||
uniform uint uExpected;
|
||||
out vec4 oColor;
|
||||
void main() {
|
||||
uint value = texelFetch(uTex, ivec2(0, 0), 0).r;
|
||||
oColor = (value == uExpected) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
};
|
||||
|
||||
class TextureParamsWithoutASamplerViewScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != "DirectGLES") {
|
||||
GTEST_SKIP() << "DirectGLES only: this scenario is about Espryt's own reachability "
|
||||
"table - SyncNeccessaryTextures' attachment list walks the DRAW "
|
||||
"slot only (DirectGLES.cpp:1944) and SyncAttachmentObject syncs "
|
||||
"storage and not parameters (Managers.cpp:7161). "
|
||||
<< Gl().BackendName()
|
||||
<< " answers the same GL question through a different path, so a "
|
||||
"verdict here would be evidence about that backend rather than "
|
||||
"about this gate (P4a touches no DirectVulkan source but "
|
||||
"MagmaPipeArms.h, D-Q).";
|
||||
}
|
||||
std::string error;
|
||||
m_fetchProgram = CompileProgram(kQuadVS, kFetchFS, &error);
|
||||
ASSERT_NE(m_fetchProgram, 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);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the scene setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||
if (m_fetchProgram != 0) glDeleteProgram(m_fetchProgram);
|
||||
}
|
||||
|
||||
// Direct State Access is how every setup step below touches a texture, and it is the
|
||||
// whole reason the cases can claim "this texture never had a sampler view": the classic
|
||||
// entry points (glTexImage2D, glTexParameteri) all require the texture to be BOUND to a
|
||||
// unit first, and a bind is exactly what puts it on SyncNeccessaryTextures' unit list.
|
||||
// A case that used them would sync the parameters through the path it is trying to
|
||||
// exclude and would be green for the wrong reason, on every tree, forever.
|
||||
bool DirectStateAccessIsAvailable() {
|
||||
GLuint probe = 0;
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &probe);
|
||||
const bool ok = FirstGLError() == GLenum(GL_NO_ERROR) && probe != 0;
|
||||
if (probe != 0) glDeleteTextures(1, &probe);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SkipWithoutDirectStateAccess() {
|
||||
if (!DirectStateAccessIsAvailable()) {
|
||||
GTEST_SKIP() << "glCreateTextures is not usable here, and every case in this file "
|
||||
"needs the DSA entry points: the classic ones bind the texture to "
|
||||
"a unit, which is the reachability path these cases exist to "
|
||||
"exclude. A case that fell back to them would be green for the "
|
||||
"wrong reason rather than measuring anything.";
|
||||
}
|
||||
}
|
||||
|
||||
// A 4x4 RGBA8 texture, one solid colour, created and filled WITHOUT EVER BINDING IT.
|
||||
GLuint MakeSolidTextureWithoutBinding(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
std::vector<std::uint8_t> texels(kTextureSize * kTextureSize * 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;
|
||||
}
|
||||
GLuint texture = 0;
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
glTextureStorage2D(texture, 1, GL_RGBA8, kTextureSize, kTextureSize);
|
||||
glTextureSubImage2D(texture, 0, 0, 0, kTextureSize, kTextureSize, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// THE OBSERVATION, shared by every case: bind the texture to a unit for the first time
|
||||
// in its life and read one texel back through the default framebuffer.
|
||||
Image SampleAndRead(GLuint program, GLuint texture) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||
Gl().EndFrame();
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return image;
|
||||
}
|
||||
|
||||
::testing::AssertionResult WholeViewportIs(const Image& image, const char* expected,
|
||||
const std::string& when) {
|
||||
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||
image.Height() - kInset, expected, 0.0, when);
|
||||
}
|
||||
|
||||
// R -> ZERO and G -> ONE, so a RED texel samples as GREEN if and only if the swizzle
|
||||
// reached the driver, and as RED if it did not. Two colours the harness can name, from
|
||||
// one parameter change, with no third outcome that could be mistaken for either.
|
||||
void SwizzleRedIntoGreen(GLuint texture) {
|
||||
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_R, GL_ZERO);
|
||||
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_G, GL_ONE);
|
||||
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_B, GL_ZERO);
|
||||
}
|
||||
|
||||
// Which of the two parameters a case moved, and therefore which one the white-box
|
||||
// reading has to find on both sides of the seam. Two, because they are the two the
|
||||
// four cases use and because a peek that reported "some parameter" would be green for
|
||||
// a backend that applied the wrong one.
|
||||
enum class MovedParameter { Swizzle, DepthStencilMode };
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// G9's WHITE-BOX READING (ID-19). Called by every case at the point where its
|
||||
// texture is reachable only its own way and BEFORE the observing sample - which is
|
||||
// the whole of the design, because the sample repairs what it observes.
|
||||
//
|
||||
// `expectedSwizzle` is the four GL enums the case set (or left at their defaults);
|
||||
// `expectedDepthStencilMode` is GL_DEPTH_COMPONENT or GL_STENCIL_INDEX. Both are
|
||||
// always passed and `moved` says which one is the case's subject, so a reader of a
|
||||
// failure can see the untouched half beside the moved one.
|
||||
void TakeTheWhiteBoxReadingBeforeAnySample(GLuint texture, GLenum target,
|
||||
MovedParameter moved,
|
||||
const GLint expectedSwizzle[4],
|
||||
GLint expectedDepthStencilMode,
|
||||
const char* whatMadeItReachable) {
|
||||
const char* const movedName =
|
||||
moved == MovedParameter::Swizzle ? "GL_TEXTURE_SWIZZLE_*"
|
||||
: "GL_DEPTH_STENCIL_TEXTURE_MODE";
|
||||
|
||||
PipeTextureParamsRecordPeek record{};
|
||||
if (!PeekPipeTextureParamsRecord(static_cast<unsigned>(texture), &record)) {
|
||||
DeclineTheWhiteBoxReading(
|
||||
"no set_texture_params record for this texture in the applier. Either "
|
||||
"there is no applier here (a PULL build: MGPipeApplierState is "
|
||||
"#if MOBILEGL_PIPE_PUSH), or this lane's MOBILEGL_PIPE_PUSH leaves "
|
||||
"kMGPipeSubsystemTextureResources (bit 10) clear, or no backend "
|
||||
"registered MGPipeResourceOps so the client never emitted (c0f). The "
|
||||
"end-to-end half of this case below is unaffected and still decides it.");
|
||||
return;
|
||||
}
|
||||
|
||||
// From here the reading WAS taken, so everything is a hard assertion: a record
|
||||
// that exists and does not carry the parameter is exactly the finding.
|
||||
EXPECT_NE(record.ParamsSerial, 0u)
|
||||
<< "the applier holds a resource record for texture " << texture
|
||||
<< " at handle {" << record.Slot << ", " << record.Gen
|
||||
<< "} but its ParamsSerial is 0, i.e. NO set_texture_params has ever been "
|
||||
"applied to it - and this case moved " << movedName << " while the texture "
|
||||
"was " << whatMadeItReachable
|
||||
<< ". A parameter change on a texture with no sampler view has to produce a "
|
||||
"record addressed BY RESOURCE (D10, D-E1); a zero here means the client "
|
||||
"never emitted one, which is the coupling P4a exists to remove reappearing "
|
||||
"on the emitter's side of the seam.";
|
||||
|
||||
if (moved == MovedParameter::Swizzle) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(record.Swizzle[channel], static_cast<int>(expectedSwizzle[channel]))
|
||||
<< "the applier's set_texture_params record for texture " << texture
|
||||
<< " carries the wrong swizzle in channel " << channel
|
||||
<< " (record 0x" << std::hex << record.Swizzle[channel] << ", expected 0x"
|
||||
<< expectedSwizzle[channel] << std::dec
|
||||
<< "). The record is what Espryt reads, so a wrong value here is a "
|
||||
"wrong value everywhere downstream of it.";
|
||||
}
|
||||
} else {
|
||||
EXPECT_EQ(record.DepthStencilMode, static_cast<int>(expectedDepthStencilMode))
|
||||
<< "the applier's set_texture_params record for texture " << texture
|
||||
<< " carries GL_DEPTH_STENCIL_TEXTURE_MODE 0x" << std::hex
|
||||
<< record.DepthStencilMode << ", expected 0x" << expectedDepthStencilMode
|
||||
<< std::dec << ".";
|
||||
}
|
||||
|
||||
// (c) - and it is checked BEFORE (b) is read, because (b) reads the driver and a
|
||||
// reader of a failure needs to know the view question was answered on the state
|
||||
// this case built rather than on anything the peek did.
|
||||
bool hasSamplerView = true;
|
||||
if (!PeekEsprytHasSamplerViewForTexture(static_cast<unsigned>(texture),
|
||||
&hasSamplerView)) {
|
||||
DeclineTheWhiteBoxReading(
|
||||
"Espryt holds no twin for this texture, so neither the sampler-view "
|
||||
"question nor the applied-value one can be asked here. On a backend other "
|
||||
"than DirectGLES that is the designed state (P4a touches no DirectVulkan "
|
||||
"source but MagmaPipeArms.h, D-Q).");
|
||||
return;
|
||||
}
|
||||
EXPECT_FALSE(hasSamplerView)
|
||||
<< "Espryt already holds a SAMPLER VIEW for texture " << texture
|
||||
<< ", which was " << whatMadeItReachable
|
||||
<< " and has never been bound to a sampler unit in this case. The whole claim "
|
||||
"of D10 is that a texture reached this way has no view, so if one exists "
|
||||
"the reading below cannot separate 'applied by resource' from 'applied "
|
||||
"through the view' and this case has stopped measuring G9.";
|
||||
|
||||
EsprytAppliedTextureParamsPeek applied{};
|
||||
if (!PeekEsprytAppliedTextureParams(static_cast<unsigned>(texture),
|
||||
static_cast<unsigned>(target), &applied)) {
|
||||
DeclineTheWhiteBoxReading(
|
||||
"Espryt's applied value could not be read back from the driver (no twin, "
|
||||
"no ES name yet, or a target this peek has no binding query for).");
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "[ TextureParamsWithoutASamplerView ] white-box: texture " << texture
|
||||
<< " -> applier handle {" << record.Slot << ", " << record.Gen
|
||||
<< "} paramsSerial " << record.ParamsSerial << ", Espryt ES name "
|
||||
<< applied.BackendTextureId << ", sampler view: none, " << movedName
|
||||
<< " applied before any sample" << std::endl;
|
||||
|
||||
if (moved == MovedParameter::Swizzle) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(applied.Swizzle[channel], static_cast<int>(expectedSwizzle[channel]))
|
||||
<< "ESPRYT HAS NOT APPLIED THE SWIZZLE YET. Channel " << channel
|
||||
<< " of the driver texture (ES name " << applied.BackendTextureId
|
||||
<< ") reads 0x" << std::hex << applied.Swizzle[channel] << ", the "
|
||||
<< "application set 0x" << expectedSwizzle[channel] << std::dec
|
||||
<< ", and the applier's record already carries the right value - so "
|
||||
"the record reached the server and the server has not pushed it. "
|
||||
"The texture was " << whatMadeItReachable
|
||||
<< " and has NO sampler view (asserted above), which makes this "
|
||||
"exactly the deferred-to-first-view shape G9 exists to catch: the "
|
||||
"sample at the end of this case would repair it, and the "
|
||||
"end-to-end assertion below would then pass on a driver that was "
|
||||
"told late. That is the half no public-GL case can see.";
|
||||
}
|
||||
} else {
|
||||
if (!applied.DepthStencilModeIsReadable) {
|
||||
DeclineTheWhiteBoxReading(
|
||||
"this driver would not answer glGetTexParameteriv("
|
||||
"GL_DEPTH_STENCIL_TEXTURE_MODE), so the applied aspect mode cannot be "
|
||||
"read back. The record half above was still asserted.");
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(applied.DepthStencilMode, static_cast<int>(expectedDepthStencilMode))
|
||||
<< "ESPRYT HAS NOT APPLIED THE DEPTH/STENCIL ASPECT MODE YET. The driver "
|
||||
"texture (ES name " << applied.BackendTextureId << ") reads 0x"
|
||||
<< std::hex << applied.DepthStencilMode << ", the application set 0x"
|
||||
<< expectedDepthStencilMode << std::dec
|
||||
<< ", and the applier's record already carries the right value. The "
|
||||
"texture was " << whatMadeItReachable
|
||||
<< " and has no sampler view, so this is D-E3's gap measured directly "
|
||||
"rather than through a sample that would repair it: a driver left at "
|
||||
"GL_DEPTH_COMPONENT samples the DEPTH bits where the application asked "
|
||||
"for stencil.";
|
||||
}
|
||||
}
|
||||
|
||||
// Printed, recorded and named, never silent - a lane that stopped taking the reading
|
||||
// must be visible in the log. See this file's header for why it is not a GTEST_SKIP.
|
||||
void DeclineTheWhiteBoxReading(const std::string& why) {
|
||||
std::cout << "[ TextureParamsWithoutASamplerView ] white-box reading DECLINED: "
|
||||
<< why << std::endl;
|
||||
RecordProperty("g9_white_box", "declined");
|
||||
RecordProperty("g9_white_box_reason", why.c_str());
|
||||
}
|
||||
|
||||
GLuint m_fetchProgram = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_quadBuffer = 0;
|
||||
};
|
||||
|
||||
// The swizzle SwizzleRedIntoGreen leaves behind, as GL enums: R -> ZERO, G -> ONE,
|
||||
// B -> ZERO and A untouched at its GL default. Written once here because both the
|
||||
// applier record and the driver read-back are compared against it.
|
||||
constexpr GLint kRedIntoGreenSwizzle[4] = {GL_ZERO, GL_ONE, GL_ZERO, GL_ALPHA};
|
||||
// A texture whose aspect mode was never touched, i.e. the GL initial value - which is
|
||||
// also what a zeroed MGPTextureParams::DepthStencilMode decodes to (MGPipeTypes.h).
|
||||
constexpr GLint kUntouchedDepthStencilMode = GL_DEPTH_COMPONENT;
|
||||
// ...and the identity swizzle, for the case whose subject is the aspect mode: the moved
|
||||
// half is asserted, and the untouched half is carried so a failure prints both.
|
||||
constexpr GLint kUntouchedSwizzle[4] = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA};
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// 1. DRAW ATTACHMENT ONLY. Green today (SyncNeccessaryTextures' FBO list walks the draw
|
||||
// slot and calls SyncTextureObjectToBackend, which syncs parameters) and green after.
|
||||
// It is the regression net for the half of D-E3 that already works: P4a moves the
|
||||
// parameter sync from the sync list onto the record, and this case is what says the
|
||||
// move did not lose the case that used to be covered.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureParamsWithoutASamplerViewScenario, AnAttachmentOnlyTexturesSwizzleReachesTheDriver) {
|
||||
if (!Ready()) return;
|
||||
SkipWithoutDirectStateAccess();
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeSolidTextureWithoutBinding(255, 0, 0);
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
// The parameter moves while the texture is reachable ONLY as a draw attachment...
|
||||
SwizzleRedIntoGreen(texture);
|
||||
// ...and a frame runs with it bound that way, so whatever the draw path syncs, syncs.
|
||||
glViewport(0, 0, kTextureSize, kTextureSize);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
BindDefaultFramebuffer();
|
||||
Gl().EndFrame();
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the draw-attachment frame left a GL error";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
|
||||
// G9's white-box reading, taken here: the texture has been a draw attachment and
|
||||
// nothing else, and the sample below has not happened yet.
|
||||
TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, MovedParameter::Swizzle,
|
||||
kRedIntoGreenSwizzle, kUntouchedDepthStencilMode,
|
||||
"an attachment of the DRAW framebuffer and "
|
||||
"nothing else");
|
||||
|
||||
const Image image = SampleAndRead(m_fetchProgram, texture);
|
||||
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||
"a texture that was only ever a DRAW attachment, sampled "
|
||||
"after its swizzle moved"))
|
||||
<< "the swizzle set while this texture was reachable only as a draw-framebuffer "
|
||||
"attachment did not reach the driver: a red texel with R->ZERO, G->ONE must "
|
||||
"sample as green.";
|
||||
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// 2. READ ATTACHMENT ONLY - THE MANDATORY RED (ROADMAP.md:20, D-E3, G9).
|
||||
//
|
||||
// The parameter is GL_DEPTH_STENCIL_TEXTURE_MODE and the texture is a packed
|
||||
// depth/stencil one, because that is the parameter whose absence is not a mis-filtered
|
||||
// picture but the WRONG ASPECT: a driver left at the GL default samples the depth bits
|
||||
// where the application asked for stencil, and the value that comes back is not the
|
||||
// stencil that was written. RecreateBackendTexture's own comment says exactly this -
|
||||
// "the mode makes it visible because falling back to the default silently samples the
|
||||
// wrong aspect rather than merely mis-filtering" (Managers.cpp:4826-4833).
|
||||
//
|
||||
// The framebuffer is bound to GL_READ_FRAMEBUFFER and the DRAW target is left on the
|
||||
// default framebuffer for the whole window, which is the ONE thing that separates this
|
||||
// case from the one above.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureParamsWithoutASamplerViewScenario,
|
||||
AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver) {
|
||||
if (!Ready()) return;
|
||||
SkipWithoutDirectStateAccess();
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint stencilProgram = CompileProgram(kQuadVS, kStencilFetchFS, &error);
|
||||
ASSERT_NE(stencilProgram, 0u) << error;
|
||||
|
||||
GLuint texture = 0;
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
glTextureStorage2D(texture, 1, GL_DEPTH24_STENCIL8, kTextureSize, kTextureSize);
|
||||
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
if (FirstGLError() != GLenum(GL_NO_ERROR)) {
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(stencilProgram);
|
||||
GTEST_SKIP() << "this driver would not create an immutable DEPTH24_STENCIL8 texture, "
|
||||
"so there is no packed depth/stencil aspect here to sample and the "
|
||||
"case cannot answer";
|
||||
}
|
||||
|
||||
// Write a stencil value through the texture AS A DRAW ATTACHMENT once, so that there is
|
||||
// something in the stencil aspect to read. This is setup, not the window: the window
|
||||
// below never makes it a draw attachment again.
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) {
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(stencilProgram);
|
||||
GTEST_SKIP() << "a depth-stencil-only framebuffer is incomplete on this driver, so "
|
||||
"the stencil aspect cannot be written and the case cannot answer";
|
||||
}
|
||||
constexpr GLint kStencil = 42;
|
||||
glViewport(0, 0, kTextureSize, kTextureSize);
|
||||
glStencilMask(0xFFu);
|
||||
glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.5f, kStencil);
|
||||
BindDefaultFramebuffer();
|
||||
Gl().EndFrame();
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "clearing the stencil aspect left a GL error";
|
||||
|
||||
// ---- the window: the texture is reachable ONLY as a READ attachment ----
|
||||
//
|
||||
// The DRAW binding is the default framebuffer throughout, so SyncNeccessaryTextures'
|
||||
// attachment list - which walks the DRAW slot only - never sees this texture, and
|
||||
// SyncCurrentFBO's read path reaches it through SyncAttachmentObject, which syncs
|
||||
// storage and not parameters. That is the gap.
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glTextureParameteri(texture, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
// A frame with the read binding live, and it has to contain a REAL DRAW: SyncCurrentFBO
|
||||
// and the whole sync-list walk run at the validate point, so a frame that only cleared
|
||||
// and swapped would never reach the read-side path this case is about.
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(m_fetchProgram);
|
||||
glUniform1i(glGetUniformLocation(m_fetchProgram, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
Gl().EndFrame();
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the read-attachment frame left a GL error";
|
||||
|
||||
// G9's white-box reading, and this is the case it matters most for: the aspect
|
||||
// mode was set while the texture was reachable ONLY as a read attachment, and the
|
||||
// observation below is a sample that would repair an unsynced parameter on its way
|
||||
// to reporting it.
|
||||
TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D,
|
||||
MovedParameter::DepthStencilMode,
|
||||
kUntouchedSwizzle, GL_STENCIL_INDEX,
|
||||
"an attachment of the READ framebuffer and "
|
||||
"nothing else");
|
||||
|
||||
// ---- the observation ----
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(stencilProgram);
|
||||
glUniform1i(glGetUniformLocation(stencilProgram, "uTex"), 0);
|
||||
glUniform1ui(glGetUniformLocation(stencilProgram, "uExpected"),
|
||||
static_cast<GLuint>(kStencil));
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||
Gl().EndFrame();
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
const GLenum sampleError = FirstGLError();
|
||||
std::cout << "[ TextureParamsWithoutASamplerView ] read-attachment-only "
|
||||
"GL_DEPTH_STENCIL_TEXTURE_MODE=GL_STENCIL_INDEX, expecting stencil "
|
||||
<< kStencil << "; sample GL error 0x" << std::hex << sampleError << std::dec
|
||||
<< std::endl;
|
||||
|
||||
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||
"a texture that was only ever a READ attachment, sampled "
|
||||
"through its stencil aspect"))
|
||||
<< "GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX was set while this texture was "
|
||||
"reachable ONLY as an attachment of the READ framebuffer, and the sample did not "
|
||||
"come back as the stencil value that was cleared into it. The parameter did not "
|
||||
"reach the driver, and - because the sample itself would have repaired an "
|
||||
"unsynced parameter (see this file's header: the unit sync list calls "
|
||||
"SyncTextureParamsToBackend whenever the params version moved) - a red here means "
|
||||
"something stronger than the D-E3 gap: a reachability path that does not sync "
|
||||
"parameters AT ALL, i.e. the sampler-view coupling ARCHITECTURE.md:100 (D10) "
|
||||
"exists to remove. Read the case's own stdout line for the GL error the sample "
|
||||
"raised before assuming an aspect-mode bug.";
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(stencilProgram);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// 3. IMAGE UNIT ONLY, across a storage RE-MINT. Green today and green after, and the
|
||||
// reason it is green is the thing P4a must not lose: glBindImageTexture drives
|
||||
// RequireImageBindableStorage, which re-mints the storage in a possibly WIDENED carrier
|
||||
// and sets m_forceTextureParamsResync (Managers.cpp:4787) precisely because the
|
||||
// frontend's parameter version does not move across that transition. A parameter sync
|
||||
// gated only on the frontend version would leave the driver at its defaults and sample
|
||||
// the carrier's surplus channels raw.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureParamsWithoutASamplerViewScenario,
|
||||
AnImageUnitOnlyTexturesSwizzleSurvivesARequireImageBindableStorageRemint) {
|
||||
if (!Ready()) return;
|
||||
SkipWithoutDirectStateAccess();
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint texture = MakeSolidTextureWithoutBinding(255, 0, 0);
|
||||
|
||||
// The parameter moves while the texture is reachable only as an image-unit binding...
|
||||
SwizzleRedIntoGreen(texture);
|
||||
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
const GLenum bindError = FirstGLError();
|
||||
if (bindError != GLenum(GL_NO_ERROR)) {
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
GTEST_SKIP() << "glBindImageTexture is not usable here (GL error 0x" << std::hex
|
||||
<< bindError << std::dec
|
||||
<< "), so the RequireImageBindableStorage transition this case is about "
|
||||
"cannot be reached";
|
||||
}
|
||||
// ...and a frame runs with the image binding live, which is what drives the re-mint.
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
Gl().EndFrame();
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
|
||||
// G9's white-box reading. RGBA8 is a core image format, so no widening carrier is
|
||||
// minted and the driver's swizzle is the application's own - the composition
|
||||
// RecreateBackendTexture applies for a NON-core format would show up here as a
|
||||
// legitimate difference and this case deliberately does not use one.
|
||||
TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, MovedParameter::Swizzle,
|
||||
kRedIntoGreenSwizzle, kUntouchedDepthStencilMode,
|
||||
"an image-unit binding and nothing else, across "
|
||||
"a RequireImageBindableStorage re-mint");
|
||||
|
||||
const Image image = SampleAndRead(m_fetchProgram, texture);
|
||||
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||
"a texture that was only ever an image-unit binding, sampled "
|
||||
"after its swizzle moved and its storage was re-minted"))
|
||||
<< "the swizzle did not survive the RequireImageBindableStorage re-mint. The re-mint "
|
||||
"creates a new driver texture at the ES defaults without moving the frontend's "
|
||||
"parameter version, so the forced resync (Managers.cpp:4787) is the only thing "
|
||||
"that puts the application's parameters back.";
|
||||
|
||||
GLuint cleanup = texture;
|
||||
glDeleteTextures(1, &cleanup);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// 4. glCopyImageSubData ENDPOINT ONLY. Green today (MakeGLESCopyImageEndpoint calls
|
||||
// SyncTextureObjectToBackend, DirectGLES.cpp:7588) and green after. The endpoint is the
|
||||
// DESTINATION, so the case also proves the copy itself still lands: a swizzled read of
|
||||
// the copied texel is only meaningful if the texel arrived.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureParamsWithoutASamplerViewScenario,
|
||||
ACopyImageEndpointOnlyTexturesParamsReachTheDriver) {
|
||||
if (!Ready()) return;
|
||||
SkipWithoutDirectStateAccess();
|
||||
if (IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeSolidTextureWithoutBinding(255, 0, 0);
|
||||
const GLuint destination = MakeSolidTextureWithoutBinding(0, 0, 255);
|
||||
|
||||
// The parameter moves while the destination is reachable only as a copy endpoint...
|
||||
SwizzleRedIntoGreen(destination);
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, destination, GL_TEXTURE_2D, 0, 0, 0,
|
||||
0, kTextureSize, kTextureSize, 1);
|
||||
const GLenum copyError = FirstGLError();
|
||||
if (copyError != GLenum(GL_NO_ERROR)) {
|
||||
GLuint cleanup[2] = {source, destination};
|
||||
glDeleteTextures(2, cleanup);
|
||||
GTEST_SKIP() << "glCopyImageSubData is not usable here (GL error 0x" << std::hex
|
||||
<< copyError << std::dec << "), so there is no copy endpoint to be";
|
||||
}
|
||||
Gl().EndFrame();
|
||||
|
||||
// G9's white-box reading, on the DESTINATION - the endpoint whose parameters moved.
|
||||
TakeTheWhiteBoxReadingBeforeAnySample(destination, GL_TEXTURE_2D,
|
||||
MovedParameter::Swizzle, kRedIntoGreenSwizzle,
|
||||
kUntouchedDepthStencilMode,
|
||||
"a glCopyImageSubData destination and nothing "
|
||||
"else");
|
||||
|
||||
// ...and the destination now holds the source's RED texel, which the swizzle must turn
|
||||
// into GREEN when it is finally sampled.
|
||||
const Image image = SampleAndRead(m_fetchProgram, destination);
|
||||
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||
"a texture that was only ever a glCopyImageSubData endpoint, "
|
||||
"sampled after its swizzle moved"))
|
||||
<< "the three readings are distinct and each names its own cause: GREEN is the pass "
|
||||
"(the swizzle reached the driver); RED means the copy landed and the swizzle did "
|
||||
"not; BLUE means neither happened, i.e. the destination is still its own original "
|
||||
"texel and glCopyImageSubData wrote nothing. The swizzle is what turns any texel "
|
||||
"into (0,1,0), so the colour separates the two failures rather than merging them.";
|
||||
|
||||
GLuint cleanup[2] = {source, destination};
|
||||
glDeleteTextures(2, cleanup);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,396 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.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 TEXTURE UPLOAD SHAPE, RECORDED (BRIEF-P4A.md D-D4). NOT A GATE IN P4a.
|
||||
//
|
||||
// WHAT IT IS FOR. SSIM is completely blind to the difference between "one union box" and "N
|
||||
// separate rects", and that difference is the Mali upload cliff: Mali prices an upload by the
|
||||
// number of JOBS, and ~100 one-rect sprite jobs against one union box measured +6 ms/frame
|
||||
// (ARCHITECTURE.md:249-251). Every P4a gate can be green while the emission shape has silently
|
||||
// inverted, so the shape needs a number - and there are TWO numbers, deliberately:
|
||||
//
|
||||
// tex[emit= box= rect= jobs=] the SERVER's count, Espryt's own, which has existed since P2
|
||||
// emit[ctu=] the CLIENT's count of the same records (CallClass::
|
||||
// ClientTextureUploadEmissions, minted by the P4a contract commit)
|
||||
//
|
||||
// The two agreeing is the whole reason both are printed (D-L). An emission-shape divergence between
|
||||
// the client that decides the rect model and the server that pays the GPU cost is then a difference
|
||||
// of two published numbers rather than something only a device can see.
|
||||
//
|
||||
// WHY IT IS RECORDED AND NOT GATED, and this is a scope decision rather than a hedge. ROADMAP.md:23
|
||||
// puts "dirty 归属反转(按存储属主键控的发射游标)" in the P3b/P4b cell: P4a lands the flat drain
|
||||
// list, and the per-storage-owner emission cursor with view/owner index remapping - which is what
|
||||
// actually decides the shape for a texture uploaded through a VIEW - is the next phase's. Gating a
|
||||
// shape the phase has not finished deciding would either pin today's shape as if it were the
|
||||
// answer, or fail on a change that is the point of the next phase. So P4a BUILDS the scenario (it
|
||||
// is meaningless without P4a's records) and runs it as a RECORDED comparison; P3b/P4b turns it into
|
||||
// a gate with the Mali frame-time delta published beside it (D-D4).
|
||||
//
|
||||
// WHAT IT THEREFORE ASSERTS, and it is not nothing:
|
||||
//
|
||||
// 1. the numbers could be READ AT ALL - the counters exist, the window covers the workload, and
|
||||
// the workload really uploaded (a run that uploaded nothing would record four zeroes and look
|
||||
// exactly like a healthy run whose emitter had been switched off);
|
||||
// 2. the internal ARITHMETIC of the server's own bracket holds: emit == box + rect, and
|
||||
// jobs >= emit, because a box emission is one job and a rect-list emission is N;
|
||||
// 3. the client and the server agree on the RECORD COUNT when both are non-zero (ctu == emit).
|
||||
// Not "when the client is non-zero": on the P4a contract tree the client emits nothing and
|
||||
// that is a SKIP-shaped observation, not a divergence.
|
||||
//
|
||||
// Everything else - which shape each texture took, and whether that is the right shape - is
|
||||
// RECORDED with RecordProperty and printed, for MEASUREMENTS.md and for the P3b/P4b gate to be
|
||||
// written against.
|
||||
//
|
||||
// THE WORKLOAD is the shape the decision is about: one texture receiving MANY SMALL SCATTERED
|
||||
// SUB-REGIONS per frame (the sprite-atlas / chunk-renderer shape), and one receiving a single large
|
||||
// contiguous one. The first is where the box-versus-rect choice is made - MipmapStorage's 96-rect
|
||||
// cascade and the summedArea*4 >= unionArea*3 union-box fallback - and the second is the control
|
||||
// that must always be one box whatever the policy is.
|
||||
//
|
||||
// DIRECTGLES ONLY. The server-side counters are Espryt's (Managers.cpp:6360-6395); Magma's upload
|
||||
// path is P7 and contributes nothing to them, so a DirectVulkan lane would record a bracket of
|
||||
// zeroes and call it a shape.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeStatsWindow.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 {
|
||||
|
||||
// Set by the TextureUploadShape. ctest entry and by nothing else; a harness marker, never
|
||||
// read by the library.
|
||||
constexpr const char* kLaneMarker = "MGITEST_TEXTURE_UPLOAD_SHAPE_LANE";
|
||||
|
||||
constexpr int kInset = 2;
|
||||
constexpr int kAtlasSize = 64;
|
||||
// Enough scattered rects that the box-versus-rect policy has a real decision to make: the
|
||||
// rect cascade caps at MipmapStorage::kMaxDirtyRects = 96, and the union-box fallback fires
|
||||
// on summedArea*4 >= unionArea*3, so a handful of rects would take neither branch
|
||||
// interestingly.
|
||||
constexpr int kScatteredRects = 40;
|
||||
constexpr int kRectSize = 2;
|
||||
constexpr int kFrames = 3;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
bool BuildMarkerIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||
}
|
||||
|
||||
class TextureUploadShapeScenario : 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);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
if (m_scattered != 0) glDeleteTextures(1, &m_scattered);
|
||||
if (m_contiguous != 0) glDeleteTextures(1, &m_contiguous);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
void SkipUnlessTheLaneIsAssertableHere() {
|
||||
if (std::getenv(kLaneMarker) == nullptr) {
|
||||
GTEST_SKIP() << "runs only in its own lane: the TextureUploadShape. ctest entry "
|
||||
"sets " << kLaneMarker
|
||||
<< " together with MOBILEGL_PIPE_STATS=1, "
|
||||
"MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
|
||||
"MOBILEGL_LOG_FILE_PATH. None of that is configured in the "
|
||||
"ambient entries, and the ambient log is shared, so a read here "
|
||||
"would race.";
|
||||
return;
|
||||
}
|
||||
if (Gl().BackendName() != "DirectGLES") {
|
||||
GTEST_SKIP() << "DirectGLES only: the upload-shape counters are Espryt's "
|
||||
"(Managers.cpp:6360-6395) and " << Gl().BackendName()
|
||||
<< " contributes nothing to them, so this lane would record a "
|
||||
"bracket of zeroes and call it a shape.";
|
||||
return;
|
||||
}
|
||||
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: the client's "
|
||||
"half of the comparison (CallClass::ClientTextureUploadEmissions, "
|
||||
"the ctu= field) does not exist there, and a one-sided reading is "
|
||||
"not the comparison this scenario is for. The entry is registered "
|
||||
"in every build so that `ctest -L integration-gpu` names the same "
|
||||
"tests in the pull build and the push build (gate G2).";
|
||||
return;
|
||||
}
|
||||
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||
"summary line is the only channel this module has for reading "
|
||||
"PipeStats";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GLuint MakeAtlas(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kAtlasSize) * kAtlasSize * 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;
|
||||
}
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kAtlasSize, kAtlasSize, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_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, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
Image DrawSampled(GLuint texture) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glUseProgram(m_program);
|
||||
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
return ReadPixels(Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_quadBuffer = 0;
|
||||
GLuint m_scattered = 0;
|
||||
GLuint m_contiguous = 0;
|
||||
};
|
||||
|
||||
TEST_F(TextureUploadShapeScenario, TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessTheLaneIsAssertableHere();
|
||||
if (IsSkipped()) return;
|
||||
|
||||
m_scattered = MakeAtlas(0, 255, 0);
|
||||
m_contiguous = MakeAtlas(0, 255, 0);
|
||||
// The first draw of each texture uploads its whole level, which is not the shape this
|
||||
// scenario is about; it happens in the SETUP window, before the one that is read.
|
||||
DrawSampled(m_scattered);
|
||||
DrawSampled(m_contiguous);
|
||||
BindDefaultFramebuffer();
|
||||
Gl().EndFrame();
|
||||
|
||||
// ---- the counted window ----
|
||||
Image lastScattered;
|
||||
Image lastContiguous;
|
||||
for (int frame = 0; frame < kFrames; ++frame) {
|
||||
// MANY SMALL SCATTERED RECTS: the shape whose box-versus-rect decision is the whole
|
||||
// subject. They are spread over the atlas on a coarse stride so that their union
|
||||
// box is most of the texture and their summed area is a small fraction of it -
|
||||
// which is the input the summedArea*4 >= unionArea*3 fallback is written for.
|
||||
glBindTexture(GL_TEXTURE_2D, m_scattered);
|
||||
const std::uint8_t patch[kRectSize * kRectSize * 4] = {
|
||||
0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255};
|
||||
for (int rect = 0; rect < kScatteredRects; ++rect) {
|
||||
const int x = ((rect * 7) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||
const int y = ((rect * 5) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, kRectSize, kRectSize, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, patch);
|
||||
}
|
||||
lastScattered = DrawSampled(m_scattered);
|
||||
|
||||
// ONE LARGE CONTIGUOUS REGION: the control. Whatever the policy is, this is one
|
||||
// box and one job, and a reading where it is not says the policy has stopped
|
||||
// looking at the region at all.
|
||||
glBindTexture(GL_TEXTURE_2D, m_contiguous);
|
||||
std::vector<std::uint8_t> band(static_cast<std::size_t>(kAtlasSize) * 8 * 4);
|
||||
for (std::size_t i = 0; i < band.size(); i += 4) {
|
||||
band[i] = 0;
|
||||
band[i + 1] = 255;
|
||||
band[i + 2] = 0;
|
||||
band[i + 3] = 255;
|
||||
}
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kAtlasSize, 8, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
band.data());
|
||||
lastContiguous = DrawSampled(m_contiguous);
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the upload workload left a GL error behind";
|
||||
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||
|
||||
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||
ASSERT_TRUE(window.found)
|
||||
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||
<< ", so the shape could not be read at all";
|
||||
RecordProperty("stats_line", window.line.c_str());
|
||||
|
||||
const long long emissions = PipeStatsWindow::CounterOrAbsent(window, "emit");
|
||||
const long long box = PipeStatsWindow::CounterOrAbsent(window, "box");
|
||||
const long long rect = PipeStatsWindow::CounterOrAbsent(window, "rect");
|
||||
const long long jobs = PipeStatsWindow::CounterOrAbsent(window, "jobs");
|
||||
const long long clientEmissions = PipeStatsWindow::CounterOrAbsent(window, "ctu");
|
||||
ASSERT_GE(emissions, 0) << "the summary line carries no tex[emit=]: " << window.line;
|
||||
ASSERT_GE(box, 0) << "no box=: " << window.line;
|
||||
ASSERT_GE(rect, 0) << "no rect=: " << window.line;
|
||||
ASSERT_GE(jobs, 0) << "no jobs=: " << window.line;
|
||||
|
||||
// THE RECORD. This is the deliverable of this scenario in P4a: a number, in the ctest
|
||||
// XML and in the log, for MEASUREMENTS.md and for the P3b/P4b gate to be written
|
||||
// against. Printed as well as recorded, because a RecordProperty is invisible in a
|
||||
// console run.
|
||||
std::cout << "[ TextureUploadShape ] backend=" << Gl().BackendName() << " frames=" << kFrames
|
||||
<< " scattered_rects_per_frame=" << kScatteredRects
|
||||
<< " server[emit=" << emissions << " box=" << box << " rect=" << rect
|
||||
<< " jobs=" << jobs << "] client[ctu=" << clientEmissions << "]" << std::endl;
|
||||
RecordProperty("server_emissions", static_cast<int>(emissions));
|
||||
RecordProperty("server_box_emissions", static_cast<int>(box));
|
||||
RecordProperty("server_rect_emissions", static_cast<int>(rect));
|
||||
RecordProperty("server_upload_jobs", static_cast<int>(jobs));
|
||||
RecordProperty("client_emissions", static_cast<int>(clientEmissions));
|
||||
|
||||
// 1. the workload really uploaded. Without this the three assertions below are all
|
||||
// 0 == 0 and a run whose emitter was switched off records the same "healthy" shape
|
||||
// as one that worked.
|
||||
ASSERT_GT(emissions, 0)
|
||||
<< "the server counted no texture upload emission at all over " << kFrames
|
||||
<< " frames of " << kScatteredRects
|
||||
<< " sub-regions each plus a contiguous band. Either the uploads never reached the "
|
||||
"backend or the counter stopped counting; in both cases every shape number below "
|
||||
"would be a zero that means nothing. "
|
||||
<< window.line;
|
||||
|
||||
// 2. the server bracket's own arithmetic.
|
||||
EXPECT_EQ(box + rect, emissions)
|
||||
<< "tex[box=] + tex[rect=] must be tex[emit=]: every emission takes exactly one of "
|
||||
"the two shapes. " << window.line;
|
||||
EXPECT_GE(jobs, emissions)
|
||||
<< "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 - 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
|
||||
<< " for the same workload in the same window. The two counting the same records "
|
||||
"is the entire reason both are published (D-L): a divergence here is an "
|
||||
"emission-shape divergence that SSIM is blind to and that costs ~+6 ms/frame "
|
||||
"on Mali when it goes the wrong way. "
|
||||
<< window.line;
|
||||
} else {
|
||||
// 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");
|
||||
}
|
||||
|
||||
// The pixels, so that a recorded shape cannot be the shape of a workload that drew
|
||||
// nothing.
|
||||
EXPECT_TRUE(RegionIsMostly(lastScattered, kInset, lastScattered.Width() - kInset, kInset,
|
||||
lastScattered.Height() - kInset, "green", 0.0,
|
||||
"the scattered-rect atlas"));
|
||||
EXPECT_TRUE(RegionIsMostly(lastContiguous, kInset, lastContiguous.Width() - kInset, kInset,
|
||||
lastContiguous.Height() - kInset, "green", 0.0,
|
||||
"the contiguous-band atlas"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -299,11 +299,16 @@
|
||||
/* begins with one of the ten words the pattern matched. */ \
|
||||
/* UseProgram is bit 6's whole subject: the shutter is */ \
|
||||
/* Mix(GetCurrentProgram()->GetLifetimeId(), GetLinkVersion()) and glUseProgram is */ \
|
||||
/* what moves the object it reads through. Two call sites. */ \
|
||||
/* what moves the object it reads through. Two call sites. AND SINCE THE FABLE */ \
|
||||
/* SEAM ROUND (F-1 / F-2) IT IS BITS 12 AND 14's TOO: set_sampler_views is */ \
|
||||
/* resolved for the program in use and set_shader_images' window is the highest */ \
|
||||
/* image unit the program in use names, so both shutters mix the same identity */ \
|
||||
/* bit 6 reads, and a glUseProgram alone moves all three. Undecided for the same */ \
|
||||
/* reason as bit 6 (the taint below), marked the same way. */ \
|
||||
/* BindVertexArray is bit 5's, for the same reason one level down: the shutter mixes */ \
|
||||
/* the bound VAO's identity with its configuration version, and this is the bind. */ \
|
||||
/* Three call sites. */ \
|
||||
X(UseProgram, NEW_SHADER) \
|
||||
X(UseProgram, NEW_SHADER|NEW_SAMPLER_VIEWS|NEW_SHADER_IMAGES) \
|
||||
X(BindVertexArray, NEW_VERTEX_ELEMENTS) \
|
||||
/* NOT NEW_SHADER, and the derivation refutes it outright rather than leaving it a */ \
|
||||
/* judgement: this mutator writes m_boundProgramPipeline (plus the pipeline name table) */ \
|
||||
@@ -387,11 +392,12 @@
|
||||
// row nor outlive its reason.
|
||||
//
|
||||
// IT WAS EMPTY UNTIL P4a, and it stops being empty for a reason that is a property of the
|
||||
// SCANNER rather than of the two rows. Both entries below are bit answers that are plainly
|
||||
// true - glUseProgram is what moves the object bit 6's shutter reads through, and
|
||||
// glBindVertexArray is what moves the object bit 5's shutter reads through - and the write
|
||||
// analysis cannot say so, because each of them reaches, BY NAME, a body that writes a member
|
||||
// with no m_ prefix:
|
||||
// SCANNER rather than of the rows. Every entry below is a bit answer that is plainly true -
|
||||
// glUseProgram is what moves the object bits 6, 12 and 14's shutters read through (the
|
||||
// program in use; bits 12 and 14 since the fable seam round, F-1 / F-2), and glBindVertexArray
|
||||
// is what moves the object bit 5's shutter reads through - and the write analysis cannot say
|
||||
// so, because each of the two mutators reaches, BY NAME, a body that writes a member with no
|
||||
// m_ prefix:
|
||||
//
|
||||
// UseProgram -> DestroyProgramSlot() writes `attachedShaders`
|
||||
// BindVertexArray -> a call spelled `Bind(` resolves to every body of that name, one of
|
||||
@@ -403,13 +409,16 @@
|
||||
// never be claimed about code the script could not read. Widening the taint rule to ignore
|
||||
// non-m_ writes would weaken the one mechanism that catches a genuine under-fire, so the rows
|
||||
// are MARKED, with the tool's own reason, rather than the tool being made more permissive.
|
||||
// Control 9c is what proves a marked row still needs the mark, and control 18 is what fails
|
||||
// the moment either of these becomes decidable and the mark outlives its reason.
|
||||
// Control 21 is what proves every marked row still needs its mark (it reads this list, so a
|
||||
// row that gains a bit here is counted rather than assumed), and control 18 is what fails the
|
||||
// moment any of them becomes decidable and the mark outlives its reason.
|
||||
//
|
||||
// The ten mutators that reach a tainted body (--check prints the count) all carry a prose
|
||||
// answer, which no derivation checks; these two are the first that carry a bit answer.
|
||||
// answer, which no derivation checks; these two mutators are the first that carry a bit answer.
|
||||
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X) \
|
||||
X(UseProgram, NEW_SHADER) \
|
||||
X(UseProgram, NEW_SAMPLER_VIEWS) \
|
||||
X(UseProgram, NEW_SHADER_IMAGES) \
|
||||
X(BindVertexArray, NEW_VERTEX_ELEMENTS)
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -1094,6 +1094,32 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; }
|
||||
const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; }
|
||||
|
||||
namespace {
|
||||
// P4a's BELT (ID-39). See MGPipeApplierState::RefusedNoConsumer for the whole argument;
|
||||
// in one line: acceptance is a contract with the client, and accepting a record on a
|
||||
// backend that consumes none of them makes the emitter clear a dirty flag the legacy
|
||||
// pull path still owed.
|
||||
//
|
||||
// IT IS THE SAME SIGNAL THE CLIENT'S GATE READS, deliberately - g_resourceOps is the
|
||||
// table a backend installs at its own bring-up and uninstalls at teardown, and
|
||||
// MGPipeApplierReset does NOT touch it (see there), so the gate cannot flap between a
|
||||
// make-current and the emissions that follow it. The two therefore agree on every path
|
||||
// except the one this exists for: an emitter called directly, without passing PipeFill.
|
||||
//
|
||||
// EVERY CALLER PLACES IT AFTER THE RECORD'S OWN SHAPE CHECKS AND BEFORE ANYTHING MOVES.
|
||||
// A malformed record is Fatal{ProtocolCorruption} whether or not anything would have
|
||||
// read it - a trip wire that fires only on some backends is a trip wire nobody can
|
||||
// trust - so the refusal is the LAST thing asked and the first thing that stops the
|
||||
// write. The three unit-set entry points are the exception and say so at their call
|
||||
// site: ApplyUnitWindow validates and writes in one step, so the question has to be
|
||||
// asked in front of it.
|
||||
Bool NoP4aConsumer() {
|
||||
if (g_resourceOps != nullptr) return false;
|
||||
++g_applier.RefusedNoConsumer;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MGPipeApplierReset() {
|
||||
g_applier.RenderStateCsos.clear();
|
||||
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
|
||||
@@ -1123,6 +1149,7 @@ namespace MobileGL::MG_Pipe {
|
||||
g_applier.RefusedResourceCalls = 0;
|
||||
g_applier.RefusedVertexInputCalls = 0;
|
||||
g_applier.RefusedObjectCalls = 0;
|
||||
g_applier.RefusedNoConsumer = 0;
|
||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
||||
g_applier.VertexBuffers = {};
|
||||
g_applier.VertexBufferStart = 0;
|
||||
@@ -1553,6 +1580,15 @@ namespace MobileGL::MG_Pipe {
|
||||
kMGPipeMaxResourceSlots);
|
||||
return false;
|
||||
}
|
||||
// P4a's belt, and only over the P4a half of this call: the BUFFER row is P3a's and its
|
||||
// consumer question is the frontend's (MGPipeResourceSubsystemEnabled), which has
|
||||
// already answered it before the emission - a buffer record can only arrive here on a
|
||||
// backend that registered the table, so asking again would be a second copy of that
|
||||
// rule in the one path that runs. A texture or renderbuffer create declined here leaves
|
||||
// the client's publication latch at false, which is what makes the next respecify's
|
||||
// self-healing create do the right thing if a table appears later.
|
||||
if (desc.Target != kMGPipeResourceTargetBuffer && NoP4aConsumer()) return false;
|
||||
|
||||
*record = MGPipeResourceRecord{};
|
||||
record->Gen = desc.Resource.Gen;
|
||||
record->Live = true;
|
||||
@@ -1583,6 +1619,12 @@ namespace MobileGL::MG_Pipe {
|
||||
desc.Resource.Slot, desc.Resource.Gen, desc.GlNameForDiag, desc.Target);
|
||||
return false;
|
||||
}
|
||||
// P4a's belt, for MGPipeApplyResourceCreate's reason and in front of the resolution
|
||||
// rather than after it: with no consumer no create was ever accepted, so resolving
|
||||
// first would report the absence as RefusedResourceCalls - the counter that means "a
|
||||
// seam defect" - for a state that is by design.
|
||||
if (desc.Target != kMGPipeResourceTargetBuffer && NoP4aConsumer()) return false;
|
||||
|
||||
MGPipeResourceRecord* record = ResolveResourceIn(*table, "resource_respecify", desc.Resource);
|
||||
if (record == nullptr) return false;
|
||||
PinNoLiveHostWrites(*record, desc.Resource, "resource_respecify");
|
||||
@@ -1711,6 +1753,10 @@ namespace MobileGL::MG_Pipe {
|
||||
static_cast<Uint32>(MGPipeSubDataUploadTargetOf(record.Target)));
|
||||
return false;
|
||||
}
|
||||
// P4a's belt, AFTER the target fault above and before the upload is accumulated: this
|
||||
// is the call whose acceptance clears a level's dirty flags (D-D5 / ID-18 M3), so it is
|
||||
// the one that turned "no consumer" into lost texels on Magma.
|
||||
if (NoP4aConsumer()) return false;
|
||||
return ApplyTextureUpload(record, bytes, regions);
|
||||
}
|
||||
|
||||
@@ -2200,6 +2246,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// The surfaces' Res handles are not resolved either: an attachment PINS its texture,
|
||||
// and in monolith the frontend's own SharedPtr is that keep-alive, so a refusal here
|
||||
// would be enforcing a lifetime rule monolith cannot need and split has not defined.
|
||||
// P4a's belt, after the record's own shape checks and before the table is touched. This
|
||||
// entry point is the ONE the client can reach without passing PipeFill's gate -
|
||||
// GL_Framebuffer.cpp's PipePublishFramebufferByName calls the emitter directly at the
|
||||
// fifteen DSA sites - so on a backend with no consumer this is where those records
|
||||
// stop. RefusedNoConsumer and not RefusedObjectCalls: the framebuffer family's counter
|
||||
// contract (see the header) is that no framebuffer call ever moves that one.
|
||||
if (NoP4aConsumer()) return;
|
||||
|
||||
MGPipeFramebufferRecord* record =
|
||||
RecordAt(g_applier.FramebufferRecords, state.Fbo.Slot, kMGPipeMaxFramebufferSlots);
|
||||
// Unreachable: the bound was checked above, before anything moved. The null check is
|
||||
@@ -2311,6 +2365,9 @@ namespace MobileGL::MG_Pipe {
|
||||
desc.Cso.Slot, desc.Cso.Gen, kMGPipeMaxSamplerCsoSlots);
|
||||
return;
|
||||
}
|
||||
// P4a's belt, after the blob rule and the slot bound and before the record moves.
|
||||
if (NoP4aConsumer()) return;
|
||||
|
||||
MGPipeSamplerCsoRecord& record = *recordAt;
|
||||
// A CREATE STARTS THE RECORD OVER AND LEAVES Serial AT 0; A RE-ISSUE ON A LIVE IDENTITY
|
||||
// COUNTS UP. The first half is what stops a recycled slot contributing one field of its
|
||||
@@ -2367,6 +2424,9 @@ namespace MobileGL::MG_Pipe {
|
||||
view.Cso.Slot, view.Cso.Gen, kMGPipeMaxSamplerViewSlots);
|
||||
return;
|
||||
}
|
||||
// P4a's belt, after the slot bound and before the record moves.
|
||||
if (NoP4aConsumer()) return;
|
||||
|
||||
MGPipeSamplerViewRecord& record = *recordAt;
|
||||
// RE-ISSUING ON THE SAME HANDLE IS HOW A RESTRICTION CHANGE TRAVELS - a view is
|
||||
// identity-addressed one per texture object, minted off that object's lifetime id, and
|
||||
@@ -2411,6 +2471,11 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void 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;
|
||||
|
||||
// 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
|
||||
// glCopyImageSubData endpoint has no sampler view to hang its parameters on, and the
|
||||
@@ -2458,7 +2523,16 @@ namespace MobileGL::MG_Pipe {
|
||||
// unit with no texture carries a null resource. None of the three is resolved against a
|
||||
// record either - a set is WORKING STATE, the records it names are OBJECT state, and the
|
||||
// backend resolves the pair at its own sync point where both are current.
|
||||
//
|
||||
// P4a's belt sits IN FRONT of ApplyUnitWindow on all three, and this is the one place it
|
||||
// is not last: ApplyUnitWindow validates the window and writes it in the same step, so
|
||||
// there is no point between the two to stand at. The cost is that on a backend with no
|
||||
// consumer a malformed window is declined rather than Fatal - which is the right trade the
|
||||
// one way round it can be made, because that backend would never have been handed the
|
||||
// window at all (the client's gate stops it) and the shipped configuration that DOES
|
||||
// consume these still trips the wire.
|
||||
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
|
||||
if (NoP4aConsumer()) return;
|
||||
if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
||||
g_applier.BoundSamplerViews, g_applier.SamplerViewStart,
|
||||
g_applier.SamplerViewCount)) {
|
||||
@@ -2468,6 +2542,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
|
||||
if (NoP4aConsumer()) return;
|
||||
if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
||||
g_applier.BoundSamplerStates, g_applier.SamplerStateStart,
|
||||
g_applier.SamplerStateCount)) {
|
||||
@@ -2482,6 +2557,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// sent and recast on the server - the record carries the application's format, and the
|
||||
// bind-format recast that turns a GL_RG32F bind into something 19 of 26 non-core
|
||||
// formats on Adreno will accept is the backend's, not this applier's.
|
||||
if (NoP4aConsumer()) return;
|
||||
if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
||||
g_applier.BoundShaderImages, g_applier.ShaderImageStart,
|
||||
g_applier.ShaderImageCount)) {
|
||||
@@ -2552,6 +2628,10 @@ namespace MobileGL::MG_Pipe {
|
||||
PinProgramArchiveRoundTrip(desc, *link, *spirv);
|
||||
#endif
|
||||
|
||||
// P4a's belt, after the blob rule, the slot bound and the verify round trip - all three
|
||||
// are checks on the RECORD and stay honest on every backend - and before it is stored.
|
||||
if (NoP4aConsumer()) return;
|
||||
|
||||
MGPipeShaderCsoRecord& record = *recordAt;
|
||||
// A RE-ISSUE ON THE SAME HANDLE IS HOW A RELINK TRAVELS: the handle is minted per
|
||||
// frontend program and Gen moves only on slot reuse, so an existing record of the same
|
||||
@@ -2583,6 +2663,11 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"bind_shader_state on kind %u", handle.Kind);
|
||||
// P4a's belt, and first for set_texture_params' reason: with no consumer this applier
|
||||
// holds no shader CSO record, so the resolution below would report the designed state
|
||||
// as RefusedObjectCalls. The null-handle unbind is behind it too - a binding this
|
||||
// applier never made is not one it may clear.
|
||||
if (NoP4aConsumer()) return;
|
||||
// The null handle is legal and means "nothing bound", which is a GL state and not an
|
||||
// error; a DEAD handle leaves the previous binding untouched and is counted, which is
|
||||
// bind_render_state's precedent for the same question.
|
||||
@@ -2639,6 +2724,7 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"set_draw_program on kind %u", handle.Kind);
|
||||
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
|
||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
@@ -2653,6 +2739,7 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) {
|
||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::ShaderCso),
|
||||
"set_dispatch_program on kind %u", handle.Kind);
|
||||
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
|
||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
||||
++g_applier.ProgramBindingSerial;
|
||||
@@ -2668,6 +2755,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// ON THE PROGRAM'S RECORD, not in the working state, and that is what makes it survive a
|
||||
// make-current: the block is (ShaderCso, Version) keyed and belongs to the program, not
|
||||
// to the context that last uploaded it.
|
||||
//
|
||||
// P4a's belt goes in front of the resolution for set_texture_params' reason, and it is
|
||||
// in front of the fault block too because THIS call's bounds come out of the resolved
|
||||
// program's own GlobalUboSize - there is nothing to validate against until the record
|
||||
// is in hand.
|
||||
if (NoP4aConsumer()) return;
|
||||
MGPipeShaderCsoRecord* stored = ResolveShaderCso("set_global_constants", record.ShaderCso);
|
||||
if (stored == nullptr) return;
|
||||
|
||||
|
||||
@@ -523,6 +523,35 @@ namespace MobileGL::MG_Pipe {
|
||||
// that is Fatal{ProtocolCorruption}, not a dropped call.
|
||||
Uint64 RefusedObjectCalls = 0;
|
||||
|
||||
// P4a's BELT (ID-39): every call in one of the four families P4a migrates that this
|
||||
// applier declined because NO BACKEND HAS REGISTERED MGPipeResourceOps - i.e. because
|
||||
// nothing in this process consumes what the record publishes.
|
||||
//
|
||||
// WHY THE APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL, when it is otherwise
|
||||
// backend-neutral: acceptance is a CONTRACT WITH THE CLIENT since ID-18 M3. The
|
||||
// emitters clear a texture level's dirty flags, advance their descriptor mirrors and
|
||||
// latch their suppressors on the answer this applier returns, so an applier that
|
||||
// accepts a record nothing will ever read makes the client forget work the legacy pull
|
||||
// path still owed - which is exactly how 66 texture-upload-shaped DirectVulkan cases
|
||||
// went red on the push build (ID-39). The client's own gate
|
||||
// (MG_Impl/Pipe/PipeFill.cpp's FamilyIsLive) stops the emission upstream; this is the
|
||||
// belt under it, so a record that reaches here by any other route - GL_Framebuffer.cpp's
|
||||
// PipePublishFramebufferByName calls its emitter directly, without passing PipeFill -
|
||||
// is declined rather than accepted.
|
||||
//
|
||||
// IT IS NOT A DEFECT COUNTER, WHICH IS WHY IT IS SILENT. RefusedResourceCalls,
|
||||
// RefusedVertexInputCalls and RefusedObjectCalls each mean "a record named something
|
||||
// this applier should have had"; a non-zero value there is a seam defect. A non-zero
|
||||
// value HERE is the designed steady state of a backend with no P4a twins, so logging it
|
||||
// would put an ERROR line in every ordinary Magma run. The number is the observable.
|
||||
//
|
||||
// THE DEATH PATHS ARE DELIBERATELY NOT ON THIS LIST. resource_destroy,
|
||||
// delete_sampler_state, delete_sampler_view and delete_shader_state are idempotent
|
||||
// cleanup that must keep working whatever the registration did, and with no consumer
|
||||
// there is no record for them to find anyway (they count their own refusal). Per
|
||||
// context and cleared by MGPipeApplierReset, like the three above it.
|
||||
Uint64 RefusedNoConsumer = 0;
|
||||
|
||||
// ---- working state: what the next draw fetches with. All of it is per context and
|
||||
// all of it is cleared by MGPipeApplierReset, EXCEPT the two serials, which only ever
|
||||
// advance (see there).
|
||||
|
||||
@@ -56,7 +56,11 @@ namespace MobileGL::MG_Pipe {
|
||||
enum class MGPipeAggregate : Uint32 {
|
||||
// VertexArrayState: any VAO attribute format / buffer / enable moved.
|
||||
VaoAttribute = 0,
|
||||
// FramebufferState: any FBO attachment or default-geometry write, or a bind.
|
||||
// FramebufferState: any FBO attachment or default-geometry write, or a bind - and, since
|
||||
// P4a (fable seam F-3), any STORAGE DEFINITION of a texture or a renderbuffer, because
|
||||
// set_framebuffer_state inlines an attachment's format, extent and samples and those
|
||||
// setters are the only writers of what it inlines (TextureObject.cpp /
|
||||
// RenderbufferObject.cpp, PipePublishDescriptor).
|
||||
FramebufferAttachment,
|
||||
// TextureState: any texture object CONTENT moved (an upload, a dirty region).
|
||||
TextureContent,
|
||||
|
||||
@@ -25,15 +25,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
FramebufferObject::~FramebufferObject() {
|
||||
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
|
||||
// garbage sweep. This is the last SharedPtr to this object dropping - not the
|
||||
// glDelete* that only marks the name and leaves a still-bound object very much
|
||||
// alive - so it is the exact moment the backend's twin, and the driver storage
|
||||
// that twin owns, stop being reachable. The notice carries the lifetime id
|
||||
// because the object no longer exists to be passed, and because the lifetime id
|
||||
// is what the client slot allocator resolves the handle from. No-op unless a
|
||||
// backend registered the ops (a pull build declares none at all).
|
||||
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::Framebuffer, m_lifetimeId);
|
||||
// P4a D-I2: A FRAMEBUFFER HAS A HANDLE AND NO WIRE LIFETIME. PipeCalls.def carries
|
||||
// resource_destroy and five delete_* rows and NO framebuffer delete, because a
|
||||
// framebuffer is not a resource and is not a CSO - it is STATE, and
|
||||
// set_framebuffer_state is the only call that names one - and the catalogue is closed,
|
||||
// so P4a invents no row. The helper is therefore steps 2 and 3 only: the death notice,
|
||||
// raised while the handle still resolves (this is the P2 step-e2 announcement that used
|
||||
// to stand here alone - the last SharedPtr to this object dropping, not the glDelete*
|
||||
// that only marks the name and leaves a still-bound object very much alive), and then
|
||||
// the slot.
|
||||
//
|
||||
// What makes a dangling Fbo unreachable is the frontend's own
|
||||
// MarkFramebufferObjectForDeletion path, which already rebinds any slot holding the
|
||||
// victim to framebuffer 0; and a RECYCLED framebuffer handle can never be suppressed
|
||||
// against its predecessor's record, because Fbo carries Gen and Gen is inside the
|
||||
// record's ContentHash.
|
||||
MG_Pipe::MGPipeEmitFramebufferDestroyAndFree(m_lifetimeId);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/StateObjectDeathNotice.h>
|
||||
|
||||
const char* kDefaultFragmentShaderSource = R"(#version 460 core
|
||||
@@ -40,7 +41,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
// object no longer exists to be passed, and because the lifetime id is what the client
|
||||
// slot allocator resolves the handle from. No-op unless a backend registered the ops
|
||||
// (a pull build declares none at all).
|
||||
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::ShaderCso, m_lifetimeId);
|
||||
//
|
||||
// P4a D-I1: the notice is no longer raised directly - it is step 2 of the ONE
|
||||
// client-side death helper for this kind, which emits delete_shader_state first,
|
||||
// raises the notice second and frees the slot last. The client mints the ShaderCso, so
|
||||
// the client is where its death has to be spoken from: a backend death-ops table is a
|
||||
// redundant, idempotent SECOND path, and under a backend that installs none it was
|
||||
// previously the ONLY one, which is how a slot leaks for the life of the process.
|
||||
//
|
||||
// AN ORDINARY PROGRAM AND A PIPELINE COMPOSITE TAKE THIS SAME LINE. A composite is an
|
||||
// ordinary ProgramObject with its own lifetime id, its slot merely comes out of the
|
||||
// reserved band, and its OTHER release path - the pipeline cache dropping it when the
|
||||
// draw-program signature moves - goes through the same helper. Whichever runs second is
|
||||
// a proven no-op, because the slot allocator refuses a slot that is not live at that
|
||||
// generation.
|
||||
MG_Pipe::MGPipeEmitShaderCsoDestroyAndFree(m_lifetimeId);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1111,6 +1111,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
return Artifacts();
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The phase-B twin of GetLinkReflection, and it exists for exactly one caller:
|
||||
// create_shader_state publishes the WHOLE SpirvArtifacts beside the whole
|
||||
// LinkArtifacts, because the archive the far side deserialises into is those two
|
||||
// structs and nothing else. The granular getters above (GetGeneratedSpirv,
|
||||
// GetSpirvStatus, UsesNativeFloat64, PointSizeDemoted, GetSpirvValidationEnabled,
|
||||
// GetUBOSize) cover what the GL layer needs and deliberately do not reach
|
||||
// reservedNumSamplesOffset or uniformOffsets, which the descriptor does need.
|
||||
//
|
||||
// PUSH-ONLY, so a pull build's symbol set and this class's layout are byte-for-byte
|
||||
// what they were: an inline const-ref accessor adds nothing to a pull build that is
|
||||
// never compiled into one. It joins both phases, like every other Spirv() reader.
|
||||
const SpirvArtifacts& GetSpirvReflection() const {
|
||||
return Spirv();
|
||||
}
|
||||
#endif
|
||||
|
||||
static Bool IsValidUniformLocation(const LinkArtifacts& artifacts, Int location) {
|
||||
if (location < 0 || location > static_cast<Int>(artifacts.maxUniformLocation)) return false;
|
||||
if (static_cast<SizeT>(location) >= artifacts.uniformIndexInTProgram.size()) return false;
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "RenderbufferObject.h"
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
#include <MG_State/GLState/StateObjectDeathNotice.h>
|
||||
// The contract's own door, exactly as the texture half takes it (c0b): the four renderbuffer
|
||||
// hooks this file calls are declared in MG_Pipe/PipeMutation.h and defined in
|
||||
// MG_Impl/Pipe/PipeFill.cpp, so no MG_State translation unit includes the client's emitter.
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
@@ -25,19 +29,33 @@ namespace MobileGL {
|
||||
return g_nextRenderbufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
RenderbufferObject::RenderbufferObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
|
||||
RenderbufferObject::RenderbufferObject(Uint externalIndex) : m_externalIndex(externalIndex) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a D-D1: resource_create from the constructor, carrying no storage. A
|
||||
// renderbuffer is an INDEPENDENT class on the wire - it shares MGPResourceDesc's
|
||||
// shape with textures and buffers and nothing else - and its handle is minted
|
||||
// whatever the subsystem bitmask says, because MGPSurface::Res names it out of the
|
||||
// framebuffer subsystem.
|
||||
// TWO CALLS AND NOT ONE (c0b): the mint is unconditional in a push build
|
||||
// because a renderbuffer is named by handle out of the framebuffer subsystem
|
||||
// whether or not its own family is switched on; the create is what the gate in
|
||||
// PipeFill.cpp decides.
|
||||
MG_Pipe::MGPipeMintRenderbufferHandle(*this);
|
||||
MG_Pipe::MGPipeEmitRenderbufferResourceCreate(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
RenderbufferObject::~RenderbufferObject() {
|
||||
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
|
||||
// garbage sweep. This is the last SharedPtr to this object dropping - not
|
||||
// glDeleteRenderbuffers, which only marks the name and leaves a still-bound object very much
|
||||
// alive - so it is the exact moment the backend's twin, and the driver storage that
|
||||
// twin owns, stop being reachable. The notice carries the lifetime id because the
|
||||
// object no longer exists to be passed, and because the lifetime id is what the client
|
||||
// slot allocator resolves the handle from. No-op unless a backend registered the ops
|
||||
// (a pull build declares none at all).
|
||||
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::Renderbuffer, m_lifetimeId);
|
||||
// P4a D-I1, the fixed three-step order: the wire delete first (published-gated,
|
||||
// because a slot is not evidence of a record), then the death notice - the P2
|
||||
// step-e2 announcement that used to stand here alone, raised while the handle
|
||||
// still resolves - and the slot last. Steps 2 and 3 are the contract's helper;
|
||||
// step 1 is this package's, one statement earlier, because the helper's file
|
||||
// belongs to the contract package for the whole phase.
|
||||
// All three steps are the contract's helper (c0b); v1's separate step-1 call
|
||||
// is deleted, not kept, for the reason ~TextureObjectBase states in full.
|
||||
MG_Pipe::MGPipeEmitRenderbufferDestroyAndFree(m_lifetimeId);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -96,17 +114,53 @@ namespace MobileGL {
|
||||
void RenderbufferObject::SetInternalFormat(TextureInternalFormat format) {
|
||||
m_internalFormat = format;
|
||||
m_componentSizes = MG_Util::GetComponentSizesForInternalFormat(format);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
void RenderbufferObject::AllocateStorage(IntVec2 size) {
|
||||
m_width = size.x();
|
||||
m_height = size.y();
|
||||
m_allocated = true;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
void RenderbufferObject::SetSamples(Int samples) {
|
||||
m_samples = samples;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// D-D2: THE RENDERBUFFER PUBLICATION HOLE, CLOSED BY EMISSION AND NOT BY A NEW
|
||||
// VERSION. These three setters bump no version and raise no notice, so
|
||||
// `glBindRenderbuffer; glRenderbufferStorage(newSize)` on an attached renderbuffer
|
||||
// was invisible to everything downstream. Emitting from the storage entry point
|
||||
// closes the RESOURCE half; a version counter here would resize the pull build's
|
||||
// object and break G1.
|
||||
//
|
||||
// THE FRAMEBUFFER HALF IS THE AGGREGATE BUMP BELOW (P4a fable seam F-3), and the
|
||||
// sentence that used to end the paragraph above - "widening the shutter would fire
|
||||
// the framebuffer emission on an unrelated renderbuffer write" - was the seam:
|
||||
// set_framebuffer_state inlines an attachment's InternalFormat, extent and Samples at
|
||||
// emission (D-C1), so re-storaging an ATTACHED renderbuffer left the framebuffer
|
||||
// record - and the handle arm's four cross-object masks - describing the previous
|
||||
// storage while the resource record described the new one. The bump costs one
|
||||
// framebuffer re-emission per storage definition, whether or not the object is
|
||||
// attached, which the emitter's content hash suppresses when nothing it inlines
|
||||
// moved; it is not a counter on this object.
|
||||
//
|
||||
// The emitter dedupes on the built descriptor, so glRenderbufferStorage's three-setter
|
||||
// sequence publishes once rather than three times.
|
||||
void RenderbufferObject::PipePublishDescriptor() {
|
||||
MG_Pipe::MGPipeEmitRenderbufferResourceRespecify(*this);
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||
}
|
||||
#endif
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -59,6 +59,13 @@ namespace MobileGL {
|
||||
|
||||
private:
|
||||
static Uint64 AllocateLifetimeId();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a D-D2: resource_respecify, from every storage-defining setter. Non-virtual
|
||||
// and push-only, so the pull build's object layout is untouched (P4a's
|
||||
// admitted-resize set is EMPTY); defined in RenderbufferObject.cpp, which is the
|
||||
// one translation unit that includes the client emitter.
|
||||
void PipePublishDescriptor();
|
||||
#endif
|
||||
|
||||
Uint m_externalIndex = 0;
|
||||
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
||||
|
||||
@@ -36,7 +36,22 @@ namespace MobileGL {
|
||||
// because the object no longer exists to be passed, and because the lifetime id
|
||||
// is what the client slot allocator resolves the handle from. No-op unless a
|
||||
// backend registered the ops (a pull build declares none at all).
|
||||
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::SamplerCso, m_lifetimeId);
|
||||
//
|
||||
// P4a D-I1: the notice is no longer raised directly - it is step 2 of the ONE
|
||||
// client-side death helper for this kind, which emits delete_sampler_state
|
||||
// first, raises the notice second and frees the slot last. Making the client
|
||||
// the only death path is what stops a slot leaking under a backend that
|
||||
// installs no death-ops table at all, and the backend's own notice becomes a
|
||||
// redundant, idempotent second path rather than the only one.
|
||||
//
|
||||
// FOR A CONTENT-ADDRESSED SAMPLER CSO THIS HELPER CORRECTLY FREES NOTHING, and
|
||||
// that is the design rather than a gap: the CSO belongs to a VALUE, not to this
|
||||
// object (two identical SamplerObjects share one), so it is allocated with no
|
||||
// lifetime id, the helper resolves nothing for this one, and the only death
|
||||
// path for that slot is the CSO cache's LRU eviction - which is client-side and
|
||||
// therefore backend-neutral on day one. What still goes out, unconditionally
|
||||
// and exactly as before, is the notice.
|
||||
MG_Pipe::MGPipeEmitSamplerCsoDestroyAndFree(m_lifetimeId);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
#include "MG_Util/Types.h"
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
// NO MG_State TRANSLATION UNIT SEES THE CLIENT'S EMITTER ANY MORE (c0b, ID-13). v1 included
|
||||
// MG_Impl/Pipe/TextureEmit.h here and in RenderbufferObject.cpp because at the contract tag
|
||||
// MG_Pipe/PipeMutation.h carried no texture row; it now declares the four mints, the nine
|
||||
// emissions and the publication latch, so this file sees a DECLARATION exactly as
|
||||
// BufferObject.cpp does and the closure gate's mutation-header probe has nothing to find.
|
||||
// The three PipePublish* helpers stay on TextureObjectBase so the cube's, the view's and the
|
||||
// buffer texture's translation units keep calling an inherited member.
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State {
|
||||
@@ -29,15 +36,60 @@ namespace MobileGL {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
TextureObjectBase::~TextureObjectBase() {
|
||||
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
|
||||
// garbage sweep. This is the last SharedPtr to this object dropping - not the
|
||||
// glDelete* that only marks the name and leaves a still-bound object very much
|
||||
// alive - so it is the exact moment the backend's twin, and the driver storage
|
||||
// that twin owns, stop being reachable. The notice carries the lifetime id
|
||||
// because the object no longer exists to be passed, and because the lifetime id
|
||||
// is what the client slot allocator resolves the handle from. No-op unless a
|
||||
// backend registered the ops (a pull build declares none at all).
|
||||
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::Texture, m_lifetimeId);
|
||||
// P4a D-I1: BACKEND-NEUTRAL FROM DAY ONE, and the three-step order is fixed.
|
||||
//
|
||||
// 1. the wire delete FIRST - it drops the applier's record while the record
|
||||
// still exists, so a recycled slot cannot inherit a field. Published-gated
|
||||
// rather than slot-gated: a backend twin table mints a slot through
|
||||
// MGPipeSlots().Acquire whether or not the subsystem ever asked this client
|
||||
// to emit a create, and a resource_destroy on such a handle is a refused
|
||||
// call the applier counts and asserts on;
|
||||
// 2. the death notice SECOND - it resolves the handle through the allocator,
|
||||
// and a backend told after the free could no longer find its twin. This is
|
||||
// the P2 step-e2 announcement that used to stand here alone: the last
|
||||
// SharedPtr to this object dropping, not the glDelete* that only marks the
|
||||
// name and leaves a still-bound object very much alive;
|
||||
// 3. the slot LAST, and a double free on a stale generation is a proven no-op.
|
||||
//
|
||||
// ALL THREE STEPS ARE THE CONTRACT'S HELPER (c0b): it reads the publication
|
||||
// latch, emits the resource_destroy itself, raises the notice while the handle
|
||||
// still resolves and frees the slot last. v1 emitted step 1 from a second
|
||||
// statement here because at the tag the helper hard-coded `published = false`;
|
||||
// that statement is deleted rather than kept, since a second delete for a
|
||||
// record the helper has already dropped is a refused call the applier asserts
|
||||
// on. The SamplerViewCso minted off this same lifetime id goes with it. The
|
||||
// BUILT-IN SAMPLER does not: it is a real SamplerObject with its own lifetime
|
||||
// id and its own destructor, which takes the same helper shape.
|
||||
MG_Pipe::MGPipeEmitTextureDestroyAndFree(m_lifetimeId);
|
||||
}
|
||||
|
||||
// ---- P4a's three client emission points (see TextureObject.h) ----
|
||||
|
||||
void TextureObjectBase::PipePublishDescriptor() {
|
||||
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this);
|
||||
// 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
|
||||
// Complete at emission (D-C1), so redefining the storage of a texture that is
|
||||
// ATTACHED changed those fields with nothing bit 11 reads moving - the format
|
||||
// and shape setters bump the two TEXTURE aggregates and never the attachment
|
||||
// one, and no path from a texture reaches its framebuffers. The handle arm then
|
||||
// answered its four cross-object masks from the stale copy while the legacy arm
|
||||
// re-read the frontend at the same re-sync. This is the one funnel every
|
||||
// storage-defining entry point takes (see AllocateStorage), so the bump lives
|
||||
// here and not per setter, it is push-only like the rest of this block, and it
|
||||
// over-fires the framebuffer bit once per storage definition of an unattached
|
||||
// texture - at load time, where a 304-byte hash is nothing.
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||
}
|
||||
|
||||
void TextureObjectBase::PipePublishParams() {
|
||||
MG_Pipe::MGPipeEmitTextureParams(*this);
|
||||
}
|
||||
|
||||
void TextureObjectBase::PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
|
||||
MG_Pipe::MGPipeNoteTextureLevelDirty(*this, static_cast<Uint32>(uploadTarget),
|
||||
static_cast<Uint32>(mipmapLevel));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -69,6 +121,34 @@ namespace MobileGL {
|
||||
m_sampler->SetWrapT(SamplerWrapMode::ClampToEdge);
|
||||
m_sampler->SetWrapR(SamplerWrapMode::ClampToEdge);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a D-D1: A RESOURCE EXISTS BEFORE ANYTHING CAN NAME IT, so resource_create is
|
||||
// emitted from the constructor and carries no storage - the store is defined
|
||||
// lazily by the first respecify and every backend already tolerates a resource
|
||||
// with none. The handle itself is minted whatever the subsystem bitmask says,
|
||||
// because set_framebuffer_state and set_sampler_views name this texture by handle
|
||||
// out of two different subsystems.
|
||||
//
|
||||
// NOTHING THE DERIVED CLASS IMPLEMENTS IS TOUCHED HERE and that is a
|
||||
// correctness requirement rather than a style: the derived object does not exist
|
||||
// yet, so ITextureObject::GetStorageType and ::GetUploadTargets - PURE, with no
|
||||
// body on this base - would be undefined behaviour. The emitter reads GetTarget()
|
||||
// and GetExternalIndex(), which TextureObjectBase itself overrides and which
|
||||
// therefore dispatch to this class's own bodies over members the mem-init list
|
||||
// has already written; the storage kind is derived from the target, which is
|
||||
// exact (TextureObjectBuffer is the only class that reports Buffer and
|
||||
// TextureBuffer is the only target it is constructed with).
|
||||
//
|
||||
// TWO CALLS AND NOT ONE (c0b): the MINT is unconditional in a push build -
|
||||
// set_framebuffer_state and set_sampler_views name this texture by handle out of
|
||||
// two other subsystems, so gating it would make them emit null handles in exactly
|
||||
// the A/B arm that exists to isolate the families - and the CREATE is what the
|
||||
// subsystem gate in PipeFill.cpp decides.
|
||||
(void)target;
|
||||
(void)externalIndex;
|
||||
MG_Pipe::MGPipeMintTextureHandle(*this);
|
||||
MG_Pipe::MGPipeEmitTextureResourceCreate(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
TextureInternalFormat TextureObjectBase::GetFormat() const {
|
||||
@@ -116,6 +196,14 @@ namespace MobileGL {
|
||||
BumpShapeVersion();
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The format is BOTH halves: a descriptor field the backend allocates from and a
|
||||
// parameter the swizzle / depth-stencil push reads, so both records move. It is
|
||||
// also the last statement of both glTexBuffer entry points, which is what
|
||||
// publishes a buffer texture's window without a call site in MG_Impl/GLImpl.
|
||||
PipePublishDescriptor();
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
Uint TextureObjectBase::GetExternalIndex() const {
|
||||
@@ -142,6 +230,9 @@ namespace MobileGL {
|
||||
m_sampler->SetBorderColor(color);
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
const IntVec4& TextureObjectBase::GetBorderColorI() const {
|
||||
@@ -157,6 +248,9 @@ namespace MobileGL {
|
||||
m_sampler->SetBorderColorI(color);
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
const UintVec4& TextureObjectBase::GetBorderColorUI() const {
|
||||
@@ -172,6 +266,9 @@ namespace MobileGL {
|
||||
m_sampler->SetBorderColorUI(color);
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
BorderColorForm TextureObjectBase::GetBorderColorForm() const {
|
||||
@@ -222,6 +319,9 @@ namespace MobileGL {
|
||||
}
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureObjectBase::SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) {
|
||||
@@ -230,6 +330,9 @@ namespace MobileGL {
|
||||
m_swizzleParams = values;
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
const UintVec2& TextureObjectBase::GetLevelRange() const {
|
||||
@@ -249,6 +352,12 @@ namespace MobileGL {
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
BumpShapeVersion();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The level range is PARAMS, not a descriptor field: the record carries
|
||||
// BaseLevel / MaxLevel and the storage is untouched. The descriptor is deduped on
|
||||
// its own bytes, so the shape bump above costs nothing here.
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
|
||||
@@ -261,6 +370,9 @@ namespace MobileGL {
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
BumpShapeVersion();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::IsImmutable() const {
|
||||
@@ -281,6 +393,14 @@ namespace MobileGL {
|
||||
}
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Immutable is a descriptor fact the backend reads - and NOT a request for an
|
||||
// acknowledgement: MGPipeResourceRespecifyNeedsAck names the buffer target
|
||||
// explicitly, because texture allocation is lazy in monolith and stays lazy in
|
||||
// split. The level clamp above is params.
|
||||
PipePublishDescriptor();
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
Uint16 TextureObjectBase::GetTextureParamsVersion() const {
|
||||
@@ -319,6 +439,9 @@ namespace MobileGL {
|
||||
m_samples = samples;
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::HasFixedSampleLocations() const {
|
||||
@@ -329,6 +452,9 @@ namespace MobileGL {
|
||||
m_fixedSampleLocations = fixedSampleLocations;
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
Uint64 TextureObjectBase::GetLifetimeId() const {
|
||||
@@ -360,11 +486,23 @@ namespace MobileGL {
|
||||
MipmapInput input) {
|
||||
BumpShapeVersion();
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// AFTER the allocation, never before: BumpShapeVersion runs first and a descriptor
|
||||
// built there would describe the level set this call is about to change. Every
|
||||
// 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();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
BumpShapeVersion();
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
@@ -383,6 +521,11 @@ namespace MobileGL {
|
||||
MGP_NOTE_AGGREGATE(TextureContent);
|
||||
}
|
||||
m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// THE DRAIN LIST HAS NO CLEAN ARM (see TextureEmit.h): a level that goes clean
|
||||
// is collected at the next drain, whose first test is IsStorageDirty.
|
||||
if (dirty) PipeNoteLevelDirty(uploadTarget, mipmapLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool TextureObjectWithOneMipmap::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
@@ -395,6 +538,13 @@ namespace MobileGL {
|
||||
MGP_NOTE_AGGREGATE(TextureContent);
|
||||
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
|
||||
size);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// THE DRAIN LIST IS KEYED ON THE STORAGE OWNER FOR FREE: TextureObjectView
|
||||
// forwards this call to the OWNER's method after remapping the level and the
|
||||
// region, so an upload through a view and an upload through the owner arrive here
|
||||
// on the same object with the same owner-side coordinates.
|
||||
PipeNoteLevelDirty(uploadTarget, mipmapLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
|
||||
@@ -190,6 +190,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_depthStencilTextureMode = mode;
|
||||
++m_textureParamsVersion;
|
||||
MGP_NOTE_AGGREGATE(TextureParams);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// D-E3's whole point, at the one site that proves it: the depth-stencil mode of a
|
||||
// texture that is ONLY the READ framebuffer's attachment reaches the driver, because
|
||||
// set_texture_params is addressed by resource and is independent of every binding.
|
||||
PipePublishParams();
|
||||
#endif
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -201,6 +207,34 @@ namespace MobileGL::MG_State::GLState {
|
||||
// otherwise invisible to such a memo (no bind moved).
|
||||
void BumpShapeVersion();
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P4a's client emission points (brief D-D1, D-D3, D-E1, D-I1) ----
|
||||
//
|
||||
// NON-VIRTUAL AND PUSH-ONLY, both deliberately: a virtual would grow the vtable and a
|
||||
// member would grow the object, and P4a's admitted-resize set is EMPTY - every edit
|
||||
// that reaches the pull build is inside this guard, so the pull build's symbol set is
|
||||
// byte-for-byte the one it had before the phase.
|
||||
//
|
||||
// DECLARED HERE AND DEFINED IN TextureObject.cpp, which calls the contract's own hooks
|
||||
// in MG_Pipe/PipeMutation.h - the same door BufferObject.cpp uses, and no MG_State
|
||||
// translation unit sees MG_Impl/Pipe/TextureEmit.h at all (c0b, ID-13). They stay
|
||||
// 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
|
||||
// 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.
|
||||
void PipePublishDescriptor();
|
||||
// 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
|
||||
// arm: the contract's hook (MG_Pipe/PipeMutation.h) carries no `dirty` flag, and a
|
||||
// level that goes clean is collected at the next drain, where !IsStorageDirty is the
|
||||
// first test EmitOneLevel makes.
|
||||
void PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel);
|
||||
#endif
|
||||
|
||||
const Uint m_externalIndex;
|
||||
const Uint64 m_lifetimeId;
|
||||
const TextureTarget m_target = TextureTarget::Unknown;
|
||||
|
||||
@@ -31,11 +31,20 @@ namespace MobileGL {
|
||||
MipmapInput input) {
|
||||
BumpShapeVersion();
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
#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();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
BumpShapeVersion();
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
@@ -53,6 +62,11 @@ namespace MobileGL {
|
||||
MGP_NOTE_AGGREGATE(TextureContent);
|
||||
}
|
||||
m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// SIX FACES, SIX BLOBS, SIX DRAIN KEYS: the upload target is the face, and it is
|
||||
// what the sub-data record's Target byte carries beside the resource target.
|
||||
if (dirty) PipeNoteLevelDirty(uploadTarget, mipmapLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool TextureObject2DCube::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
@@ -65,6 +79,9 @@ namespace MobileGL {
|
||||
MGP_NOTE_AGGREGATE(TextureContent);
|
||||
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
|
||||
size);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipeNoteLevelDirty(uploadTarget, mipmapLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObject2DCube::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
|
||||
@@ -29,6 +29,15 @@ namespace MobileGL {
|
||||
void SetBufferRange(SizeT offset, SizeT size) {
|
||||
m_bufferRangeOffset = offset;
|
||||
m_bufferRangeSize = size;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Both glTexBuffer entry points bind the backing buffer and then set the
|
||||
// window, so this is the first statement at which the descriptor's
|
||||
// BufferForTexBuffer / BufOffset / BufSize trio is complete. SetInternalFormat
|
||||
// publishes again one statement later and is deduped away when the format did
|
||||
// not move - which is exactly the case a re-attach of a DIFFERENT buffer at
|
||||
// the same format would otherwise fall through.
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
SizeT GetBufferRangeOffset() const { return m_bufferRangeOffset; }
|
||||
// Resolved against the buffer's current size, so kWholeBuffer tracks it.
|
||||
|
||||
@@ -79,6 +79,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
// to the view. GetImmutableLevels() forwards to the owner for the actual GL query, which
|
||||
// GL 4.6 core 8.18 defines as the ORIGINAL texture's value.
|
||||
SetImmutableLevels(numLevels);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ViewOf, and it is published from HERE rather than left to SetImmutableLevels above,
|
||||
// which early-returns when the composed level count happens to be the base class's
|
||||
// current value - a degenerate view (the spec's min() composition narrowed to zero
|
||||
// levels) would otherwise never publish the one field that makes it a view. The
|
||||
// descriptor is deduped on its own bytes, so the ordinary case pays one compare.
|
||||
//
|
||||
// ONE HOP ALWAYS REACHES STORAGE: glTextureView composes a view-of-a-view onto the ROOT
|
||||
// at creation, which is what the spec's additive min-level rule describes and what the
|
||||
// assertion above pins, so the owner named here is never itself a view.
|
||||
PipePublishDescriptor();
|
||||
#endif
|
||||
}
|
||||
|
||||
Uint TextureObjectView::GetImmutableLevels() const {
|
||||
|
||||
@@ -50,8 +50,14 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Program/GL_ProgramPipeline.h>
|
||||
#include <MG_Impl/Pipe/CompositeResolver.h>
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
// create_shader_state takes the two artefact structs by pointer beside the record, so a case
|
||||
// that mints a composite record needs their definitions.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
@@ -72,13 +78,24 @@ namespace {
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
// `from` is a byte offset, and it exists because of the fork below: the library's log file
|
||||
// is already open by the time a case runs, so the child's lines are APPENDED to it rather
|
||||
// than written to a fresh file, and only what the child appended is this drive's evidence.
|
||||
std::string ReadLog(std::streamoff from = 0) {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
if (from > 0) in.seekg(from, std::ios::beg);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Where the library's log file currently ends. Reading from here after the child has
|
||||
// aborted gives exactly the lines that drive produced.
|
||||
std::streamoff LogEnd() {
|
||||
std::ifstream in(g_logPath, std::ios::binary | std::ios::ate);
|
||||
return in ? static_cast<std::streamoff>(in.tellg()) : std::streamoff{0};
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
@@ -104,8 +121,14 @@ namespace {
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
// THE LOG PATH IS NOT UNLINKED HERE, and that is what this helper had to learn when the
|
||||
// client's cases landed in the same file as the applier's: main() calls
|
||||
// MobileGL::Initialize(), so the library's log FILE* is already open on this path and
|
||||
// fork() duplicates it. Removing the path would leave the child writing into a deleted
|
||||
// inode and the parent reading an empty file - the child would still abort, and the
|
||||
// assertion on WHAT it named could never see the line. So the log's end is remembered
|
||||
// and only what the child appended is read back.
|
||||
const std::streamoff before = LogEnd();
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
@@ -116,7 +139,7 @@ namespace {
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
result.Log = ReadLog(before);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -347,6 +370,393 @@ TEST(CompositeResolver, ASlotAtTheShaderCsoLimitIsRefusedWhileTheLastBandSlotIsN
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name.
|
||||
#define MGL_COMPOSITE_RESOLVER_TEST_LIST(X) \
|
||||
X(CompositeResolver, ACompositeIsMintedFromTheReservedBand) \
|
||||
X(CompositeResolver, ASignatureThatHasNotMovedReusesOneComposite) \
|
||||
X(CompositeResolver, TwoPipelinesWithTheSameSignatureKeepTheirOwnComposite) \
|
||||
X(CompositeResolver, EvictionThenDestructionFreesTheSlotExactlyOnce) \
|
||||
X(CompositeResolver, DestructionThenEvictionFreesTheSlotExactlyOnce) \
|
||||
X(CompositeResolver, ASignatureMoveAfterAMakeCurrentStillReleasesThroughTheResolver) \
|
||||
X(CompositeResolver, TwoContextsHoldingOnePipelineNameKeepTheirOwnComposites) \
|
||||
X(CompositeResolver, ADestroyedContextsEntryIsDroppedRatherThanReleasedASecondTime)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_COMPOSITE_RESOLVER_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::ProgramObject;
|
||||
using MG_State::GLState::ProgramPipelineObject;
|
||||
|
||||
struct ResolverScope {
|
||||
ResolverScope() { Clear(); }
|
||||
~ResolverScope() {
|
||||
GL::BindProgramPipeline(0);
|
||||
GL::UseProgram(0);
|
||||
Clear();
|
||||
}
|
||||
ResolverScope(const ResolverScope&) = delete;
|
||||
ResolverScope& operator=(const ResolverScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().ResetCounters();
|
||||
MGPipeCompositeResolverInstance().ResetCounters();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
|
||||
const char* kVs = R"(#version 430 core
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
const char* kFs = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0); }
|
||||
)";
|
||||
// A SECOND fragment stage, so a pipeline's draw-program signature can be made to move for
|
||||
// real: ComputeDrawProgramSignature is the per-stage {lifetimeId, GetLinkVersion()} array,
|
||||
// and a different ProgramObject is a different lifetime id.
|
||||
const char* kFs2 = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.5); }
|
||||
)";
|
||||
|
||||
// Built by hand rather than through glCreateShaderProgramv, for ProgramPipelineCompositeTest's
|
||||
// reason: that entry point detaches the shader right after linking, so a relink would leave
|
||||
// the stage program with nothing to composite from.
|
||||
GLuint MakeSeparableProgram(GLenum stage, const char* source) {
|
||||
const GLuint shader = GL::CreateShader(stage);
|
||||
GL::ShaderSource(shader, 1, &source, nullptr);
|
||||
GL::CompileShader(shader);
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
GL::AttachShader(program, shader);
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "separable stage program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeBoundPipeline(GLuint vs, GLuint fs) {
|
||||
GLuint pipeline = 0;
|
||||
GL::GenProgramPipelines(1, &pipeline);
|
||||
GL::BindProgramPipeline(pipeline);
|
||||
GL::UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||
GL::UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||
GL::UseProgram(0);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
TEST(CompositeResolver, ACompositeIsMintedFromTheReservedBand) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
MakeBoundPipeline(vs, fs);
|
||||
|
||||
const SharedPtr<ProgramObject> composite = Ctx().GetProgramForDraw();
|
||||
ASSERT_TRUE(composite) << "the frontend has to flatten the pipeline for this to mean anything";
|
||||
// The composite is the one ProgramObject in the system with external index 0: it is
|
||||
// deliberately not a named program, must not answer glIsProgram and must not consume a
|
||||
// name, and glCreateProgram never returns 0.
|
||||
EXPECT_TRUE(MGPipeProgramIsPipelineComposite(*composite));
|
||||
EXPECT_EQ(composite->GetExternalIndex(), 0u);
|
||||
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle cso = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(cso.Slot))
|
||||
<< "a composite's slot comes out of the reserved band and nowhere else";
|
||||
// AND IT IS AN ORDINARY create_shader_state. The server never learns it is a composite.
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().LastProgramDesc().Cso, cso);
|
||||
EXPECT_TRUE(MGPipeProgramEmitterInstance().RecordIsPublished(cso));
|
||||
EXPECT_EQ(MGPipeCompositeResolverInstance().GetCounters().Mints, 1u);
|
||||
}
|
||||
|
||||
TEST(CompositeResolver, ASignatureThatHasNotMovedReusesOneComposite) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
MakeBoundPipeline(vs, fs);
|
||||
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle first = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(first));
|
||||
ASSERT_EQ(MGPipeProgramEmitterInstance().CreateCount(), 1u);
|
||||
|
||||
// The stage set did not move, so the frontend hands back the cached composite and the
|
||||
// resolver reuses its handle - no second identity, no second record, and nothing
|
||||
// released. THE KEY IS ComputeDrawProgramSignature's {lifetimeId, linkVersion} array
|
||||
// and deliberately NOT GetBackendStateVersion, which a glUniform1i to a sampler moves
|
||||
// and which used to rebuild the composite on every draw.
|
||||
MGPipeProgramEmitterInstance().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().DrawCso(), first);
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().CreateCount(), 1u);
|
||||
EXPECT_EQ(MGPipeCompositeResolverInstance().GetCounters().Releases, 0u);
|
||||
EXPECT_GE(MGPipeCompositeResolverInstance().GetCounters().Reuses, 1u);
|
||||
}
|
||||
|
||||
// [deviation] The brief names this case "TwoPipelinesWithTheSameSignatureShareOneComposite".
|
||||
// Sharing one HANDLE between two pipeline objects is not implementable safely and the
|
||||
// property that is true is the opposite one, so the case is named for what it asserts.
|
||||
//
|
||||
// The reason is the death path: a composite is an ordinary ProgramObject with its OWN
|
||||
// lifetime id, and the client-side death helper resolves the handle FROM that lifetime id.
|
||||
// Two composites sharing one handle would put only one of the two ids in the allocator's
|
||||
// map, so the first ~ProgramObject would free a slot the second still names - a premature
|
||||
// free that reappears later as slot theft, which is the exact class this whole band exists
|
||||
// to prevent. The frontend does not share either: each ProgramPipelineObject carries its
|
||||
// own one-slot draw-program cache, so two pipeline objects with identical stage sets are
|
||||
// two composites in the frontend too.
|
||||
TEST(CompositeResolver, TwoPipelinesWithTheSameSignatureKeepTheirOwnComposite) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
|
||||
MakeBoundPipeline(vs, fs);
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle firstCso = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(firstCso));
|
||||
|
||||
MakeBoundPipeline(vs, fs); // a SECOND pipeline object, the same stage set
|
||||
MGPipeProgramEmitterInstance().EmitShaderState(Ctx());
|
||||
const MGPipeHandle secondCso = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(secondCso));
|
||||
|
||||
EXPECT_NE(firstCso, secondCso);
|
||||
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(firstCso.Slot));
|
||||
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(secondCso.Slot));
|
||||
// Both are still resolvable, which is the property a shared handle would have broken.
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, firstCso));
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, secondCso));
|
||||
}
|
||||
|
||||
// THE TWO RELEASE ORDERS, driven at the level where both of them are representable. Either
|
||||
// order frees the slot exactly once and the second call is a PROVEN no-op, because
|
||||
// MGPipeSlotAllocator::Free refuses a slot that is not live at that generation and bumps no
|
||||
// generation of its own - so a double release cannot skip a generation either.
|
||||
TEST(CompositeResolver, EvictionThenDestructionFreesTheSlotExactlyOnce) {
|
||||
ResolverScope scope;
|
||||
constexpr Uint64 kCompositeLifetimeId = 918273645ull;
|
||||
const Uint32 liveBefore = MGPipeSlots().LiveCount(MGPipeKind::ShaderCso);
|
||||
const MGPipeHandle cso = MGPipeSlots().AllocateComposite(kCompositeLifetimeId);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(cso.Slot));
|
||||
ASSERT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore + 1);
|
||||
|
||||
// 1. the pipeline cache drops it
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, cso));
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore);
|
||||
// 2. and then ~ProgramObject runs, and finds nothing to do
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore);
|
||||
}
|
||||
|
||||
TEST(CompositeResolver, DestructionThenEvictionFreesTheSlotExactlyOnce) {
|
||||
ResolverScope scope;
|
||||
constexpr Uint64 kCompositeLifetimeId = 918273646ull;
|
||||
const Uint32 liveBefore = MGPipeSlots().LiveCount(MGPipeKind::ShaderCso);
|
||||
const MGPipeHandle cso = MGPipeSlots().AllocateComposite(kCompositeLifetimeId);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
|
||||
// The mirror order, and it is the one that actually happens today: the frontend's
|
||||
// one-slot cache drops its SharedPtr as it overwrites it, so ~ProgramObject usually
|
||||
// runs first and the resolver's release is the second, redundant path.
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, cso));
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore);
|
||||
|
||||
// And the slot really came back: the next composite is handed the same slot with a
|
||||
// bumped generation, so the stale handle can never resolve to it.
|
||||
const MGPipeHandle recycled = MGPipeSlots().AllocateComposite(kCompositeLifetimeId + 1);
|
||||
EXPECT_EQ(recycled.Slot, cso.Slot);
|
||||
EXPECT_NE(recycled.Gen, cso.Gen);
|
||||
MGPipeSlots().Free(MGPipeKind::ShaderCso, recycled);
|
||||
}
|
||||
|
||||
// THE RELEASE PATH THE TWO CASES ABOVE DO NOT TOUCH. Both of them call the death helper
|
||||
// directly, so the resolver is not in the picture at all and its own release - the
|
||||
// pipeline-cache path, the one the header says exists precisely because "usually" is not a
|
||||
// contract - had no case of its own. This drives it, and it drives it AFTER A
|
||||
// MAKE-CURRENT, which is where it used to be permanently disarmed:
|
||||
//
|
||||
// Reset() cleared the entry's one flag; the next Observe took the reuse branch and
|
||||
// returned before anything could restore it; from then on ReleaseEntry saw !Live and
|
||||
// returned immediately - no delete_shader_state, no Free, no counter movement - and the
|
||||
// old composite's handle was simply overwritten out of the resolver. Nothing leaked
|
||||
// today only because the frontend's one-slot pipeline cache drops the last SharedPtr on
|
||||
// the overwrite, which is exactly the "a client that only reacted to destructors" case
|
||||
// the design refuses to rely on.
|
||||
//
|
||||
// The release obligation now lives in its own flag and a make-current does not touch it.
|
||||
TEST(CompositeResolver, ASignatureMoveAfterAMakeCurrentStillReleasesThroughTheResolver) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
const GLuint pipeline = MakeBoundPipeline(vs, fs);
|
||||
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle first = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(first));
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(first.Slot));
|
||||
ASSERT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, first));
|
||||
ASSERT_EQ(MGPipeCompositeResolverInstance().GetCounters().Releases, 0u);
|
||||
|
||||
// THE MAKE-CURRENT. This is exactly what PipeFill's FreshlyPrimed arm does, and it
|
||||
// reaches MGPipeCompositeResolver::Reset() through the program emitter's own Reset().
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
// ... followed by a draw whose stage set has NOT moved, which is the reuse branch.
|
||||
MGPipeProgramEmitterInstance().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().DrawCso(), first)
|
||||
<< "the same stage set is the same composite and the same handle";
|
||||
EXPECT_GE(MGPipeCompositeResolverInstance().GetCounters().Reuses, 1u);
|
||||
EXPECT_EQ(MGPipeCompositeResolverInstance().GetCounters().Releases, 0u)
|
||||
<< "a reuse releases nothing";
|
||||
|
||||
// NOW THE STAGE SET REALLY MOVES: a different fragment stage program is a different
|
||||
// lifetime id, so ComputeDrawProgramSignature moves and the frontend builds a second
|
||||
// composite. The resolver has to speak the release for the first one.
|
||||
const GLuint fs2 = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs2);
|
||||
GL::UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs2);
|
||||
GL::UseProgram(0);
|
||||
|
||||
MGPipeProgramEmitterInstance().EmitShaderState(Ctx());
|
||||
const MGPipeHandle second = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(second));
|
||||
EXPECT_NE(second, first) << "a moved signature is a second composite with its own handle";
|
||||
EXPECT_EQ(MGPipeCompositeResolverInstance().GetCounters().Releases, 1u)
|
||||
<< "the resolver's own release path, spoken after a make-current";
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, first))
|
||||
<< "and the first composite's slot really went back, exactly once";
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, second));
|
||||
}
|
||||
|
||||
// THE RESOLVER IS A PROCESS SINGLETON AND A PIPELINE's GL NAME IS PER CONTEXT (C2-M1).
|
||||
// GLContext owns m_programPipelines AND its own name generator m_programPipelineNames, so
|
||||
// name N names two different ProgramPipelineObjects in two contexts, each with its own
|
||||
// composite and its own handle. Keyed on the name alone, the first Observe after a
|
||||
// make-current found the OTHER context's entry: the signature matched - two composites of
|
||||
// the same stage set have the same signature, and here two default pipelines have the same
|
||||
// all-zero one - while the handle could not, because two composites are two ProgramObjects
|
||||
// with two lifetime ids. So it fell into ReleaseEntry and emitted delete_shader_state for a
|
||||
// LIVE composite, cleared its publication latch and handed its band slot back while the
|
||||
// frontend ProgramObject was still alive.
|
||||
//
|
||||
// DRIVEN AT THE RESOLVER RATHER THAN THROUGH GL, because one test process has one
|
||||
// GLContext. Everything the case supplies is what the single production call site supplies:
|
||||
// the context id is GLContext::GetTextureContextId()'s value, the two same-named pipeline
|
||||
// objects are what two contexts hold, and the composites are ordinary ProgramObjects at
|
||||
// external index 0 out of the reserved band, exactly as Core.cpp builds them.
|
||||
TEST(CompositeResolver, TwoContextsHoldingOnePipelineNameKeepTheirOwnComposites) {
|
||||
ResolverScope scope;
|
||||
auto& resolver = MGPipeCompositeResolverInstance();
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
|
||||
constexpr Uint kSharedPipelineName = 9u;
|
||||
constexpr Uint64 kContextA = 0x51A00001ull;
|
||||
constexpr Uint64 kContextB = 0x51A00002ull;
|
||||
const ProgramPipelineObject pipelineA{kSharedPipelineName};
|
||||
const ProgramPipelineObject pipelineB{kSharedPipelineName};
|
||||
const SharedPtr<ProgramObject> compositeA = MakeShared<ProgramObject>(0u);
|
||||
const SharedPtr<ProgramObject> compositeB = MakeShared<ProgramObject>(0u);
|
||||
ASSERT_TRUE(MGPipeProgramIsPipelineComposite(*compositeA));
|
||||
ASSERT_EQ(pipelineA.GetExternalIndex(), pipelineB.GetExternalIndex());
|
||||
|
||||
const MGPipeHandle handleA = MGPipeSlots().AllocateComposite(compositeA->GetLifetimeId());
|
||||
const MGPipeHandle handleB = MGPipeSlots().AllocateComposite(compositeB->GetLifetimeId());
|
||||
ASSERT_NE(handleA, handleB);
|
||||
for (const MGPipeHandle handle : {handleA, handleB}) {
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(handle, 0x3u), &link, &spirv);
|
||||
MGPipeNoteHandlePublished(MGPipeKind::ShaderCso, handle);
|
||||
}
|
||||
const Uint64 releasesBefore = resolver.GetCounters().Releases;
|
||||
|
||||
EXPECT_EQ(resolver.Observe(kContextA, pipelineA, *compositeA, handleA), handleA);
|
||||
// THE MAKE-CURRENT, which is what PipeFill's FreshlyPrimed arm reaches through the
|
||||
// program emitter's own Reset()...
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
// ... and then context B draws with ITS pipeline of the same name.
|
||||
EXPECT_EQ(resolver.Observe(kContextB, pipelineB, *compositeB, handleB), handleB);
|
||||
|
||||
EXPECT_EQ(resolver.GetCounters().Releases, releasesBefore)
|
||||
<< "the other context's entry was released - it is not this pipeline's entry";
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, handleA))
|
||||
<< "A's band slot went back while A's composite ProgramObject was still alive";
|
||||
EXPECT_TRUE(MGPipeHandleIsPublished(MGPipeKind::ShaderCso, handleA))
|
||||
<< "delete_shader_state went out for a live composite and cleared its latch";
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, handleB));
|
||||
|
||||
// AND BACK TO A. Its obligation stayed owed to its own context, so the unmoved
|
||||
// signature is a reuse of the same handle and still nothing is released.
|
||||
const Uint64 reusesBefore = resolver.GetCounters().Reuses;
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
EXPECT_EQ(resolver.Observe(kContextA, pipelineA, *compositeA, handleA), handleA);
|
||||
EXPECT_EQ(resolver.GetCounters().Reuses, reusesBefore + 1u);
|
||||
EXPECT_EQ(resolver.GetCounters().Releases, releasesBefore);
|
||||
}
|
||||
|
||||
// WHERE A DESTROYED CONTEXT's ENTRIES ARE RELEASED, and it is not in the resolver. The
|
||||
// context's death drops m_programPipelines, which drops the ProgramPipelineObject, which
|
||||
// drops the composite it cached; ~ProgramObject then runs the one client-side death helper
|
||||
// and the band slot goes back EXACTLY ONCE. The resolver speaks no second delete - the
|
||||
// entry can never be found again, and the allocator has erased the lifetime-id mapping
|
||||
// anyway - and the next make-current DROPS the stranded entry, which is what keeps the
|
||||
// vector bounded now that its key carries the context.
|
||||
TEST(CompositeResolver, ADestroyedContextsEntryIsDroppedRatherThanReleasedASecondTime) {
|
||||
ResolverScope scope;
|
||||
auto& resolver = MGPipeCompositeResolverInstance();
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
|
||||
constexpr Uint kPipelineName = 11u;
|
||||
constexpr Uint64 kDoomedContext = 0x51A00003ull;
|
||||
// ResolverScope's Clear() has already run one Reset(), so every entry standing here has
|
||||
// a live composite slot and nothing but this case's own entry can be swept below.
|
||||
const SizeT sizeBefore = resolver.Size();
|
||||
const Uint32 liveBefore = MGPipeSlots().LiveCount(MGPipeKind::ShaderCso);
|
||||
|
||||
const ProgramPipelineObject pipeline{kPipelineName};
|
||||
SharedPtr<ProgramObject> composite = MakeShared<ProgramObject>(0u);
|
||||
const MGPipeHandle handle = MGPipeSlots().AllocateComposite(composite->GetLifetimeId());
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(handle, 0x3u), &link, &spirv);
|
||||
MGPipeNoteHandlePublished(MGPipeKind::ShaderCso, handle);
|
||||
ASSERT_EQ(resolver.Observe(kDoomedContext, pipeline, *composite, handle), handle);
|
||||
ASSERT_EQ(resolver.Size(), sizeBefore + 1u);
|
||||
const Uint64 releasesBefore = resolver.GetCounters().Releases;
|
||||
const Uint64 sweepsBefore = resolver.GetCounters().Sweeps;
|
||||
|
||||
// THE CONTEXT DIES: the last SharedPtr to its composite goes with its pipeline.
|
||||
composite.reset();
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, handle))
|
||||
<< "~ProgramObject is the release path for this entry and it freed the slot";
|
||||
EXPECT_FALSE(MGPipeHandleIsPublished(MGPipeKind::ShaderCso, handle))
|
||||
<< "and it took the publication latch with the delete";
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore) << "exactly once";
|
||||
EXPECT_EQ(resolver.GetCounters().Releases, releasesBefore)
|
||||
<< "the resolver spoke no release of its own for it";
|
||||
|
||||
// THE NEXT CONTEXT's FIRST VERB. The stranded entry is dropped, not released.
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
EXPECT_EQ(resolver.GetCounters().Sweeps, sweepsBefore + 1u);
|
||||
EXPECT_EQ(resolver.GetCounters().Releases, releasesBefore)
|
||||
<< "a dropped entry emits nothing and frees nothing - the obligation was discharged";
|
||||
EXPECT_EQ(resolver.Size(), sizeBefore)
|
||||
<< "the vector is bounded by the pairs whose composite slot is actually live";
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -358,6 +768,9 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -58,7 +58,15 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2DCube.h>
|
||||
|
||||
#include <algorithm>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -202,6 +210,570 @@ TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// P4a package B's EMITTER-SIDE cases. The contract commit landed the file, its ctest
|
||||
// registration and one shape pin; the wire package's applier-side cases and these are disjoint
|
||||
// TEST bodies in one file, and a collision between them is resolved by UNION, never by
|
||||
// choosing a side.
|
||||
//
|
||||
// EVERY CASE FAILS BY FIELD NAME, never by a bare count: G7's scripted control stops the
|
||||
// conversion copying exactly one member (MGPSurface::Layered) and expects this suite to go red
|
||||
// NAMING that field, and a case that reported only "the records differ" could not answer it.
|
||||
// ============================================================================
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
#define MGL_FRAMEBUFFER_EMIT_CLIENT_TEST_LIST(X) \
|
||||
X(FramebufferEmit, EveryRecordsReadSurfaceComesFromItsOwnFramebuffersReadBuffer) \
|
||||
X(FramebufferEmit, OneObjectBoundToBothTargetsEmitsOneRecordWithTargetBoth) \
|
||||
X(FramebufferEmit, ADrawBufferChangeAloneStillMovesTheContentHash) \
|
||||
X(FramebufferEmit, ARecycledFramebufferHandleIsNeverSuppressedAgainstItsPredecessor) \
|
||||
X(FramebufferEmit, EveryAttachmentFieldSurvivesTheSurfaceConversion) \
|
||||
X(FramebufferEmit, AnAttachmentPointAboveTheWireWidthIsRefusedNotTruncated) \
|
||||
X(FramebufferEmit, ARestoragedAttachedRenderbufferPublishesItsNewExtent) \
|
||||
X(FramebufferEmit, AnUnchangedBindingPairEmitsNothing) \
|
||||
X(FramebufferEmit, AFramebufferHandedOverByNameGetsANamedRecordWithoutMovingABinding) \
|
||||
X(FramebufferEmit, ANamedRecordIsSuppressedPerObjectAndNeverAgainstABoundRecord) \
|
||||
X(FramebufferEmit, ADrawBufferTokenAboveTheWireWidthIsRefusedNotTruncated) \
|
||||
X(FramebufferEmit, ALayeredCubeAttachmentDoesNotAssertAFaceItCannotKnow) \
|
||||
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_FRAMEBUFFER_EMIT_CLIENT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
namespace {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::FramebufferObject;
|
||||
using MG_State::GLState::MipmapInput;
|
||||
using MG_State::GLState::RenderbufferObject;
|
||||
using MG_State::GLState::TextureObject2D;
|
||||
using MG_State::GLState::TextureObject2DCube;
|
||||
|
||||
// AN RAII SCOPE RATHER THAN A gtest FIXTURE, for VertexInputEmitTest's reason, and it arms
|
||||
// BOTH bits this family needs: the framebuffer bit for the emission itself, and the
|
||||
// texture-resource bit because MGPSurface::Res names a texture or renderbuffer handle and
|
||||
// bit 9 requires bit 10 for exactly that reason.
|
||||
struct FramebufferScope {
|
||||
FramebufferScope() {
|
||||
m_previousPush = MG_Config::Features.PipePush;
|
||||
// D-K2, ALL THREE ROWS THAT REACH THIS SUITE, because the client enforces them since
|
||||
// S-3 / ID-41 and not only Espryt's Resolve*SubsystemArm: bit 9 requires bit 10
|
||||
// (MGPSurface::Res names a texture or renderbuffer handle), bit 10 requires bit 11
|
||||
// (MGPTextureParams::BuiltinSampler is a SamplerCso out of the sampler family's
|
||||
// content-addressed cache) and bit 10 requires bit 7 (a buffer texture's
|
||||
// BufferForTexBuffer names a Buffer handle, D-D1). Bit 7 changes nothing else here:
|
||||
// this file constructs no BufferObject.
|
||||
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
|
||||
kMGPipeSubsystemFramebuffer |
|
||||
kMGPipeSubsystemTextureResources |
|
||||
kMGPipeSubsystemSamplers;
|
||||
m_previousContext = Move(MG_State::pGLContext);
|
||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||
MGPipeFramebufferEmitterInstance().ResetForTest();
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
// The applier is a process singleton and its object records outlive a case; with both
|
||||
// families' wired constants set the emissions actually land, so a case must start
|
||||
// from an empty table (v1 armed the texture emitter here instead - ArmForTest is
|
||||
// gone with the flip).
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~FramebufferScope() {
|
||||
MGPipeFramebufferEmitterInstance().ResetForTest();
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pGLContext = Move(m_previousContext);
|
||||
MG_Config::Features.PipePush = m_previousPush;
|
||||
}
|
||||
FramebufferScope(const FramebufferScope&) = delete;
|
||||
FramebufferScope& operator=(const FramebufferScope&) = delete;
|
||||
|
||||
UniquePtr<GLContext> m_previousContext;
|
||||
Uint64 m_previousPush = 0;
|
||||
};
|
||||
|
||||
MGPipeFramebufferEmitter& Framebuffers() { return MGPipeFramebufferEmitterInstance(); }
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
|
||||
SharedPtr<TextureObject2D> MakeColorTexture(Uint name, Int size, Uint levels = 1) {
|
||||
auto texture = MakeShared<TextureObject2D>(name);
|
||||
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
for (Uint level = 0; level < levels; ++level) {
|
||||
const Int extent = std::max<Int>(size >> level, 1);
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, level,
|
||||
MipmapInput{IntVec3{extent, extent, 1},
|
||||
static_cast<SizeT>(extent) * static_cast<SizeT>(extent) * 4});
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void BindDrawAndRead(const SharedPtr<FramebufferObject>& draw, const SharedPtr<FramebufferObject>& read) {
|
||||
Ctx().GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(draw);
|
||||
Ctx().GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(read);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ============================ D-C1 / D-C2 ============================
|
||||
//
|
||||
// THE RESOLVED READ SURFACE IS WHAT STRUCTURALLY CLOSES THE read-buffer-shared-FBO DEFECT
|
||||
// CLASS. The record carries the surface itself rather than an index, and EVERY record resolves
|
||||
// it from the framebuffer ITS OWN Fbo names - Named included (c0e / MGPipeTypes.h). v1 resolved
|
||||
// a DRAW record's ReadSurface from the READ-bound object, which was D-C2's letter and muddled
|
||||
// in substance: no field of this record may refer to "whatever is bound", and a glReadBuffer on
|
||||
// the read FBO moved the draw record's ContentHash and forced a redundant draw emission.
|
||||
TEST(FramebufferEmit, EveryRecordsReadSurfaceComesFromItsOwnFramebuffersReadBuffer) {
|
||||
FramebufferScope scope;
|
||||
const auto drawColor = MakeColorTexture(1, 32);
|
||||
const auto readColor0 = MakeColorTexture(2, 32);
|
||||
const auto readColor1 = MakeColorTexture(3, 32);
|
||||
|
||||
const auto drawFbo = MakeShared<FramebufferObject>(1);
|
||||
drawFbo->AttachTexture(FramebufferAttachmentType::Color0, drawColor, TextureUploadTarget::Texture2D);
|
||||
const auto readFbo = MakeShared<FramebufferObject>(2);
|
||||
readFbo->AttachTexture(FramebufferAttachmentType::Color0, readColor0, TextureUploadTarget::Texture2D);
|
||||
readFbo->AttachTexture(FramebufferAttachmentType::Color1, readColor1, TextureUploadTarget::Texture2D);
|
||||
readFbo->SetReadBuffer(FramebufferAttachmentType::Color1);
|
||||
BindDrawAndRead(drawFbo, readFbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 2u) << "two distinct bindings are two records";
|
||||
|
||||
const MGPipeHandle readColor1Handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, readColor1->GetLifetimeId());
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(readColor1Handle));
|
||||
const MGPipeHandle drawColorHandle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, drawColor->GetLifetimeId());
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(drawColorHandle));
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Draw));
|
||||
EXPECT_TRUE(Framebuffers().LastDraw().ReadSurface.Res == drawColorHandle)
|
||||
<< "the DRAW record's ReadSurface names a surface that is not part of the framebuffer its "
|
||||
"own Fbo names";
|
||||
EXPECT_EQ(Framebuffers().LastRead().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Read));
|
||||
EXPECT_TRUE(Framebuffers().LastRead().ReadSurface.Res == readColor1Handle)
|
||||
<< "MGPFramebufferState::ReadSurface on the READ record did not come from that "
|
||||
"framebuffer's own read buffer";
|
||||
|
||||
// AND THE DRAW RECORD DOES NOT MOVE WHEN THE READ FRAMEBUFFER'S READ BUFFER DOES.
|
||||
const Uint64 drawHashBefore = Framebuffers().LastDraw().ContentHash;
|
||||
const Uint64 emissionsBefore = Framebuffers().EmissionCount();
|
||||
readFbo->SetReadBuffer(FramebufferAttachmentType::Color0);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), emissionsBefore + 1)
|
||||
<< "only the READ record moved, so exactly one record goes out";
|
||||
EXPECT_EQ(Framebuffers().LastDraw().ContentHash, drawHashBefore)
|
||||
<< "a glReadBuffer on the read framebuffer moved the DRAW record's content hash";
|
||||
// And the draw-buffer array belongs to the DRAW object, whichever record carries it.
|
||||
EXPECT_EQ(Framebuffers().LastDraw().DrawBuffers[0], 0);
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, OneObjectBoundToBothTargetsEmitsOneRecordWithTargetBoth) {
|
||||
FramebufferScope scope;
|
||||
const auto color0 = MakeColorTexture(4, 32);
|
||||
const auto color1 = MakeColorTexture(5, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(3);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color0, TextureUploadTarget::Texture2D);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color1, color1, TextureUploadTarget::Texture2D);
|
||||
fbo->SetReadBuffer(FramebufferAttachmentType::Color1);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u) << "one object on both targets is ONE record";
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Both));
|
||||
const MGPipeHandle color1Handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, color1->GetLifetimeId());
|
||||
EXPECT_TRUE(Framebuffers().LastDraw().ReadSurface.Res == color1Handle)
|
||||
<< "the shared-FBO record's ReadSurface is not that object's own read buffer";
|
||||
}
|
||||
|
||||
// ============================ D-C4 ============================
|
||||
//
|
||||
// THE fragColor BROADCAST TRAP. The backend derives the broadcast count from the draw-buffer
|
||||
// array, at the verb, from the framebuffer state it then holds - precisely so a program can
|
||||
// relink inside the same draw. A hash that did not cover the array would let a suppressed
|
||||
// set_framebuffer_state mean "the draw buffers did not move" when they had, and the shader
|
||||
// would be specialised for the previous output shape.
|
||||
TEST(FramebufferEmit, ADrawBufferChangeAloneStillMovesTheContentHash) {
|
||||
FramebufferScope scope;
|
||||
const auto color0 = MakeColorTexture(6, 32);
|
||||
const auto color1 = MakeColorTexture(7, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(4);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color0, TextureUploadTarget::Texture2D);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color1, color1, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
const Uint64 before = Framebuffers().LastDraw().ContentHash;
|
||||
const Int8 slot1Before = Framebuffers().LastDraw().DrawBuffers[1];
|
||||
|
||||
// NOTHING BUT THE DRAW-BUFFER ARRAY MOVES: the same attachments, the same extent, the same
|
||||
// completeness answer, the same handle.
|
||||
fbo->SetDrawBuffer(1, FramebufferAttachmentType::Color1);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 2u)
|
||||
<< "a draw-buffer change alone was suppressed, which would freeze the fragColor "
|
||||
"broadcast count at the previous output shape";
|
||||
EXPECT_NE(Framebuffers().LastDraw().ContentHash, before)
|
||||
<< "MGPFramebufferState::ContentHash does not cover DrawBuffers[]";
|
||||
EXPECT_NE(Framebuffers().LastDraw().DrawBuffers[1], slot1Before);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().DrawBuffers[1], 1);
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, ARecycledFramebufferHandleIsNeverSuppressedAgainstItsPredecessor) {
|
||||
FramebufferScope scope;
|
||||
// The SAME attachment set on both objects, so every other field of the record is identical
|
||||
// and Fbo is the only thing that can move the hash.
|
||||
const auto color = MakeColorTexture(8, 32);
|
||||
Uint64 firstHash = 0;
|
||||
MGPipeHandle firstHandle{};
|
||||
{
|
||||
const auto first = MakeShared<FramebufferObject>(5);
|
||||
first->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(first, first);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
firstHash = Framebuffers().LastDraw().ContentHash;
|
||||
firstHandle = Framebuffers().LastDraw().Fbo;
|
||||
BindDrawAndRead(nullptr, nullptr);
|
||||
}
|
||||
const auto second = MakeShared<FramebufferObject>(5);
|
||||
second->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(second, second);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
const MGPFramebufferState& record = Framebuffers().LastDraw();
|
||||
EXPECT_EQ(record.Fbo.Slot, firstHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
||||
EXPECT_NE(record.Fbo.Gen, firstHandle.Gen) << "a recycled slot must carry a new generation";
|
||||
EXPECT_NE(record.ContentHash, firstHash)
|
||||
<< "MGPFramebufferState::ContentHash does not cover Fbo, so a recycled framebuffer handle "
|
||||
"would be suppressed against its predecessor's record";
|
||||
}
|
||||
|
||||
// ============================ G6 ============================
|
||||
//
|
||||
// "For every framebuffer configuration the emitted MGPFramebufferState reproduces exactly the
|
||||
// values the backend's SyncToBackend family reads from the frontend today, field by field."
|
||||
// The oracle is the frontend attachment itself, read back through the same getters the twin
|
||||
// uses, so this cannot drift into asserting what the emitter happens to do.
|
||||
TEST(FramebufferEmit, EveryAttachmentFieldSurvivesTheSurfaceConversion) {
|
||||
FramebufferScope scope;
|
||||
const auto fbo = MakeShared<FramebufferObject>(6);
|
||||
Vector<SharedPtr<TextureObject2D>> colors;
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
colors.push_back(MakeColorTexture(static_cast<Uint>(20 + i), 32, 2));
|
||||
// Distinct level, layer and layered flag per point, so a conversion that dropped one
|
||||
// field could not be masked by another point's value.
|
||||
fbo->AttachTexture(static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i)),
|
||||
colors.back(), TextureUploadTarget::Texture2D,
|
||||
static_cast<int>(i % 2), static_cast<int>(i), (i % 2) == 0);
|
||||
}
|
||||
const auto depth = MakeShared<RenderbufferObject>(2);
|
||||
depth->SetInternalFormat(TextureInternalFormat::Depth24Stencil8);
|
||||
depth->AllocateStorage(IntVec2{32, 32});
|
||||
fbo->AttachRenderbuffer(FramebufferAttachmentType::Depth, depth);
|
||||
const auto stencil = MakeColorTexture(40, 32);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Stencil, stencil, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
const MGPFramebufferState& record = Framebuffers().LastDraw();
|
||||
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
const auto type = static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
|
||||
const auto& attachment = fbo->GetAttachment(type);
|
||||
const MGPSurface& surface = record.Color[i];
|
||||
const MGPipeHandle expected =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, attachment.GetTexture()->GetLifetimeId());
|
||||
EXPECT_TRUE(surface.Res == expected) << "MGPSurface::Res at colour point " << i;
|
||||
EXPECT_EQ(surface.Kind, kMGPipeSurfaceKindTexture) << "MGPSurface::Kind at colour point " << i;
|
||||
EXPECT_EQ(surface.InternalFormat, static_cast<Uint32>(attachment.GetTexture()->GetFormat()))
|
||||
<< "MGPSurface::InternalFormat at colour point " << i;
|
||||
EXPECT_EQ(surface.Level, static_cast<Uint16>(attachment.GetTextureLevel()))
|
||||
<< "MGPSurface::Level at colour point " << i;
|
||||
EXPECT_EQ(surface.Layer, static_cast<Uint32>(attachment.GetTextureLayer()))
|
||||
<< "MGPSurface::Layer at colour point " << i;
|
||||
EXPECT_EQ(surface.Layered, attachment.IsLayered() ? 1 : 0)
|
||||
<< "MGPSurface::Layered at colour point " << i;
|
||||
EXPECT_EQ(surface.UploadTarget, static_cast<Uint16>(TextureUploadTarget::Texture2D))
|
||||
<< "MGPSurface::UploadTarget at colour point " << i;
|
||||
// ID-12 DV-5: three of the four cross-object masks reduce to (format, TEXTURE TARGET)
|
||||
// and no TextureUploadTarget -> TextureTarget inverse exists anywhere in the tree.
|
||||
EXPECT_EQ(surface.TextureTarget,
|
||||
static_cast<Uint16>(attachment.GetTexture()->GetTarget()))
|
||||
<< "MGPSurface::TextureTarget at colour point " << i;
|
||||
}
|
||||
|
||||
EXPECT_EQ(record.Depth.Kind, kMGPipeSurfaceKindRenderbuffer) << "MGPSurface::Kind on the depth point";
|
||||
EXPECT_EQ(record.Depth.InternalFormat, static_cast<Uint32>(depth->GetInternalFormat()))
|
||||
<< "MGPSurface::InternalFormat on the depth point";
|
||||
EXPECT_TRUE(record.Depth.Res ==
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, depth->GetLifetimeId()))
|
||||
<< "MGPSurface::Res on the depth point";
|
||||
EXPECT_EQ(record.Depth.Layered, 0) << "MGPSurface::Layered on the depth point";
|
||||
EXPECT_EQ(record.Depth.TextureTarget, kMGPipeSurfaceNoTextureTarget)
|
||||
<< "MGPSurface::TextureTarget on the RENDERBUFFER point must be the Unknown sentinel";
|
||||
EXPECT_EQ(record.Stencil.Kind, kMGPipeSurfaceKindTexture) << "MGPSurface::Kind on the stencil point";
|
||||
EXPECT_TRUE(record.Stencil.Res ==
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, stencil->GetLifetimeId()))
|
||||
<< "MGPSurface::Res on the stencil point";
|
||||
|
||||
EXPECT_EQ(record.Width, 32u) << "MGPFramebufferState::Width";
|
||||
EXPECT_EQ(record.Height, 32u) << "MGPFramebufferState::Height";
|
||||
EXPECT_EQ(record.IsDefault, 0) << "MGPFramebufferState::IsDefault";
|
||||
// Complete is FramebufferObject::CheckCompleteness(), the FRONTEND-only answer, and never
|
||||
// glCheckFramebufferStatus's - that entry point additionally consults the backend's probed
|
||||
// format-capability cache, which a client emitting it would be reading from the wrong side.
|
||||
EXPECT_EQ(record.Complete, fbo->CheckCompleteness() ? 1 : 0) << "MGPFramebufferState::Complete";
|
||||
// And an empty point really is {null, None} and every other field zero.
|
||||
const auto emptyFbo = MakeShared<FramebufferObject>(7);
|
||||
emptyFbo->AttachTexture(FramebufferAttachmentType::Color0, colors[0], TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(emptyFbo, emptyFbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Framebuffers().LastDraw().Color[1].Res));
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].Kind, kMGPipeSurfaceKindNone);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].InternalFormat, 0u);
|
||||
// BOTH TARGET FIELDS CARRY THE SENTINEL, not 0 (m6 / ID-12 DV-5): a Uint16 zero is
|
||||
// TextureUploadTarget::Texture1D and TextureTarget::Texture1D, so a reader that forgot to
|
||||
// gate on Kind would read a plausible wrong answer instead of a nonsense one.
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].UploadTarget,
|
||||
static_cast<Uint16>(TextureUploadTarget::Unknown));
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].TextureTarget, kMGPipeSurfaceNoTextureTarget);
|
||||
}
|
||||
|
||||
// ============================ D-C3 ============================
|
||||
TEST(FramebufferEmit, AnAttachmentPointAboveTheWireWidthIsRefusedNotTruncated) {
|
||||
FramebufferScope scope;
|
||||
const auto color = MakeColorTexture(50, 32);
|
||||
const auto beyond = MakeColorTexture(51, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(8);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
ASSERT_EQ(Framebuffers().RefusedCount(), 0u);
|
||||
|
||||
// MGPFramebufferState::Color[] is eight wide and MaxColorAttachments is the driver's raw ES
|
||||
// cap, not clamped to eight on the GLES path. Truncating silently is the bug class this
|
||||
// phase is closing, so the record is REFUSED and the legacy arm runs.
|
||||
fbo->AttachTexture(static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||
static_cast<Int>(kMGPipeMaxColorAttachments)),
|
||||
beyond, TextureUploadTarget::Texture2D);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u)
|
||||
<< "an attachment above the wire width was truncated into a record instead of refused";
|
||||
EXPECT_GE(Framebuffers().RefusedCount(), 1u) << "the refusal was not counted";
|
||||
}
|
||||
|
||||
// ============================ D-D2 ============================
|
||||
//
|
||||
// RED BEFORE THIS PACKAGE LANDED. RenderbufferObject's three storage setters bump no version
|
||||
// and raise no notice, and the framebuffer bit's shutter does not move when an ALREADY-ATTACHED
|
||||
// renderbuffer is re-storaged - so `glBindRenderbuffer; glRenderbufferStorage(newSize)` on an
|
||||
// attached renderbuffer published nothing at all.
|
||||
TEST(FramebufferEmit, ARestoragedAttachedRenderbufferPublishesItsNewExtent) {
|
||||
FramebufferScope scope;
|
||||
const auto renderbuffer = MakeShared<RenderbufferObject>(3);
|
||||
renderbuffer->SetInternalFormat(TextureInternalFormat::Depth24Stencil8);
|
||||
renderbuffer->AllocateStorage(IntVec2{64, 64});
|
||||
const auto fbo = MakeShared<FramebufferObject>(9);
|
||||
fbo->AttachRenderbuffer(FramebufferAttachmentType::Depth, renderbuffer);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId());
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
const Uint64 respecifiesBefore = MGPipeTextureEmitterInstance().RespecifyCount();
|
||||
|
||||
// NO BIND, NO ATTACHMENT CHANGE, NO DIRTY BIT - only the storage entry point.
|
||||
renderbuffer->AllocateStorage(IntVec2{128, 96});
|
||||
EXPECT_GT(MGPipeTextureEmitterInstance().RespecifyCount(), respecifiesBefore)
|
||||
<< "a re-storaged attached renderbuffer published nothing";
|
||||
const MGPResourceDesc desc = MGPipeTextureEmitterInstance().LastDesc();
|
||||
EXPECT_TRUE(desc.Resource == handle);
|
||||
EXPECT_EQ(desc.Width, 128u);
|
||||
EXPECT_EQ(desc.Height, 96u);
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, AnUnchangedBindingPairEmitsNothing) {
|
||||
FramebufferScope scope;
|
||||
const auto color = MakeColorTexture(60, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(10);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
const Uint64 bytes = Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(bytes, sizeof(MGPFramebufferState));
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
// The version-first skip: nothing moved, so nothing goes out and nothing is hashed twice.
|
||||
EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), 0u);
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
}
|
||||
|
||||
// ============================ ID-19(c) ============================
|
||||
//
|
||||
// THE HOLE esprytobj's C-1 FOUND. The applier keeps framebuffer records PER OBJECT, but v1 only
|
||||
// ever built the two BOUND-target records at the validate point - so glClearNamedFramebufferfv
|
||||
// on an fbo bound to NEITHER binding reached a backend that minted a driver framebuffer with no
|
||||
// attachments, found no record for it, declined, and cleared against it anyway.
|
||||
TEST(FramebufferEmit, AFramebufferHandedOverByNameGetsANamedRecordWithoutMovingABinding) {
|
||||
FramebufferScope scope;
|
||||
const auto boundColor = MakeColorTexture(70, 32);
|
||||
const auto namedColor = MakeColorTexture(71, 16);
|
||||
const auto bound = MakeShared<FramebufferObject>(20);
|
||||
bound->AttachTexture(FramebufferAttachmentType::Color0, boundColor, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(bound, bound);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
const MGPipeHandle boundHandle = MGPipeApplier().DrawFramebuffer()->Fbo;
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
// The object the DSA entry point is about to hand over, bound to neither binding.
|
||||
const auto named = MakeShared<FramebufferObject>(21);
|
||||
named->AttachTexture(FramebufferAttachmentType::Color0, namedColor, TextureUploadTarget::Texture2D);
|
||||
const Uint64 bytes = Framebuffers().EmitFramebufferByName(*named);
|
||||
EXPECT_EQ(bytes, sizeof(MGPFramebufferState));
|
||||
EXPECT_EQ(Framebuffers().LastNamed().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Named));
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, serialBefore) << "a Named record must publish";
|
||||
|
||||
const MGPFramebufferState* stored =
|
||||
MGPipeApplier().FramebufferRecordFor(Framebuffers().LastNamed().Fbo);
|
||||
ASSERT_NE(stored, nullptr) << "the framebuffer the server is about to receive by name has no record";
|
||||
EXPECT_EQ(stored->Width, 16u) << "the Named record does not describe the object it names";
|
||||
|
||||
// AND IT MOVED NEITHER BINDING.
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_TRUE(MGPipeApplier().DrawFramebuffer()->Fbo == boundHandle)
|
||||
<< "a Named record took the draw binding";
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_TRUE(MGPipeApplier().ReadFramebuffer()->Fbo == boundHandle)
|
||||
<< "a Named record took the read binding";
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, ANamedRecordIsSuppressedPerObjectAndNeverAgainstABoundRecord) {
|
||||
FramebufferScope scope;
|
||||
const auto colorA = MakeColorTexture(72, 32);
|
||||
const auto colorB = MakeColorTexture(73, 32);
|
||||
const auto first = MakeShared<FramebufferObject>(22);
|
||||
first->AttachTexture(FramebufferAttachmentType::Color0, colorA, TextureUploadTarget::Texture2D);
|
||||
const auto second = MakeShared<FramebufferObject>(23);
|
||||
second->AttachTexture(FramebufferAttachmentType::Color0, colorB, TextureUploadTarget::Texture2D);
|
||||
|
||||
// TWO DIFFERENT OBJECTS' Named RECORDS IN A ROW MUST BOTH GO OUT: the suppressor is keyed by
|
||||
// the framebuffer the record names and never by one global slot.
|
||||
EXPECT_GT(Framebuffers().EmitFramebufferByName(*first), 0u);
|
||||
EXPECT_GT(Framebuffers().EmitFramebufferByName(*second), 0u);
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 2u);
|
||||
// A repeat of the same object with nothing moved costs nothing.
|
||||
EXPECT_EQ(Framebuffers().EmitFramebufferByName(*second), 0u);
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 2u);
|
||||
// ...and the first object's record is still its own, not the second's.
|
||||
EXPECT_EQ(Framebuffers().EmitFramebufferByName(*first), 0u);
|
||||
|
||||
// A BOUND OBJECT IS NEVER HANDED A Named RECORD, because the record would then say "no
|
||||
// binding" while BoundFramebuffer still resolves through it.
|
||||
BindDrawAndRead(first, nullptr);
|
||||
Framebuffers().EmitFramebufferByName(*first);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Target, static_cast<Uint8>(MGPipeFramebufferTarget::Draw))
|
||||
<< "a by-name publish of a DRAW-bound framebuffer overwrote its record with Target = Named";
|
||||
}
|
||||
|
||||
// ============================ m2 ============================
|
||||
TEST(FramebufferEmit, ADrawBufferTokenAboveTheWireWidthIsRefusedNotTruncated) {
|
||||
FramebufferScope scope;
|
||||
const auto color = MakeColorTexture(74, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(24);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
ASSERT_EQ(Framebuffers().RefusedCount(), 0u);
|
||||
|
||||
// A LEGAL STATE THE ATTACHMENT SCAN CANNOT SEE: nothing is attached at the point the token
|
||||
// names, so D-C3's attachment loop passes, and MGPipeDrawBufferIndex would write an index of
|
||||
// 8 or more into an 8-wide array.
|
||||
fbo->SetDrawBuffer(0, static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||
static_cast<Int>(kMGPipeMaxColorAttachments) + 2));
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u)
|
||||
<< "a draw-buffer token naming a colour point at or above the wire width was truncated "
|
||||
"into a record instead of refused";
|
||||
EXPECT_GE(Framebuffers().RefusedCount(), 1u) << "the refusal was not counted";
|
||||
}
|
||||
|
||||
// ============================ m1 ============================
|
||||
TEST(FramebufferEmit, ALayeredCubeAttachmentDoesNotAssertAFaceItCannotKnow) {
|
||||
FramebufferScope scope;
|
||||
const auto cube = MakeShared<TextureObject2DCube>(75);
|
||||
cube->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
for (const TextureUploadTarget face : cube->GetUploadTargets()) {
|
||||
cube->AllocateStorage(face, 0, MipmapInput{IntVec3{16, 16, 1}, 16 * 16 * 4});
|
||||
}
|
||||
ASSERT_GT(cube->GetUploadTargets().size(), 1u);
|
||||
|
||||
const auto fbo = MakeShared<FramebufferObject>(25);
|
||||
// The LAYERED entry point carries no face token, so the attachment stores Unknown. v1 fell
|
||||
// back to GetUploadTargets()[0] - which is CubeMapPositiveX - and the record then ASSERTED a
|
||||
// face that is not the truth for an attachment naming all six. The precedent it copied
|
||||
// (FramebufferAttachmentObject::GetSize) only needs an EXTENT, which is identical across the
|
||||
// six faces; face identity is not.
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, cube, TextureUploadTarget::Unknown, 0, 0, true);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[0].Layered, 1);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[0].UploadTarget,
|
||||
static_cast<Uint16>(TextureUploadTarget::Unknown))
|
||||
<< "a layered cube attachment resolved to one face, so the record asserts a face the "
|
||||
"attachment does not name";
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[0].TextureTarget,
|
||||
static_cast<Uint16>(TextureTarget::TextureCubeMap))
|
||||
<< "MGPSurface::TextureTarget is what a cross-object mask reads instead";
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero) {
|
||||
FramebufferScope scope;
|
||||
const auto color = MakeColorTexture(76, 32);
|
||||
const auto depth = MakeShared<RenderbufferObject>(4);
|
||||
depth->SetInternalFormat(TextureInternalFormat::Depth24Stencil8);
|
||||
depth->AllocateStorage(IntVec2{32, 32});
|
||||
const auto fbo = MakeShared<FramebufferObject>(26);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
fbo->AttachRenderbuffer(FramebufferAttachmentType::Depth, depth);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
const MGPFramebufferState& record = Framebuffers().LastDraw();
|
||||
|
||||
// The RENDERBUFFER point, the EMPTY colour points and the STENCIL point that names nothing.
|
||||
for (const MGPSurface* surface : {&record.Depth, &record.Stencil, &record.Color[1]}) {
|
||||
EXPECT_NE(surface->Kind, kMGPipeSurfaceKindTexture);
|
||||
EXPECT_EQ(surface->UploadTarget, static_cast<Uint16>(TextureUploadTarget::Unknown))
|
||||
<< "a non-texture point says TextureUploadTarget::Texture1D, which is what 0 means";
|
||||
EXPECT_EQ(surface->TextureTarget, kMGPipeSurfaceNoTextureTarget)
|
||||
<< "a non-texture point says TextureTarget::Texture1D, which is what 0 means";
|
||||
}
|
||||
// And the texture point is unaffected.
|
||||
EXPECT_EQ(record.Color[0].TextureTarget, static_cast<Uint16>(TextureTarget::Texture2D));
|
||||
|
||||
// THE FIELD IS IN THE HASH, so a texture-target change alone cannot be suppressed.
|
||||
MGPSurface probe = record.Color[0];
|
||||
MGPFramebufferState probeState = record;
|
||||
probeState.Color[0].TextureTarget = static_cast<Uint16>(TextureTarget::TextureRectangle);
|
||||
EXPECT_NE(MGPipeFramebufferStateContentHash(probeState), MGPipeFramebufferStateContentHash(record))
|
||||
<< "MGPipeCopySurfaceForHash does not copy MGPSurface::TextureTarget, so a record whose "
|
||||
"only moved field is the attachment's texture target would be suppressed";
|
||||
(void)probe;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of set_framebuffer_state (the wire commits'). The emitter's half - the
|
||||
// resolved read surface, the draw-buffer array in the content hash, a recycled handle never
|
||||
@@ -575,6 +1147,20 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ID-39: A BACKEND IS PRESENT, for the whole binary. Since ID-39 every P4a-family entry
|
||||
// point in MG_Pipe/PipeApply.cpp declines a record - and the client's gate in
|
||||
// MG_Impl/Pipe/PipeFill.cpp emits none at all - when no backend has registered
|
||||
// MGPipeResourceOps, because acceptance is a contract with the emitter and an accepted
|
||||
// record nothing reads makes the client clear work the legacy pull path still owed. Every
|
||||
// case in this suite is about the arm where a backend DOES consume the records, which is
|
||||
// the shipped DirectGLES configuration, so it installs the same signal that backend
|
||||
// installs. The table is empty because none of its hooks is on a framebuffer path at all:
|
||||
// set_framebuffer_state stores a record and dispatches nothing. The two arms of the rule
|
||||
// itself are pinned in ResourceEmitTest and TextureEmitTest.
|
||||
static const MGPipeResourceOps kConsumerPresent{};
|
||||
MGPipeSetResourceOps(&kConsumerPresent);
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -6,20 +6,16 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P4a's image-unit set: set_shader_images, the third of the three kVarTail unit sets. It rides
|
||||
// the sampler family's subsystem bit - one family, one A/B - and has its own suite because its
|
||||
// content hash has to cover two fields the other two sets do not carry.
|
||||
// P4a's third kVarTail unit set, set_shader_images. It rides the sampler family's subsystem
|
||||
// bit: one family, one A/B.
|
||||
//
|
||||
// THE TWO CASES THIS SUITE EXISTS FOR: an ACCESS-mode change alone, and an INTERNAL-FORMAT
|
||||
// change alone, each has to move the hash and emit the set. Both are live glBindImageTexture
|
||||
// state, the format-less image bake keys on the format the shader was built against, and a
|
||||
// hash over the bindings alone would suppress exactly the record that says the bake is stale.
|
||||
// The behavioural gates beside them are the format-less bake and non-core-format scenarios,
|
||||
// and the photon fixture on desktop retrace - the only fixture that has ever caught an
|
||||
// image-binding-semantics regression, and one that must never be run on the Adreno.
|
||||
// WHAT THIS SUITE IS FOR. The image set is the one whose ContentHash has to cover more than
|
||||
// the binding: InternalFormat and Access are live glBindImageTexture state that the
|
||||
// format-less image bake keys on, so a set whose only movement is an access mode still has to
|
||||
// go out. And the zero early-out is the property an optimisation deletes by accident - it is
|
||||
// what makes every draw of every application that never binds an image pay one integer test.
|
||||
//
|
||||
// THE SUITE IS `ImageEmit`, not `ImageEmitTest`: the file is XTest.cpp and the suite is X,
|
||||
// this directory's convention, and it is what the gates grep for.
|
||||
// THE SUITE IS `ImageEmit`, not `ImageEmitTest`: the file is XTest.cpp and the suite is X.
|
||||
//
|
||||
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT.
|
||||
//
|
||||
@@ -48,8 +44,14 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include "Init.h"
|
||||
#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/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -67,13 +69,24 @@ namespace {
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
// `from` is a byte offset, and it exists because of the fork below: the library's log file
|
||||
// is already open by the time a case runs, so the child's lines are APPENDED to it rather
|
||||
// than written to a fresh file, and only what the child appended is this drive's evidence.
|
||||
std::string ReadLog(std::streamoff from = 0) {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
if (from > 0) in.seekg(from, std::ios::beg);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Where the library's log file currently ends. Reading from here after the child has
|
||||
// aborted gives exactly the lines that drive produced.
|
||||
std::streamoff LogEnd() {
|
||||
std::ifstream in(g_logPath, std::ios::binary | std::ios::ate);
|
||||
return in ? static_cast<std::streamoff>(in.tellg()) : std::streamoff{0};
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
@@ -99,8 +112,14 @@ namespace {
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
// THE LOG PATH IS NOT UNLINKED HERE, and that is what this helper had to learn when the
|
||||
// client's cases landed in the same file as the applier's: main() calls
|
||||
// MobileGL::Initialize(), so the library's log FILE* is already open on this path and
|
||||
// fork() duplicates it. Removing the path would leave the child writing into a deleted
|
||||
// inode and the parent reading an empty file - the child would still abort, and the
|
||||
// assertion on WHAT it named could never see the line. So the log's end is remembered
|
||||
// and only what the child appended is read back.
|
||||
const std::streamoff before = LogEnd();
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
@@ -111,7 +130,7 @@ namespace {
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
result.Log = ReadLog(before);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -157,10 +176,9 @@ namespace {
|
||||
TEST(ImageEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
EXPECT_EQ(&MGPipeImageEmitterInstance(), &MGPipeImageEmitterInstance());
|
||||
// The image set's window is bounded by the same merged unit space the other two sets use;
|
||||
// there is no separate image-unit capacity and there must not be one, because a record
|
||||
// whose window is checked against a different bound from the array it indexes is the shape
|
||||
// the applier's Fatal{ProtocolCorruption} exists to make impossible.
|
||||
// The image set has no bit of its own: set_shader_images rides the SAMPLER subsystem,
|
||||
// because the three unit sets are one family and an operator switching them off has to get
|
||||
// the whole family's legacy arm.
|
||||
EXPECT_EQ(kMGPipeMaxImageUnits, kMGPipeMaxTextureUnits);
|
||||
#else
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||
@@ -323,6 +341,184 @@ TEST(ImageEmit, AMakeCurrentClearsTheImageSetAndAdvancesItsSerial) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name.
|
||||
#define MGL_IMAGE_EMIT_TEST_LIST(X) \
|
||||
X(ImageEmit, AZeroHighWaterMarkEmitsNothingWithoutHashing) \
|
||||
X(ImageEmit, AnAccessModeChangeAloneStillEmitsTheSet) \
|
||||
X(ImageEmit, AnInternalFormatChangeAloneStillEmitsTheSet) \
|
||||
X(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_IMAGE_EMIT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
struct EmitterScope {
|
||||
EmitterScope() { Clear(); }
|
||||
~EmitterScope() { Clear(); }
|
||||
EmitterScope(const EmitterScope&) = delete;
|
||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeImageEmitterInstance().Reset();
|
||||
MGPipeImageEmitterInstance().ResetCounters();
|
||||
MGPipeProgramOpaqueUnitsShared().Invalidate();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
MGPipeImageEmitter& Emitter() { return MGPipeImageEmitterInstance(); }
|
||||
|
||||
GLuint MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = GL::CreateShader(GL_COMPUTE_SHADER);
|
||||
GL::ShaderSource(shader, 1, &source, nullptr);
|
||||
GL::CompileShader(shader);
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::AttachShader(program, shader);
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the compute program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeImageTexture() {
|
||||
GLuint name = 0;
|
||||
GL::GenTextures(1, &name);
|
||||
GL::BindTexture(GL_TEXTURE_2D, name);
|
||||
GL::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||
return name;
|
||||
}
|
||||
|
||||
// PROPERTY 1 OF D-G4, and it is the one an optimisation deletes: an application that never
|
||||
// binds an image pays one integer test per draw, taken BEFORE any hash and before any
|
||||
// 192-entry walk.
|
||||
//
|
||||
// The frontend has no image-unit high-water mark of its own - NoteUnitTouched is the
|
||||
// TEXTURE-unit path and glBindImageTexture does not reach it - so the window is derived
|
||||
// from the highest image unit the CURRENT PROGRAM names. A program with no image uniform
|
||||
// names none, and the set is not emitted at all.
|
||||
TEST(ImageEmit, AZeroHighWaterMarkEmitsNothingWithoutHashing) {
|
||||
EmitterScope scope;
|
||||
static const char* kNoImages = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Out { uint value; } outBuf;
|
||||
void main() { outBuf.value = 1u; }
|
||||
)";
|
||||
const GLuint program = MakeComputeProgram(kNoImages);
|
||||
GL::UseProgram(program);
|
||||
|
||||
// A texture IS bound to an image unit. The set still does not go out, because no
|
||||
// shader can read it - which is the whole point of deriving the window from the
|
||||
// program rather than walking 192 units to find out.
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
|
||||
EXPECT_EQ(Emitter().EmitShaderImages(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().ImageSetCount(), 0u);
|
||||
EXPECT_EQ(Emitter().Window(), 0u);
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
const char* kImageCompute = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 1, rgba8) uniform image2D img;
|
||||
void main() { imageStore(img, ivec2(0), vec4(1.0)); }
|
||||
)";
|
||||
|
||||
// The record carries the APPLICATION's format and access verbatim. The bind-format recast -
|
||||
// a GL_RG32F bind is INVALID_VALUE on most non-core formats on Adreno - and the
|
||||
// buffer-texture split view are SERVER-side and stay there, so a client that pre-applied
|
||||
// either of them would be answering a driver question from the wrong side of the boundary.
|
||||
TEST(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast) {
|
||||
EmitterScope scope;
|
||||
const GLuint program = MakeComputeProgram(kImageCompute);
|
||||
GL::UseProgram(program);
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
|
||||
|
||||
// THE FIXTURE HAS TO SET UP WHAT THE CASE IS ABOUT, and it is asserted rather than
|
||||
// assumed: the window comes from the program's own image uniforms, so a shader whose
|
||||
// image uniform did not reach the reflection would make every case below pass for the
|
||||
// wrong reason - by emitting nothing at all.
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& object = Ctx().GetProgramObject(program);
|
||||
ASSERT_TRUE(object);
|
||||
const auto& resolution = MGPipeProgramOpaqueUnitsShared().For(object.get());
|
||||
ASSERT_EQ(resolution.MaxImageUnit, 1)
|
||||
<< "maxUniformLocation=" << object->GetMaxUniformLocation()
|
||||
<< " unit@0=" << object->GetUniformSamplerOrImageUnitIndex(0)
|
||||
<< " linked=" << object->GetLinkStatus();
|
||||
|
||||
// ASSERTED ON THE COUNTER, NOT ON THIS CALL'S RETURN VALUE, and the reason is worth
|
||||
// writing down because it surprised this suite: the glBindImageTexture above ALREADY
|
||||
// reached the validate point and emitted the set, so a direct call afterwards is
|
||||
// correctly suppressed as unchanged. What the case is about is what went out, not who
|
||||
// sent it.
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
ASSERT_GE(Emitter().ImageSetCount(), 1u);
|
||||
ASSERT_GE(Emitter().LastShaderImages().Count, 2u);
|
||||
const MGPImageView& view = Emitter().LastImageViews()[1];
|
||||
EXPECT_EQ(view.Unit, 1u);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(view.Res));
|
||||
EXPECT_EQ(view.InternalFormat, static_cast<Uint32>(GL_RGBA8));
|
||||
EXPECT_EQ(view.Access, 1u) << "GL_WRITE_ONLY, folded into the one byte the wire carries";
|
||||
EXPECT_EQ(view.Level, 0u);
|
||||
EXPECT_EQ(view.Layered, 0u);
|
||||
EXPECT_EQ(view.Layer, 0u);
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
TEST(ImageEmit, AnAccessModeChangeAloneStillEmitsTheSet) {
|
||||
EmitterScope scope;
|
||||
const GLuint program = MakeComputeProgram(kImageCompute);
|
||||
GL::UseProgram(program);
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
const Uint64 before = Emitter().ImageSetCount();
|
||||
ASSERT_GE(before, 1u);
|
||||
ASSERT_EQ(Emitter().LastImageViews()[1].Access, 0u) << "GL_READ_ONLY";
|
||||
|
||||
// The same texture, the same unit, the same format - only the access mode moves. The
|
||||
// hash has to cover it, or a shader that now writes where it used to read is bound with
|
||||
// the previous barrier and coherence semantics.
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
EXPECT_GT(Emitter().ImageSetCount(), before);
|
||||
EXPECT_EQ(Emitter().LastImageViews()[1].Access, 2u);
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
TEST(ImageEmit, AnInternalFormatChangeAloneStillEmitsTheSet) {
|
||||
EmitterScope scope;
|
||||
const GLuint program = MakeComputeProgram(kImageCompute);
|
||||
GL::UseProgram(program);
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
const Uint64 before = Emitter().ImageSetCount();
|
||||
ASSERT_GE(before, 1u);
|
||||
ASSERT_EQ(Emitter().LastImageViews()[1].InternalFormat, static_cast<Uint32>(GL_RGBA8));
|
||||
|
||||
// The format the shader was built against is live glBindImageTexture state, and the
|
||||
// format-less image bake keys on it: a set suppressed because "the binding did not
|
||||
// move" would leave the server baking against the previous format.
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8UI);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
EXPECT_GT(Emitter().ImageSetCount(), before);
|
||||
EXPECT_EQ(Emitter().LastImageViews()[1].InternalFormat, static_cast<Uint32>(GL_RGBA8UI));
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -334,6 +530,9 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -6,20 +6,23 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P4a's program family: create/bind/delete_shader_state, set_draw_program,
|
||||
// P4a's program family on the client: create/bind/delete_shader_state, set_draw_program,
|
||||
// set_dispatch_program and set_global_constants.
|
||||
//
|
||||
// THE ONE PIN THAT IS EASIEST TO LOSE AND WORST TO LOSE: set_global_constants' Version is
|
||||
// GetUBOContentVersion(), and ~0u is the BACKENDS' "never uploaded" sentinel - the wrap skips
|
||||
// it - so the client must never emit it. A record carrying the sentinel would tell a backend
|
||||
// that a block it has just been handed was never uploaded.
|
||||
// THE TWO PROPERTIES THIS SUITE EXISTS FOR, and both are invisible from the emitted bytes:
|
||||
// * THE STAGE MASK COMES FROM THE LINKED SNAPSHOT, never from the live attach list.
|
||||
// glAttachShader and glCompileShader take effect only at the NEXT link and neither moves
|
||||
// the link version, so a descriptor built from the attach list describes a program that
|
||||
// does not exist yet - and it would agree with nothing, because the SPIR-V array beside it
|
||||
// is indexed by the snapshot.
|
||||
// * THE EMITTER JOINS AND THE TRACKER DOES NOT. Bit 6's shutter reads GetCurrentProgram()
|
||||
// deliberately and not GetProgramForDraw(), because the tracker must not force a compile
|
||||
// just to answer "did the shader move"; the join belongs to the emitter, which makes the
|
||||
// same call the verb is about to make anyway.
|
||||
//
|
||||
// THE SUITE IS `ProgramEmit`, not `ProgramEmitTest`: the file is XTest.cpp and the suite is X,
|
||||
// this directory's convention, and it is what the gates grep for.
|
||||
// THE SUITE IS `ProgramEmit`, not `ProgramEmitTest`: the file is XTest.cpp and the suite is X.
|
||||
//
|
||||
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT: the
|
||||
// applier-side cases are the wire commits' and the emitter-side cases are the client
|
||||
// package's, and neither has to come back to MG_Test/Pipe/CMakeLists.txt to add one.
|
||||
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT.
|
||||
//
|
||||
// IT HAS ITS OWN main() for ResourceEmitTest's reason. Every case is a visible SKIP in a pull
|
||||
// build rather than a vanishing test, so `ctest -N` stays name-for-name identical between the
|
||||
@@ -46,12 +49,17 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// The applier takes the two artefact structs BY POINTER beside the record, so a case that
|
||||
// drives create_shader_state needs their definitions - the applier's own header deliberately
|
||||
// only forward-declares them.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -69,13 +77,24 @@ namespace {
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
// `from` is a byte offset, and it exists because of the fork below: the library's log file
|
||||
// is already open by the time a case runs, so the child's lines are APPENDED to it rather
|
||||
// than written to a fresh file, and only what the child appended is this drive's evidence.
|
||||
std::string ReadLog(std::streamoff from = 0) {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
if (from > 0) in.seekg(from, std::ios::beg);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Where the library's log file currently ends. Reading from here after the child has
|
||||
// aborted gives exactly the lines that drive produced.
|
||||
std::streamoff LogEnd() {
|
||||
std::ifstream in(g_logPath, std::ios::binary | std::ios::ate);
|
||||
return in ? static_cast<std::streamoff>(in.tellg()) : std::streamoff{0};
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
@@ -101,8 +120,14 @@ namespace {
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
// THE LOG PATH IS NOT UNLINKED HERE, and that is what this helper had to learn when the
|
||||
// client's cases landed in the same file as the applier's: main() calls
|
||||
// MobileGL::Initialize(), so the library's log FILE* is already open on this path and
|
||||
// fork() duplicates it. Removing the path would leave the child writing into a deleted
|
||||
// inode and the parent reading an empty file - the child would still abort, and the
|
||||
// assertion on WHAT it named could never see the line. So the log's end is remembered
|
||||
// and only what the child appended is read back.
|
||||
const std::streamoff before = LogEnd();
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
@@ -113,7 +138,7 @@ namespace {
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
result.Log = ReadLog(before);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -161,10 +186,6 @@ TEST(ProgramEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
EXPECT_EQ(&MGPipeProgramEmitterInstance(), &MGPipeProgramEmitterInstance());
|
||||
EXPECT_TRUE(kMGPipeWiredProgramSubsystem == 0 ||
|
||||
kMGPipeWiredProgramSubsystem == kMGPipeSubsystemPrograms);
|
||||
// The record the applier starts from carries the sentinel, not 0: a program that has never
|
||||
// published a default-uniform-block image must not look like one that published version 0.
|
||||
const MGPipeShaderCsoRecord fresh{};
|
||||
EXPECT_EQ(fresh.GlobalConstantsVersion, ~Uint32{0});
|
||||
#else
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||
#endif
|
||||
@@ -471,6 +492,291 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name.
|
||||
#define MGL_PROGRAM_EMIT_TEST_LIST(X) \
|
||||
X(ProgramEmit, TheStageMaskComesFromTheLinkedSnapshotAndNotTheAttachList) \
|
||||
X(ProgramEmit, TheNeverUploadedSentinelIsNeverEmitted) \
|
||||
X(ProgramEmit, TheEmitterJoinsAndTheTrackerDoesNot) \
|
||||
X(ProgramEmit, AReLinkReIssuesOnTheSameHandle) \
|
||||
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
|
||||
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) \
|
||||
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_PROGRAM_EMIT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::ProgramObject;
|
||||
|
||||
struct EmitterScope {
|
||||
EmitterScope() { Clear(); }
|
||||
~EmitterScope() {
|
||||
GL::UseProgram(0);
|
||||
Clear();
|
||||
}
|
||||
EmitterScope(const EmitterScope&) = delete;
|
||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().ResetCounters();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
MGPipeProgramEmitter& Emitter() { return MGPipeProgramEmitterInstance(); }
|
||||
|
||||
const char* kVs = R"(#version 430 core
|
||||
uniform vec4 u_value;
|
||||
void main() { gl_Position = u_value; }
|
||||
)";
|
||||
const char* kFs = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0); }
|
||||
)";
|
||||
const char* kGs = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() { gl_Position = vec4(0.0); EmitVertex(); }
|
||||
)";
|
||||
|
||||
GLuint MakeShader(GLenum stage, const char* source) {
|
||||
const GLuint shader = GL::CreateShader(stage);
|
||||
GL::ShaderSource(shader, 1, &source, nullptr);
|
||||
GL::CompileShader(shader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint MakeVsFsProgram() {
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::AttachShader(program, MakeShader(GL_VERTEX_SHADER, kVs));
|
||||
GL::AttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFs));
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the vertex/fragment program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
constexpr Uint32 StageBit(ShaderStage stage) { return Uint32{1} << static_cast<Uint32>(stage); }
|
||||
|
||||
// ProgramObject.h says it in as many words and this is the case that holds it: a stage mask
|
||||
// built from the ATTACH list would describe a program that does not exist yet, because
|
||||
// glAttachShader takes effect only at the next link and does not move the link version.
|
||||
// The SPIR-V array beside the mask is indexed by the same snapshot, so the two halves of
|
||||
// the descriptor agree by construction rather than by care.
|
||||
TEST(ProgramEmit, TheStageMaskComesFromTheLinkedSnapshotAndNotTheAttachList) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
const Uint32 linkedMask = MGPipeStageMaskOf(*program);
|
||||
EXPECT_EQ(linkedMask, StageBit(ShaderStage::Vertex) | StageBit(ShaderStage::Fragment));
|
||||
|
||||
// A third stage is attached and NOT linked. The live attach list now has three
|
||||
// shaders; the mask must not move.
|
||||
GL::AttachShader(name, MakeShader(GL_GEOMETRY_SHADER, kGs));
|
||||
EXPECT_EQ(program->GetAttachedShaders().size(), 3u) << "the attach really has to land";
|
||||
EXPECT_EQ(MGPipeStageMaskOf(*program), linkedMask)
|
||||
<< "glAttachShader takes effect at the NEXT link and moves no link version";
|
||||
|
||||
GL::LinkProgram(name);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(name, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_TRUE) {
|
||||
EXPECT_EQ(MGPipeStageMaskOf(*program), linkedMask | StageBit(ShaderStage::Geometry))
|
||||
<< "and after the relink the snapshot really does carry it";
|
||||
}
|
||||
}
|
||||
|
||||
// D-H6. ~0u is the backends' "never uploaded" sentinel and the frontend's own wrap skips
|
||||
// it; the client must never put it on the wire either, or a server would read its own
|
||||
// record as "nothing has ever been uploaded here" and re-upload for ever.
|
||||
//
|
||||
// PINNED AS A PREDICATE rather than by driving the counter to ~0u, and the reason is
|
||||
// written down instead of hidden: reaching that value takes four billion
|
||||
// MarkUBOContentDirty calls, which is not a test. The predicate is the thing
|
||||
// EmitGlobalConstants consults, so pinning it pins the behaviour, and a deletion of the
|
||||
// guard is a compile error here.
|
||||
TEST(ProgramEmit, TheNeverUploadedSentinelIsNeverEmitted) {
|
||||
EmitterScope scope;
|
||||
EXPECT_EQ(kMGPipeGlobalConstantsNeverUploaded, ~Uint32{0});
|
||||
EXPECT_FALSE(MGPipeGlobalConstantsVersionIsEmittable(kMGPipeGlobalConstantsNeverUploaded));
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(0u));
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(1u));
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(~Uint32{0} - 1u));
|
||||
|
||||
// And the live path never produces it either: whatever the frontend's counter is at,
|
||||
// the record the emitter last built carries an emittable version.
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
program->MarkUBOContentDirty();
|
||||
if (Emitter().EmitGlobalConstants(Ctx()) > 0u) {
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(Emitter().LastGlobalConstants().Version));
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Version, program->GetUBOContentVersion());
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Blob.Size, 0u)
|
||||
<< "the one Blob rule: a monolith emission does not declare its blob";
|
||||
}
|
||||
}
|
||||
|
||||
// D-H4, and it is a statement about the TRACKER as much as about the emitter: Update() may
|
||||
// not move a program's link completeness in either direction, because answering "did the
|
||||
// shader move" from a version counter is what keeps a compile off the dirty walk. The
|
||||
// emitter is where the join belongs, and it is the same GetProgramForDraw() the verb is
|
||||
// about to make anyway.
|
||||
TEST(ProgramEmit, TheEmitterJoinsAndTheTrackerDoesNot) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
|
||||
const Bool completeBefore = program->IsLinkComplete();
|
||||
MGPipeTrackerInstance().Update(Ctx(), MGPipeVerbClass::kDraw);
|
||||
EXPECT_EQ(program->IsLinkComplete(), completeBefore)
|
||||
<< "the dirty walk must not force a compile, in either direction";
|
||||
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_TRUE(program->IsLinkComplete()) << "the emitter joins, because the verb would";
|
||||
MGPipeTrackerInstance().Reset();
|
||||
}
|
||||
|
||||
TEST(ProgramEmit, AReLinkReIssuesOnTheSameHandle) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
ASSERT_EQ(Emitter().CreateCount(), 1u);
|
||||
const MGPipeHandle cso = Emitter().LastProgramDesc().Cso;
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(cso));
|
||||
EXPECT_EQ(Emitter().DrawCso(), cso);
|
||||
|
||||
// Nothing moved: no second record, no second bind.
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(Emitter().CreateCount(), 1u);
|
||||
EXPECT_EQ(Emitter().BindCount(), 1u);
|
||||
|
||||
const Uint32 linkVersionBefore = program->GetLinkVersion();
|
||||
GL::LinkProgram(name);
|
||||
ASSERT_NE(program->GetLinkVersion(), linkVersionBefore) << "the relink really has to move it";
|
||||
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(Emitter().CreateCount(), 2u);
|
||||
// THE SAME HANDLE. Gen increments only on slot reuse and never on a respecify, so a
|
||||
// relinked program is the same GL object and the server's twin table must not be asked
|
||||
// to mint a second identity for it.
|
||||
EXPECT_EQ(Emitter().LastProgramDesc().Cso, cso);
|
||||
EXPECT_EQ(Emitter().BindCount(), 1u) << "and a re-issue on the bound handle is not a rebind";
|
||||
}
|
||||
|
||||
// Two calls because the frontend has two joins and two PipeInputs slots. With a plain
|
||||
// glUseProgram they name one object, and the record has to say so rather than leaving the
|
||||
// server to guess which of the two a verb meant.
|
||||
TEST(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().DrawCso(), Emitter().DispatchCso());
|
||||
EXPECT_EQ(Emitter().BoundCso(), Emitter().DrawCso());
|
||||
EXPECT_EQ(Emitter().DrawProgramSetCount(), 1u);
|
||||
EXPECT_EQ(Emitter().DispatchProgramSetCount(), 1u);
|
||||
|
||||
// A null handle is legal and means exactly "nothing bound".
|
||||
GL::UseProgram(0);
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().DrawCso()));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().DispatchCso()));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().BoundCso()));
|
||||
EXPECT_EQ(Emitter().DrawProgramSetCount(), 2u);
|
||||
}
|
||||
|
||||
TEST(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
// The version-first skip: nothing hashed, nothing copied, nothing emitted, and the
|
||||
// return value is the bytes that went on the wire - zero.
|
||||
EXPECT_EQ(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().CreateCount(), 1u);
|
||||
EXPECT_EQ(Emitter().BindCount(), 1u);
|
||||
EXPECT_EQ(Emitter().DrawProgramSetCount(), 1u);
|
||||
}
|
||||
|
||||
// set_global_constants is suppressed against a (ShaderCso, Version) key, and A RE-ISSUED
|
||||
// create_shader_state CLEARS THE APPLIER's DEFAULT UNIFORM BLOCK (wire's W6). So the key
|
||||
// has to die with the re-issue, or the block is never re-sent and the server draws the
|
||||
// program with a zeroed one while the client's scratch still holds the live values.
|
||||
//
|
||||
// WHICH HALF OF THIS CASE IS THE DISCRIMINATOR, said plainly. The second half - "the block
|
||||
// goes out again after a relink" - is also true without the fix, because
|
||||
// BumpLinkObservableVersions bumps the UBO content version alongside the link version, so
|
||||
// the Version half of the key moves on its own for the one re-issue trigger that exists
|
||||
// today. The FIRST half is the one that goes red when the invalidation is deleted, and it
|
||||
// is the property the contract actually needs: after a re-issue the emitter must hold NO
|
||||
// key at all, so no future re-issue trigger - a recycled slot, a re-issue driven by
|
||||
// anything that does not happen to move the content version - can leave the applier's
|
||||
// cleared block latched as "already sent".
|
||||
TEST(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
ASSERT_EQ(Emitter().CreateCount(), 1u);
|
||||
// Driven to the fixed point rather than assumed to settle in one call: the first
|
||||
// EmitGlobalConstants reads GetUBOContentVersion() BEFORE GetUBOSize() joins phase B,
|
||||
// and that join bumps the counter, so the first key is one behind by construction.
|
||||
for (int i = 0; i < 8 && Emitter().EmitGlobalConstants(Ctx()) > 0u; ++i) {
|
||||
}
|
||||
const Uint64 setsBefore = Emitter().GlobalConstantsSetCount();
|
||||
ASSERT_GT(setsBefore, 0u) << "this program declares a uniform, so a block must have gone out";
|
||||
const MGPipeHandle cso = Emitter().GlobalConstantsCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
EXPECT_EQ(cso, Emitter().DrawCso());
|
||||
// The latch really is latched.
|
||||
EXPECT_EQ(Emitter().EmitGlobalConstants(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().GlobalConstantsSetCount(), setsBefore);
|
||||
|
||||
// A RELINK, which is the one thing that makes AcquireShaderCso re-issue on the same
|
||||
// handle. A failed relink is the case the design worries about - GL keeps a program
|
||||
// that is active for a stage running on its previous executable - and this frontend
|
||||
// additionally clears the phase-B scratch in Link()'s prologue, so the populated-block
|
||||
// half of that scenario is not reachable here; the re-issue is, and it is what the
|
||||
// applier reacts to.
|
||||
const Uint32 linkVersionBefore = program->GetLinkVersion();
|
||||
GL::LinkProgram(name);
|
||||
ASSERT_NE(program->GetLinkVersion(), linkVersionBefore) << "the relink really has to move it";
|
||||
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
ASSERT_EQ(Emitter().CreateCount(), 2u) << "the create really has to be re-issued";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().GlobalConstantsCso()))
|
||||
<< "the applier cleared the record's default uniform block on the re-issue, so the "
|
||||
"key that suppresses set_global_constants must not survive it";
|
||||
EXPECT_EQ(Emitter().GlobalConstantsVersion(), kMGPipeGlobalConstantsNeverUploaded);
|
||||
|
||||
// And the block really is sent again.
|
||||
for (int i = 0; i < 8 && Emitter().EmitGlobalConstants(Ctx()) > 0u; ++i) {
|
||||
}
|
||||
EXPECT_GT(Emitter().GlobalConstantsSetCount(), setsBefore);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -482,6 +788,9 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -156,6 +156,27 @@ namespace {
|
||||
}
|
||||
};
|
||||
|
||||
// "A BACKEND IS PRESENT", which since ID-39 is a thing the applier ASKS: every P4a-family
|
||||
// entry point declines a record - and counts RefusedNoConsumer - when no backend has
|
||||
// registered MGPipeResourceOps, because acceptance is a contract with the emitter and an
|
||||
// accepted record nothing will read makes the client clear a dirty flag the legacy pull
|
||||
// path still owed. A case that wants the P4a half of this applier to behave as it does
|
||||
// under DirectGLES scopes this on; the case that wants the OTHER arm simply does not.
|
||||
//
|
||||
// THE TABLE IS EMPTY AND THAT IS DELIBERATE. Its hooks are the BUFFER family's, and every
|
||||
// non-buffer resource row is stored and returned rather than dispatched (see
|
||||
// MGPipeApplyResourceCreate) - so what registering it changes here is the consumer question
|
||||
// and nothing else. It nests: the previous table is restored, not nulled.
|
||||
struct ScopedResourceOps {
|
||||
ScopedResourceOps() : m_saved(MGPipeGetResourceOps()) {
|
||||
static const MGPipeResourceOps kEmpty{};
|
||||
MGPipeSetResourceOps(&kEmpty);
|
||||
}
|
||||
~ScopedResourceOps() { MGPipeSetResourceOps(m_saved); }
|
||||
|
||||
const MGPipeResourceOps* m_saved;
|
||||
};
|
||||
|
||||
MGPResourceDesc BufferDesc(MGPipeHandle res, Uint32 width, Uint32 glName) {
|
||||
MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
@@ -1355,6 +1376,9 @@ namespace {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// The texture and renderbuffer rows below are P4a's, and P4a's belt declines those on a
|
||||
// backend that consumes none of them (ID-39) - so this case says which arm it is about.
|
||||
ScopedResourceOps consumer;
|
||||
const MGPipeHandle shared{7, 3};
|
||||
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Buffer, 0, 11));
|
||||
@@ -1393,6 +1417,195 @@ namespace {
|
||||
#endif
|
||||
}
|
||||
|
||||
// ID-39: THE APPLIER'S HALF OF THE "NO CONSUMER" RULE, over every P4a-family entry point.
|
||||
//
|
||||
// WHY AN APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL is written beside
|
||||
// MGPipeApplierState::RefusedNoConsumer: acceptance became a CONTRACT WITH THE CLIENT at
|
||||
// ID-18 M3 - the emitters clear a texture level's dirty flags, advance their descriptor
|
||||
// mirrors and latch their suppressors on the answer these calls return - so an applier that
|
||||
// accepts a record nothing in the process will ever read makes the client forget work the
|
||||
// legacy pull path still owed. On DirectVulkan, which registers no MGPipeResourceOps and
|
||||
// has none of P4a's twins, that put 66 texture-upload-shaped integration-gpu cases red on
|
||||
// the push build while the pull build stayed 966/966 green.
|
||||
//
|
||||
// IT IS A BELT AND NOT THE GATE. The client's gate is FamilyIsLive in
|
||||
// MG_Impl/Pipe/PipeFill.cpp (pinned by TextureEmit.WithNoBackendConsumerTheFamilyGateIsFalse
|
||||
// AndNothingReachesTheApplier) and it stops the emission upstream. This is under it, and it
|
||||
// is not redundant: GL_Framebuffer.cpp's PipePublishFramebufferByName reaches the
|
||||
// framebuffer emitter DIRECTLY at the fifteen DSA sites, without passing through PipeFill,
|
||||
// so set_framebuffer_state is a record that can arrive here on a backend with no consumer.
|
||||
//
|
||||
// THE DEATH PATHS ARE DELIBERATELY NOT IN THE LIST and the second half of the case says so:
|
||||
// a destroy is idempotent cleanup that must keep working whatever the registration did.
|
||||
TEST(ResourceEmit, EveryP4aFamilyEntryPointDeclinesWhenNoBackendRegisteredTheConsumer) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard; // leaves the table UNREGISTERED, which is this half's whole point
|
||||
ASSERT_EQ(MGPipeGetResourceOps(), nullptr);
|
||||
|
||||
const MGPipeHandle texture{7, 3};
|
||||
const MGPipeHandle cso{9, 1};
|
||||
const MGPipeHandle fbo{4, 2};
|
||||
|
||||
MGPTextureParams params{};
|
||||
params.Res = texture;
|
||||
params.BuiltinSampler = cso;
|
||||
|
||||
MGPSubData upload{};
|
||||
upload.Res = texture;
|
||||
upload.Target = MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::Tex2D), 0u);
|
||||
|
||||
MGPFramebufferState fboState{};
|
||||
fboState.Fbo = fbo;
|
||||
fboState.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Draw);
|
||||
|
||||
MGPSamplerDesc samplerDesc{};
|
||||
samplerDesc.Cso = cso;
|
||||
const SamplerParameters samplerParams{};
|
||||
|
||||
MGPSamplerView view{};
|
||||
view.Cso = cso;
|
||||
view.Texture = texture;
|
||||
|
||||
const MGPSamplerViews viewSet{0, 1, 0};
|
||||
const MGPBoundView viewTail[1]{};
|
||||
const MGPSamplerStates stateSet{0, 1, 0};
|
||||
const MGPipeHandle stateTail[1]{kMGPipeNullHandle};
|
||||
const MGPShaderImages imageSet{0, 1, 0};
|
||||
const MGPImageView imageTail[1]{};
|
||||
|
||||
MGPProgramDesc program{};
|
||||
program.Cso = cso;
|
||||
program.StageMask = 0x3u;
|
||||
const MG_State::GLState::LinkArtifacts link;
|
||||
const MG_State::GLState::SpirvArtifacts spirv;
|
||||
|
||||
MGPGlobalConstants constants{};
|
||||
constants.ShaderCso = cso;
|
||||
|
||||
const MGPHandleOnly csoHandle{cso, static_cast<Uint32>(MGPipeKind::ShaderCso), 0};
|
||||
|
||||
// ---- with no consumer: every one of them declines, and NOTHING is stored ----
|
||||
EXPECT_FALSE(MGPipeApplyResourceCreate(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 0, 22)));
|
||||
EXPECT_FALSE(
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 64, 22), nullptr));
|
||||
EXPECT_FALSE(MGPipeApplyResourceSubData(upload, nullptr));
|
||||
MGPipeApplySetTextureParams(params);
|
||||
MGPipeApplySetFramebufferState(fboState);
|
||||
MGPipeApplyCreateSamplerState(samplerDesc, &samplerParams);
|
||||
MGPipeApplyCreateSamplerView(view);
|
||||
MGPipeApplySetSamplerViews(viewSet, viewTail);
|
||||
MGPipeApplyBindSamplerStates(stateSet, stateTail);
|
||||
MGPipeApplySetShaderImages(imageSet, imageTail);
|
||||
MGPipeApplyCreateShaderState(program, &link, &spirv);
|
||||
MGPipeApplyBindShaderState(csoHandle);
|
||||
MGPipeApplySetDrawProgram(csoHandle);
|
||||
MGPipeApplySetDispatchProgram(csoHandle);
|
||||
MGPipeApplySetGlobalConstants(constants, nullptr);
|
||||
|
||||
// FIFTEEN CALLS, FIFTEEN REFUSALS, AND THE NUMBER IS THE ASSERTION: an entry point that
|
||||
// is added to a P4a family later and forgets the belt makes this line fail rather than
|
||||
// silently accepting a record on a backend that reads none.
|
||||
EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, 15u);
|
||||
// AND NOT ONE OF THE OTHER THREE MOVED. The refusal is a configuration fact, not a seam
|
||||
// defect, so it must not read as one to an operator grepping the counters.
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, 0u);
|
||||
|
||||
// NOTHING IS LIVE, which is the property, rather than "no table exists". The belt stands
|
||||
// AFTER each entry point's own record-shape checks so that a malformed record is
|
||||
// Fatal{ProtocolCorruption} on every backend and not only on the ones that consume - and
|
||||
// the bound check for the four create-shaped calls IS RecordAt, which grows the table to
|
||||
// the slot on its way to answering. So a refused create may leave a zeroed row behind
|
||||
// and stores nothing in it. It costs nothing where it matters: on a backend with no
|
||||
// consumer the client's gate emits none of these at all, and the one record that reaches
|
||||
// this applier without passing that gate - set_framebuffer_state, published by name from
|
||||
// GL_Framebuffer.cpp - is declined in front of its RecordAt.
|
||||
const auto nothingLiveAt = [](const auto& table, SizeT slot) {
|
||||
return table.size() <= slot || !table[slot].Live;
|
||||
};
|
||||
EXPECT_TRUE(nothingLiveAt(MGPipeApplier().TextureResources, 7));
|
||||
EXPECT_TRUE(nothingLiveAt(MGPipeApplier().SamplerCsos, 9));
|
||||
EXPECT_TRUE(nothingLiveAt(MGPipeApplier().SamplerViewCsos, 9));
|
||||
EXPECT_TRUE(nothingLiveAt(MGPipeApplier().ShaderCsos, 9));
|
||||
// set_framebuffer_state's table is the one that must not even be grown: it is declined
|
||||
// in front of its RecordAt, because it is the one call a backend with no consumer can
|
||||
// actually receive.
|
||||
EXPECT_TRUE(MGPipeApplier().FramebufferRecords.empty());
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderCso));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DispatchProgram));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(
|
||||
MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Draw)]));
|
||||
|
||||
// ---- and with one, every one of them lands. Same records, same order ----
|
||||
{
|
||||
ScopedResourceOps consumer;
|
||||
const Uint64 refusalsBefore = MGPipeApplier().RefusedNoConsumer;
|
||||
|
||||
EXPECT_TRUE(
|
||||
MGPipeApplyResourceCreate(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 0, 22)));
|
||||
EXPECT_TRUE(MGPipeApplyResourceRespecify(
|
||||
TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 64, 22), nullptr));
|
||||
MGPipeApplySetTextureParams(params);
|
||||
MGPipeApplySetFramebufferState(fboState);
|
||||
MGPipeApplyCreateSamplerState(samplerDesc, &samplerParams);
|
||||
MGPipeApplyCreateSamplerView(view);
|
||||
MGPipeApplySetSamplerViews(viewSet, viewTail);
|
||||
MGPipeApplyBindSamplerStates(stateSet, stateTail);
|
||||
MGPipeApplySetShaderImages(imageSet, imageTail);
|
||||
MGPipeApplyCreateShaderState(program, &link, &spirv);
|
||||
MGPipeApplyBindShaderState(csoHandle);
|
||||
MGPipeApplySetDrawProgram(csoHandle);
|
||||
MGPipeApplySetDispatchProgram(csoHandle);
|
||||
MGPipeApplySetGlobalConstants(constants, nullptr);
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, refusalsBefore);
|
||||
ASSERT_GT(MGPipeApplier().TextureResources.size(), 7u);
|
||||
EXPECT_TRUE(MGPipeApplier().TextureResources[7].Live);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Desc.Width, 64u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Params.BuiltinSampler, cso);
|
||||
ASSERT_GT(MGPipeApplier().FramebufferRecords.size(), 4u);
|
||||
EXPECT_TRUE(MGPipeApplier().FramebufferRecords[4].Live);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Draw)],
|
||||
fbo);
|
||||
ASSERT_GT(MGPipeApplier().SamplerCsos.size(), 9u);
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos[9].Live);
|
||||
ASSERT_GT(MGPipeApplier().SamplerViewCsos.size(), 9u);
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[9].Live);
|
||||
ASSERT_GT(MGPipeApplier().ShaderCsos.size(), 9u);
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos[9].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, cso);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, cso);
|
||||
EXPECT_EQ(MGPipeApplier().DispatchProgram, cso);
|
||||
// resource_subdata's ACCEPTED path wants a real destination box against real
|
||||
// storage, which is a texture-emitter fixture and not this file's; it is proved end
|
||||
// to end by TextureEmit.WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReaches
|
||||
// TheApplier's second half (SubDataCount 1, RefusedSubDataCount 0). Driving a
|
||||
// half-built record through it here would trip the upload validator's own wire,
|
||||
// which is a different rule and not this case's.
|
||||
|
||||
// AND THE DEATH PATHS ARE NOT BELTED, which is the other half of the ruling: they
|
||||
// are idempotent cleanup and they run on whatever the registration is. Driven with
|
||||
// the table registered here and asserted UNCOUNTED, so that a later commit which
|
||||
// adds them to the belt has to change this line.
|
||||
MGPipeApplyDeleteShaderState(csoHandle);
|
||||
MGPipeApplyResourceDestroy(KindHandle(texture, MGPipeKind::Texture));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, refusalsBefore);
|
||||
EXPECT_FALSE(MGPipeApplier().ShaderCsos[9].Live);
|
||||
EXPECT_FALSE(MGPipeApplier().TextureResources[7].Live);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Neither branch may fall through to a table it was not named. A target or a kind outside
|
||||
// the catalogue would otherwise land in whichever table the code happened to reach first,
|
||||
// and destroy a live object of a kind the call was never about.
|
||||
|
||||
@@ -53,8 +53,16 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <cstring>
|
||||
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -72,13 +80,24 @@ namespace {
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
// `from` is a byte offset, and it exists because of the fork below: the library's log file
|
||||
// is already open by the time a case runs, so the child's lines are APPENDED to it rather
|
||||
// than written to a fresh file, and only what the child appended is this drive's evidence.
|
||||
std::string ReadLog(std::streamoff from = 0) {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
if (from > 0) in.seekg(from, std::ios::beg);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Where the library's log file currently ends. Reading from here after the child has
|
||||
// aborted gives exactly the lines that drive produced.
|
||||
std::streamoff LogEnd() {
|
||||
std::ifstream in(g_logPath, std::ios::binary | std::ios::ate);
|
||||
return in ? static_cast<std::streamoff>(in.tellg()) : std::streamoff{0};
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
@@ -104,8 +123,14 @@ namespace {
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
// THE LOG PATH IS NOT UNLINKED HERE, and that is what this helper had to learn when the
|
||||
// client's cases landed in the same file as the applier's: main() calls
|
||||
// MobileGL::Initialize(), so the library's log FILE* is already open on this path and
|
||||
// fork() duplicates it. Removing the path would leave the child writing into a deleted
|
||||
// inode and the parent reading an empty file - the child would still abort, and the
|
||||
// assertion on WHAT it named could never see the line. So the log's end is remembered
|
||||
// and only what the child appended is read back.
|
||||
const std::streamoff before = LogEnd();
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
@@ -116,7 +141,7 @@ namespace {
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
result.Log = ReadLog(before);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -524,6 +549,548 @@ TEST(SamplerEmit, AMakeCurrentTakesTheUnitSetsAndLeavesTheCsoAndViewRecordsStand
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name, so every
|
||||
// push-only case is present here and SKIPS rather than being absent.
|
||||
#define MGL_SAMPLER_EMIT_TEST_LIST(X) \
|
||||
X(SamplerEmit, EverySamplerParameterFieldSurvivesTheBlobConversion) \
|
||||
X(SamplerEmit, PaddingCannotChangeTheHash) \
|
||||
X(SamplerEmit, TwoIdenticalSamplersShareOneCso) \
|
||||
X(SamplerEmit, ABorderColorFormChangeAloneMintsANewCso) \
|
||||
X(SamplerEmit, AHashCollisionDoesNotAliasTwoSamplerStates) \
|
||||
X(SamplerEmit, AViewIsReIssuedOnTheSameHandleWhenItsRestrictionsMove) \
|
||||
X(SamplerEmit, AnUnchangedTextureReIssuesNothing) \
|
||||
X(SamplerEmit, OnlyTheProgramResolvedUnitsAreEmitted) \
|
||||
X(SamplerEmit, AnUnchangedSetEmitsNothing) \
|
||||
X(SamplerEmit, ARedundantRebindOfTheSameSamplerEmitsNothing) \
|
||||
X(SamplerEmit, ABoundSamplerStateHoldsItsCsoUntilTheUnitMoves) \
|
||||
X(SamplerEmit, AReferencedCsoIsNeverTheLruVictim) \
|
||||
X(SamplerEmit, AFullyPinnedCacheMintsBeyondItsCapacityAndCountsIt) \
|
||||
X(SamplerEmit, AReleaseThisCacheNeverHandedOutIsCountedRatherThanAbsorbed)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_SAMPLER_EMIT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::ITextureObject;
|
||||
using MG_State::GLState::SamplerObject;
|
||||
|
||||
// AN RAII SCOPE RATHER THAN A gtest FIXTURE, for VertexInputEmitTest's reason: both gates
|
||||
// grep `ctest -R 'SamplerEmit\.'`, a TEST_F files its cases under the FIXTURE's name, and
|
||||
// gtest refuses to mix TEST and TEST_F under one suite name.
|
||||
//
|
||||
// UNLIKE VertexInputEmitTest's, this scope does NOT replace pGLContext: half these cases
|
||||
// need a really linked program, so the process is initialised once in main() and the cases
|
||||
// share that context, each using GL names of its own. What the scope does is put the
|
||||
// emitter, its counters and the suppressor back to a known state.
|
||||
struct EmitterScope {
|
||||
EmitterScope() { Clear(); }
|
||||
~EmitterScope() { Clear(); }
|
||||
EmitterScope(const EmitterScope&) = delete;
|
||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeSamplerEmitterInstance().Reset();
|
||||
MGPipeSamplerEmitterInstance().ResetCounters();
|
||||
MGPipeSamplerCsoCacheInstance().ResetForTest();
|
||||
MGPipeSamplerCsoCacheInstance().ResetCounters();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
MGPipeSamplerEmitter& Emitter() { return MGPipeSamplerEmitterInstance(); }
|
||||
MGPipeSamplerCsoCache& Cache() { return MGPipeSamplerCsoCacheInstance(); }
|
||||
|
||||
// Every field of SamplerParameters set to a value that is not its default, so a conversion
|
||||
// that dropped one is caught by the field's own EXPECT rather than by a count.
|
||||
SamplerParameters DistinctParameters() {
|
||||
SamplerParameters params{};
|
||||
params.wrapS = SamplerWrapMode::ClampToBorder;
|
||||
params.wrapT = SamplerWrapMode::MirroredRepeat;
|
||||
params.wrapR = SamplerWrapMode::MirrorClampToEdge;
|
||||
params.minFilter = SamplerFilterMode::Linear;
|
||||
params.magFilter = SamplerFilterMode::Nearest;
|
||||
params.mipmapMode = SamplerMipmapMode::Nearest;
|
||||
params.minLod = -3.5f;
|
||||
params.maxLod = 7.25f;
|
||||
params.lodBias = 1.5f;
|
||||
params.maxAnisotropy = 2.0f;
|
||||
params.compareFunc = SamplerCompareFunc::Greater;
|
||||
params.compareMode = SamplerCompareMode::CompareToTexture;
|
||||
params.borderColor = {0.25f, 0.5f, 0.75f, 1.0f};
|
||||
params.borderColorI = {-1, 2, -3, 4};
|
||||
params.borderColorUI = {5u, 6u, 7u, 8u};
|
||||
params.borderColorForm = BorderColorForm::Int;
|
||||
return params;
|
||||
}
|
||||
|
||||
// A 2D texture with a single level and a mipmap mode of None, so it is mipmap-complete for
|
||||
// its filter and therefore actually reaches the emitted set. A texture that samples as
|
||||
// incomplete is dropped to a null view on purpose, which is the resolution DirectGLES
|
||||
// performs by leaving the native target unbound.
|
||||
const SharedPtr<ITextureObject>& MakeCompleteTexture(GLuint& name, Int width) {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
GL::GenTextures(1, &name);
|
||||
GL::BindTexture(GL_TEXTURE_2D, name);
|
||||
GL::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, width, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
GL::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
GL::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return Ctx().GetTextureObject(name);
|
||||
}
|
||||
|
||||
void BindTextureToUnit(Uint32 unit, const SharedPtr<ITextureObject>& texture) {
|
||||
Ctx().GetTextureUnitObject(static_cast<Int>(unit))
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.Bind(texture);
|
||||
Ctx().NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
}
|
||||
|
||||
// ============================ D-F1: the CSO cache ============================
|
||||
|
||||
// G6 for this family: every one of SamplerParameters' sixteen members survives the
|
||||
// canonical copy the cache hashes and confirms over. Each member is its own EXPECT naming
|
||||
// that member, which is what G7's scripted control needs - it stops the conversion copying
|
||||
// borderColorForm and expects this case to go red NAMING it.
|
||||
TEST(SamplerEmit, EverySamplerParameterFieldSurvivesTheBlobConversion) {
|
||||
EmitterScope scope;
|
||||
const SamplerParameters source = DistinctParameters();
|
||||
SamplerParameters canon;
|
||||
MGPipeCanonicaliseSamplerParameters(source, canon);
|
||||
|
||||
EXPECT_EQ(canon.wrapS, source.wrapS);
|
||||
EXPECT_EQ(canon.wrapT, source.wrapT);
|
||||
EXPECT_EQ(canon.wrapR, source.wrapR);
|
||||
EXPECT_EQ(canon.minFilter, source.minFilter);
|
||||
EXPECT_EQ(canon.magFilter, source.magFilter);
|
||||
EXPECT_EQ(canon.mipmapMode, source.mipmapMode);
|
||||
EXPECT_EQ(canon.minLod, source.minLod);
|
||||
EXPECT_EQ(canon.maxLod, source.maxLod);
|
||||
EXPECT_EQ(canon.lodBias, source.lodBias);
|
||||
EXPECT_EQ(canon.maxAnisotropy, source.maxAnisotropy);
|
||||
EXPECT_EQ(canon.compareFunc, source.compareFunc);
|
||||
EXPECT_EQ(canon.compareMode, source.compareMode);
|
||||
EXPECT_EQ(canon.borderColor, source.borderColor);
|
||||
EXPECT_EQ(canon.borderColorI, source.borderColorI);
|
||||
EXPECT_EQ(canon.borderColorUI, source.borderColorUI);
|
||||
EXPECT_EQ(canon.borderColorForm, source.borderColorForm)
|
||||
<< "borderColorForm decides which of glSamplerParameterIiv / fv applies, and the "
|
||||
"three representations are always numerically populated, so the value alone "
|
||||
"cannot say";
|
||||
}
|
||||
|
||||
// THE PADDING TRAP. SamplerParameters is 100 bytes and its members occupy 97 of them, so
|
||||
// bytes 97..99 are padding no writer ever touches. A cache that hashed the object's own
|
||||
// bytes would read them, miss on every probe and mint a fresh CSO per call - a 256-entry
|
||||
// cache with a hit rate of zero that nobody notices, because the pixels are right.
|
||||
TEST(SamplerEmit, PaddingCannotChangeTheHash) {
|
||||
EmitterScope scope;
|
||||
SamplerParameters clean = DistinctParameters();
|
||||
SamplerParameters dirty = clean;
|
||||
auto* bytes = reinterpret_cast<Uint8*>(&dirty);
|
||||
// Written THROUGH A BYTE POINTER, past the last member and inside the object, which is
|
||||
// the only way to make the difference this case is about.
|
||||
for (SizeT i = 97; i < sizeof(SamplerParameters); ++i) bytes[i] = static_cast<Uint8>(0xA5 + i);
|
||||
|
||||
SamplerParameters canonClean;
|
||||
SamplerParameters canonDirty;
|
||||
MGPipeCanonicaliseSamplerParameters(clean, canonClean);
|
||||
MGPipeCanonicaliseSamplerParameters(dirty, canonDirty);
|
||||
EXPECT_EQ(MGPipeHashSamplerParameters(canonClean), MGPipeHashSamplerParameters(canonDirty));
|
||||
// And the CONFIRM, not only the hash: the cache reuses a handle on a memcmp over these
|
||||
// same canonical bytes, so the padding has to be deterministically zero in both.
|
||||
EXPECT_EQ(std::memcmp(&canonClean, &canonDirty, sizeof(SamplerParameters)), 0);
|
||||
|
||||
Uint64 payload = 0;
|
||||
const MGPipeHandle first = Cache().Acquire(clean, payload);
|
||||
const MGPipeHandle second = Cache().Acquire(dirty, payload);
|
||||
EXPECT_EQ(first, second) << "uninitialised padding must not mint a second CSO";
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 1u);
|
||||
EXPECT_EQ(Cache().GetCounters().Hits, 1u);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, TwoIdenticalSamplersShareOneCso) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
const SamplerParameters params = DistinctParameters();
|
||||
const MGPipeHandle a = Cache().Acquire(params, payload);
|
||||
const MGPipeHandle b = Cache().Acquire(params, payload);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(a));
|
||||
EXPECT_EQ(a, b);
|
||||
EXPECT_EQ(Cache().Size(), 1u);
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 1u);
|
||||
// A SamplerObject is a pure value with no driver-side per-object binding state, so two
|
||||
// identical samplers really can share one CSO and one server-side twin. That is what
|
||||
// makes content addressing right for this kind and wrong for vertex elements.
|
||||
EXPECT_GT(payload, 0u);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, ABorderColorFormChangeAloneMintsANewCso) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
SamplerParameters params = DistinctParameters();
|
||||
const MGPipeHandle asInt = Cache().Acquire(params, payload);
|
||||
params.borderColorForm = BorderColorForm::Uint;
|
||||
const MGPipeHandle asUint = Cache().Acquire(params, payload);
|
||||
EXPECT_NE(asInt, asUint) << "the form is the only thing that says which driver entry "
|
||||
"point applies; the three colour values did not move";
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 2u);
|
||||
}
|
||||
|
||||
// The memcmp confirm exists because a bare 64-bit equality would alias two DIFFERENT
|
||||
// sampler states onto one CSO - silent wrong filtering with no gate that can see it.
|
||||
//
|
||||
// A GENUINE XXH64 COLLISION CANNOT BE MANUFACTURED, so this case drives the branch through
|
||||
// the cache's forced-hash test seam instead. Without it the case could not go red for its
|
||||
// own name: two states that differ by one float ULP have different hashes, the confirm is
|
||||
// never consulted, and the memcmp branch, the Collisions counter and the probe's
|
||||
// continuation past a rejected entry were all completely uncovered.
|
||||
TEST(SamplerEmit, AHashCollisionDoesNotAliasTwoSamplerStates) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
SamplerParameters first = DistinctParameters();
|
||||
SamplerParameters second = DistinctParameters();
|
||||
second.maxLod = second.maxLod + 0.0009765625f; // one representable step, nothing else moves
|
||||
ASSERT_NE(std::memcmp(&first, &second, sizeof(SamplerParameters)), 0);
|
||||
|
||||
// ONE hash for two different values, which is exactly what a collision is.
|
||||
constexpr Uint64 kForcedHash = 0x0123456789ABCDEFull;
|
||||
const MGPipeHandle a = Cache().AcquireWithForcedHashForTest(first, kForcedHash, payload);
|
||||
const MGPipeHandle b = Cache().AcquireWithForcedHashForTest(second, kForcedHash, payload);
|
||||
EXPECT_NE(a, b) << "reusing the handle would filter one state with the other's parameters";
|
||||
EXPECT_EQ(Cache().GetCounters().Collisions, 1u) << "and the rejection is counted, not silent";
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 2u);
|
||||
EXPECT_EQ(Cache().Size(), 2u);
|
||||
|
||||
// AND THE PROBE KEEPS GOING PAST THE REJECTED ENTRY rather than stopping at the first
|
||||
// hash match: the first value is still resident behind the colliding one, so asking for
|
||||
// it again is a HIT and not a third mint. Stopping at the first match would mint a
|
||||
// duplicate CSO for a value the cache already holds.
|
||||
const MGPipeHandle again = Cache().AcquireWithForcedHashForTest(first, kForcedHash, payload);
|
||||
EXPECT_EQ(again, a);
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 2u);
|
||||
EXPECT_EQ(Cache().GetCounters().Hits, 1u);
|
||||
|
||||
// The ordinary path, for completeness: two states that differ in ONE field never share
|
||||
// a handle and never reach the confirm at all.
|
||||
Cache().ResetForTest();
|
||||
Cache().ResetCounters();
|
||||
const MGPipeHandle plainFirst = Cache().Acquire(first, payload);
|
||||
const MGPipeHandle plainSecond = Cache().Acquire(second, payload);
|
||||
EXPECT_NE(plainFirst, plainSecond);
|
||||
EXPECT_EQ(Cache().GetCounters().Collisions, 0u);
|
||||
}
|
||||
|
||||
// ============================ D-F2: the sampler view ============================
|
||||
|
||||
TEST(SamplerEmit, AnUnchangedTextureReIssuesNothing) {
|
||||
EmitterScope scope;
|
||||
GLuint name = 0;
|
||||
const SharedPtr<ITextureObject> texture = MakeCompleteTexture(name, 4);
|
||||
ASSERT_TRUE(texture);
|
||||
Uint64 payload = 0;
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
const MGPipeHandle view = Emitter().AcquireSamplerView(*texture, handle, payload);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(view));
|
||||
EXPECT_EQ(Emitter().ViewCreateCount(), 1u);
|
||||
// THE VERSION-FIRST SKIP: nothing moved, so nothing is hashed, copied or emitted.
|
||||
EXPECT_EQ(Emitter().AcquireSamplerView(*texture, handle, payload), view);
|
||||
EXPECT_EQ(Emitter().ViewCreateCount(), 1u);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, AViewIsReIssuedOnTheSameHandleWhenItsRestrictionsMove) {
|
||||
EmitterScope scope;
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
GLuint name = 0;
|
||||
const SharedPtr<ITextureObject> texture = MakeCompleteTexture(name, 4);
|
||||
ASSERT_TRUE(texture);
|
||||
Uint64 payload = 0;
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
const MGPipeHandle view = Emitter().AcquireSamplerView(*texture, handle, payload);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(view));
|
||||
ASSERT_EQ(Emitter().ViewCreateCount(), 1u);
|
||||
const Uint64 shapeBefore = texture->GetShapeVersion();
|
||||
|
||||
GL::BindTexture(GL_TEXTURE_2D, name);
|
||||
GL::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_NE(texture->GetShapeVersion(), shapeBefore) << "the storage really has to move";
|
||||
|
||||
// THE SAME HANDLE, a second record. Gen increments only on slot reuse and never on a
|
||||
// respecify, so re-issuing on the same handle is legal and is what keeps a server's
|
||||
// twin table from minting a second identity for one texture.
|
||||
const MGPipeHandle reissued = Emitter().AcquireSamplerView(*texture, handle, payload);
|
||||
EXPECT_EQ(reissued, view);
|
||||
EXPECT_EQ(Emitter().ViewCreateCount(), 2u);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().Cso, view);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().Texture, handle);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().Target, static_cast<Uint8>(TextureTarget::Texture2D));
|
||||
EXPECT_EQ(Emitter().LastCreatedView().InternalFormat, static_cast<Uint32>(texture->GetFormat()));
|
||||
// An ordinary texture carries no view restriction, and zero is that statement rather
|
||||
// than a second spelling of it: glTextureView always writes NumLevels >= 1.
|
||||
EXPECT_EQ(Emitter().LastCreatedView().NumLevels, 0u);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().NumLayers, 0u);
|
||||
}
|
||||
|
||||
// ============================ D-G: the two unit sets ============================
|
||||
|
||||
Uint MakeSamplerProgram() {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
static const char* kVs = "#version 330 core\nvoid main(){ gl_Position = vec4(0.0); }\n";
|
||||
static const char* kFs =
|
||||
"#version 330 core\n"
|
||||
"uniform sampler2D sampled;\n"
|
||||
"out vec4 color;\n"
|
||||
"void main(){ color = texture(sampled, vec2(0.0)); }\n";
|
||||
const Uint program = GL::CreateProgram();
|
||||
const Uint vs = GL::CreateShader(GL_VERTEX_SHADER);
|
||||
GL::ShaderSource(vs, 1, &kVs, nullptr);
|
||||
GL::CompileShader(vs);
|
||||
GL::AttachShader(program, vs);
|
||||
const Uint fs = GL::CreateShader(GL_FRAGMENT_SHADER);
|
||||
GL::ShaderSource(fs, 1, &kFs, nullptr);
|
||||
GL::CompileShader(fs);
|
||||
GL::AttachShader(program, fs);
|
||||
GL::LinkProgram(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// ARCHITECTURE's third merge rule: the client emits the PROGRAM-RESOLVED set. A unit the
|
||||
// shader does not sample carries no view, whatever is bound to it - which is exactly the
|
||||
// gallium one-view-per-slot resolved form, and exactly what DirectGLES arrives at by
|
||||
// asking the program which of a unit's aliased bindings is the sampled one.
|
||||
TEST(SamplerEmit, OnlyTheProgramResolvedUnitsAreEmitted) {
|
||||
EmitterScope scope;
|
||||
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);
|
||||
ASSERT_EQ(Emitter().ViewSetCount(), 1u);
|
||||
ASSERT_GE(Emitter().LastSamplerViews().Count, 6u);
|
||||
EXPECT_EQ(Emitter().LastSamplerViews().Start, 0u);
|
||||
|
||||
const MGPBoundView& resolved = Emitter().LastBoundViews()[3];
|
||||
EXPECT_EQ(resolved.Unit, 3u);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(resolved.Texture)) << "unit 3 is what the shader samples";
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(resolved.View));
|
||||
|
||||
const MGPBoundView& ignored = Emitter().LastBoundViews()[5];
|
||||
EXPECT_EQ(ignored.Unit, 5u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(ignored.Texture))
|
||||
<< "unit 5 has a texture bound but no sampler uniform resolves to it";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(ignored.View));
|
||||
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, AnUnchangedSetEmitsNothing) {
|
||||
EmitterScope scope;
|
||||
GLuint name = 0;
|
||||
const SharedPtr<ITextureObject> texture = MakeCompleteTexture(name, 4);
|
||||
BindTextureToUnit(1, texture);
|
||||
Emitter().EmitSamplerViews(Ctx());
|
||||
const Uint64 after = Emitter().ViewSetCount();
|
||||
// The suppressor's whole job: an unchanged resolved set is not sent again. Without it
|
||||
// this would be a several-hundred-byte variable-length record per batch, because a
|
||||
// redundant re-bind moves the bind generation and the dirty bit with it.
|
||||
Emitter().EmitSamplerViews(Ctx());
|
||||
EXPECT_EQ(Emitter().ViewSetCount(), after);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, ARedundantRebindOfTheSameSamplerEmitsNothing) {
|
||||
// THE SCOPE IS LOAD-BEARING and not decoration: without it this case's counter
|
||||
// assertions would be reading whatever the previous case's destructor happened to
|
||||
// leave, which is a suite-order dependence of exactly the class the padding bug was.
|
||||
EmitterScope scope;
|
||||
const SharedPtr<MG_State::GLState::SamplerObject>& sampler = Ctx().CreateSamplerObject(4131);
|
||||
ASSERT_TRUE(sampler);
|
||||
Ctx().GetTextureUnitObject(2).SetSamplerObject(sampler);
|
||||
Ctx().NoteTextureUnitTouched(2);
|
||||
|
||||
ASSERT_GT(Emitter().EmitSamplerStates(Ctx()), 0u);
|
||||
ASSERT_EQ(Emitter().StateSetCount(), 1u);
|
||||
ASSERT_GE(Emitter().LastSamplerStates().Count, 3u);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(Emitter().LastSamplerStateHandles()[2]));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().LastSamplerStateHandles()[0]))
|
||||
<< "a unit with no sampler object carries the null handle, and the texture's "
|
||||
"built-in sampler then applies exactly as today";
|
||||
|
||||
// 26.2's idiom: the same sampler object re-bound at every texture-unit switch. The bind
|
||||
// generation moves, so the dirty bit fires and this emitter runs - and emits nothing.
|
||||
Ctx().BumpTextureBindGeneration();
|
||||
Emitter().EmitSamplerStates(Ctx());
|
||||
EXPECT_EQ(Emitter().StateSetCount(), 1u);
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 1u) << "and it mints no second CSO either";
|
||||
|
||||
Ctx().GetTextureUnitObject(2).SetSamplerObject(SharedPtr<SamplerObject>());
|
||||
}
|
||||
|
||||
// ==================== ID-17: the reference count on a cache entry ====================
|
||||
|
||||
// A BOUND SAMPLER STATE HOLDS A REFERENCE WHILE IT IS BOUND. The set the applier holds is
|
||||
// "the last set as received" and outlives the pass that sent it, so a handle in it must not
|
||||
// become the LRU's victim - nothing re-emits bind_sampler_states when a CSO is evicted,
|
||||
// because an eviction is not a state change and no dirty bit fires for it.
|
||||
//
|
||||
// AND THE REFERENCE DOES NOT ACCUMULATE. Every Acquire takes one, and this emitter runs
|
||||
// once per firing of the sampler bit - which 26.2 makes happen at every texture-unit
|
||||
// switch - so an emitter that took a reference per pass and never gave one back would pin
|
||||
// every CSO it ever bound for the life of the process.
|
||||
TEST(SamplerEmit, ABoundSamplerStateHoldsItsCsoUntilTheUnitMoves) {
|
||||
EmitterScope scope;
|
||||
const SharedPtr<SamplerObject>& sampler = Ctx().CreateSamplerObject(4137);
|
||||
ASSERT_TRUE(sampler);
|
||||
Ctx().GetTextureUnitObject(2).SetSamplerObject(sampler);
|
||||
Ctx().NoteTextureUnitTouched(2);
|
||||
|
||||
ASSERT_GT(Emitter().EmitSamplerStates(Ctx()), 0u);
|
||||
const MGPipeHandle bound = Emitter().LastSamplerStateHandles()[2];
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(bound));
|
||||
EXPECT_EQ(Cache().RefCountOf(bound), 1u) << "the applier's standing set names this handle";
|
||||
|
||||
Ctx().BumpTextureBindGeneration();
|
||||
Emitter().EmitSamplerStates(Ctx());
|
||||
EXPECT_EQ(Cache().RefCountOf(bound), 1u) << "one binding is one reference, not one per pass";
|
||||
|
||||
// The unit stops naming it, and the reference goes with the binding rather than with
|
||||
// the entry: the value is still worth caching, it is merely evictable again.
|
||||
Ctx().GetTextureUnitObject(2).SetSamplerObject(SharedPtr<SamplerObject>());
|
||||
Emitter().EmitSamplerStates(Ctx());
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().LastSamplerStateHandles()[2]));
|
||||
EXPECT_EQ(Cache().RefCountOf(bound), 0u);
|
||||
EXPECT_TRUE(Cache().RecordIsPublished(bound)) << "released is not evicted";
|
||||
}
|
||||
|
||||
// THE FINDING ID-17 RULES ON. MGPTextureParams::BuiltinSampler names a sampler CSO out of
|
||||
// this cache, the applier deliberately does NOT resolve that handle (an unresolvable one is
|
||||
// an ordering fact, not a corrupt one), and set_texture_params is re-emitted only when a
|
||||
// texture's parameters move - so an LRU eviction of a built-in sampler's entry would leave
|
||||
// a published record naming a handle whose slot has been re-handed out to a different
|
||||
// value, with nothing that refuses, counts, logs or re-emits. The capacity argument closes
|
||||
// only the intra-pass case; the reference count is what closes this one.
|
||||
TEST(SamplerEmit, AReferencedCsoIsNeverTheLruVictim) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
const SamplerParameters pinnedParams = DistinctParameters();
|
||||
const MGPipeHandle pinned = Cache().Acquire(pinnedParams, payload);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(pinned));
|
||||
ASSERT_EQ(Cache().RefCountOf(pinned), 1u);
|
||||
|
||||
// A CTS sampler sweep, in miniature: twice the cache's capacity in distinct values,
|
||||
// none of which anything keeps naming. The LRU has to run, hard.
|
||||
SamplerParameters filler = DistinctParameters();
|
||||
for (SizeT i = 0; i < kMGPipeSamplerCsoCacheCapacity * 2; ++i) {
|
||||
filler.minLod = static_cast<float>(i) + 0.5f;
|
||||
Cache().Release(Cache().Acquire(filler, payload));
|
||||
}
|
||||
ASSERT_GT(Cache().GetCounters().Evictions, 0u) << "the LRU really has to have run";
|
||||
EXPECT_EQ(Cache().GetCounters().OverCapacityMints, 0u)
|
||||
<< "there were unreferenced entries to take, so nothing had to grow";
|
||||
|
||||
EXPECT_TRUE(Cache().RecordIsPublished(pinned))
|
||||
<< "a handle a standing record names is not the LRU's to take";
|
||||
Uint64 second = 0;
|
||||
EXPECT_EQ(Cache().Acquire(pinnedParams, second), pinned);
|
||||
EXPECT_EQ(second, 0u) << "and it was a hit, not a re-mint under the same value";
|
||||
|
||||
Cache().Release(pinned);
|
||||
Cache().Release(pinned);
|
||||
}
|
||||
|
||||
// AND WHEN EVERY ENTRY IS PINNED THE CACHE GROWS AND SAYS SO. Growing is the safe
|
||||
// direction - a slot too many costs memory, a handle pulled out from under a live record
|
||||
// costs correctness - and OverCapacityMints is a RECORDED NUMBER rather than a gate, so a
|
||||
// workload that pins more than the capacity is visible instead of being wrong.
|
||||
TEST(SamplerEmit, AFullyPinnedCacheMintsBeyondItsCapacityAndCountsIt) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
SamplerParameters params = DistinctParameters();
|
||||
Vector<MGPipeHandle> held;
|
||||
for (SizeT i = 0; i < kMGPipeSamplerCsoCacheCapacity; ++i) {
|
||||
params.minLod = static_cast<float>(i) + 0.5f;
|
||||
held.push_back(Cache().Acquire(params, payload));
|
||||
}
|
||||
ASSERT_EQ(Cache().Size(), kMGPipeSamplerCsoCacheCapacity);
|
||||
ASSERT_EQ(Cache().GetCounters().Evictions, 0u);
|
||||
ASSERT_EQ(Cache().GetCounters().OverCapacityMints, 0u);
|
||||
|
||||
params.minLod = -12.5f; // one more distinct value, with nothing evictable
|
||||
const MGPipeHandle extra = Cache().Acquire(params, payload);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(extra));
|
||||
EXPECT_EQ(Cache().Size(), kMGPipeSamplerCsoCacheCapacity + 1);
|
||||
EXPECT_EQ(Cache().GetCounters().Evictions, 0u) << "no pinned entry may be taken";
|
||||
EXPECT_EQ(Cache().GetCounters().OverCapacityMints, 1u) << "and the growth is counted";
|
||||
|
||||
for (const MGPipeHandle& handle : held) {
|
||||
EXPECT_TRUE(Cache().RecordIsPublished(handle));
|
||||
Cache().Release(handle);
|
||||
}
|
||||
Cache().Release(extra);
|
||||
}
|
||||
|
||||
// THE COUNT IS PER HANDLE, NOT PER HOLDER, and this is what makes that visible to whoever
|
||||
// holds one. Entries are content-addressed and shared by design - package B's texture-params
|
||||
// record and this file's own bind_sampler_states name the SAME handle when the values match,
|
||||
// each owing exactly one Release - so a holder that releases twice takes the OTHER holder's
|
||||
// pin and the LRU may then evict a handle a published record still names, with nothing to
|
||||
// refuse and nothing to re-emit. The cache cannot repair that, so it counts it; an assert
|
||||
// could not, because MOBILEGL_ASSERT compiles out at INFO, i.e. in every build that runs.
|
||||
TEST(SamplerEmit, AReleaseThisCacheNeverHandedOutIsCountedRatherThanAbsorbed) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
const SamplerParameters params = DistinctParameters();
|
||||
const MGPipeHandle cso = Cache().Acquire(params, payload);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
ASSERT_EQ(Cache().RefCountOf(cso), 1u);
|
||||
ASSERT_EQ(Cache().GetCounters().UnknownReleases, 0u);
|
||||
ASSERT_EQ(Cache().GetCounters().UnderflowedReleases, 0u);
|
||||
|
||||
// A HANDLE THIS CACHE NEVER MINTED: a stale one from before a reset, or one of another
|
||||
// kind passed by mistake. It falls off the end of the probe and used to leave no trace
|
||||
// at all - Releases counts only releases that found an entry.
|
||||
Cache().Release(MGPipeHandle{cso.Slot + 4096u, cso.Gen});
|
||||
EXPECT_EQ(Cache().GetCounters().UnknownReleases, 1u);
|
||||
EXPECT_EQ(Cache().RefCountOf(cso), 1u) << "and it took nobody else's pin on the way";
|
||||
|
||||
// A SECOND RELEASE OF A REFERENCE ONLY ONE HOLDER OWED. The count does not underflow -
|
||||
// that half was already right - but the attempt is now named.
|
||||
Cache().Release(cso);
|
||||
EXPECT_EQ(Cache().RefCountOf(cso), 0u);
|
||||
EXPECT_EQ(Cache().GetCounters().UnderflowedReleases, 0u);
|
||||
Cache().Release(cso);
|
||||
EXPECT_EQ(Cache().RefCountOf(cso), 0u) << "no underflow";
|
||||
EXPECT_EQ(Cache().GetCounters().UnderflowedReleases, 1u);
|
||||
|
||||
// A NULL HANDLE IS NEITHER: an out-of-window unit holds one and releasing it is the
|
||||
// ordinary no-op the reconciliation depends on.
|
||||
const auto before = Cache().GetCounters();
|
||||
Cache().Release(kMGPipeNullHandle);
|
||||
EXPECT_EQ(Cache().GetCounters().UnknownReleases, before.UnknownReleases);
|
||||
EXPECT_EQ(Cache().GetCounters().Releases, before.Releases);
|
||||
// NOTHING WAS EVICTED WHILE REFERENCED, which is the ID-17 invariant the same round
|
||||
// turned from a compiled-out assert into a number.
|
||||
EXPECT_EQ(Cache().GetCounters().ReferencedEvictions, 0u);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -535,6 +1102,11 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ONE process-wide context for the whole suite, because half these cases need a really
|
||||
// linked program and glslang lives behind this call. Each case uses GL names of its own.
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,11 @@ namespace {
|
||||
X(TrackerWalk, ARestagedProgramPipelineFiresTheProgramBits) \
|
||||
X(TrackerWalk, ARelinkOfAStageProgramFiresTheProgramBits) \
|
||||
X(TrackerWalk, UseProgramZeroLeavesTheBoundPipelineDrivingTheProgramBits) \
|
||||
X(TrackerAggregates, ATextureStorageDefinitionMovesTheFramebufferAggregateToo) \
|
||||
X(TrackerAggregates, ARenderbufferStorageDefinitionMovesTheFramebufferAggregate) \
|
||||
X(TrackerWalk, AProgramSwitchAloneFiresTheSamplerViewBit) \
|
||||
X(TrackerWalk, ATextureParameterAloneFiresTheSamplerViewBit) \
|
||||
X(TrackerWalk, AProgramSwitchBetweenEqualImageUnitCountersFiresTheShaderImageBit) \
|
||||
X(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) \
|
||||
X(TrackerAttribPayload, AnIntWriteCarriesTheIntWordsAndNamesItsClass) \
|
||||
X(TrackerAttribPayload, AUintWriteCarriesTheUintWordsAndNamesItsClass) \
|
||||
@@ -232,6 +237,39 @@ namespace {
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
// P4a FABLE SEAM F-3. set_framebuffer_state INLINES an attachment's format, extent and
|
||||
// samples (D-C1), so the setters that define a texture's storage are setters of a
|
||||
// framebuffer-record field - and the record-field -> setter -> shutter rule (Tracker.h's
|
||||
// table) says they must move the aggregate bit 11 reads. They still move the params
|
||||
// aggregate they always moved; what this case pins is the SECOND bump, which the
|
||||
// "moves only" cases above cannot see and which is the whole of F-3's fix.
|
||||
TEST_F(TrackerAggregates, ATextureStorageDefinitionMovesTheFramebufferAggregateToo) {
|
||||
const auto& tex = Ctx().CreateTextureObject(3, TextureTarget::Texture2D);
|
||||
ASSERT_TRUE(tex != nullptr);
|
||||
const Snapshot before = Snap();
|
||||
tex->SetInternalFormat(MobileGL::TextureInternalFormat::RGBA8);
|
||||
const Snapshot after = Snap();
|
||||
EXPECT_GT(after[MGPipeAggregate::FramebufferAttachment], before[MGPipeAggregate::FramebufferAttachment])
|
||||
<< "a texture whose storage is redefined WHILE ATTACHED left set_framebuffer_state "
|
||||
"describing the previous format (F-3)";
|
||||
EXPECT_GT(after[MGPipeAggregate::TextureParams], before[MGPipeAggregate::TextureParams]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::TextureContent], before[MGPipeAggregate::TextureContent]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::VaoAttribute], before[MGPipeAggregate::VaoAttribute]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::BufferChange], before[MGPipeAggregate::BufferChange]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::VertexAttribDefault], before[MGPipeAggregate::VertexAttribDefault]);
|
||||
}
|
||||
|
||||
// The renderbuffer twin, and the one that had NO aggregate at all before: its three storage
|
||||
// setters bumped no version and raised no notice (D-D2 closed the resource record by emitting
|
||||
// from the entry point and left the framebuffer record stale).
|
||||
TEST_F(TrackerAggregates, ARenderbufferStorageDefinitionMovesTheFramebufferAggregate) {
|
||||
const auto& rbo = Ctx().CreateRenderbufferObject(1);
|
||||
ASSERT_TRUE(rbo != nullptr);
|
||||
const Snapshot before = Snap();
|
||||
rbo->AllocateStorage(IntVec2{8, 8});
|
||||
ExpectOnly(MGPipeAggregate::FramebufferAttachment, before, Snap());
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// The dirty walk itself, and the render-state emission it drives (P2 brief D4, D6, D7)
|
||||
// ===================================================================================
|
||||
@@ -751,6 +789,68 @@ namespace {
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u);
|
||||
}
|
||||
|
||||
// P4a FABLE SEAM F-1. set_sampler_views is resolved for the PROGRAM IN USE (the sampler
|
||||
// uniform's type picks which of a unit's targets is the view), and bit 12's shutter read
|
||||
// only the texture-content aggregate and the bind generation - so `glUseProgram(P1); draw;
|
||||
// glUseProgram(P2); draw` never re-emitted the set and the record went on describing P1's
|
||||
// units. A program switch alone, with no bind and no texture change, has to fire it.
|
||||
TEST_F(TrackerWalk, AProgramSwitchAloneFiresTheSamplerViewBit) {
|
||||
const Uint first = Ctx().CreateProgram();
|
||||
const Uint second = Ctx().CreateProgram();
|
||||
Ctx().UseProgram(first);
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
Ctx().UseProgram(second);
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u)
|
||||
<< "the view set is resolved for the program in use and a glUseProgram alone did not "
|
||||
"re-emit it (F-1)";
|
||||
EXPECT_EQ(Walk(), 0u) << "the widened shutter fires forever";
|
||||
}
|
||||
|
||||
// The other input F-1 added: the params aggregate. SamplerEmit.h drops a unit's view to
|
||||
// null when SamplesAsIncompleteTexture says so, and that predicate reads the effective
|
||||
// sampler's filters and the level range - a glTexParameteri that completes a texture fired
|
||||
// bit 13 and left the view entry null.
|
||||
TEST_F(TrackerWalk, ATextureParameterAloneFiresTheSamplerViewBit) {
|
||||
const auto& tex = Ctx().CreateTextureObject(1, TextureTarget::Texture2D);
|
||||
ASSERT_TRUE(tex != nullptr);
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
tex->SetMaxLevel(4);
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u)
|
||||
<< "completeness is a view-set input and a parameter change did not re-resolve it";
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplers), 0u);
|
||||
EXPECT_EQ(Walk(), 0u);
|
||||
}
|
||||
|
||||
// P4a FABLE SEAM F-2 (and E's SD-4, which is this bit through a buffer image). Bit 14's
|
||||
// plain-program arm mixed GetImageUnitVersion() ALONE - a per-program counter that two
|
||||
// programs routinely share, 0 == 0 for any pair that never moved an image unit through
|
||||
// glUniform1i - so a glUseProgram between them fired nothing and set_shader_images' window
|
||||
// stayed the previous program's. The pipeline arm already mixed stageLinks; the plain arm
|
||||
// now mixes the same identity bit 6 reads.
|
||||
TEST_F(TrackerWalk, AProgramSwitchBetweenEqualImageUnitCountersFiresTheShaderImageBit) {
|
||||
const Uint first = Ctx().CreateProgram();
|
||||
const Uint second = Ctx().CreateProgram();
|
||||
ASSERT_EQ(Ctx().GetProgramObject(first)->GetImageUnitVersion(),
|
||||
Ctx().GetProgramObject(second)->GetImageUnitVersion())
|
||||
<< "the premise of this case is two programs whose image-unit counters are equal";
|
||||
Ctx().UseProgram(first);
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
Ctx().UseProgram(second);
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderImages), 0u)
|
||||
<< "set_shader_images' window is the program's and a glUseProgram alone did not "
|
||||
"re-emit it (F-2)";
|
||||
EXPECT_EQ(Walk(), 0u) << "the widened shutter fires forever";
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// set_vertex_attrib_defaults' payload (P2 brief D10)
|
||||
// ===================================================================================
|
||||
|
||||
@@ -3129,6 +3129,177 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// What glTexParameteri actually reached the driver, and which backend texture was bound
|
||||
// when it did. The G9 probe below is a WHITE-BOX assertion (ID-19): the parameter push is
|
||||
// not observable through public GL without creating the very sampler view whose absence is
|
||||
// the subject, so the observation is taken at the driver boundary instead.
|
||||
struct TexParamCall {
|
||||
GLuint texture;
|
||||
GLenum pname;
|
||||
GLint value;
|
||||
};
|
||||
MobileGL::Vector<TexParamCall>* g_texParamCalls = nullptr;
|
||||
GLuint g_texParamBoundTexture = 0;
|
||||
|
||||
void TP_BindTexture(GLenum, GLuint texture) { g_texParamBoundTexture = texture; }
|
||||
void TP_ActiveTexture(GLenum) {}
|
||||
void TP_TexParameteri(GLenum, GLenum pname, GLint value) {
|
||||
if (g_texParamCalls) {
|
||||
g_texParamCalls->push_back({g_texParamBoundTexture, pname, value});
|
||||
}
|
||||
}
|
||||
void TP_TexParameterf(GLenum, GLenum, GLfloat) {}
|
||||
void TP_TexParameterfv(GLenum, GLenum, const GLfloat*) {}
|
||||
void TP_PixelStorei(GLenum, GLint) {}
|
||||
void TP_BindBuffer(GLenum, GLuint) {}
|
||||
|
||||
struct ScopedTexParamRecording {
|
||||
explicit ScopedTexParamRecording(MobileGL::Vector<TexParamCall>& sink) {
|
||||
g_texParamCalls = &sink;
|
||||
g_texParamBoundTexture = 0;
|
||||
}
|
||||
~ScopedTexParamRecording() { g_texParamCalls = nullptr; }
|
||||
ScopedTexParamRecording(const ScopedTexParamRecording&) = delete;
|
||||
ScopedTexParamRecording& operator=(const ScopedTexParamRecording&) = delete;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// G9 AS A WHITE-BOX ASSERTION (gates review R1, ID-19), AND IT COVERS THE HALF THE PUBLIC-GL
|
||||
// SCENARIO CANNOT.
|
||||
//
|
||||
// TextureParamsWithoutASamplerViewScenario catches "the parameters were EMITTED and marked
|
||||
// synced but never applied": its observation is a sample, the sample creates the sampler view,
|
||||
// and IsDrawSyncClean then skips the sync. What it cannot catch is a backend that merely DEFERS
|
||||
// the apply to the first sampler view - the observation creates that view, the parameters land
|
||||
// at that moment, and the case is green. Here the reading is taken while the texture is still
|
||||
// attachment-only: nothing is ever bound to a unit, no sampler view is minted, and the
|
||||
// assertion is that the parameter reached the driver ANYWAY.
|
||||
//
|
||||
// RED ON THE PRE-P4a BEHAVIOUR: with set_texture_params addressed by resource, this twin's
|
||||
// parameter push no longer needs anything to be bound. A backend that reinstated the deferral -
|
||||
// resolving the params through a sampler view, or gating the push on a unit binding - leaves
|
||||
// the recording empty and this case fails, which is exactly the regression the scenario's
|
||||
// self-repair hides.
|
||||
//
|
||||
// WHAT IT DOES NOT COVER, AND THE NEXT READER MUST NOT OVER-TRUST IT (review N-9): the probe
|
||||
// drives SyncTextureParamsToBackend DIRECTLY, so the only deferral shape it can see is one
|
||||
// INSIDE that function. A regression that gates the CALL on a sampler view existing - in
|
||||
// SyncNeccessaryTextures, or in package E's per-unit walk - leaves this case green. That
|
||||
// caller-level half is a scenario's job and the scenario is package F's (G9's scenario half,
|
||||
// gates review R1); this is the backend-side probe R1 asked for and nothing wider.
|
||||
TEST(DirectGLESTextureSync, AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView) {
|
||||
using namespace MobileGL;
|
||||
ScopedDirectGLESTextureBindings scoped; // fresh GLContext + registry + binding caches
|
||||
Vector<TexParamCall> params;
|
||||
ScopedTexParamRecording recording(params);
|
||||
|
||||
auto functions = MG_Backend::DirectGLES::g_GLESFuncs;
|
||||
functions.glBindTexture = TP_BindTexture;
|
||||
functions.glActiveTexture = TP_ActiveTexture;
|
||||
functions.glTexParameteri = TP_TexParameteri;
|
||||
functions.glTexParameterf = TP_TexParameterf;
|
||||
functions.glTexParameterfv = TP_TexParameterfv;
|
||||
functions.glPixelStorei = TP_PixelStorei;
|
||||
functions.glBindBuffer = TP_BindBuffer;
|
||||
MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||
|
||||
GLuint name = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &name);
|
||||
const auto texture = MakeComplete2DTexture(name, 8);
|
||||
ASSERT_NE(texture, nullptr);
|
||||
// ATTACHMENT-ONLY from here on: the unit that specified it is released, so nothing in this
|
||||
// test ever binds this texture for sampling and nothing mints a sampler view for it.
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// The parameter that has to travel. Red -> Green is not any texture's default, so a driver
|
||||
// that never hears about it is distinguishable from one that does.
|
||||
texture->SetSwizzleParam(TextureSwizzleParam::Red, TextureSwizzleParam::Green);
|
||||
|
||||
auto& registry = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects;
|
||||
auto& twin = registry.GetOrCreate(texture);
|
||||
if (!twin) {
|
||||
twin = MakeShared<MG_Backend::DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
}
|
||||
ASSERT_NE(twin, nullptr);
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
const MG_Pipe::MGPipeHandle res = registry.HandleOf(texture.get());
|
||||
MG_Pipe::MGPipeHandle builtinSampler = MG_Pipe::kMGPipeNullHandle;
|
||||
if (MG_Backend::DirectGLES::TextureResourceSubsystemEnabled()) {
|
||||
// The handle arm reads the applier, so the applier is what this probe writes - which is
|
||||
// also what makes it a white-box test rather than a scenario: no client emitter exists
|
||||
// on this tree, and the point is Espryt's behaviour given a record, not the client's.
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(res));
|
||||
MG_Pipe::MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
|
||||
desc.StorageKind = static_cast<Uint8>(TextureStorageType::Mipmap);
|
||||
desc.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8);
|
||||
desc.Width = 8;
|
||||
desc.Height = 8;
|
||||
desc.Depth = 1;
|
||||
desc.ArrayLayers = 1;
|
||||
desc.Levels = 1;
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
|
||||
|
||||
// Every ITextureObject owns a sampler object, so the built-in sampler CSO is not
|
||||
// optional (a null one is Fatal{ProtocolCorruption} in the applier). It is a SAMPLER
|
||||
// CSO and not a sampler VIEW - the distinction this case exists for.
|
||||
auto samplerOwner = MakeShared<MG_State::GLState::SamplerObject>(0u);
|
||||
builtinSampler = MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::SamplerCso,
|
||||
samplerOwner->GetLifetimeId());
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(builtinSampler));
|
||||
MG_Pipe::MGPSamplerDesc samplerDesc{};
|
||||
samplerDesc.Cso = builtinSampler;
|
||||
SamplerParameters samplerParams{};
|
||||
MG_Pipe::MGPipeApplyCreateSamplerState(samplerDesc, &samplerParams);
|
||||
|
||||
MG_Pipe::MGPTextureParams pushed{};
|
||||
pushed.Res = res;
|
||||
pushed.BuiltinSampler = builtinSampler;
|
||||
pushed.MaxLevel = 0;
|
||||
pushed.Swizzle[0] = static_cast<Uint8>(TextureSwizzleParam::Green);
|
||||
pushed.Swizzle[1] = static_cast<Uint8>(TextureSwizzleParam::Green);
|
||||
pushed.Swizzle[2] = static_cast<Uint8>(TextureSwizzleParam::Blue);
|
||||
pushed.Swizzle[3] = static_cast<Uint8>(TextureSwizzleParam::Alpha);
|
||||
MG_Pipe::MGPipeApplySetTextureParams(pushed);
|
||||
|
||||
// THE OBSERVATION IS TAKEN WHILE THE TEXTURE IS STILL ATTACHMENT-ONLY: no sampler view
|
||||
// record exists, and no sampler-view twin does either. This is the assertion the public
|
||||
// scenario cannot make, because making it there would create the view.
|
||||
EXPECT_EQ(MG_Backend::DirectGLES::SamplerViewImpl::g_backendSamplerViews.FindByHandle(
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::SamplerViewCso,
|
||||
texture->GetLifetimeId())),
|
||||
nullptr)
|
||||
<< "a sampler view was minted for a texture nothing sampled";
|
||||
}
|
||||
#endif
|
||||
|
||||
const SizeT before = params.size();
|
||||
twin->SyncTextureParamsToBackend(texture);
|
||||
|
||||
Bool sawSwizzleR = false;
|
||||
for (SizeT i = before; i < params.size(); ++i) {
|
||||
if (params[i].pname == GL_TEXTURE_SWIZZLE_R) {
|
||||
sawSwizzleR = true;
|
||||
EXPECT_EQ(params[i].value, static_cast<GLint>(GL_GREEN))
|
||||
<< "the swizzle reached the driver with the wrong value";
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(sawSwizzleR)
|
||||
<< "an attachment-only texture's GL_TEXTURE_SWIZZLE_R never reached the driver: the "
|
||||
"parameter push is gated on something being bound, which is the deferral G9's public "
|
||||
"scenario cannot observe";
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(builtinSampler)) {
|
||||
MG_Pipe::MGPipeApplyDeleteSamplerState(MG_Pipe::MGPHandleOnly{builtinSampler});
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerCso, builtinSampler);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMip) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
|
||||
|
||||
@@ -3954,6 +4125,238 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr
|
||||
table.ReleaseByHandle(second);
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, second);
|
||||
}
|
||||
|
||||
// P4a: the EIGHTH table and the SIXTH kind - the sampler view, which is the only kind in
|
||||
// the phase with no frontend object at all. MobileGL has no sampler-view class: GL binds a
|
||||
// texture to a unit and the sampler uniform's type plus the two completeness predicates
|
||||
// decide what the shader sees, gallium's one-view-per-slot IS that resolved form, and the
|
||||
// resolution moves to the CLIENT (D-F3). So this table is handle-keyed only, like the
|
||||
// buffer resource table above, and its twin owns no driver id whatsoever - what it holds is
|
||||
// the server's memo of one resolved view plus the raw-depth-fetch substitution decision,
|
||||
// which is one of the two backend post-processings ARCHITECTURE.md:206 keeps on the server.
|
||||
//
|
||||
// The handle is minted off the TEXTURE's lifetime id (D-F2: one view per ITextureObject),
|
||||
// which is what makes HandleOf resolve at all - and it is legal precisely because the two
|
||||
// kinds have separate slot spaces, so the same lifetime id names a Texture slot and a
|
||||
// SamplerViewCso slot without either shadowing the other.
|
||||
{
|
||||
using MobileGL::MG_Backend::DirectGLES::SamplerViewImpl::g_backendSamplerViews;
|
||||
using MobileGL::MG_Backend::DirectGLES::SamplerViewImpl::BackendSamplerViewObject;
|
||||
auto& table = g_backendSamplerViews;
|
||||
|
||||
SharedPtr<ITextureObject> owner = MakeShared<TextureObject2D>(0u);
|
||||
const MG_Pipe::MGPipeHandle first =
|
||||
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::SamplerViewCso, owner->GetLifetimeId());
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(first))
|
||||
<< "SamplerViewCso: the client allocator minted no handle for a live texture's view";
|
||||
|
||||
// The same lifetime id, two kinds, two independent slot spaces.
|
||||
const MG_Pipe::MGPipeHandle textureHandle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture, owner->GetLifetimeId());
|
||||
EXPECT_FALSE(first == textureHandle)
|
||||
<< "SamplerViewCso: the view handle and the texture handle are the same {slot, gen}, so "
|
||||
"one kind's slot space is aliasing the other's";
|
||||
|
||||
auto& firstTwin = table.GetOrCreate(first);
|
||||
firstTwin = MakeShared<BackendSamplerViewObject>();
|
||||
firstTwin->SyncedSerial = 0xABCDEFull;
|
||||
const BackendSamplerViewObject* const firstRaw = firstTwin.get();
|
||||
EXPECT_EQ(table.FindByHandle(first), &firstTwin)
|
||||
<< "SamplerViewCso: the handle does not address the twin GetOrCreate handed back";
|
||||
EXPECT_EQ(table.HandleOf(owner.get()), first)
|
||||
<< "SamplerViewCso: HandleOf did not resolve the view minted off this texture's "
|
||||
"lifetime id, so a backend path that still arrives holding the object cannot find "
|
||||
"its view";
|
||||
|
||||
const SharedPtr<BackendSamplerViewObject> released = table.ReleaseByHandle(first);
|
||||
EXPECT_NE(released, nullptr) << "SamplerViewCso: delete_sampler_view found no twin to retire";
|
||||
EXPECT_EQ(table.FindByHandle(first), nullptr)
|
||||
<< "SamplerViewCso: the twin outlived its delete_sampler_view";
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, first);
|
||||
|
||||
auto successorOwner = MakeShared<TextureObject2D>(0u);
|
||||
const MG_Pipe::MGPipeHandle second = MG_Pipe::MGPipeSlots().Acquire(
|
||||
MG_Pipe::MGPipeKind::SamplerViewCso, successorOwner->GetLifetimeId());
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(second)) << "SamplerViewCso";
|
||||
EXPECT_EQ(second.Slot, first.Slot)
|
||||
<< "SamplerViewCso: the freed slot was not handed back, so this walk did not exercise "
|
||||
"the recycle it exists to test";
|
||||
EXPECT_NE(second.Gen, first.Gen)
|
||||
<< "SamplerViewCso: Gen did not move on slot reuse - the predecessor's handle would "
|
||||
"resolve to the successor's view, which is the ABA the {slot, gen} key exists to stop";
|
||||
EXPECT_EQ(table.FindByHandle(first), nullptr) << "SamplerViewCso: the STALE handle resolved";
|
||||
|
||||
auto& secondTwin = table.GetOrCreate(second);
|
||||
EXPECT_EQ(secondTwin, nullptr)
|
||||
<< "SamplerViewCso: a view at a recycled slot inherited its predecessor's memo, so the "
|
||||
"raw-depth-fetch decision of a dead texture would be replayed for a live one";
|
||||
secondTwin = MakeShared<BackendSamplerViewObject>();
|
||||
EXPECT_NE(table.FindByHandle(second)->get(), firstRaw) << "SamplerViewCso";
|
||||
|
||||
// BACKWARD generations are REFUSED rather than adopted, exactly as they are on every
|
||||
// other handle-keyed table (SlotTables.h:301-321). Forward is a recycle; adopting a
|
||||
// backward one would retire the incumbent LIVE twin and then stamp the slot back to the
|
||||
// dead view's generation.
|
||||
//
|
||||
// m-8: THE FIRST ASSERTION BELOW CANNOT DISTINGUISH THE TWO OUTCOMES and its message
|
||||
// must not claim it does. GetOrCreate(handle) calls entry.backend.reset() before it
|
||||
// stamps, so an ADOPTED backward generation yields a null BackendPtr& just as a refused
|
||||
// one does. What separates refusal from adoption is the SECOND assertion: after an
|
||||
// adoption the live twin at `second` is gone. The first is kept because a non-null there
|
||||
// would mean the table handed back the incumbent's own twin under the dead handle, which
|
||||
// is a third outcome and a worse one.
|
||||
auto& stale = table.GetOrCreate(first);
|
||||
EXPECT_EQ(stale, nullptr)
|
||||
<< "SamplerViewCso: a stale handle was answered with the LIVE twin at its slot (this "
|
||||
"assertion cannot tell a refusal from an adoption - the next one does)";
|
||||
EXPECT_NE(table.FindByHandle(second), nullptr)
|
||||
<< "SamplerViewCso: the live twin was destroyed by a handle from its slot's past - "
|
||||
"the backward generation was ADOPTED rather than refused";
|
||||
|
||||
table.ReleaseByHandle(second);
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, second);
|
||||
}
|
||||
}
|
||||
|
||||
// P4a (ID-8): the backend death notice is the REDUNDANT SECOND PATH for every kind the client
|
||||
// mints, and it must be IDEMPOTENT - the client's own helper emits the wire delete, raises this
|
||||
// notice and frees the slot, in that order, so whichever of the two frees first wins and the
|
||||
// other must find nothing and do nothing.
|
||||
//
|
||||
// Driven through the REAL consumer the backend installed rather than through a recording stub:
|
||||
// what is under test is Espryt's OnFrontendStateObjectDestroyed arm, including the SamplerViewCso
|
||||
// arm P4a adds, and a stub would prove only that the test can call itself.
|
||||
TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
using namespace MobileGL::MG_State::GLState;
|
||||
|
||||
if (!EsprytSlotTablesEnabled()) {
|
||||
GTEST_SKIP() << "the legacy arm keys twins on the frontend heap address and cannot answer "
|
||||
"a death notice at all";
|
||||
}
|
||||
|
||||
const MG_State::GLState::StateObjectDeathOps* ops = MG_State::GLState::GetStateObjectDeathOps();
|
||||
ASSERT_NE(ops, nullptr) << "the backend installed no death-notice consumer";
|
||||
ASSERT_NE(ops->OnDestroyed, nullptr);
|
||||
|
||||
// The sampler view first, because it is the arm P4a adds and the only kind whose notice
|
||||
// names a lifetime id belonging to ANOTHER object (the texture the view was minted off).
|
||||
{
|
||||
using SamplerViewImpl::BackendSamplerViewObject;
|
||||
using SamplerViewImpl::g_backendSamplerViews;
|
||||
|
||||
auto owner = MakeShared<TextureObject2D>(0u);
|
||||
const Uint64 lifetimeId = owner->GetLifetimeId();
|
||||
const MG_Pipe::MGPipeHandle view =
|
||||
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(view));
|
||||
g_backendSamplerViews.GetOrCreate(view) = MakeShared<BackendSamplerViewObject>();
|
||||
ASSERT_NE(g_backendSamplerViews.FindByHandle(view), nullptr);
|
||||
|
||||
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr)
|
||||
<< "the sampler-view notice left the twin behind, so the slot's next owner would "
|
||||
"inherit a dead texture's resolved view";
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId)))
|
||||
<< "the notice did not return the slot";
|
||||
|
||||
// The second path. The client's helper calls Free right after raising the notice, and a
|
||||
// Free on a slot that is no longer live at that generation is a proven no-op
|
||||
// (SlotAllocator.cpp:117-119) - so this is the shape that actually ships, twice over.
|
||||
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, view);
|
||||
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr);
|
||||
|
||||
// And the slot really is back: a successor gets it with a moved generation, which is
|
||||
// the property a double free would break by skipping one.
|
||||
auto successor = MakeShared<TextureObject2D>(0u);
|
||||
const MG_Pipe::MGPipeHandle reused = MG_Pipe::MGPipeSlots().Acquire(
|
||||
MG_Pipe::MGPipeKind::SamplerViewCso, successor->GetLifetimeId());
|
||||
EXPECT_EQ(reused.Slot, view.Slot);
|
||||
EXPECT_NE(reused.Gen, view.Gen);
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, reused);
|
||||
}
|
||||
|
||||
// The five kinds that DO have a frontend object: their notice is raised by the object's own
|
||||
// destructor today and by the client's helper after P4a's client packages land, so it is
|
||||
// delivered twice for one death. A second delivery must be a no-op rather than a second
|
||||
// free - which is what would drop a successor's twin under it.
|
||||
struct KindCase {
|
||||
const char* name;
|
||||
MG_Pipe::MGPipeKind kind;
|
||||
Uint64 lifetimeId;
|
||||
// m-9: the handle the object held before it died, so the ABA leg below can prove the
|
||||
// slot came back AT A MOVED GENERATION. ID-8's third case ("a notice for a slot
|
||||
// re-minted at a new generation") was covered for SamplerViewCso only; a double free
|
||||
// skips a generation, and nothing here would have caught that for the other five.
|
||||
MG_Pipe::MGPipeHandle handle;
|
||||
};
|
||||
Vector<KindCase> cases;
|
||||
{
|
||||
auto texture = MakeShared<TextureObject2D>(0u);
|
||||
auto renderbuffer = MakeShared<RenderbufferObject>(0u);
|
||||
auto framebuffer = MakeShared<FramebufferObject>(1u);
|
||||
auto sampler = MakeShared<SamplerObject>(0u);
|
||||
auto program = MakeShared<ProgramObject>(0u);
|
||||
|
||||
TextureImpl::g_backendTextureObjects.GetOrCreate(SharedPtr<ITextureObject>(texture));
|
||||
RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbuffer);
|
||||
FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
|
||||
SamplerImpl::g_backendSamplerObjects.GetOrCreate(sampler);
|
||||
PrgramImpl::g_backendProgramObjects.GetOrCreate(program);
|
||||
|
||||
const auto handleFor = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
return MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, lifetimeId);
|
||||
};
|
||||
cases.push_back({"Texture", MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId(),
|
||||
handleFor(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId())});
|
||||
cases.push_back({"Renderbuffer", MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId(),
|
||||
handleFor(MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId())});
|
||||
cases.push_back({"Framebuffer", MG_Pipe::MGPipeKind::Framebuffer, framebuffer->GetLifetimeId(),
|
||||
handleFor(MG_Pipe::MGPipeKind::Framebuffer, framebuffer->GetLifetimeId())});
|
||||
cases.push_back({"SamplerCso", MG_Pipe::MGPipeKind::SamplerCso, sampler->GetLifetimeId(),
|
||||
handleFor(MG_Pipe::MGPipeKind::SamplerCso, sampler->GetLifetimeId())});
|
||||
cases.push_back({"ShaderCso", MG_Pipe::MGPipeKind::ShaderCso, program->GetLifetimeId(),
|
||||
handleFor(MG_Pipe::MGPipeKind::ShaderCso, program->GetLifetimeId())});
|
||||
|
||||
for (const auto& one : cases) {
|
||||
EXPECT_FALSE(MG_Pipe::MGPipeHandleIsNull(
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
|
||||
<< one.name << ": nothing was twinned, so this walk proves nothing";
|
||||
}
|
||||
// The real destructors run here and raise the first notice.
|
||||
}
|
||||
|
||||
for (const auto& one : cases) {
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
|
||||
<< one.name << ": the object's own death did not return its slot";
|
||||
// Second and third deliveries: nothing to resolve, nothing to free, no abort.
|
||||
ops->OnDestroyed(one.kind, one.lifetimeId);
|
||||
ops->OnDestroyed(one.kind, one.lifetimeId);
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
|
||||
<< one.name << ": a redundant notice resurrected a mapping";
|
||||
|
||||
// m-9, THE ABA LEG: the slot really came back, and its generation MOVED. Two redundant
|
||||
// notices plus the object's own death are three chances to free one slot twice, and a
|
||||
// double free is invisible in every assertion above - it shows up here, as a successor
|
||||
// handed the same {slot, gen} the dead object held, which is precisely the handle a
|
||||
// surviving memo would still be naming.
|
||||
auto successor = MakeShared<TextureObject2D>(0u);
|
||||
const MG_Pipe::MGPipeHandle reused =
|
||||
MG_Pipe::MGPipeSlots().Acquire(one.kind, successor->GetLifetimeId());
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(reused)) << one.name;
|
||||
if (reused.Slot == one.handle.Slot) {
|
||||
EXPECT_NE(reused.Gen, one.handle.Gen)
|
||||
<< one.name << ": the slot came back at the SAME generation, so a memo holding "
|
||||
"the dead object's handle would resolve to its successor's twin";
|
||||
}
|
||||
MG_Pipe::MGPipeSlots().Free(one.kind, reused);
|
||||
}
|
||||
}
|
||||
|
||||
// The two-holder fix, end to end through the REAL Texture registry and the REAL destructor:
|
||||
@@ -4372,6 +4775,10 @@ TEST(DirectGLESSlotTable, AGenerationBehindTheLiveTwinIsRefusedRatherThanAdopted
|
||||
GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
|
||||
TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
|
||||
GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
|
||||
TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTwoDraws) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
#include "Includes.h"
|
||||
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -156,3 +160,69 @@ TEST(ObjectLifetimeIdTest, RenderbufferObjectAtARecycledAddressCarriesAFreshLife
|
||||
TEST(ObjectLifetimeIdTest, LiveRenderbufferObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::RenderbufferObject>("RenderbufferObject");
|
||||
}
|
||||
|
||||
// P4a mints a client handle off the lifetime id of FOUR more object classes - a texture, a
|
||||
// framebuffer, a sampler and a program - and resolves every one of their death paths through
|
||||
// the same lifetimeId -> slot map. So the property these cases prove stops being a statement
|
||||
// about two backend memos and becomes the thing the whole handle space rests on: a recycled
|
||||
// heap address must not reproduce a handle, because the applier's record and the backend's twin
|
||||
// are both keyed on one.
|
||||
//
|
||||
// The texture case is additionally a CHURN check that costs nothing extra: under a push build
|
||||
// each round mints a Texture slot in the constructor and returns it in the destructor, so 64
|
||||
// rounds that leaked would be visible to the leak cases the gates package owns.
|
||||
TEST(ObjectLifetimeIdTest, TextureObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::TextureObject2D;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<TextureObject2D>("TextureObject2D");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveTextureObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::TextureObject2D>("TextureObject2D");
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, FramebufferObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::FramebufferObject;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<FramebufferObject>("FramebufferObject");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveFramebufferObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::FramebufferObject>("FramebufferObject");
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, SamplerObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::SamplerObject;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<SamplerObject>("SamplerObject");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveSamplerObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::SamplerObject>("SamplerObject");
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, ProgramObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::ProgramObject;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<ProgramObject>("ProgramObject");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveProgramObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::ProgramObject>("ProgramObject");
|
||||
}
|
||||
|
||||
@@ -367,7 +367,9 @@ run_retrace() {
|
||||
mkdir -p "${result_dir}"
|
||||
# A repeat run of a case the previous invocation already installed and pushed only needs
|
||||
# the on-device copy back into a fresh app output directory.
|
||||
if [ "${reuse_fixture}" -eq 0 ]; then
|
||||
# MOBILEGL_TRACE_SKIP_INSTALL=1: the caller installed the APK itself (ColorOS shows an install-confirmation
|
||||
# dialog whose tap drops adb for a few seconds, so the install has to be done and settled outside a run).
|
||||
if [ "${reuse_fixture}" -eq 0 ] && [ "${MOBILEGL_TRACE_SKIP_INSTALL:-0}" != "1" ]; then
|
||||
"${ADB}" install -r "$(host_path_for_adb "${apk_file}")"
|
||||
fi
|
||||
copy_fixture_to_app
|
||||
|
||||
@@ -1811,17 +1811,21 @@ def self_test(scanned, bits, publishers, movers, moved, outside=None, undecided_
|
||||
"20%s (the %s row is STALE the moment the scan stops finding it)"
|
||||
% ("abcd"[index], name))
|
||||
|
||||
# 21. THE TWO UNDECIDED MARKS ARE STILL LOAD-BEARING. Dropping them has to make --check
|
||||
# refuse both rows as unmarked UNDECIDED - which is what says the marks are covering a
|
||||
# real blind spot rather than a verdict the analysis could give today. Control 18 is
|
||||
# the other direction: a mark the derivation DOES decide is itself a problem, so
|
||||
# neither of these can outlive its reason.
|
||||
# 21. EVERY UNDECIDED MARK IS STILL LOAD-BEARING. Dropping them all has to make --check
|
||||
# refuse EVERY marked (mutator, bit) as an unmarked UNDECIDED, and nothing else - which
|
||||
# is what says each mark is covering a real blind spot rather than a verdict the
|
||||
# analysis could give today. Control 18 is the other direction: a mark the derivation
|
||||
# DOES decide is itself a problem, so no mark can outlive its reason. Read from the
|
||||
# file's own list rather than spelled here, so a row that gains a bit - UseProgram
|
||||
# gained NEW_SAMPLER_VIEWS and NEW_SHADER_IMAGES at the P4a fable seam round - cannot
|
||||
# silently turn this control into one that counts the wrong number.
|
||||
problems, _, _, undecided_rows = object_class_problems(real, bits, movers, moved, outside, {})
|
||||
tripped(any(p.startswith("UNDECIDED answer NEW_SHADER for UseProgram") for p in problems)
|
||||
and any(p.startswith("UNDECIDED answer NEW_VERTEX_ELEMENTS for BindVertexArray")
|
||||
for p in problems)
|
||||
and len(undecided_rows) == 2,
|
||||
"21 (the two P4a undecided marks are still needed)")
|
||||
marked_pairs = sorted((mutator, bit) for mutator, marks in real_marks.items() for bit in marks)
|
||||
tripped(marked_pairs
|
||||
and all(any(p.startswith("UNDECIDED answer %s for %s" % (bit, mutator)) for p in problems)
|
||||
for mutator, bit in marked_pairs)
|
||||
and len(undecided_rows) == len(marked_pairs),
|
||||
"21 (every P4a undecided mark - %d of them - is still needed)" % len(marked_pairs))
|
||||
|
||||
# THE POSITIVE CONTROLS. (a) The row that was wrong in round 3: SetPixelStoreParam writes
|
||||
# NEW_PIXEL_PACK's shutter member sixteen times, through a token-pasting macro; it has
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
#!/usr/bin/env bash
|
||||
# G7's negative controls for P4a: drop one field from each of two descriptor conversions on purpose
|
||||
# and prove the emission-consistency suites say so, NAMING the field.
|
||||
#
|
||||
# WHAT G6 CLAIMS. For every framebuffer configuration, texture object, sampler object and program
|
||||
# the client's emitted MGPFramebufferState / MGPResourceDesc / MGPTextureParams / MGPSamplerDesc /
|
||||
# MGPSamplerView / MGPProgramDesc reproduce EXACTLY the values Espryt's SyncToBackend family reads
|
||||
# from the frontend today, field by field. MG_Test/Pipe/{FramebufferEmit,TextureEmit,SamplerEmit,
|
||||
# ImageEmit,ProgramEmit}Test.cpp are what walk it.
|
||||
#
|
||||
# WHY A CONTROL IS NEEDED AT ALL. Those suites are green on a correct conversion, and they would be
|
||||
# just as green on a conversion they had stopped looking at: a walk that drove no configurations, a
|
||||
# comparison that stopped reading the record, an assertion someone loosened. Green says nothing
|
||||
# about whether a suite can still fail. This script makes each of them fail, for the one reason it
|
||||
# exists to catch, and reports a NON-ZERO ctest THAT NAMES THE DROPPED FIELD as the pass.
|
||||
#
|
||||
# THE TWO BREAKS, one per new conversion family, each a break the COMPILER CANNOT SEE - the struct
|
||||
# still has the member, the record is still its pinned size, the PipeFields.def row still names it
|
||||
# and the generated comparator still compares it. What breaks is the VALUE:
|
||||
#
|
||||
# 1. MGPSurface::Layered stops being copied in MG_Impl/Pipe/FramebufferEmit.h. A layered
|
||||
# attachment travels as a non-layered one, which is the shape that turns a whole-array render
|
||||
# target into slice 0 - and SupportsLayeredImageBinding's rule (DirectGLES.cpp:1992-2013,
|
||||
# D-O) forces `layer` to 0 for a non-layerable target, so the wrong answer is silently
|
||||
# plausible rather than an error. It is also the field with the least other coverage: the
|
||||
# surface's Res, Level and Layer are all still right, so nothing about the ATTACHMENT changes
|
||||
# except the one bit that says how much of the texture it is.
|
||||
# TWO MECHANISMS, NOT ONE, on a tree that carries the framebuffer emitter (review F-m8): the
|
||||
# regex also matches the field's copy inside the ContentHash staging helper, so the run drops
|
||||
# Layered from the emitted record AND from the framebuffer content hash. The suite still goes
|
||||
# red naming the field, which is what the control asks; the sentence above is narrowed here
|
||||
# rather than in the code because excluding the hash copy would mean hard-coding another
|
||||
# package's helper name into a regex that deliberately does not know one.
|
||||
# 2. SamplerParameters::borderColorForm stops being copied in MG_Impl/Pipe/SamplerEmit.h. All
|
||||
# four border-colour VALUES still cross; what is lost is which of the three forms
|
||||
# (float / int / uint) they are to be read as, and D-F4 is explicit that the form crosses and
|
||||
# that all four values are compared. The failure it models is IntegerBorderColorScenario's:
|
||||
# an integer border colour read as floats is not an error anywhere, it is just the wrong
|
||||
# colour at the clamped edge of every sampled texture.
|
||||
#
|
||||
# Each patch is applied by REGEX rather than by an exact line, because both headers belong to OTHER
|
||||
# PACKAGES (C.7: FramebufferEmit.h is package B's, SamplerEmit.h is package C's) and their spelling
|
||||
# is theirs to choose: any `<something>.<field> = <expr>;` - or the designated-initializer
|
||||
# `.<field> = <expr>,` - has its right-hand side replaced by a constant. If a header does not
|
||||
# assign its field at all, because the emitter has not landed yet or because the conversion is
|
||||
# spelled some other way, that is exit 2, "could not run", never a pass.
|
||||
#
|
||||
# THE CONSTANT IS `{}` AND NOT `0`, and the one character is the difference between a control that
|
||||
# can answer and one that cannot (review F-M1). SamplerParameters::borderColorForm is a SCOPED enum
|
||||
# (MG_Pipe/MGPipeValueTypes.h, `enum class BorderColorForm : Uint8`), and `x = 0` on one is
|
||||
# `cannot convert 'int' to 'BorderColorForm' in assignment` - the patched header does not compile,
|
||||
# the script takes its "the patched header did not compile" path, and the control reports
|
||||
# could-not-run FOREVER, on every tree, with a summary line saying that exit 2 is the expected
|
||||
# answer here. `x = {}` is valid for the scoped enum AND for MGPSurface::Layered (a Uint8), so ONE
|
||||
# replacement covers both controls and neither of them needs to know its field's type.
|
||||
#
|
||||
# A CONTROL'S FIELD MUST MATTER IN AT LEAST ONE CASE. `{}` is the type's zero, so a suite whose
|
||||
# every case happens to expect the zero value of the field would stay green with the copy dropped -
|
||||
# which this script would then report, correctly, as "the negative control did not trip". Both
|
||||
# suites' headers say they must fail BY FIELD NAME, and both drive a non-default value; that is the
|
||||
# owning package's contract, and this script is what checks it rather than assuming it.
|
||||
#
|
||||
# ON THE P4a CONTRACT TREE THIS SCRIPT EXITS 2 AND SAYS SO. Both headers EXIST there - the contract
|
||||
# commit creates all five emit headers with STUB emitters that return 0 payload bytes (contract-v1
|
||||
# D1) - but neither assigns anything, so there is no field copy to drop. That is the honest report:
|
||||
# a control that "passed" because there was nothing to break would be the worst outcome available.
|
||||
#
|
||||
# WHY IT IS NOT A CI LANE. It rebuilds the library up to four times. It is run by hand, and by the
|
||||
# integrator at the P4a five-part gate (D.3 part 3, beside P2's g7_negative_control.sh and P3a's
|
||||
# p3a_vertex_input_negative_control.sh, both of which keep running unchanged).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/p4a_descriptor_negative_control.sh <build-dir>
|
||||
#
|
||||
# <build-dir> a configured build directory carrying the push-only unit suites (the emission
|
||||
# cases are compiled only under MOBILEGL_PIPE_PUSH). It is CHECKED, not assumed:
|
||||
# in a pull build every emission case is a visible skip, ctest is green before and
|
||||
# after the patch, and the run would record "the negative control did not trip" -
|
||||
# a finding about the suite that is really a finding about the build directory
|
||||
# (review F-m10). MOBILEGL_PIPE_PUSH is read out of the directory's CMakeCache.txt.
|
||||
#
|
||||
# RESTORE IS NOT ENOUGH; THE REBUILD IS PART OF THE CONTRACT. Once a header has been patched, EVERY
|
||||
# way out of this script goes through repair(): restore the header, rebuild the library from it, and
|
||||
# re-run the suite to prove the tree really went back. `cp` alone leaves <build-dir> holding a
|
||||
# libMobileGL.so in which the field is hard-wired, `ctest` does not rebuild, and nothing in an exit
|
||||
# status tells a caller to. The path that needs this most is the one that reads "NEGATIVE CONTROL
|
||||
# DID NOT TRIP": an engineer reacts to it by opening the suite and re-running ctest against this
|
||||
# very build directory, and every reading they take there would come from a deliberately corrupted
|
||||
# library. So repair() runs from the EXIT trap as well - a mid-way failure (a patched header that
|
||||
# would not compile, an interrupt) repairs too - and a repair that itself fails downgrades the
|
||||
# verdict to 2. That downgrade is delivered from THREE places, and REPAIR_RC is what makes the
|
||||
# last two of them possible (review F-v2-m4, which found it set and never read): run_control's
|
||||
# own `if ! repair` turns a failed repair into 'could-not-run' for that control; the check after
|
||||
# the control loop catches a repair that failed on any earlier path and forces the run's exit
|
||||
# code to 2; and the EXIT trap reads it too, so a repair that fails while the script is on its
|
||||
# way out through an early `exit` cannot leave a corrupted build directory behind a 0 or a 1. The state repair() reads lives in THIS shell and not in a command substitution,
|
||||
# which is what makes the trap version of it a real repair rather than a no-op, and every child's
|
||||
# exit status is additionally checked for "killed by a signal" so that a Ctrl-C stops the run
|
||||
# instead of carrying on into the next control and signing off with a verdict about a control
|
||||
# nobody ran. On the exits that happen BEFORE any patch (bad arguments, a missing header, a
|
||||
# missing suite) the tree was never touched and the build directory still holds what the caller
|
||||
# built: there is nothing to restore and nothing to rebuild, and the script says which of the two
|
||||
# situations it is leaving behind.
|
||||
#
|
||||
# Nothing is written into the repository. Logs and the header backups live in
|
||||
# <build-dir>/p4a-g7-logs/, which is inside the build tree and therefore neither committed nor
|
||||
# picked up by `git status`; the paths are printed with every verdict.
|
||||
#
|
||||
# Exit codes: 0 BOTH controls tripped AND named their field;
|
||||
# 1 a control did not answer: a suite stayed green with its field dropped, or it went
|
||||
# red without ever naming the field, so the red cannot be attributed to the drop.
|
||||
# Both are findings about the TEST, not errors in this script - and both leave the
|
||||
# tree restored AND rebuilt AND re-run;
|
||||
# 2 a control could not be run at all (bad arguments; a build directory that is not a
|
||||
# push one; a header or a field absent on this tree; no matching test; a build that
|
||||
# was already broken; a patched header that did not compile; a failed restore or a
|
||||
# failed rebuild after one; the run was INTERRUPTED). Exit 2 wins over exit 1:
|
||||
# "could not run" is never reported as "did not answer".
|
||||
set -u -o pipefail
|
||||
|
||||
BUILD_DIR=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-*) echo "unknown arg: $1" >&2; exit 2 ;;
|
||||
*) BUILD_DIR=$1; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BUILD_DIR" ] || { echo "usage: $0 <build-dir>" >&2; exit 2; }
|
||||
|
||||
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) || exit 2
|
||||
cd "$REPO_ROOT" || exit 2
|
||||
[ -f "$BUILD_DIR/CMakeCache.txt" ] || { echo "$BUILD_DIR is not a configured build directory" >&2; exit 2; }
|
||||
|
||||
# F-m10: a PULL build directory would take both controls through "did-not-trip", which reads as a
|
||||
# finding about the suites and is a finding about the argument. Every emission case is
|
||||
# `#if MOBILEGL_PIPE_PUSH` and a visible skip otherwise, so ctest is green before the patch and
|
||||
# green after it, and the script would report a defect in somebody else's test suite.
|
||||
#
|
||||
# BOTH CACHE ENTRIES ARE READ, because MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH on for
|
||||
# the configure WITHOUT writing it back to the cache (CMakeLists.txt: the `set(... ON)` shadows the
|
||||
# cached OFF). A verify build directory therefore compiles the emission cases while its cache still
|
||||
# says MOBILEGL_PIPE_PUSH:BOOL=OFF, and refusing it would be exactly the wrong answer.
|
||||
mgl_cache_is_on() {
|
||||
local line
|
||||
line=$(grep -m1 "^$1:" "$BUILD_DIR/CMakeCache.txt" 2>/dev/null) || return 1
|
||||
case "${line#*=}" in
|
||||
ON|On|on|1|TRUE|True|true|YES|Yes|yes|Y|y) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
if ! mgl_cache_is_on MOBILEGL_PIPE_PUSH && ! mgl_cache_is_on MOBILEGL_PIPE_VERIFY; then
|
||||
echo "$BUILD_DIR does not carry the push-only emission cases:" >&2
|
||||
grep -E '^MOBILEGL_PIPE_(PUSH|VERIFY):' "$BUILD_DIR/CMakeCache.txt" >&2 || \
|
||||
echo " (its CMakeCache.txt names neither MOBILEGL_PIPE_PUSH nor MOBILEGL_PIPE_VERIFY)" >&2
|
||||
echo "Every case in MG_Test/Pipe/{Framebuffer,Sampler}EmitTest.cpp is #if MOBILEGL_PIPE_PUSH and" >&2
|
||||
echo "a visible skip otherwise, so here ctest is green with the field dropped as well as without" >&2
|
||||
echo "it and this script would report 'the negative control did not trip' about a suite that" >&2
|
||||
echo "never ran - a finding about the build directory dressed up as a finding about the test." >&2
|
||||
echo "Point it at the push (or verify) build directory." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# One row per control: <header>@<field>@<ctest regex>@<owning package>. The ctest regex is the
|
||||
# SHORTEST string that selects only that suite, the way the CI filters are written.
|
||||
CONTROLS="\
|
||||
MobileGL/MG_Impl/Pipe/FramebufferEmit.h@Layered@FramebufferEmit\.@B (clientfb)
|
||||
MobileGL/MG_Impl/Pipe/SamplerEmit.h@borderColorForm@SamplerEmit\.@C (clientsp)"
|
||||
|
||||
# Inside the build tree, never in the repository: a run must not leave untracked files behind, and
|
||||
# .gitignore carries no rule for a p4a-*.log at the root. Falls back to a temp directory only if
|
||||
# the build directory cannot be written, which would be a strange build directory.
|
||||
LOG_DIR=$(cd "$BUILD_DIR" && pwd)/p4a-g7-logs
|
||||
mkdir -p "$LOG_DIR" 2>/dev/null || LOG_DIR=$(mktemp -d) || exit 2
|
||||
|
||||
say() { echo "[p4a-g7] $*" >&2; }
|
||||
|
||||
# --- the repair, shared by every exit path ---------------------------------------------------
|
||||
# PATCHED_HEADER is the one header currently patched, empty when the tree is pristine. Only one
|
||||
# control is ever in flight at a time, deliberately: two simultaneous drops would make a red
|
||||
# unattributable to either.
|
||||
PATCHED_HEADER=""
|
||||
PATCHED_BACKUP=""
|
||||
PATCHED_TEST=""
|
||||
# Sticky across every repair this run performs: 0 while every repair put the tree back, 2 once
|
||||
# any of them did not. Read in three places (see the header): run_control's `if ! repair`, the
|
||||
# check after the control loop, and the EXIT trap. It is sticky rather than per-call because a
|
||||
# build directory that was once left un-restored stays untrustworthy even if a later repair of a
|
||||
# different header succeeds.
|
||||
REPAIR_RC=0
|
||||
repair() {
|
||||
[ -n "$PATCHED_HEADER" ] || return 0
|
||||
local header=$PATCHED_HEADER backup=$PATCHED_BACKUP test=$PATCHED_TEST
|
||||
# Cleared FIRST, so a repair that is re-entered (the explicit call, then the EXIT trap) does the
|
||||
# work once and reports the same answer twice.
|
||||
PATCHED_HEADER=""
|
||||
# From the byte-for-byte copy taken before the patch, never from git: someone running this on a
|
||||
# dirty tree must get their own tree back, not HEAD.
|
||||
if [ -f "$backup" ]; then cp -f "$backup" "$header" || { REPAIR_RC=2; return 2; }; fi
|
||||
say "restored $header; rebuilding $BUILD_DIR from it"
|
||||
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" </dev/null > "$LOG_DIR/build-restored.log" 2>&1; then
|
||||
say "the tree did NOT rebuild after the restore - see $LOG_DIR/build-restored.log"
|
||||
say "THE BUILD DIRECTORY IS NOT TRUSTWORTHY: repair it before reading anything out of it."
|
||||
REPAIR_RC=2
|
||||
return 2
|
||||
fi
|
||||
if ! ctest --test-dir "$BUILD_DIR" -R "$test" --no-tests=error </dev/null \
|
||||
> "$LOG_DIR/ctest-restored.log" 2>&1; then
|
||||
say "the tree did NOT go back to green after the restore - see $LOG_DIR/ctest-restored.log"
|
||||
REPAIR_RC=2
|
||||
return 2
|
||||
fi
|
||||
say "the tree is restored, rebuilt and green again"
|
||||
return 0
|
||||
}
|
||||
|
||||
# INT and TERM as well as EXIT (p3a-g7 m3). A Ctrl-C during a rebuild would otherwise leave the
|
||||
# patched header in the tree - bash runs no EXIT trap for an uncaught SIGINT - and the next thing
|
||||
# that reader does is build, from a hard-wired field, with nothing saying so. The two extra traps
|
||||
# repair and then re-raise with the default disposition, so the exit status still reports the
|
||||
# signal. Armed for the whole run: before the first patch repair() is a no-op.
|
||||
#
|
||||
# AND THE TRAPS ARE NOT THE ONLY DETECTION, because bash's SIGINT semantics do not guarantee that
|
||||
# they run. A Ctrl-C in a terminal goes to the whole PROCESS GROUP, so the `cmake` this script is
|
||||
# waiting on dies first and returns 130 - and bash, having a handler installed, may go on to the
|
||||
# next command rather than to the trap. Measured exactly that way: the tree WAS repaired (through
|
||||
# the explicit failure path below, which is why the state has to live in this shell), but the run
|
||||
# then carried on into the second control and signed off with the summary it prints on an
|
||||
# untouched contract tree. Repaired and wrong is still wrong. So every child's status is checked
|
||||
# for "killed by a signal" as well, INTERRUPTED latches either way, and the run stops and says so.
|
||||
INTERRUPTED=""
|
||||
note_interrupt() { [ -n "$INTERRUPTED" ] || INTERRUPTED=$1; }
|
||||
# 128 + signal number is how a shell reports a child that died on a signal; nothing this script
|
||||
# runs exits above 128 for any other reason (ctest uses 8 for failing tests).
|
||||
child_was_signalled() { [ "${1:-0}" -ge 128 ]; }
|
||||
trap 'note_interrupt INT; repair; trap - INT; kill -INT $$' INT
|
||||
trap 'note_interrupt TERM; repair; trap - TERM; kill -TERM $$' TERM
|
||||
# The EXIT trap covers the paths that leave through an early `exit` with a header still patched
|
||||
# (a patch that would not compile, a build that failed). On those the status is already chosen,
|
||||
# so the trap has to OVERRIDE it when the repair did not put the tree back - which is what makes
|
||||
# REPAIR_RC readable at all from here. Calling `exit` inside an EXIT trap replaces the status and
|
||||
# does not re-enter the trap.
|
||||
on_exit() {
|
||||
repair || true
|
||||
if [ "$REPAIR_RC" -ne 0 ]; then
|
||||
say "a repair did NOT put the tree back; exit 2 regardless of what this run was going to say."
|
||||
say "THE BUILD DIRECTORY IS NOT TRUSTWORTHY: repair it before reading anything out of it."
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
trap 'on_exit' EXIT
|
||||
|
||||
# --- one control -----------------------------------------------------------------------------
|
||||
# Sets the GLOBAL CONTROL_VERDICT to "tripped" / "did-not-trip" / "wrong-reason" /
|
||||
# "could-not-run"; everything it has to say goes to stderr. The tree is repaired before it
|
||||
# returns, whatever the answer.
|
||||
#
|
||||
# A GLOBAL AND NOT AN ECHO, AND IT IS CALLED PLAINLY AND NOT IN `$(...)` (review F-M2 and F-M3,
|
||||
# two separate defects that the same change closes). The earlier form was
|
||||
# `verdict=$(run_control ...)`, and a command substitution is a SUBSHELL:
|
||||
#
|
||||
# * F-M2 - anything the function or anything it called wrote to stdout became part of
|
||||
# `$verdict`. The patcher's own success line did, so on a tree where a control really tripped
|
||||
# the verdict was a TWO-LINE string, the `case` fell through to `*)`, and the run scored a
|
||||
# control that had answered perfectly as "a control did not answer". Exit 0 was unreachable.
|
||||
# The patcher now writes to stderr as well (belt and braces), but the verdict no longer
|
||||
# travels through a stream that anything else can write to, which is the actual fix.
|
||||
# * F-M3 - PATCHED_HEADER was assigned INSIDE that subshell. Bash resets a script's traps in a
|
||||
# command substitution, so the subshell had no traps to fire, and its assignments never
|
||||
# reached the parent, so the parent's EXIT/INT/TERM traps ran with PATCHED_HEADER empty and
|
||||
# repair() returned 0 immediately. A Ctrl-C during a rebuild left the patched header in the
|
||||
# tree - the exact thing the header's own paragraph promises cannot happen, and the p3a-g7 m3
|
||||
# lesson it cites. Called plainly, the state is the parent's and the traps repair.
|
||||
CONTROL_VERDICT=""
|
||||
run_control() {
|
||||
local header=$1 field=$2 test=$3 owner=$4
|
||||
local tag matched
|
||||
tag=$(basename "$header" .h)-$field
|
||||
CONTROL_VERDICT=could-not-run
|
||||
|
||||
# 0. the control has to have something to break.
|
||||
if [ ! -f "$header" ]; then
|
||||
say "$header does not exist on this tree."
|
||||
say "It is P4a package $owner's file (BRIEF-P4A.md C.5/C.7): the client-side emitter that"
|
||||
say "carries the ${field} copy. Until it lands there is no field copy to drop, so this control"
|
||||
say "cannot run and MUST NOT report a pass. Re-run on a tree that carries that package."
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
if ! grep -qE "\.${field}[[:space:]]*=" "$header"; then
|
||||
say "$header exists but assigns no .${field}."
|
||||
say "On the P4a CONTRACT tree that is the expected answer: the contract commit creates all five"
|
||||
say "emit headers with STUB emitters (contract-v1 D1) that return 0 payload bytes and copy"
|
||||
say "nothing, and package $owner fills the body in. Later in the phase it means something else"
|
||||
say "and worse - either the conversion moved out of this header, or it does not copy ${field}"
|
||||
say "at all, and the second one would mean G6 is already broken in exactly the way this control"
|
||||
say "is supposed to create. Neither is something this script may report as a pass; look at the"
|
||||
say "header."
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
|
||||
# 1. the suite has to exist, and be green, BEFORE the break. A missing test is NOT a pass:
|
||||
# without this the script would patch, watch ctest match nothing, read that as "the test
|
||||
# failed" and report the control as tripped.
|
||||
matched=$(ctest --test-dir "$BUILD_DIR" -N -R "$test" 2>/dev/null | grep -cE '^ *Test *#[0-9]+:')
|
||||
if [ "${matched:-0}" -eq 0 ]; then
|
||||
say "no test matches $test in $BUILD_DIR."
|
||||
say "The suite is registered by the P4a contract commit; until it carries the emission cases"
|
||||
say "this control has nothing to trip and cannot report a pass."
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
say "[$tag] $matched matching test(s) before the patch"
|
||||
|
||||
say "[$tag] building $BUILD_DIR as it is"
|
||||
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" </dev/null > "$LOG_DIR/build-before-$tag.log" 2>&1; then
|
||||
say "[$tag] the build is already broken before any patch - see $LOG_DIR/build-before-$tag.log"
|
||||
tail -20 "$LOG_DIR/build-before-$tag.log" >&2
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
if ! ctest --test-dir "$BUILD_DIR" -R "$test" --no-tests=error --output-on-failure </dev/null \
|
||||
> "$LOG_DIR/ctest-before-$tag.log" 2>&1; then
|
||||
say "[$tag] $test is already red before the patch - fix that first, the control proves nothing"
|
||||
tail -30 "$LOG_DIR/ctest-before-$tag.log" >&2
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
say "[$tag] $test is green before the patch"
|
||||
|
||||
# 2. stop copying the field.
|
||||
PATCHED_BACKUP=$LOG_DIR/$(basename "$header").orig
|
||||
PATCHED_TEST=$test
|
||||
cp -f "$header" "$PATCHED_BACKUP" || { CONTROL_VERDICT=could-not-run; return; }
|
||||
# Set BEFORE the patcher runs, not after: a python that died half-way through the write must
|
||||
# still be repaired. The cost is one unnecessary rebuild in the case where the patcher matched
|
||||
# nothing and the file is byte-identical (cp refreshes its mtime).
|
||||
PATCHED_HEADER=$header
|
||||
say "[$tag] dropping the ${field} copy from $header"
|
||||
if ! python3 - "$header" "$field" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
path, field = sys.argv[1], sys.argv[2]
|
||||
text = open(path, encoding='utf-8').read()
|
||||
# `<lhs>.Layered = <expr>;` and the designated-initializer `.Layered = <expr>,`. The right-hand
|
||||
# side is REPLACED rather than the line deleted, so the record still HAS the field and the break
|
||||
# stays one the compiler cannot see.
|
||||
pattern = re.compile(r'(\.' + re.escape(field) + r'\s*=\s*)([^;,\n]+)([;,])')
|
||||
# `{}` and not `0`: borderColorForm is a SCOPED enum (enum class BorderColorForm : Uint8) and
|
||||
# `= 0` does not convert, so the patched header would never compile and this control could
|
||||
# never answer (review F-M1). `= {}` is valid for the scoped enum and for MGPSurface::Layered's
|
||||
# Uint8 alike, in an assignment and in a designated initializer, so one string covers both.
|
||||
patched, count = pattern.subn(r'\g<1>{} /* G7 NEGATIVE CONTROL: was \g<2> */\g<3>', text)
|
||||
if count == 0:
|
||||
sys.stderr.write('[p4a-g7] no assignment to .%s to patch - the header changed shape since this '
|
||||
'control was written; update the control, do not delete it.\n' % field)
|
||||
sys.exit(1)
|
||||
open(path, 'w', encoding='utf-8', newline='\n').write(patched)
|
||||
# STDERR, like every other diagnostic here: this used to be the one line in the file that
|
||||
# went to stdout, and stdout was the channel the caller captured the verdict through.
|
||||
sys.stderr.write('[p4a-g7] neutralised %d assignment(s) to .%s\n' % (count, field))
|
||||
PY
|
||||
then
|
||||
say "[$tag] the patcher did not apply; the tree is repaired on the way out"
|
||||
repair || true
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
|
||||
# 3. it must still COMPILE. A build break here would prove the static_asserts work, not that the
|
||||
# suite still checks.
|
||||
say "[$tag] rebuilding with the dropped field"
|
||||
cmake --build "$BUILD_DIR" -j "$(nproc)" </dev/null > "$LOG_DIR/build-after-$tag.log" 2>&1
|
||||
buildRc=$?
|
||||
if child_was_signalled "$buildRc" || [ -n "$INTERRUPTED" ]; then
|
||||
note_interrupt "exit $buildRc"
|
||||
say "[$tag] INTERRUPTED during the rebuild ($INTERRUPTED). Repairing the tree and stopping:"
|
||||
say "a run that carried on here would report a verdict about a control that never ran."
|
||||
repair || true
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
if [ "$buildRc" -ne 0 ]; then
|
||||
say "[$tag] the patched header did not compile, so the control cannot tell 'the test failed'"
|
||||
say "from 'nothing was built'. The break is supposed to be invisible to the compiler - if the"
|
||||
say "field is read somewhere that needs its value, say so in the control rather than working"
|
||||
say "around it."
|
||||
grep -m10 -E 'error:' "$LOG_DIR/build-after-$tag.log" >&2
|
||||
repair || true
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
say "[$tag] the patched header still compiles, so the record still has the field and its size"
|
||||
|
||||
# 4. the suite must now be RED, and NAME the field. The verdict is only RECORDED here; nothing is
|
||||
# reported and nothing returns until step 5 has put the tree back, because all three outcomes
|
||||
# leave the same corrupted build directory behind.
|
||||
say "[$tag] running $test against the dropped field"
|
||||
ctest --test-dir "$BUILD_DIR" -R "$test" --no-tests=error --output-on-failure </dev/null \
|
||||
> "$LOG_DIR/ctest-after-$tag.log" 2>&1
|
||||
ctestRc=$?
|
||||
if child_was_signalled "$ctestRc" || [ -n "$INTERRUPTED" ]; then
|
||||
note_interrupt "exit $ctestRc"
|
||||
say "[$tag] INTERRUPTED while running $test ($INTERRUPTED). A suite that was killed is not a"
|
||||
say "suite that went red: repairing the tree and stopping rather than scoring the control."
|
||||
repair || true
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
if [ "$ctestRc" -eq 0 ]; then
|
||||
CONTROL_VERDICT=did-not-trip
|
||||
elif awk '/: Failure$/ || /: error:/ { block = 1 } block { print } /^[[:space:]]*$/ { block = 0 }' \
|
||||
"$LOG_DIR/ctest-after-$tag.log" | grep -q "$field"; then
|
||||
# A red is not yet a pass: a suite that had started failing for an unrelated reason satisfies
|
||||
# the first half of the claim and none of the second. Matched against the FAILING ASSERTIONS'
|
||||
# OWN BLOCKS rather than the whole ctest log (p3a-g7 m3): gtest prints a failure as
|
||||
# `<file>:<line>: Failure` followed by the compared expressions and their values, terminated by
|
||||
# a blank line, and the field name appears among those expressions - so the block, not the
|
||||
# line, is the right unit and the whole file is the wrong one. A case NAMED after the field, a
|
||||
# skip reason quoting it or a compiler note echoed into the log would all have made "tripped"
|
||||
# mean "the string exists somewhere in the output".
|
||||
CONTROL_VERDICT=tripped
|
||||
else
|
||||
CONTROL_VERDICT=wrong-reason
|
||||
fi
|
||||
|
||||
# 5. put it back, and prove it went back. SHARED BY ALL THREE OUTCOMES, and that is the whole
|
||||
# point of doing it before the verdict is reported.
|
||||
if ! repair; then
|
||||
say "[$tag] the control's own verdict was '$CONTROL_VERDICT', but the repair failed, so that"
|
||||
say "verdict is not what this run reports: a build directory that could not be put back is"
|
||||
say "'could not run'."
|
||||
CONTROL_VERDICT=could-not-run
|
||||
return
|
||||
fi
|
||||
|
||||
case "$CONTROL_VERDICT" in
|
||||
did-not-trip)
|
||||
say "[$tag] NEGATIVE CONTROL DID NOT TRIP: $test was still green with ${field} no longer"
|
||||
say "copied into the record. The emission comparison did not notice a field it claims to"
|
||||
say "compare, so G6 is not checking what it claims to check."
|
||||
say "The run's output is kept at $LOG_DIR/ctest-after-$tag.log"
|
||||
;;
|
||||
wrong-reason)
|
||||
say "[$tag] INCONCLUSIVE: $test went red with ${field} dropped but never named it, so the red"
|
||||
say "cannot be attributed to the dropped field. The suite failed for some other reason."
|
||||
grep -m20 -E 'Failure|error|Expected|Actual' "$LOG_DIR/ctest-after-$tag.log" >&2
|
||||
say "The run's output is kept at $LOG_DIR/ctest-after-$tag.log"
|
||||
;;
|
||||
tripped)
|
||||
say "[$tag] negative control tripped, naming $field, and the tree is green again"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- both controls, then one verdict ----------------------------------------------------------
|
||||
# Both are ALWAYS run, even when the first could not run: "FramebufferEmit has no emitter yet and
|
||||
# SamplerEmit does" is a different tree from "neither does", and an engineer reading this output
|
||||
# needs to know which. The exit code is the worst of the two, with 2 (could not run) outranking
|
||||
# 1 (did not answer).
|
||||
WORST=0
|
||||
SUMMARY=""
|
||||
# The rows are collected FIRST and the loop over them carries no redirection, so that
|
||||
# run_control - which is called plainly now, in this shell, holding this shell's repair state -
|
||||
# cannot have the remaining rows eaten out from under it by a child that reads stdin. (cmake and
|
||||
# ctest are also given /dev/null explicitly below; this is the belt to that pair of braces.)
|
||||
CONTROL_ROWS=()
|
||||
while IFS= read -r mglRow; do
|
||||
[ -n "$mglRow" ] && CONTROL_ROWS+=("$mglRow")
|
||||
done <<EOF
|
||||
$CONTROLS
|
||||
EOF
|
||||
|
||||
for mglRow in "${CONTROL_ROWS[@]}"; do
|
||||
IFS='@' read -r header field test owner <<< "$mglRow"
|
||||
[ -n "$header" ] || continue
|
||||
run_control "$header" "$field" "$test" "$owner"
|
||||
verdict=$CONTROL_VERDICT
|
||||
SUMMARY="$SUMMARY
|
||||
$field ($header): $verdict"
|
||||
case "$verdict" in
|
||||
tripped) ;;
|
||||
could-not-run) WORST=2 ;;
|
||||
*) [ "$WORST" -eq 2 ] || WORST=1 ;;
|
||||
esac
|
||||
if [ -n "$INTERRUPTED" ]; then
|
||||
SUMMARY="$SUMMARY
|
||||
(INTERRUPTED - the remaining control(s) did not run)"
|
||||
WORST=2
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Every repair this run performed, read once. run_control already turns a failed repair of the
|
||||
# control it is running into 'could-not-run', so this only ever fires for a repair that failed
|
||||
# somewhere run_control does not report from - but the whole point of a sticky flag is that the
|
||||
# verdict may not be better than the state of the build directory it was measured in.
|
||||
if [ "$REPAIR_RC" -ne 0 ]; then
|
||||
SUMMARY="$SUMMARY
|
||||
(a repair did NOT put the tree back - the build directory is not trustworthy)"
|
||||
WORST=2
|
||||
fi
|
||||
|
||||
trap - EXIT
|
||||
say "---- G7 (P4a descriptor emission) ----$SUMMARY"
|
||||
case "$WORST" in
|
||||
0) say "both controls tripped and named their field; exit 0" ;;
|
||||
1) say "a control did not answer; exit 1" ;;
|
||||
2) if [ -n "$INTERRUPTED" ]; then
|
||||
say "the run was INTERRUPTED ($INTERRUPTED); exit 2. The tree was repaired before this line -"
|
||||
say "\`git status\` is the check - and NOTHING here is a verdict about the controls."
|
||||
else
|
||||
say "a control could not be run; exit 2 (this is the expected answer on the P4a contract tree,"
|
||||
say "where both emit headers are the contract's stubs and copy nothing)"
|
||||
fi ;;
|
||||
esac
|
||||
# A run this shell interrupted still exits 2 rather than by signal, because the INT trap's
|
||||
# re-raise only happens when bash actually reaches the trap. The summary above says which it was.
|
||||
exit "$WORST"
|
||||
@@ -0,0 +1,709 @@
|
||||
#!/usr/bin/env bash
|
||||
# G5's gate for P4a: the Espryt do-not-touch list is LITERAL - SEVENTEEN regions across THREE
|
||||
# files stay byte-identical after P4a.
|
||||
#
|
||||
# WHAT G5 CLAIMS, and why a diff of the files cannot say it. ARCHITECTURE.md:318 is the Espryt
|
||||
# do-not-touch list and :321 names the ONE item moved out of it (the sub-rect upload decision and
|
||||
# the stride computation, which is P4a's D-D3/D-D6 subject). P3a made the buffer half of that list
|
||||
# literal with a sha gate over eleven functions in Managers.cpp (scripts/p3a_untouched_regions.sh);
|
||||
# P4a rewrites the rest of Managers.cpp, DirectGLES.cpp and Utils.cpp by design - the twins become
|
||||
# handle-shaped, the descriptors replace the frontend reads - so the files' diffs are large and say
|
||||
# nothing about whether the ring, the permutation, the depth/stencil sampling core or the format
|
||||
# caveat moved. This gate extracts the seventeen BODIES and compares them on their own.
|
||||
#
|
||||
# THE SEVENTEEN. P3a's eleven are carried forward unchanged (BRIEF-P4A.md D-N: "P4a must not touch
|
||||
# any of the eleven"), so this script is a superset of its parent and the two run side by side -
|
||||
# the parent keeps answering P3a's question against P3a's base ref, this one answers P4a's.
|
||||
#
|
||||
# -- P3a's eleven, in Managers.cpp, in the parent's fixed order --
|
||||
# IsPoolable takes the server-side resource, never the frontend object
|
||||
# EnrollIntoPool the retireSerial = CurrentFrameSerial() + 1 stamp is load-bearing
|
||||
# AcquireFromPool hands back only entries whose GPU work is complete
|
||||
# TrimBufferPool called once per frame from Present
|
||||
# ClearBufferPool context loss
|
||||
# ProcessDeferredBufferReleases drained per draw, fast-outs on an atomic flag
|
||||
# CreateRingStorage glBufferStorageEXT + persistent|coherent, retires at serial + 1
|
||||
# RingAvailable self-heals a stale context generation
|
||||
# RingAllocate the fast path on the hot upload route
|
||||
# FlushPendingRangesNow the three-tier drain, pull arm
|
||||
# FlushPendingRangesFrom the SAME three-tier drain, on the arm that ships (ID-15). Born
|
||||
# in P3a, so its baseline is PINNED, not read from <ref-a>
|
||||
#
|
||||
# -- P4a's six (BRIEF-P4A.md D-N), all of which predate the phase --
|
||||
# StageBlocksIntoUnpackRing Managers.cpp: the unpack-PBO staging repack. It is what makes the
|
||||
# ring path issue NO glPixelStorei, and the +6 ms/frame Mali cliff
|
||||
# lives on the other side of it
|
||||
# UnpackRingAvailable Managers.cpp: honours Features.EsprytDisableUnpackRing and
|
||||
# self-heals a stale context generation
|
||||
# UnpackRingAllocate Managers.cpp: on the hot upload route
|
||||
# RecomputeBackendColorSlots Managers.cpp: the attachment permutation - three passes plus the
|
||||
# forced ~0 version memo on every moved attachment; removing the
|
||||
# empty-point detach breaks the invariant (Managers.cpp:7651-7658)
|
||||
# DepthStencilSamplingReadImpl DirectGLES.cpp: the D24S8 sampling-emulation core (memory
|
||||
# better-clouds-fullmode), entered from :8617 and :8721
|
||||
# ShouldUseCaveatTextureFormat Utils.cpp: the format handler P4a's InternalFormat descriptors
|
||||
# feed; a change here silently changes what every texture is
|
||||
# allocated as
|
||||
#
|
||||
# TWO THINGS THIS SCRIPT ADDS TO ITS PARENT, AND NOTHING ELSE (BRIEF-P4A.md D-N: "keeps every other
|
||||
# property of its parent verbatim"):
|
||||
#
|
||||
# 1. A PER-REGION SOURCE PATH. The parent's SOURCE_PATH is a single file (:85); the seventeen live
|
||||
# in three. Each row below carries its own path and the extractor reads each file once.
|
||||
# 2. A REGION KIND. [declared deviation, see DEVIATIONS below] Sixteen of the seventeen are
|
||||
# FUNCTIONS. `DepthStencilSamplingReadImpl` is a NAMESPACE (DirectGLES.cpp:8117-8578) - the
|
||||
# brief's D-N table names it as though it were a function, and the parent's extractor, which
|
||||
# finds a definition as the one `<name> (` whose closing paren is followed by `{`, finds ZERO
|
||||
# definitions of it and exits 2 forever. Hashing the whole namespace block is also the
|
||||
# stronger reading of "the D24S8 sampling-emulation CORE": the core is the ~460-line block of
|
||||
# staging, conversion and readback, not any one function inside it.
|
||||
#
|
||||
# Everything else is the parent verbatim, and deliberately so:
|
||||
#
|
||||
# HOW A BODY IS EXTRACTED. The file is masked first - comments, string, char and raw-string
|
||||
# literals are replaced by spaces of the same length, so a brace or a parenthesis inside one can
|
||||
# never be counted - and the DEFINITION is then found as the one occurrence of `<name> (` whose
|
||||
# closing parenthesis is followed (past qualifiers like const/noexcept) by `{`. That is what tells
|
||||
# a definition from a forward declaration and from a call site: a call's `)` is followed by `)`,
|
||||
# `;` or `,`, never by `{`. A NAMESPACE row is found as the one occurrence of `namespace <name>`
|
||||
# followed by `{`. The body is then brace matched in the masked text and hashed from the ORIGINAL
|
||||
# text, so a comment change inside one of these is a difference too - which is deliberate: the
|
||||
# claim is "byte-identical", and a comment that stopped describing what the code does is exactly
|
||||
# the kind of drift a "verbatim move" is supposed to be checked for.
|
||||
#
|
||||
# THE HASH'S EXACT EXTENT, so that the claim is not read wider than it is (review F-m3, inherited
|
||||
# from the parent verbatim): it starts at the beginning of the LINE THAT CARRIES THE NAME and ends
|
||||
# at the region's closing brace. A return type, an attribute or a template header sitting on an
|
||||
# EARLIER line of a multi-line signature is therefore OUTSIDE the hash, and changing one of those
|
||||
# alone does not move this gate. The end IS covered - the self-test's tail controls below prove
|
||||
# the extent reaches the closing brace - and G1's symbol report is what catches a signature that
|
||||
# changed shape.
|
||||
#
|
||||
# Exactly one definition must be found per name. Zero or two is exit 2 (could not run), never a
|
||||
# silent pass: a rename this gate could not follow must not read as "nothing moved". Two is the
|
||||
# expected shape of a #if/#else pair that re-spells one of these bodies beside an untouched copy -
|
||||
# a finding, not a limitation, because two ladders drift (ID-11, ID-15).
|
||||
#
|
||||
# A SHARED TEMPLATE OVER AN ACCESSOR INTERFACE IS REJECTED (ID-13, MEASUREMENTS.md:521): it resizes
|
||||
# pull symbols and breaks G1. If a P4a arm needs a variant of one of the seventeen it gets its own
|
||||
# `#if MOBILEGL_PIPE_PUSH` function with its own name and its own pinned sha - the
|
||||
# FlushPendingRangesFrom shape.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/p4a_untouched_regions.sh <ref-a> <ref-b> compare the seventeen at two git refs
|
||||
# scripts/p4a_untouched_regions.sh <ref> print the seventeen shas at one ref (a
|
||||
# baseline capture: ... > p4a-before.sha)
|
||||
# scripts/p4a_untouched_regions.sh --self-test prove the comparison can go red
|
||||
#
|
||||
# stdout is always the sha list - `<sha256> <region>`, one per line, in the fixed order above - so
|
||||
# a baseline capture is a plain redirect. Everything else goes to stderr.
|
||||
#
|
||||
# Both arguments are GIT REFS: the gate is about what landed, so an uncommitted edit is invisible
|
||||
# by design. Use HEAD after committing, which is what D.1 and the CI row do.
|
||||
#
|
||||
# Exit codes: 0 the seventeen are identical at both refs (or a single ref was listed);
|
||||
# 1 at least one moved - the first one in the fixed order is named on stderr;
|
||||
# 2 the gate could not run: a bad ref, a missing file, a name that is not defined
|
||||
# exactly once, or a self-test whose control failed to trip.
|
||||
#
|
||||
# DEVIATIONS from BRIEF-P4A.md D-N, both declared here rather than in a commit message so that the
|
||||
# next reader of this file meets them:
|
||||
# D-N/1 the namespace region kind, above.
|
||||
# D-N/2 the PINNED baseline is CONSULTED UNCONDITIONALLY for FlushPendingRangesFrom, where the
|
||||
# parent consults it only when <ref-a> does not define the function. D-N says that row is
|
||||
# compared "against its pinned 3e298c9a sha", and at P4a's base ref the function DOES
|
||||
# exist - so the parent's fallback would silently never fire and the pin would stop being
|
||||
# the baseline the brief names. Both readings agree on this tree (measured: the body at
|
||||
# 37da3c3a hashes to the pinned value); where they would ever disagree, this script says
|
||||
# so on stderr and keeps the PIN, because the pin is the reviewed text.
|
||||
set -u -o pipefail
|
||||
|
||||
# One row per region: <name>@<kind>@<path>. The ORDER is the fixed order the sha list is printed
|
||||
# in and the order `compare_lists` names the first mover from; P3a's eleven keep their parent's
|
||||
# positions so a reader can diff the two scripts' outputs.
|
||||
REGIONS="\
|
||||
IsPoolable@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
EnrollIntoPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
AcquireFromPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
TrimBufferPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
ClearBufferPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
ProcessDeferredBufferReleases@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
CreateRingStorage@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
RingAvailable@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
RingAllocate@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
FlushPendingRangesNow@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
FlushPendingRangesFrom@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
StageBlocksIntoUnpackRing@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
UnpackRingAvailable@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
UnpackRingAllocate@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
RecomputeBackendColorSlots@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
DepthStencilSamplingReadImpl@namespace@MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
|
||||
ShouldUseCaveatTextureFormat@function@MobileGL/MG_Backend/DirectGLES/Utils.cpp"
|
||||
|
||||
EXPECTED_FUNCTION_COUNT=17
|
||||
|
||||
# The one region born in P3a, so there is no body at P4a's base ref that this phase reviewed: its
|
||||
# baseline is the sha captured at 3e298c9a, the commit at which the two-arm shape was reviewed and
|
||||
# accepted (ID-15). See DEVIATIONS D-N/2 for why it is consulted unconditionally.
|
||||
PINNED_FUNCTIONS="FlushPendingRangesFrom"
|
||||
PINNED_BASELINE_REF=3e298c9a
|
||||
PINNED_SHA_FlushPendingRangesFrom=37fc94ffc5991923d222d585daa3af6511d2352d255623026ce35a3b6963c4a6
|
||||
|
||||
# The regions the self-test perturbs, one negative control each. FOUR, exactly as D-N requires, and
|
||||
# each is a different shape so that a control which only ever perturbed the easy one cannot leave
|
||||
# the others unproven:
|
||||
# ClearBufferPool P3a's easy control - small, no forward declaration, no overload, so
|
||||
# a failure there is about the COMPARISON rather than the extraction
|
||||
# FlushPendingRangesNow P3a's hard control - the longest body in the set, three nested
|
||||
# tiers, its own early returns, and one of a PAIR of identically
|
||||
# shaped bodies in two preprocessor arms
|
||||
# RecomputeBackendColorSlots P4a's method-shaped control: a member function spelled
|
||||
# `BackendFramebufferObject::RecomputeBackendColorSlots(` with a
|
||||
# multi-line signature and a call site of its own, so a naive
|
||||
# extraction picks the wrong occurrence
|
||||
# StageBlocksIntoUnpackRing P4a's static-in-an-anonymous-namespace control, on the hot upload
|
||||
# route, and the one whose file position sits between two other
|
||||
# protected bodies
|
||||
SELF_TEST_FUNCTIONS="ClearBufferPool FlushPendingRangesNow RecomputeBackendColorSlots StageBlocksIntoUnpackRing"
|
||||
|
||||
say() { echo "[p4a-untouched] $*" >&2; }
|
||||
|
||||
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) || exit 2
|
||||
cd "$REPO_ROOT" || exit 2
|
||||
|
||||
WORK_DIR=$(mktemp -d) || exit 2
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
# Every distinct source path the rows name, in first-appearance order.
|
||||
region_paths() {
|
||||
printf '%s\n' "$REGIONS" | awk -F@ '!seen[$3]++ { print $3 }'
|
||||
}
|
||||
|
||||
# The extractor. Three modes, all over a SPEC FILE of `<name>\t<kind>\t<file>` rows so that the
|
||||
# self-test can drive it without inventing a commit: `extract` prints one `<sha> <name>` line per
|
||||
# row in spec order, `perturb` writes a copy of one file with one statement inserted at the top of
|
||||
# one region's body, `count` is unused by the shell and kept out.
|
||||
PY=$WORK_DIR/extract.py
|
||||
cat > "$PY" <<'PYTHON'
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def mask(text):
|
||||
"""Comments and literals replaced by spaces of the same length, offsets preserved."""
|
||||
out = list(text)
|
||||
i, n = 0, len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '/' and i + 1 < n and text[i + 1] == '/':
|
||||
while i < n and text[i] != '\n':
|
||||
out[i] = ' '
|
||||
i += 1
|
||||
elif c == '/' and i + 1 < n and text[i + 1] == '*':
|
||||
out[i] = out[i + 1] = ' '
|
||||
i += 2
|
||||
while i + 1 < n and not (text[i] == '*' and text[i + 1] == '/'):
|
||||
if text[i] != '\n':
|
||||
out[i] = ' '
|
||||
i += 1
|
||||
if i + 1 < n:
|
||||
out[i] = out[i + 1] = ' '
|
||||
i += 2
|
||||
elif c == 'R' and i + 1 < n and text[i + 1] == '"':
|
||||
# R"delim( ... )delim" - a shader source is one of these, and it is full of braces.
|
||||
close = text.find('(', i + 2)
|
||||
if close < 0:
|
||||
i += 1
|
||||
continue
|
||||
delim = text[i + 2:close]
|
||||
end = text.find(')' + delim + '"', close)
|
||||
end = n if end < 0 else end + len(delim) + 2
|
||||
for j in range(i, end):
|
||||
if text[j] != '\n':
|
||||
out[j] = ' '
|
||||
i = end
|
||||
elif c == "'" and i > 0 and (text[i - 1].isdigit() or
|
||||
(text[i - 1] in 'abcdefABCDEF' and i > 1 and
|
||||
text[i - 2] in "0123456789abcdefABCDEFxX")):
|
||||
# A C++14 DIGIT SEPARATOR (16'777'216, 0xff'ff), not a char literal. Treating it as one
|
||||
# would blank forward to the next apostrophe - which can be a whole function away, in a
|
||||
# comment - and silently swallow a brace, shifting a body's extent with no diagnostic.
|
||||
i += 1
|
||||
elif c in '"\'':
|
||||
quote = c
|
||||
out[i] = ' '
|
||||
i += 1
|
||||
while i < n and text[i] != quote:
|
||||
if text[i] == '\\' and i + 1 < n:
|
||||
out[i] = ' '
|
||||
i += 1
|
||||
if text[i] != '\n':
|
||||
out[i] = ' '
|
||||
i += 1
|
||||
if i < n:
|
||||
out[i] = ' '
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def match_forward(masked, start, opener, closer):
|
||||
depth = 0
|
||||
for i in range(start, len(masked)):
|
||||
if masked[i] == opener:
|
||||
depth += 1
|
||||
elif masked[i] == closer:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def find_function(text, masked, name):
|
||||
"""Every (begin, end) at which `name` is DEFINED as a function."""
|
||||
hits = []
|
||||
for m in re.finditer(r'\b' + re.escape(name) + r'\s*\(', masked):
|
||||
open_paren = m.end() - 1
|
||||
close_paren = match_forward(masked, open_paren, '(', ')')
|
||||
if close_paren < 0:
|
||||
continue
|
||||
tail = masked[close_paren + 1:close_paren + 96]
|
||||
# Past whatever qualifiers a definition may carry; anything else means this was a call
|
||||
# or a declaration.
|
||||
stripped = re.sub(r'^(\s|const\b|noexcept\b|override\b|final\b)*', '', tail)
|
||||
if not stripped.startswith('{'):
|
||||
continue
|
||||
brace = masked.index('{', close_paren)
|
||||
end = match_forward(masked, brace, '{', '}')
|
||||
if end < 0:
|
||||
continue
|
||||
begin = text.rfind('\n', 0, m.start()) + 1
|
||||
hits.append((begin, end + 1))
|
||||
return hits
|
||||
|
||||
|
||||
def find_namespace(text, masked, name):
|
||||
"""Every (begin, end) at which `name` is DEFINED as a namespace.
|
||||
|
||||
P4a's DepthStencilSamplingReadImpl is a namespace, not a function (DirectGLES.cpp:8117-8578):
|
||||
the D24S8 sampling-emulation CORE is the whole block, and a function-shaped search finds no
|
||||
definition of it at all. The closing `} // namespace <name>` comment is masked away by the
|
||||
time this runs, so the extent is decided by brace matching exactly as a function's is - and a
|
||||
USE of the namespace (`DepthStencilSamplingReadImpl::Read(...)`) is not matched, because the
|
||||
`namespace` keyword is not in front of it.
|
||||
"""
|
||||
hits = []
|
||||
for m in re.finditer(r'\bnamespace\s+' + re.escape(name) + r'\b', masked):
|
||||
rest = masked[m.end():m.end() + 96]
|
||||
if not rest.lstrip().startswith('{'):
|
||||
continue
|
||||
brace = masked.index('{', m.end())
|
||||
end = match_forward(masked, brace, '{', '}')
|
||||
if end < 0:
|
||||
continue
|
||||
begin = text.rfind('\n', 0, m.start()) + 1
|
||||
hits.append((begin, end + 1))
|
||||
return hits
|
||||
|
||||
|
||||
def find_definition(text, masked, name, kind):
|
||||
if kind == 'namespace':
|
||||
return find_namespace(text, masked, name)
|
||||
return find_function(text, masked, name)
|
||||
|
||||
|
||||
def read_spec(path):
|
||||
rows = []
|
||||
with open(path, encoding='utf-8') as spec:
|
||||
for line in spec:
|
||||
line = line.rstrip('\n')
|
||||
if not line:
|
||||
continue
|
||||
name, kind, source = line.split('\t')
|
||||
rows.append((name, kind, source))
|
||||
return rows
|
||||
|
||||
|
||||
class Sources(object):
|
||||
"""Each file read and masked once, however many regions name it."""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
|
||||
def get(self, path):
|
||||
if path not in self.cache:
|
||||
text = open(path, encoding='utf-8', newline='').read()
|
||||
self.cache[path] = (text, mask(text))
|
||||
return self.cache[path]
|
||||
|
||||
|
||||
def extract(rows):
|
||||
sources = Sources()
|
||||
out, problems = [], []
|
||||
for name, kind, source in rows:
|
||||
try:
|
||||
text, masked = sources.get(source)
|
||||
except OSError as err:
|
||||
problems.append('%s: cannot read %s (%s)' % (name, source, err))
|
||||
continue
|
||||
hits = find_definition(text, masked, name, kind)
|
||||
if len(hits) != 1:
|
||||
problems.append('%s: expected exactly one %s definition in %s, found %d'
|
||||
% (name, kind, source, len(hits)))
|
||||
if len(hits) > 1:
|
||||
# The expected shape of this failure, and it is a FINDING rather than a limitation:
|
||||
# a `#if MOBILEGL_PIPE_PUSH` arm that re-spells one of these bodies beside an
|
||||
# untouched `#else` copy satisfies G1 (the pull build's text did not move) and
|
||||
# defeats G5 (the push build compiles a second copy that can drift). The gate
|
||||
# cannot say which of the two is "the" body, and must not pick one.
|
||||
for begin, end in hits:
|
||||
problems.append(' ...definition at line %d, %d lines'
|
||||
% (text[:begin].count('\n') + 1,
|
||||
text[:end].count('\n') - text[:begin].count('\n') + 1))
|
||||
problems.append(' the P4a arm must CALL the untouched %s, not carry a copy of it: '
|
||||
'two ladders drift (BRIEF-P4A.md D-N, ID-11, ID-15)' % name)
|
||||
continue
|
||||
begin, end = hits[0]
|
||||
body = text[begin:end]
|
||||
out.append((hashlib.sha256(body.encode('utf-8')).hexdigest(), name))
|
||||
return out, problems
|
||||
|
||||
|
||||
def perturb(rows, target, src, dst, where='head'):
|
||||
"""Insert one line into a region's body, at its HEAD or at its TAIL.
|
||||
|
||||
TWO POSITIONS, AND THE SECOND ONE IS REVIEW FINDING F-m2. Every control used to insert at the
|
||||
very first byte after the opening brace, so all four of them would still have tripped if
|
||||
find_definition() returned an extent that stopped short of the closing brace - nothing in the
|
||||
self-test proved that a region reaches its own end, and an extractor that hashed all but the
|
||||
last statement of every body would have passed the whole self-test while being blind to a
|
||||
change in that statement. The `tail` position inserts immediately BEFORE the closing brace,
|
||||
which is the byte such an extractor would have dropped.
|
||||
"""
|
||||
for name, kind, source in rows:
|
||||
if name != target:
|
||||
continue
|
||||
text = open(src, encoding='utf-8', newline='').read()
|
||||
masked = mask(text)
|
||||
hits = find_definition(text, masked, name, kind)
|
||||
if len(hits) != 1:
|
||||
sys.stderr.write('[p4a-untouched] cannot perturb %s: %d definitions\n'
|
||||
% (target, len(hits)))
|
||||
return 2
|
||||
begin, end = hits[0]
|
||||
if where == 'tail':
|
||||
# end is one PAST the closing brace (find_function / find_namespace both return
|
||||
# `match_forward(...) + 1`), so end - 1 is the brace itself and this lands inside the
|
||||
# body, one character before it ends.
|
||||
note = '\n // p4a_untouched_regions.sh --self-test: a body whose TAIL moved.\n'
|
||||
patched = text[:end - 1] + note + text[end - 1:]
|
||||
else:
|
||||
# The opening brace is located in the MASKED text and then used as an offset into the
|
||||
# original: a brace inside a comment or a string on the signature line would otherwise
|
||||
# send the perturbation somewhere that is not the body, and the control would be
|
||||
# proving the wrong thing. Offsets are identical between the two by construction
|
||||
# (mask() preserves length).
|
||||
brace = masked.index('{', begin)
|
||||
patched = (text[:brace + 1] +
|
||||
'\n // p4a_untouched_regions.sh --self-test: a body that MOVED.\n' +
|
||||
text[brace + 1:])
|
||||
open(dst, 'w', encoding='utf-8', newline='').write(patched)
|
||||
return 0
|
||||
sys.stderr.write('[p4a-untouched] %s is not one of the regions\n' % target)
|
||||
return 2
|
||||
|
||||
|
||||
def main(argv):
|
||||
# The sha list is parsed by awk, and on Windows (Git Bash, MSYS python) text-mode stdout
|
||||
# translates '\n' into CRLF - after which `$2 == n` never matches and the gate exits 1 on an
|
||||
# untouched tree. Linux CI never saw it; a developer running the gate locally always did.
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
sys.stdout.reconfigure(newline='\n')
|
||||
mode = argv[1]
|
||||
rows = read_spec(argv[2])
|
||||
if mode == 'extract':
|
||||
found, problems = extract(rows)
|
||||
for problem in problems:
|
||||
sys.stderr.write('[p4a-untouched] %s\n' % problem)
|
||||
for sha, name in found:
|
||||
sys.stdout.write('%s %s\n' % (sha, name))
|
||||
return 2 if problems else 0
|
||||
if mode == 'perturb':
|
||||
return perturb(rows, argv[3], argv[4], argv[5], argv[6] if len(argv) > 6 else 'head')
|
||||
sys.stderr.write('[p4a-untouched] unknown mode %r\n' % mode)
|
||||
return 2
|
||||
|
||||
|
||||
sys.exit(main(sys.argv))
|
||||
PYTHON
|
||||
|
||||
# Write a spec file whose rows point at the blobs in "$2" (a directory holding one file per
|
||||
# region path, named by a sanitised path so two files of the same basename could never collide).
|
||||
blob_name() { printf '%s' "$1" | tr '/' '_'; }
|
||||
|
||||
write_spec() {
|
||||
local dir=$1 spec=$2 row name kind source
|
||||
: > "$spec"
|
||||
printf '%s\n' "$REGIONS" | while IFS='@' read -r name kind source; do
|
||||
[ -n "$name" ] || continue
|
||||
printf '%s\t%s\t%s/%s\n' "$name" "$kind" "$dir" "$(blob_name "$source")" >> "$spec"
|
||||
done
|
||||
}
|
||||
|
||||
# Materialise every region source at a git ref into "$2".
|
||||
checkout_ref() {
|
||||
local ref=$1 dir=$2 source
|
||||
mkdir -p "$dir" || return 2
|
||||
for source in $(region_paths); do
|
||||
if ! git show "$ref:$source" > "$dir/$(blob_name "$source")" 2>"$WORK_DIR/show.err"; then
|
||||
say "cannot read $source at '$ref':"
|
||||
sed 's/^/[p4a-untouched] /' "$WORK_DIR/show.err" >&2
|
||||
return 2
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extract at a git ref into "$WORK_DIR/$2.sha". Every region must be defined exactly once there;
|
||||
# this is the side the gate is ABOUT (<ref-b>, and the single-ref listing's ref).
|
||||
extract_ref() {
|
||||
local ref=$1 out=$2
|
||||
checkout_ref "$ref" "$WORK_DIR/$out" || return 2
|
||||
write_spec "$WORK_DIR/$out" "$WORK_DIR/$out.spec"
|
||||
python3 "$PY" extract "$WORK_DIR/$out.spec" > "$WORK_DIR/$out.sha"
|
||||
return $?
|
||||
}
|
||||
|
||||
# The BASELINE side (<ref-a>). The sixteen ordinary regions are extracted strictly, so a rename of
|
||||
# one of THOSE is exit 2 rather than a silently short list. FlushPendingRangesFrom then takes the
|
||||
# PINNED sha (DEVIATIONS D-N/2) whether or not <ref-a> defines it, and a <ref-a> that defines it
|
||||
# DIFFERENTLY is reported - loudly - because the two answers disagreeing is itself a finding.
|
||||
extract_baseline() {
|
||||
local ref=$1 out=$2 name pinned atRef
|
||||
checkout_ref "$ref" "$WORK_DIR/$out" || return 2
|
||||
write_spec "$WORK_DIR/$out" "$WORK_DIR/$out.spec.all"
|
||||
# The strict pass, minus the pinned rows.
|
||||
cp -f "$WORK_DIR/$out.spec.all" "$WORK_DIR/$out.spec" || return 2
|
||||
for name in $PINNED_FUNCTIONS; do
|
||||
grep -v "^$name$(printf '\t')" "$WORK_DIR/$out.spec" > "$WORK_DIR/$out.spec.tmp" || true
|
||||
mv -f "$WORK_DIR/$out.spec.tmp" "$WORK_DIR/$out.spec" || return 2
|
||||
done
|
||||
if ! python3 "$PY" extract "$WORK_DIR/$out.spec" > "$WORK_DIR/$out.sha" 2>"$WORK_DIR/$out.err"; then
|
||||
say "the baseline ref '$ref' does not define the sixteen unpinned regions exactly once each:"
|
||||
sed 's/^/[p4a-untouched] /' "$WORK_DIR/$out.err" >&2
|
||||
return 2
|
||||
fi
|
||||
for name in $PINNED_FUNCTIONS; do
|
||||
eval "pinned=\$PINNED_SHA_$name"
|
||||
if [ -z "$pinned" ] || [ "$pinned" = "PLACEHOLDER_SHA" ]; then
|
||||
say "$name has no pinned baseline sha; that row cannot be compared"
|
||||
return 2
|
||||
fi
|
||||
grep "^$name$(printf '\t')" "$WORK_DIR/$out.spec.all" > "$WORK_DIR/$out.spec.pinned" || true
|
||||
atRef=$(python3 "$PY" extract "$WORK_DIR/$out.spec.pinned" 2>/dev/null | awk '{ print $1 }')
|
||||
if [ -n "$atRef" ] && [ "$atRef" != "$pinned" ]; then
|
||||
say "NOTE: $name IS defined at '$ref' and hashes $atRef, which is NOT the sha pinned in this"
|
||||
say " script ($pinned, captured at $PINNED_BASELINE_REF). The PIN is what is compared - it is"
|
||||
say " the reviewed text (ID-15) - but the two disagreeing means the push ladder moved between"
|
||||
say " $PINNED_BASELINE_REF and '$ref' without this gate being re-pinned. Re-pin deliberately or"
|
||||
say " revert; do not leave them disagreeing."
|
||||
fi
|
||||
printf '%s %s\n' "$pinned" "$name" >> "$WORK_DIR/$out.sha"
|
||||
done
|
||||
# ...and put the list back into the FIXED ORDER (review F-m1). The pinned rows were stripped out
|
||||
# of the spec above and appended here, so without this the baseline side emits them LAST while
|
||||
# extract_ref emits everything in REGIONS order. The gate itself never noticed - compare_lists
|
||||
# looks rows up by name - but the documented capture workflow did: the header promises "stdout is
|
||||
# always the sha list ... in the fixed order above - so a baseline capture is a plain redirect",
|
||||
# and a baseline captured that way then diffed against a two-ref stdout showed seven spurious
|
||||
# differences purely from row order.
|
||||
reorder_sha_list "$WORK_DIR/$out.sha" || return 2
|
||||
return 0
|
||||
}
|
||||
|
||||
# Rewrite a `<sha> <region>` list in REGIONS order, in place. Rows whose name is not in REGIONS
|
||||
# would be a bug in the caller rather than a difference, so they are kept at the end where they are
|
||||
# visible instead of being dropped.
|
||||
reorder_sha_list() {
|
||||
local file=$1 name
|
||||
: > "$file.ordered" || return 2
|
||||
printf '%s\n' "$REGIONS" | awk -F@ '{ print $1 }' > "$WORK_DIR/reorder.names" || return 2
|
||||
while read -r name; do
|
||||
[ -n "$name" ] || continue
|
||||
awk -v n="$name" '$2 == n { print }' "$file" >> "$file.ordered" || return 2
|
||||
done < "$WORK_DIR/reorder.names"
|
||||
awk 'NR == FNR { known[$0] = 1; next } !($2 in known) { print }' \
|
||||
"$WORK_DIR/reorder.names" "$file" >> "$file.ordered" || return 2
|
||||
mv -f "$file.ordered" "$file" || return 2
|
||||
return 0
|
||||
}
|
||||
|
||||
# Compare two sha lists. Prints the first region that moved, in REGIONS order.
|
||||
compare_lists() {
|
||||
local a=$1 b=$2 labelA=$3 labelB=$4 moved=0 name shaA shaB
|
||||
printf '%s\n' "$REGIONS" | awk -F@ '{ print $1 }' > "$WORK_DIR/order"
|
||||
while read -r name; do
|
||||
[ -n "$name" ] || continue
|
||||
shaA=$(awk -v n="$name" '$2 == n { print $1 }' "$a")
|
||||
shaB=$(awk -v n="$name" '$2 == n { print $1 }' "$b")
|
||||
if [ "$shaA" != "$shaB" ]; then
|
||||
if [ "$moved" -eq 0 ]; then
|
||||
say "FIRST REGION THAT MOVED: $name"
|
||||
say " $labelA ${shaA:-<not found>}"
|
||||
say " $labelB ${shaB:-<not found>}"
|
||||
say " G5 (ARCHITECTURE.md:318, :321, :515) says the Espryt do-not-touch list is literal:"
|
||||
say " P3a's buffer pool, deferred-release drain, three rings and BOTH arms of the three-tier"
|
||||
say " flush drain, plus P4a's unpack-PBO staging repack and its two ring helpers, the"
|
||||
say " attachment permutation, the D24S8 sampling-emulation core and the format-caveat"
|
||||
say " handler, all move VERBATIM. If this change is intended it is not a P4a change and it"
|
||||
say " needs its own commit and its own reason; if it is not, revert the body. A P4a arm that"
|
||||
say " carries its own COPY of one of these beside an untouched one is the same finding: the"
|
||||
say " new arm must CALL the untouched region, not re-spell it."
|
||||
else
|
||||
say "also moved: $name"
|
||||
fi
|
||||
moved=$((moved + 1))
|
||||
fi
|
||||
done < "$WORK_DIR/order"
|
||||
return $((moved > 0 ? 1 : 0))
|
||||
}
|
||||
|
||||
# --- self-test ------------------------------------------------------------------------------
|
||||
# A gate that always says "identical" and a gate that is working produce the same green, so the
|
||||
# comparison has to be shown failing. Both controls run: the POSITIVE ones (an untouched copy
|
||||
# compares equal; an edit OUTSIDE the regions is invisible) rule out a comparison that reports
|
||||
# every region as moved, and the eight NEGATIVE ones - D-N's four regions, each perturbed at the
|
||||
# HEAD of its body and again at its TAIL - rule out both the comparison that never reports any and
|
||||
# the extraction whose extent stops before the closing brace (F-m2).
|
||||
if [ "${1:-}" = "--self-test" ]; then
|
||||
[ $# -eq 1 ] || { say "--self-test takes no other arguments"; exit 2; }
|
||||
mkdir -p "$WORK_DIR/pristine" || exit 2
|
||||
for source in $(region_paths); do
|
||||
[ -f "$source" ] || { say "$source is not in this tree"; exit 2; }
|
||||
cp -f "$source" "$WORK_DIR/pristine/$(blob_name "$source")" || exit 2
|
||||
done
|
||||
write_spec "$WORK_DIR/pristine" "$WORK_DIR/pristine.spec"
|
||||
if ! python3 "$PY" extract "$WORK_DIR/pristine.spec" > "$WORK_DIR/pristine.sha"; then
|
||||
say "the extractor could not read the $EXPECTED_FUNCTION_COUNT regions out of the working tree"
|
||||
exit 2
|
||||
fi
|
||||
found=$(wc -l < "$WORK_DIR/pristine.sha")
|
||||
if [ "$found" -ne "$EXPECTED_FUNCTION_COUNT" ]; then
|
||||
say "extracted $found regions, expected $EXPECTED_FUNCTION_COUNT"
|
||||
exit 2
|
||||
fi
|
||||
say "positive control: $EXPECTED_FUNCTION_COUNT regions extracted from the working tree"
|
||||
|
||||
cp -r "$WORK_DIR/pristine" "$WORK_DIR/copy" || exit 2
|
||||
write_spec "$WORK_DIR/copy" "$WORK_DIR/copy.spec"
|
||||
python3 "$PY" extract "$WORK_DIR/copy.spec" > "$WORK_DIR/copy.sha" || exit 2
|
||||
if ! compare_lists "$WORK_DIR/pristine.sha" "$WORK_DIR/copy.sha" "pristine" "copy" 2>/dev/null; then
|
||||
say "POSITIVE CONTROL FAILED: an untouched copy compared as MOVED. The comparison is reporting"
|
||||
say "differences that are not there, so its verdict means nothing in either direction."
|
||||
exit 2
|
||||
fi
|
||||
say "positive control: an untouched copy compares equal"
|
||||
|
||||
# The second positive control, and it is the one that matters for P4a: the rest of these three
|
||||
# files IS going to be rewritten (the twins become handle-shaped, the descriptors replace the
|
||||
# frontend reads), so a gate that fired on any edit to them would have to be switched off in the
|
||||
# same week it landed. An edit outside the seventeen must be invisible here - in EVERY file, so
|
||||
# that a per-file extraction bug cannot hide behind the one file that was probed.
|
||||
cp -r "$WORK_DIR/pristine" "$WORK_DIR/outside" || exit 2
|
||||
for source in $(region_paths); do
|
||||
{ echo "// p4a_untouched_regions.sh --self-test: an edit OUTSIDE the seventeen regions."; \
|
||||
cat "$WORK_DIR/pristine/$(blob_name "$source")"; } > "$WORK_DIR/outside/$(blob_name "$source")"
|
||||
done
|
||||
write_spec "$WORK_DIR/outside" "$WORK_DIR/outside.spec"
|
||||
python3 "$PY" extract "$WORK_DIR/outside.spec" > "$WORK_DIR/outside.sha" || exit 2
|
||||
if ! compare_lists "$WORK_DIR/pristine.sha" "$WORK_DIR/outside.sha" "pristine" "outside" \
|
||||
2>/dev/null; then
|
||||
say "POSITIVE CONTROL FAILED: an edit OUTSIDE the seventeen regions was reported as one of them"
|
||||
say "moving. This gate would fire on every P4a commit to these three files and would have to be"
|
||||
say "silenced, which is the same as not having it."
|
||||
exit 2
|
||||
fi
|
||||
say "positive control: an edit outside the seventeen regions is invisible, in all three files"
|
||||
|
||||
# TWO negative controls per SELF_TEST_FUNCTIONS entry - one at the HEAD of the body and one at
|
||||
# its TAIL. Each is run on its own, from the pristine copy, so the message it produces has to
|
||||
# NAME that region: a control that only proved "some region moved" would not distinguish "this
|
||||
# row is compared" from "this row is extracted as an empty range and every comparison of it is
|
||||
# vacuous".
|
||||
#
|
||||
# THE TAIL HALF IS REVIEW FINDING F-m2. With head-only controls, an extraction that returned an
|
||||
# extent stopping short of the closing brace would still have tripped all four - the inserted
|
||||
# line is at the very first byte of the body - so nothing here proved that a region reaches its
|
||||
# own end, and a change to the LAST statement of a protected body would have been invisible to a
|
||||
# gate whose self-test was fully green. The tail control inserts immediately before the closing
|
||||
# brace, which is exactly the byte such an extractor would have dropped.
|
||||
controls=0
|
||||
for target in $SELF_TEST_FUNCTIONS; do
|
||||
targetSource=$(printf '%s\n' "$REGIONS" | awk -F@ -v n="$target" '$1 == n { print $3 }')
|
||||
[ -n "$targetSource" ] || { say "$target is not one of the regions"; exit 2; }
|
||||
for position in head tail; do
|
||||
rm -rf "$WORK_DIR/perturbed"
|
||||
cp -r "$WORK_DIR/pristine" "$WORK_DIR/perturbed" || exit 2
|
||||
write_spec "$WORK_DIR/perturbed" "$WORK_DIR/perturbed.spec"
|
||||
python3 "$PY" perturb "$WORK_DIR/perturbed.spec" "$target" \
|
||||
"$WORK_DIR/pristine/$(blob_name "$targetSource")" \
|
||||
"$WORK_DIR/perturbed/$(blob_name "$targetSource")" "$position" || exit 2
|
||||
python3 "$PY" extract "$WORK_DIR/perturbed.spec" > "$WORK_DIR/perturbed.sha" || exit 2
|
||||
if compare_lists "$WORK_DIR/pristine.sha" "$WORK_DIR/perturbed.sha" "pristine" "perturbed" \
|
||||
2> "$WORK_DIR/perturbed.err"; then
|
||||
say "NEGATIVE CONTROL DID NOT TRIP: $target's body was changed at its $position and the"
|
||||
say "comparison still reported every region as identical. This gate cannot go red for the"
|
||||
say "reason it exists, so every green it has ever printed means nothing."
|
||||
if [ "$position" = tail ]; then
|
||||
say " A TAIL control that does not trip while the head one does means the extracted"
|
||||
say " extent stops before the closing brace: the last statement of every protected body"
|
||||
say " is outside the hash and can be rewritten silently."
|
||||
fi
|
||||
exit 2
|
||||
fi
|
||||
if ! grep -q "FIRST REGION THAT MOVED: $target" "$WORK_DIR/perturbed.err"; then
|
||||
say "NEGATIVE CONTROL TRIPPED FOR THE WRONG REASON: the comparison went red but did not name"
|
||||
say "$target as the first region that moved ($position control). It said:"
|
||||
sed 's/^/[p4a-untouched] /' "$WORK_DIR/perturbed.err" >&2
|
||||
exit 2
|
||||
fi
|
||||
controls=$((controls + 1))
|
||||
say "negative control $controls: a perturbed $target body ($position) is reported, and named"
|
||||
done
|
||||
done
|
||||
if [ "$controls" -ne 8 ]; then
|
||||
say "expected EIGHT negative controls (BRIEF-P4A.md D-N's four regions, each at its head and at"
|
||||
say "its tail), ran $controls"
|
||||
exit 2
|
||||
fi
|
||||
say "self-test passed: $controls negative controls, all tripped and all named"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- the gate -------------------------------------------------------------------------------
|
||||
case $# in
|
||||
1)
|
||||
extract_baseline "$1" one || exit 2
|
||||
cat "$WORK_DIR/one.sha"
|
||||
say "listed the $EXPECTED_FUNCTION_COUNT regions at $1"
|
||||
exit 0
|
||||
;;
|
||||
2) ;;
|
||||
*)
|
||||
say "usage: $0 <ref-a> <ref-b> | $0 <ref> | $0 --self-test"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
extract_baseline "$1" a || exit 2
|
||||
extract_ref "$2" b || exit 2
|
||||
cat "$WORK_DIR/b.sha"
|
||||
|
||||
if compare_lists "$WORK_DIR/a.sha" "$WORK_DIR/b.sha" "$1" "$2"; then
|
||||
say "the $EXPECTED_FUNCTION_COUNT pool / ring / unpack-staging / attachment-permutation /"
|
||||
say "depth-stencil-sampling / format-caveat regions are byte-identical between $1 and $2"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
Reference in New Issue
Block a user