mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 00:58:30 +09:00
[Merge] (MG_Remote, P5b): combine d1 draw migration with f1 i1 t2 and r2
This commit is contained in:
+18
-20
@@ -962,6 +962,8 @@ jobs:
|
|||||||
# workflow passed that option. They had never been compiled by CI, let alone run.
|
# workflow passed that option. They had never been compiled by CI, let alone run.
|
||||||
- name: Unit tests on the split runtime
|
- name: Unit tests on the split runtime
|
||||||
working-directory: build-split
|
working-directory: build-split
|
||||||
|
env:
|
||||||
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
|
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
|
||||||
|
|
||||||
# --no-tests=error is half the gate, exactly as in integration-verify: the integration-split
|
# --no-tests=error is half the gate, exactly as in integration-verify: the integration-split
|
||||||
@@ -979,13 +981,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
ulimit -c unlimited
|
ulimit -c unlimited
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
ctest --output-on-failure -L integration-split --no-tests=error -j 4
|
ctest --output-on-failure -L integration-split --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/split-baseline.xml"
|
||||||
|
python3 ../scripts/ci/junit_tally.py "${RUNNER_TEMP}/split-baseline.xml" --require-split-ran
|
||||||
|
|
||||||
# ARCHITECTURE.md:521 asks for `ctest -L integration-gpu` to be name-for-name identical
|
# ID-65 supersedes broad inproc status parity: class-C aborts and named wrong-answer
|
||||||
# between the monolith and the split arm of the SAME build - the G2 shape extended to a
|
# debts are recorded. Monolith, integration-split and the controls remain hard gates.
|
||||||
# third arm. The knob goes in the JOB environment rather than in a ctest property, for the
|
|
||||||
# reason the MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH precedent in `integration` gives: a
|
|
||||||
# property would override it and the arm would not be an arm.
|
|
||||||
#
|
#
|
||||||
# The entries that name MOBILEGL_TRANSPORT in their OWN property (the Split. lanes) keep
|
# The entries that name MOBILEGL_TRANSPORT in their OWN property (the Split. lanes) keep
|
||||||
# their value in both passes, which is correct: they are the split family in both arms and
|
# their value in both passes, which is correct: they are the split family in both arms and
|
||||||
@@ -1007,14 +1007,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "integration-gpu entries in the split build: ${count}"
|
echo "integration-gpu entries in the split build: ${count}"
|
||||||
MOBILEGL_TRANSPORT=monolith ctest --output-on-failure -L integration-gpu --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/arm-monolith.xml"
|
MOBILEGL_TRANSPORT=monolith ctest --output-on-failure -L integration-gpu --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/arm-monolith.xml"
|
||||||
MOBILEGL_TRANSPORT=inproc ctest --output-on-failure -L integration-gpu --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/arm-inproc.xml"
|
# ID-65: broad inproc is a recorded debt census. Reduced split + controls gate below.
|
||||||
# NAME **AND STATUS**, and it is the comparison this step claimed to make and did not
|
inproc_rc=0
|
||||||
# (review finding N-3): the first version wrote a names file and never read it, and
|
MOBILEGL_TRANSPORT=inproc ctest --output-on-failure -L integration-gpu --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/arm-inproc.xml" || inproc_rc=$?
|
||||||
# `--output-on-failure` treats a SKIPPED test as not-a-failure - so the very failure
|
python3 ../scripts/ci/census_junit.py "${RUNNER_TEMP}/arm-inproc.xml" "${inproc_rc}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
# ARCHITECTURE.md:521 is about, "an inproc arm that skipped forty entries the monolith arm
|
# Retain the per-name status delta as evidence, not as the reduced-path gate.
|
||||||
# ran", was invisible here and caught only by the local gate. `ctest -N` cannot see it
|
|
||||||
# either: this is one build directory, so the two arms have identical name lists by
|
|
||||||
# construction and the difference is entirely in what each entry DID.
|
|
||||||
python3 - "${RUNNER_TEMP}/arm-monolith.xml" "${RUNNER_TEMP}/arm-inproc.xml" <<'PY'
|
python3 - "${RUNNER_TEMP}/arm-monolith.xml" "${RUNNER_TEMP}/arm-inproc.xml" <<'PY'
|
||||||
import sys, xml.etree.ElementTree as ET
|
import sys, xml.etree.ElementTree as ET
|
||||||
def rows(path):
|
def rows(path):
|
||||||
@@ -1031,12 +1028,9 @@ jobs:
|
|||||||
diff = sorted(set(a) ^ set(b)) + sorted(n for n in set(a) & set(b) if a[n] != b[n])
|
diff = sorted(set(a) ^ set(b)) + sorted(n for n in set(a) & set(b) if a[n] != b[n])
|
||||||
if diff:
|
if diff:
|
||||||
for name in diff[:40]:
|
for name in diff[:40]:
|
||||||
print(f"::error::{name}: monolith={a.get(name, '<absent>')} inproc={b.get(name, '<absent>')}")
|
print(f"{name}: monolith={a.get(name, '<absent>')} inproc={b.get(name, '<absent>')}")
|
||||||
print(f"::error::the monolith and inproc arms of ctest -L integration-gpu differ on "
|
print(f"Recorded ID-65 census: {len(diff)} name/status differences; not a parity gate")
|
||||||
f"{len(diff)} entries. ARCHITECTURE.md:521 requires them identical name for name "
|
else:
|
||||||
f"AND status; an entry that SKIPPED on one arm and ran on the other is the "
|
|
||||||
f"failure this compares for, and it is not a failure to --output-on-failure.")
|
|
||||||
raise SystemExit(1)
|
|
||||||
print(f"the two arms agree on all {len(a)} entries, name and status")
|
print(f"the two arms agree on all {len(a)} entries, name and status")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
@@ -1095,6 +1089,7 @@ jobs:
|
|||||||
# and FAILED as evidence the lane was live, so the controls could be measured against a
|
# and FAILED as evidence the lane was live, so the controls could be measured against a
|
||||||
# baseline that was already red.
|
# baseline that was already red.
|
||||||
- name: Negative controls - the verb barrier and the persistent-map push must be load-bearing
|
- name: Negative controls - the verb barrier and the persistent-map push must be load-bearing
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
working-directory: build-split
|
working-directory: build-split
|
||||||
env:
|
env:
|
||||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
@@ -1109,6 +1104,8 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
build-split/MobileGL/MG_IntegrationTest/*.log*
|
build-split/MobileGL/MG_IntegrationTest/*.log*
|
||||||
build-split/MobileGL/MG_IntegrationTest/split-logs/*.log
|
build-split/MobileGL/MG_IntegrationTest/split-logs/*.log
|
||||||
|
${{ runner.temp }}/arm-inproc.xml
|
||||||
|
${{ runner.temp }}/arm-monolith.xml
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: Upload core dumps
|
- name: Upload core dumps
|
||||||
@@ -1978,6 +1975,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CONTROL_TMPDIR: ${{ runner.temp }}
|
CONTROL_TMPDIR: ${{ runner.temp }}
|
||||||
LIBRARY_LOG: ${{ matrix.case }}/${{ matrix.backend }}/output/mobilegl.log
|
LIBRARY_LOG: ${{ matrix.case }}/${{ matrix.backend }}/output/mobilegl.log
|
||||||
|
FROZEN_LIBRARY: ${{ github.workspace }}/build-linux/libMobileGL.so
|
||||||
run: >-
|
run: >-
|
||||||
bash "${GITHUB_WORKSPACE}/scripts/ci/retrace_drop_draw_control.sh"
|
bash "${GITHUB_WORKSPACE}/scripts/ci/retrace_drop_draw_control.sh"
|
||||||
'${{ matrix.case }}' '${{ matrix.backend }}'
|
'${{ matrix.case }}' '${{ matrix.backend }}'
|
||||||
|
|||||||
@@ -8988,6 +8988,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
|
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
|
||||||
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
|
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
|
||||||
if (!srcMipmap || !dstMipmap) return;
|
if (!srcMipmap || !dstMipmap) return;
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P5b package i1's ONE backend edit, and the contract names this site
|
||||||
|
// (MG_Remote/CONTRACT-P5B.md §2 i1 "the copy-image-shadow-mirror emulation", ruling
|
||||||
|
// §6.7). Migrating glCopyImageSubData moves the first blocker off the client's
|
||||||
|
// Fatal{UnmigratedVerb} and onto the Fatal below, on every Espryt copy between two
|
||||||
|
// textures with CPU shadows - so the ruling is: UNDER A REAL TRANSPORT THE SERVER SKIPS
|
||||||
|
// THE MIRROR, and the client-side mirror ROADMAP P8 names ("CopyImage 镜像搬到 client")
|
||||||
|
// stays P8's.
|
||||||
|
//
|
||||||
|
// WHAT THE SKIP LOSES IS BOUNDED BY TWO FATALS, which is the whole reason it is allowed
|
||||||
|
// to be a skip rather than a port: a later glGetTexImage of the destination served from
|
||||||
|
// the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9) and a texture
|
||||||
|
// re-mint that re-uploads the level is Fatal{UnmigratedEmulation, "texture-remint-pull"}
|
||||||
|
// (Managers.cpp:5634). Neither can silently read the un-mirrored shadow.
|
||||||
|
//
|
||||||
|
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
|
||||||
|
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
|
||||||
|
// integration-gpu lanes under MOBILEGL_TRANSPORT=monolith, where this mirror is on an
|
||||||
|
// ordinary correct path and must still run.
|
||||||
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
|
||||||
|
#endif
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// P4a (D-M). glCopyImageSubData's CPU-shadow mirror copies the source level's shadow
|
// P4a (D-M). glCopyImageSubData's CPU-shadow mirror copies the source level's shadow
|
||||||
// rows into the DESTINATION's shadow so a later readback of the destination sees what
|
// rows into the DESTINATION's shadow so a later readback of the destination sees what
|
||||||
|
|||||||
@@ -150,7 +150,23 @@ namespace MobileGL::MG_Backend {
|
|||||||
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
|
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
|
||||||
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
|
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
|
||||||
// what keeps every one of them out of the first IPC frame (contract table 0).
|
// what keeps every one of them out of the first IPC frame (contract table 0).
|
||||||
session.SetCapabilityBits(0);
|
//
|
||||||
|
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
|
||||||
|
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
|
||||||
|
// SERVER's backend own the transform-feedback capture", and the server's table is
|
||||||
|
// visible here and nowhere on the client. It is read straight off the table
|
||||||
|
// ServerLoop::CreateBackend just built - Espryt registers XfbImpl::EndTransformFeedback
|
||||||
|
// (BackendObject_DirectGLES.cpp:1458) and Magma registers no XFB slot at all - so the
|
||||||
|
// bit is a statement about THIS backend rather than about a build option, which is
|
||||||
|
// what makes it survive a backend switch. The client reads it through
|
||||||
|
// MGL_BACKEND_SLOT_CAP at GL_Drawing.cpp's FixupGsStripCaptureOrder.
|
||||||
|
Uint64 capBits = 0;
|
||||||
|
if (const MG_Backend::BackendObject* serverBackend = loop.Backend();
|
||||||
|
serverBackend != nullptr &&
|
||||||
|
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
|
||||||
|
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
|
||||||
|
}
|
||||||
|
session.SetCapabilityBits(capBits);
|
||||||
session.SetBackend(loop.Backend());
|
session.SetBackend(loop.Backend());
|
||||||
|
|
||||||
// 3. the handshake, the four segments, and - at its end - the apply thread.
|
// 3. the handshake, the four segments, and - at its end - the apply thread.
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
#include <MG_Remote/Client/GpuWritePending.h>
|
#include <MG_Remote/Client/GpuWritePending.h>
|
||||||
#endif
|
#endif
|
||||||
|
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||||
|
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||||
|
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||||
|
// the check they replaced. P5b t2 converts ONE site in this file - the capture-ownership probe
|
||||||
|
// in FixupGsStripCaptureOrder - for the reason CONTRACT-P5B.md §6.5 gives.
|
||||||
|
#include <MG_Remote/Client/SlotCaps.h>
|
||||||
#include "../Getter/GL_Getter.h"
|
#include "../Getter/GL_Getter.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
@@ -1287,7 +1293,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// Only Vulkan-order captures need this. A backend that runs the capture on its
|
// Only Vulkan-order captures need this. A backend that runs the capture on its
|
||||||
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
|
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
|
||||||
// already produced GL's vertex order, and reordering it again would corrupt it.
|
// already produced GL's vertex order, and reordering it again would corrupt it.
|
||||||
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
|
//
|
||||||
|
// P5b t2 (CONTRACT-P5B.md §6.5): UNDER SPLIT THE TABLE THIS USED TO ASK IS THE CLIENT'S
|
||||||
|
// EMIT TABLE, whose EndTransformFeedback slot t2 just made non-null for every server -
|
||||||
|
// so the raw null check would answer "the backend owns the capture" even against Magma,
|
||||||
|
// which registers no XFB slot at all, and would skip a reorder Magma needs. The question
|
||||||
|
// is about the SERVER's table, so it is answered from the bit the server publishes.
|
||||||
|
// Under monolith (and in a pull build) this expands to the null check it replaced,
|
||||||
|
// character for character.
|
||||||
|
if (MGL_BACKEND_SLOT_CAP(EndTransformFeedback, MG_Pipe::kCapBackendOwnsXfbCapture)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
||||||
|
|||||||
@@ -1972,6 +1972,102 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
)
|
)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) --------------------------------
|
||||||
|
#
|
||||||
|
# THE LANE CASE THAT PROVES THE RECORDS CROSSED, and it is a RESULT and not a probe (R-16:
|
||||||
|
# a probe may not arm against a stub). Each of these four scenarios asserts a value that
|
||||||
|
# only exists if the verb ran on the apply thread - the texel a compute shader stored
|
||||||
|
# through an image unit, the word glCopyImageSubData moved, the counter an atomic add left
|
||||||
|
# behind, the block a program pipeline rebound - so a sink that DECLINED, or a client that
|
||||||
|
# fell through to the driver, is red by the number it reads back and not by a tally.
|
||||||
|
#
|
||||||
|
# Before i1 every one of these aborted at the client's Fatal{UnmigratedVerb} on the first
|
||||||
|
# glBindImageTexture / glDispatchCompute / glCopyImageSubData / glShaderStorageBlockBinding;
|
||||||
|
# all four are green under inproc now (the census: 13 + 6 + 11 + 3 DirectGLES entries).
|
||||||
|
# RED ONCE BY DOING X: return false from ServerVerbSink::OnLaunchGrid before the
|
||||||
|
# gl.DispatchCompute call and UnboundImageDescriptor / AtomicCounter / ProgramPipeline all
|
||||||
|
# read back their initial values.
|
||||||
|
#
|
||||||
|
# THE FILTERS ARE NARROWED TO THE CASES THAT ACTUALLY EMIT, and that is ScenarioFixture's
|
||||||
|
# rule rather than a convenience: an armed `DirectGLES.Split.` case must move the client
|
||||||
|
# encoder's record ordinal (ScenarioFixture.h:86), because a lane whose workload produced no
|
||||||
|
# record is the "resolved the transport and then fell through to the driver" shape that
|
||||||
|
# every pixel assertion is blind to. ProgramPipelineScenario's other nine cases are pure
|
||||||
|
# name/state cases that draw nothing, so the whole-scenario filter would arm them and they
|
||||||
|
# would be red for having nothing to say. The two named here are the ones that were
|
||||||
|
# Fatal{UnmigratedVerb, "ShaderStorageBlockBinding"} on the c0b head.
|
||||||
|
#
|
||||||
|
# One block per scenario, following the three above: a `:`-separated multi-pattern
|
||||||
|
# TEST_FILTER is unproven through gtest_discover_tests' flat PROPERTIES forwarding.
|
||||||
|
set(MGL_SPLIT_I1_SCENARIOS
|
||||||
|
UnboundImageDescriptorScenario # bind_shader_image (72) + launch_grid (60), 13 cases
|
||||||
|
CopyImageLayeredScenario # resource_copy_region (53), 6 cases
|
||||||
|
AtomicCounterScenario # launch_grid (60) + memory_barrier (61), 3 cases
|
||||||
|
ProgramPipelineScenario) # set_storage_block_binding (75), the 2 storage-block cases
|
||||||
|
set(MGL_SPLIT_I1_FILTER_UnboundImageDescriptorScenario "UnboundImageDescriptorScenario.*")
|
||||||
|
set(MGL_SPLIT_I1_FILTER_CopyImageLayeredScenario "CopyImageLayeredScenario.*")
|
||||||
|
set(MGL_SPLIT_I1_FILTER_AtomicCounterScenario "AtomicCounterScenario.*")
|
||||||
|
set(MGL_SPLIT_I1_FILTER_ProgramPipelineScenario "ProgramPipelineScenario.*StorageBlock*")
|
||||||
|
foreach(mglItestI1Scenario IN LISTS MGL_SPLIT_I1_SCENARIOS)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split."
|
||||||
|
TEST_LIST "MGL_SPLIT_I1_${mglItestI1Scenario}_TESTS"
|
||||||
|
TEST_FILTER "${MGL_SPLIT_I1_FILTER_${mglItestI1Scenario}}"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# ---- P5b t2: the transform-feedback spans, the XFB object bind, the patch parameter -------
|
||||||
|
#
|
||||||
|
# MG_Remote/CONTRACT-P5B.md §2 t2. The three P5 lanes above are the REDUCED PATH's targets
|
||||||
|
# (a clear, a triangle, a persistent map); these two are the first lane entries whose GREEN
|
||||||
|
# DEPENDS ON A t2 RECORD HAVING CROSSED AND BEEN APPLIED BY THE BACKEND, which is the only
|
||||||
|
# statement a package flipping a verb can make that a unit case cannot:
|
||||||
|
#
|
||||||
|
# TessellationXfbCaptureScenario covers BOTH halves of t2 in one workload. Every case
|
||||||
|
# calls glPatchParameteri(GL_PATCH_VERTICES, n) and then draws GL_PATCHES into a
|
||||||
|
# transform-feedback capture, and asserts the CAPTURED BYTES. A patch_parameter (73)
|
||||||
|
# that did not reach the server tessellates at the previous patch size and the capture
|
||||||
|
# is the wrong length; a begin/end_stream_output (62/63) that did not reach it leaves
|
||||||
|
# the buffer holding the scenario's poison value, which is what those cases print.
|
||||||
|
# XfbCaptureBufferReuseScenario is the span family alone, across four buffer lifetimes
|
||||||
|
# (a buffer per span, one immutable-storage buffer, one respecified buffer, a
|
||||||
|
# respecification that changes the capture size). It is the case that would notice a
|
||||||
|
# span whose END crossed but whose BEGIN did not, because the second span's bytes
|
||||||
|
# would be the first span's.
|
||||||
|
#
|
||||||
|
# Both are DirectGLES only, deliberately, and the reason is measured rather than assumed:
|
||||||
|
# under Magma the capture is written into the server's resident slice and there is no route
|
||||||
|
# back, because MG_Backend/Init.cpp's ConsumedSubsystemsFor(DirectVulkan) withholds
|
||||||
|
# kMGPipeSubsystemResources, so BufferObject::SyncGpuWrites() emits no resource_readback.
|
||||||
|
# Every DirectVulkan XFB capture entry in the inproc census fails on exactly that (t2-v1.md
|
||||||
|
# §"the 51"), and it retires with P7 (Magma's resource family) / P9 (the readback carrier) -
|
||||||
|
# not here. A DirectVulkan arm of this lane would be red for a reason t2 cannot fix.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split."
|
||||||
|
TEST_LIST MGL_SPLIT_T2_TESS_TESTS
|
||||||
|
TEST_FILTER "TessellationXfbCaptureScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split."
|
||||||
|
TEST_LIST MGL_SPLIT_T2_XFB_TESTS
|
||||||
|
TEST_FILTER "XfbCaptureBufferReuseScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
# The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split
|
# The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split
|
||||||
# the adopt tier is pinned at T2, the resource owner declines every acquisition and the client
|
# the adopt tier is pinned at T2, the resource owner declines every acquisition and the client
|
||||||
# pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
|
# pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
|
||||||
@@ -2047,3 +2143,19 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
check "${CMAKE_CTEST_COMMAND}" "${CMAKE_BINARY_DIR}")
|
check "${CMAKE_CTEST_COMMAND}" "${CMAKE_BINARY_DIR}")
|
||||||
set_tests_properties(SplitLogPaths.PrivateAndDistinct PROPERTIES LABELS "integration-split")
|
set_tests_properties(SplitLogPaths.PrivateAndDistinct PROPERTIES LABELS "integration-split")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# f1: split-only source keeps the monolith registry unchanged.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/F1WireScenario.cpp)
|
||||||
|
foreach(f1Slot ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D GenerateMipmap)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split.F1."
|
||||||
|
TEST_FILTER "F1WireScenario.${f1Slot}Pixels"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5b-f1-${f1Slot}.log"
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Included by CTest after all GoogleTest discovery files (ID-53).
|
# Included by CTest after all GoogleTest discovery files (ID-53).
|
||||||
# CTest appends ENVIRONMENT here; keep all previously discovered lane settings.
|
# CTest appends ENVIRONMENT here; keep all previously discovered lane settings.
|
||||||
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs")
|
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs")
|
||||||
foreach(entry IN LISTS MGL_SPLIT_CLEAR_TESTS MGL_SPLIT_TRIANGLE_TESTS MGL_SPLIT_PMAP_TESTS)
|
# P5b t2's two lanes ride the same rule: one private log path per entry, or
|
||||||
|
# SplitLogPaths.PrivateAndDistinct is red for them (ID-53).
|
||||||
|
foreach(entry IN LISTS MGL_SPLIT_CLEAR_TESTS MGL_SPLIT_TRIANGLE_TESTS MGL_SPLIT_PMAP_TESTS
|
||||||
|
MGL_SPLIT_T2_TESS_TESTS MGL_SPLIT_T2_XFB_TESTS)
|
||||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||||
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
|
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
|
||||||
endforeach()
|
endforeach()
|
||||||
@@ -18,4 +21,14 @@ foreach(scenario @MGL_SPLIT_D1_SCENARIOS@)
|
|||||||
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
|
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
|
||||||
endforeach()
|
endforeach()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# P5b package i1's four scenarios, the same shape as the small-ring loop above: every
|
||||||
|
# DirectGLES.Split. entry owns exactly one absolute log path and shares it with nobody, which
|
||||||
|
# is what SplitLogPaths.PrivateAndDistinct checks over the discovered set.
|
||||||
|
foreach(scenario @MGL_SPLIT_I1_SCENARIOS@)
|
||||||
|
foreach(entry IN LISTS MGL_SPLIT_I1_${scenario}_TESTS)
|
||||||
|
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||||
|
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
# PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it.
|
# PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it.
|
||||||
|
|||||||
@@ -19,10 +19,21 @@
|
|||||||
|
|
||||||
#include <MG_Remote/Client/ClientSession.h>
|
#include <MG_Remote/Client/ClientSession.h>
|
||||||
#include <MG_Remote/Client/EmitTables.h>
|
#include <MG_Remote/Client/EmitTables.h>
|
||||||
|
#include <MG_Remote/Server/ServerLoop.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
#define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1
|
#define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MGITest {
|
namespace MGITest {
|
||||||
|
void DelaySplitRetirementForTesting(bool enabled) {
|
||||||
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
||||||
|
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(
|
||||||
|
enabled ? +[] { std::this_thread::sleep_for(std::chrono::milliseconds(30)); } : nullptr);
|
||||||
|
#else
|
||||||
|
(void)enabled;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
SplitRuntimeState PeekSplitRuntime() {
|
SplitRuntimeState PeekSplitRuntime() {
|
||||||
SplitRuntimeState state;
|
SplitRuntimeState state;
|
||||||
|
|||||||
@@ -91,9 +91,8 @@ namespace MGITest {
|
|||||||
// than asserted: a uniform record stride over a power-of-two ring lands on the
|
// than asserted: a uniform record stride over a power-of-two ring lands on the
|
||||||
// boundary exactly and never straddles it.
|
// boundary exactly and never straddles it.
|
||||||
//
|
//
|
||||||
// stageReclaimWaits: SEG_STAGE allocations that only fitted after the encoder reclaimed
|
// stageReclaimWaits: allocations blocked after immediate reclamation failed;
|
||||||
// what the server had retired - P5's one real producer wait (see PipeWireCodec.h for
|
// already-retired bytes reclaimed lazily do not count as a wait.
|
||||||
// why the command ring has none while the verb barrier is armed).
|
|
||||||
unsigned long long maxRecordBytes = 0;
|
unsigned long long maxRecordBytes = 0;
|
||||||
unsigned long long maxRecordBytesCap = 0;
|
unsigned long long maxRecordBytesCap = 0;
|
||||||
unsigned long long cmdWraps = 0;
|
unsigned long long cmdWraps = 0;
|
||||||
@@ -103,6 +102,8 @@ namespace MGITest {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SplitRuntimeState PeekSplitRuntime();
|
SplitRuntimeState PeekSplitRuntime();
|
||||||
|
// Scheduling-only perturbation; never changes a watermark or counter.
|
||||||
|
void DelaySplitRetirementForTesting(bool enabled);
|
||||||
|
|
||||||
// Empty when this process is a real split run that can be asserted about; otherwise the
|
// Empty when this process is a real split run that can be asserted about; otherwise the
|
||||||
// reason to GTEST_SKIP() with, naming the first fact that is not true and the package that
|
// reason to GTEST_SKIP() with, naming the first fact that is not true and the package that
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ namespace MGITest::WireLedger {
|
|||||||
"kSmallRingLaneCmdByteTarget, which is sized for the 1 MiB ring this lane "
|
"kSmallRingLaneCmdByteTarget, which is sized for the 1 MiB ring this lane "
|
||||||
"declares (MGL_ITEST_GLES_SPLIT_SMALL_RING_ENVIRONMENT); a larger ring needs a "
|
"declares (MGL_ITEST_GLES_SPLIT_SMALL_RING_ENVIRONMENT); a larger ring needs a "
|
||||||
"larger workload and is not what this lane is for";
|
"larger workload and is not what this lane is for";
|
||||||
|
EXPECT_GE(state.stageReclaimWaits, 1u)
|
||||||
|
<< where << ": exit gate E3(e) - producer NEVER WAITED for staging retirement; "
|
||||||
|
"lazy reclamation of already-retired bytes is not back-pressure";
|
||||||
::testing::Test::RecordProperty("ring_wraps", static_cast<int>(state.cmdWraps));
|
::testing::Test::RecordProperty("ring_wraps", static_cast<int>(state.cmdWraps));
|
||||||
::testing::Test::RecordProperty("ring_wrap_pads", static_cast<int>(state.cmdWrapPads));
|
::testing::Test::RecordProperty("ring_wrap_pads", static_cast<int>(state.cmdWrapPads));
|
||||||
::testing::Test::RecordProperty("ring_waits", static_cast<int>(state.stageReclaimWaits));
|
::testing::Test::RecordProperty("ring_waits", static_cast<int>(state.stageReclaimWaits));
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ def main():
|
|||||||
for n in selected for c in by_name[n])
|
for n in selected for c in by_name[n])
|
||||||
if not_failed:
|
if not_failed:
|
||||||
raise ValueError(f"{label} control: {not_failed} selected entries did not fail")
|
raise ValueError(f"{label} control: {not_failed} selected entries did not fail")
|
||||||
|
elif mode == "assertion":
|
||||||
|
cases = ET.parse(sys.argv[4]).getroot().findall(".//testcase")
|
||||||
|
by_name = {case.get("name"): case for case in cases}
|
||||||
|
for name in selected:
|
||||||
|
case = by_name.get(name)
|
||||||
|
output = "" if case is None else " ".join(" ".join(case.itertext()).split())
|
||||||
|
if not re.search(sys.argv[5], output):
|
||||||
|
raise ValueError(f"E3(a) FAILED: {name} red lacks its persistent-map push diagnostic")
|
||||||
elif mode == "evidence":
|
elif mode == "evidence":
|
||||||
missing = []
|
missing = []
|
||||||
label = sys.argv[5] if len(sys.argv) > 5 else ""
|
label = sys.argv[5] if len(sys.argv) > 5 else ""
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
// f1 split-only pixel controls: every result depends on the migrated verb.
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
#include "../Harness/SplitRuntimePeek.h"
|
||||||
|
#include <array>
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
class F1WireScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
GLuint fbo = 0, texture = 0;
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
const auto why = SplitRuntimeSkipReason();
|
||||||
|
if (!why.empty()) GTEST_SKIP() << why;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glStencilMask(~0u);
|
||||||
|
}
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (fbo) glDeleteFramebuffers(1, &fbo);
|
||||||
|
if (texture) glDeleteTextures(1, &texture);
|
||||||
|
ScenarioTest::TearDown();
|
||||||
|
}
|
||||||
|
void Attach(GLenum format, GLenum attachment = GL_COLOR_ATTACHMENT0, int levels = 1) {
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, levels, format, 8, 8);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, texture, 0);
|
||||||
|
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); }
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)) << "F1.setup.framebuffer";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearBufferfvPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearBufferfv.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA8);
|
||||||
|
const GLfloat value[4] = {0.25f, 0.5f, 0.75f, 1.0f};
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearBufferfv(GL_COLOR, 0, value);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferfv.wire";
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferfv.error";
|
||||||
|
const int expected[4] = {64, 128, 191, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.ClearBufferfv.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearNamedFramebufferfvPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferfv.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA8);
|
||||||
|
const GLfloat value[4] = {0.25f, 0.5f, 0.75f, 1.0f};
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearNamedFramebufferfv(fbo, GL_COLOR, 0, value);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferfv.wire";
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferfv.error";
|
||||||
|
const int expected[4] = {64, 128, 191, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.ClearNamedFramebufferfv.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearBufferivPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearBufferiv.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA32I);
|
||||||
|
const GLint value[4] = {-37, 19, -11, 5};
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearBufferiv(GL_COLOR, 0, value);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferiv.wire";
|
||||||
|
GLint pixel[4]{};
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_INT, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferiv.error";
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearBufferiv.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearNamedFramebufferivPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferiv.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA32I);
|
||||||
|
const GLint value[4] = {-37, 19, -11, 5};
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearNamedFramebufferiv(fbo, GL_COLOR, 0, value);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferiv.wire";
|
||||||
|
GLint pixel[4]{};
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_INT, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferiv.error";
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearNamedFramebufferiv.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearBufferuivPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearBufferuiv.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA32UI);
|
||||||
|
const GLuint value[4] = {37, 19, 11, 5};
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearBufferuiv(GL_COLOR, 0, value);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferuiv.wire";
|
||||||
|
GLuint pixel[4]{};
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferuiv.error";
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearBufferuiv.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearNamedFramebufferuivPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferuiv.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA32UI);
|
||||||
|
const GLuint value[4] = {37, 19, 11, 5};
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearNamedFramebufferuiv(fbo, GL_COLOR, 0, value);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferuiv.wire";
|
||||||
|
GLuint pixel[4]{};
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferuiv.error";
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearNamedFramebufferuiv.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearBufferfiPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearBufferfi.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT);
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.375f, 91);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferfi.wire";
|
||||||
|
GLuint pixel = 0;
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, &pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferfi.error";
|
||||||
|
EXPECT_EQ(pixel & 255u, 91u) << "F1.ClearBufferfi.pixels";
|
||||||
|
EXPECT_NEAR(double(pixel >> 8) / 16777215.0, 0.375, 0.00001) << "F1.ClearBufferfi.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, ClearNamedFramebufferfiPixels) {
|
||||||
|
// Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferfi.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT);
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glClearNamedFramebufferfi(fbo, GL_DEPTH_STENCIL, 0, 0.375f, 91);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferfi.wire";
|
||||||
|
GLuint pixel = 0;
|
||||||
|
glReadPixels(2, 3, 1, 1, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, &pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferfi.error";
|
||||||
|
EXPECT_EQ(pixel & 255u, 91u) << "F1.ClearNamedFramebufferfi.pixels";
|
||||||
|
EXPECT_NEAR(double(pixel >> 8) / 16777215.0, 0.375, 0.00001) << "F1.ClearNamedFramebufferfi.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, CopyTexImage2DPixels) {
|
||||||
|
// Red once (executed, reverted): omit the copy sink call; F1.CopyTexImage2D.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA8);
|
||||||
|
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
GLuint destination = 0;
|
||||||
|
glGenTextures(1, &destination);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, destination);
|
||||||
|
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 3, 4, 4, 0);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.CopyTexImage2D.wire";
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0);
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.CopyTexImage2D.error";
|
||||||
|
const int expected[4] = {64, 128, 191, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.CopyTexImage2D.pixels";
|
||||||
|
glDeleteTextures(1, &destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, CopyTexSubImage2DPixels) {
|
||||||
|
// Red once (executed, reverted): omit the copy sink call; F1.CopyTexSubImage2D.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA8);
|
||||||
|
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
GLuint destination = 0;
|
||||||
|
glGenTextures(1, &destination);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, destination);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 1, 1, 2, 3, 2, 2);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.CopyTexSubImage2D.wire";
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0);
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.CopyTexSubImage2D.error";
|
||||||
|
const int expected[4] = {64, 128, 191, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.CopyTexSubImage2D.pixels";
|
||||||
|
glDeleteTextures(1, &destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(F1WireScenario, GenerateMipmapPixels) {
|
||||||
|
// Red once (executed, reverted): omit the mipmap sink call; F1.GenerateMipmap.pixels fails.
|
||||||
|
if (!Ready()) return;
|
||||||
|
Attach(GL_RGBA8, GL_COLOR_ATTACHMENT0, 4);
|
||||||
|
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
const auto before = PeekSplitRuntime().emitSeq;
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.GenerateMipmap.wire";
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 2);
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.GenerateMipmap.error";
|
||||||
|
const int expected[4] = {64, 128, 191, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.GenerateMipmap.pixels";
|
||||||
|
}
|
||||||
|
} // namespace MGITest
|
||||||
@@ -264,6 +264,21 @@ void main() { oColor = vec4(vColor, 1.0); }
|
|||||||
// the lane is that IT is the arm with a ring the workload can fill.
|
// the lane is that IT is the arm with a ring the workload can fill.
|
||||||
if (SplitLane::IsSmallRingLane()) {
|
if (SplitLane::IsSmallRingLane()) {
|
||||||
const unsigned long long driven = DriveUntilSmallRingOverruns();
|
const unsigned long long driven = DriveUntilSmallRingOverruns();
|
||||||
|
// Ordinary uploads, each fitting by itself, jointly exceed the lane's 1 MiB
|
||||||
|
// staging segment. Delay only scheduling between applied and retired: the
|
||||||
|
// production allocator, not this test, must observe capacity and wait.
|
||||||
|
GLuint pressureBuffer = 0;
|
||||||
|
glGenBuffers(1, &pressureBuffer);
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, pressureBuffer);
|
||||||
|
std::vector<unsigned char> upload(768 * 1024, 0x5a);
|
||||||
|
glBufferData(GL_COPY_WRITE_BUFFER, upload.size(), nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
DelaySplitRetirementForTesting(true);
|
||||||
|
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, upload.size(), upload.data());
|
||||||
|
upload[0] = 0xa5;
|
||||||
|
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, upload.size(), upload.data());
|
||||||
|
DelaySplitRetirementForTesting(false);
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &pressureBuffer);
|
||||||
Gl().EndFrame();
|
Gl().EndFrame();
|
||||||
WireLedger::ExpectSmallRingWrappedAtLeastOnce(
|
WireLedger::ExpectSmallRingWrappedAtLeastOnce(
|
||||||
"TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary", driven);
|
"TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary", driven);
|
||||||
|
|||||||
@@ -105,6 +105,14 @@
|
|||||||
/* DirectGLES.cpp:4497, PrepareForDraw, the second unconditional pointer read of every draw. */ \
|
/* DirectGLES.cpp:4497, PrepareForDraw, the second unconditional pointer read of every draw. */ \
|
||||||
X(GetProgramForDraw, BARRIER_PULLED, "P8 (Espryt), P7 (Magma)", \
|
X(GetProgramForDraw, BARRIER_PULLED, "P8 (Espryt), P7 (Magma)", \
|
||||||
"frontend SharedPtr<ProgramObject>; the record carries a handle") \
|
"frontend SharedPtr<ProgramObject>; the record carries a handle") \
|
||||||
|
/* P5b package i1 (CONTRACT-P5B.md §6.9, the one row the contract grants a package). This */ \
|
||||||
|
/* was FATAL "reachable only from kDispatch; there is no compute on the reduced path" - and */ \
|
||||||
|
/* i1 IS what puts compute on the path: launch_grid (60) now crosses and the backend's */ \
|
||||||
|
/* PrepareForCompute pulls this inside it (DirectGLES.cpp:5779, VulkanRenderer.cpp:7327). */ \
|
||||||
|
/* It is GetProgramForDraw's twin in every respect, so it takes its class and its retiring */ \
|
||||||
|
/* phases. Overturned by: nothing in P5b; P7/P8 retire both rows together. */ \
|
||||||
|
X(GetProgramForDispatch, BARRIER_PULLED, "P7 (Magma), P8 (Espryt)", \
|
||||||
|
"frontend SharedPtr<ProgramObject>; the record carries a handle") \
|
||||||
/* The XFB six. XFB itself is off the reduced path, but kDraw's may-read mask carries all */ \
|
/* The XFB six. XFB itself is off the reduced path, but kDraw's may-read mask carries all */ \
|
||||||
/* six and the draw walk reads them regardless - which is exactly the case a field census */ \
|
/* six and the draw walk reads them regardless - which is exactly the case a field census */ \
|
||||||
/* taken from "what the scenario does" rather than from the mask would miss. */ \
|
/* taken from "what the scenario does" rather than from the mask would miss. */ \
|
||||||
@@ -132,13 +140,13 @@
|
|||||||
X(GetPixelStoreParameters, APPLIER_DERIVED, "-", \
|
X(GetPixelStoreParameters, APPLIER_DERIVED, "-", \
|
||||||
"set_pixel_pack_state; the applier writes m_pixelStore[0] (PipeApply.cpp:1373)") \
|
"set_pixel_pack_state; the applier writes m_pixelStore[0] (PipeApply.cpp:1373)") \
|
||||||
\
|
\
|
||||||
/* ---- FATAL: three non-sticky fields, each off the reduced path for a checkable reason --- */ \
|
/* ---- FATAL: two non-sticky fields, each off the reduced path for a checkable reason ----- */ \
|
||||||
|
/* Three until P5b: GetProgramForDispatch moved up to BARRIER_PULLED when package i1 put */ \
|
||||||
|
/* compute on the path (CONTRACT-P5B.md §6.9). */ \
|
||||||
X(GetBoundTransformFeedbackName, FATAL, "-", \
|
X(GetBoundTransformFeedbackName, FATAL, "-", \
|
||||||
"DEAD: read by no backend since the D21 rekey (PipeInputs.h:232-234)") \
|
"DEAD: read by no backend since the D21 rekey (PipeInputs.h:232-234)") \
|
||||||
X(GetTransformFeedbackPausedPrimitiveCounter, FATAL, "-", \
|
X(GetTransformFeedbackPausedPrimitiveCounter, FATAL, "-", \
|
||||||
"reachable only from class kQuery, which the reduced path never enters") \
|
"reachable only from class kQuery, which the reduced path never enters") \
|
||||||
X(GetProgramForDispatch, FATAL, "-", \
|
|
||||||
"reachable only from kDispatch; there is no compute on the reduced path") \
|
|
||||||
\
|
\
|
||||||
/* ---- the seven sticky forwards, as FIELD rows ---- */ \
|
/* ---- the seven sticky forwards, as FIELD rows ---- */ \
|
||||||
/* They have no storage, so a read of the FIELD is a call of the FORWARD; the field row and */ \
|
/* They have no storage, so a read of the FIELD is a call of the FORWARD; the field row and */ \
|
||||||
|
|||||||
@@ -121,6 +121,18 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The server packs named uniform blocks into its own ring and therefore needs the
|
// The server packs named uniform blocks into its own ring and therefore needs the
|
||||||
// host bytes of a set_shader_buffers(Uniform) range (D-B8).
|
// host bytes of a set_shader_buffers(Uniform) range (D-B8).
|
||||||
kCapNeedsHostUboBytes = 1ull << 8,
|
kCapNeedsHostUboBytes = 1ull << 8,
|
||||||
|
// P5b t2 (CONTRACT-P5B.md §6.5), the one cap bit P5b adds. The SERVER's backend owns
|
||||||
|
// the transform-feedback capture, i.e. its own table registers EndTransformFeedback.
|
||||||
|
// FixupGsStripCaptureOrder (GL_Drawing.cpp:1290) asks that question to decide whether
|
||||||
|
// the CLIENT must reorder the captured records into GL's vertex order, and it used to
|
||||||
|
// ask it of gBackendFunctionsTable.GL.EndTransformFeedback - which under split is the
|
||||||
|
// client's EMIT table, where the slot is non-null the moment t2 installs an emitter,
|
||||||
|
// for every server. A client talking to Espryt would then be right by accident and a
|
||||||
|
// client talking to Magma (which registers no XFB slot at all, so the sink DECLINES)
|
||||||
|
// would skip a reorder Magma needs and hand the application a silently corrupt capture
|
||||||
|
// buffer. So the answer is the SERVER's table, published as a bit and read through
|
||||||
|
// MGL_BACKEND_SLOT_CAP. The first of the "XFB span family" bits SlotCaps.h predicted.
|
||||||
|
kCapBackendOwnsXfbCapture = 1ull << 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MGPCaps {
|
struct MGPCaps {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnership[kMGPipeInputFieldCou
|
|||||||
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetFactor
|
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetFactor
|
||||||
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetUnits
|
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetUnits
|
||||||
MGPipeFieldOwnership::kRecordSupplied, // GetPrimitiveRestartIndex
|
MGPipeFieldOwnership::kRecordSupplied, // GetPrimitiveRestartIndex
|
||||||
MGPipeFieldOwnership::kFatal, // GetProgramForDispatch
|
MGPipeFieldOwnership::kBarrierPulled, // GetProgramForDispatch
|
||||||
MGPipeFieldOwnership::kBarrierPulled, // GetProgramForDraw
|
MGPipeFieldOwnership::kBarrierPulled, // GetProgramForDraw
|
||||||
MGPipeFieldOwnership::kBarrierPulled, // GetProgramObject
|
MGPipeFieldOwnership::kBarrierPulled, // GetProgramObject
|
||||||
MGPipeFieldOwnership::kRecordSupplied, // GetProvokingVertexMode
|
MGPipeFieldOwnership::kRecordSupplied, // GetProvokingVertexMode
|
||||||
@@ -136,7 +136,7 @@ inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] =
|
|||||||
"-", // GetPolygonOffsetFactor
|
"-", // GetPolygonOffsetFactor
|
||||||
"-", // GetPolygonOffsetUnits
|
"-", // GetPolygonOffsetUnits
|
||||||
"-", // GetPrimitiveRestartIndex
|
"-", // GetPrimitiveRestartIndex
|
||||||
"-", // GetProgramForDispatch
|
"P7 (Magma), P8 (Espryt)", // GetProgramForDispatch
|
||||||
"P8 (Espryt), P7 (Magma)", // GetProgramForDraw
|
"P8 (Espryt), P7 (Magma)", // GetProgramForDraw
|
||||||
"P9", // GetProgramObject
|
"P9", // GetProgramObject
|
||||||
"-", // GetProvokingVertexMode
|
"-", // GetProvokingVertexMode
|
||||||
@@ -298,6 +298,6 @@ inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = 3;
|
|||||||
// The class sizes, as constants a test can pin without recounting the table.
|
// The class sizes, as constants a test can pin without recounting the table.
|
||||||
inline constexpr SizeT kMGPipeRecordSuppliedFieldCount = 32;
|
inline constexpr SizeT kMGPipeRecordSuppliedFieldCount = 32;
|
||||||
inline constexpr SizeT kMGPipeApplierDerivedFieldCount = 1;
|
inline constexpr SizeT kMGPipeApplierDerivedFieldCount = 1;
|
||||||
inline constexpr SizeT kMGPipeBarrierPulledFieldCount = 27;
|
inline constexpr SizeT kMGPipeBarrierPulledFieldCount = 28;
|
||||||
inline constexpr SizeT kMGPipeFatalFieldCount = 3;
|
inline constexpr SizeT kMGPipeFatalFieldCount = 2;
|
||||||
static_assert(kMGPipeRecordSuppliedFieldCount + kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount == kMGPipeInputFieldCount, "the four class sizes do not partition the field set");
|
static_assert(kMGPipeRecordSuppliedFieldCount + kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount == kMGPipeInputFieldCount, "the four class sizes do not partition the field set");
|
||||||
|
|||||||
@@ -44,9 +44,11 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Remote {
|
namespace MobileGL::MG_Remote {
|
||||||
|
|
||||||
// The consumer mask may not collide with the MGPCapBits below it. kCapNeedsHostUboBytes
|
// The consumer mask may not collide with the MGPCapBits below it. The HIGHEST allocated
|
||||||
// is 1<<8 today; this asserts the gap stays a gap rather than trusting the comment.
|
// feature bit is kCapBackendOwnsXfbCapture, 1<<9 (P5b t2 raised it from
|
||||||
static_assert((static_cast<Uint64>(MG_Pipe::kCapNeedsHostUboBytes) & kMGCapsConsumerMask) == 0,
|
// kCapNeedsHostUboBytes' 1<<8); this asserts the gap stays a gap rather than trusting the
|
||||||
|
// comment, so it has to name whichever bit is currently the top one.
|
||||||
|
static_assert((static_cast<Uint64>(MG_Pipe::kCapBackendOwnsXfbCapture) & kMGCapsConsumerMask) == 0,
|
||||||
"an MGPCapBit has grown into CallMask's consumer block (bits 32..47)");
|
"an MGPCapBit has grown into CallMask's consumer block (bits 32..47)");
|
||||||
static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
|
static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
|
||||||
MG_Pipe::kMGPipeSubsystemResources),
|
MG_Pipe::kMGPipeSubsystemResources),
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// a live RingProducer over a cursor triple nobody consumes would be the
|
// a live RingProducer over a cursor triple nobody consumes would be the
|
||||||
// half-wired shape this session exists not to have.
|
// half-wired shape this session exists not to have.
|
||||||
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &m_segments);
|
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &m_segments);
|
||||||
|
m_encoder.SetStageRetirementDoorbell(m_producer.SelfDoorbell());
|
||||||
|
|
||||||
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
|
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
|
||||||
// ONE DRAIN, ONE ADOPTER (c1): PumpControlPlane below is the only thing in the client
|
// ONE DRAIN, ONE ADOPTER (c1): PumpControlPlane below is the only thing in the client
|
||||||
|
|||||||
@@ -10,12 +10,17 @@
|
|||||||
//
|
//
|
||||||
// THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12):
|
// THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12):
|
||||||
// class A 2 slots answered locally from the caps mirror, never emitted, never Fatal
|
// class A 2 slots answered locally from the caps mirror, never emitted, never Fatal
|
||||||
// class B 24 slots emitted (P5's five; P5b d1's nineteen draw slots, all on draw_vbo)
|
// class B 48 slots emitted; class C 21 slots name their unmigrated verb.
|
||||||
// class C 45 slots Fatal{UnmigratedVerb, "<slot>"}
|
|
||||||
// The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that
|
// The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that
|
||||||
// changes class without changing the arithmetic is a build break rather than a behaviour
|
// changes class without changing the arithmetic is a build break rather than a behaviour
|
||||||
// change nobody reviewed.
|
// change nobody reviewed.
|
||||||
//
|
//
|
||||||
|
// P5b MOVES SLOTS FROM C TO B, ONE PACKAGE AT A TIME (MG_Remote/CONTRACT-P5B.md §7). The three
|
||||||
|
// numbers above are the partition AT THE P5b CONTRACT COMMIT and they are the ones the contract
|
||||||
|
// states; the arithmetic below is what the tree currently has, and the per-package ownership
|
||||||
|
// assertions say which package moved which slot. On this head t2 has landed: class B is 5 + 6
|
||||||
|
// and class C is 58.
|
||||||
|
//
|
||||||
// THE PRE-VERB HOOKS RUN BEFORE THE RECORD, NEVER AFTER (b1, ID-18). PushPersistentMapsBeforeVerb
|
// THE PRE-VERB HOOKS RUN BEFORE THE RECORD, NEVER AFTER (b1, ID-18). PushPersistentMapsBeforeVerb
|
||||||
// publishes the bytes an application wrote through a coherent map with no API call at all, and
|
// publishes the bytes an application wrote through a coherent map with no API call at all, and
|
||||||
// MarkGpuWritesForDraw builds the conservative GPU-write set the client now owns. Both describe
|
// MarkGpuWritesForDraw builds the conservative GPU-write set the client now owns. Both describe
|
||||||
@@ -36,6 +41,13 @@
|
|||||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||||
|
// P5b i1: the emitters below name a texture's, a buffer's and a program's HANDLE beside the GL
|
||||||
|
// arguments (rule D). The handle comes from the client's own slot allocator, which is
|
||||||
|
// MG_Impl/Pipe's - the same table MG_Impl/Pipe/ImageEmit.h's set_shader_images reads, so the
|
||||||
|
// two records name one identity rather than two.
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||||
|
|
||||||
// P5b d1: the handle a bound buffer already has (never minted here - the validate-time
|
// P5b d1: the handle a bound buffer already has (never minted here - the validate-time
|
||||||
// set_index_buffer / the buffer's own constructor did that), for MGPDrawInfo::IndexResource and
|
// set_index_buffer / the buffer's own constructor did that), for MGPDrawInfo::IndexResource and
|
||||||
@@ -47,6 +59,8 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "WireTables.h"
|
#include "WireTables.h"
|
||||||
|
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||||
|
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
@@ -242,6 +256,123 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
nullptr, 0, nullptr);
|
nullptr, 0, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- f1: verbatim clear/copy/mipmap records (CONTRACT-P5B §2) ----
|
||||||
|
void EmitF1Clear(const char* slot, MG_Pipe::MGPipeHandle fbo, GLenum buffer,
|
||||||
|
GLint drawbuffer, Uint8 valueClass, const void* value,
|
||||||
|
GLfloat depth = 0, GLint stencil = 0) {
|
||||||
|
auto& session = RequireSession(slot);
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
MG_Pipe::MGPClear record{};
|
||||||
|
record.Fbo = fbo;
|
||||||
|
record.DrawBufferIndex = drawbuffer;
|
||||||
|
record.ValueClass = valueClass;
|
||||||
|
switch (buffer) {
|
||||||
|
case GL_COLOR:
|
||||||
|
record.Kind = MG_Pipe::kMGPipeClearKindColor;
|
||||||
|
std::memcpy(record.ColorValue, value, sizeof(record.ColorValue));
|
||||||
|
break;
|
||||||
|
case GL_DEPTH:
|
||||||
|
record.Kind = MG_Pipe::kMGPipeClearKindDepth;
|
||||||
|
std::memcpy(&record.DepthValue, value, sizeof(record.DepthValue));
|
||||||
|
break;
|
||||||
|
case GL_STENCIL:
|
||||||
|
record.Kind = MG_Pipe::kMGPipeClearKindStencil;
|
||||||
|
std::memcpy(&record.StencilValue, value, sizeof(record.StencilValue));
|
||||||
|
break;
|
||||||
|
case GL_DEPTH_STENCIL:
|
||||||
|
record.Kind = MG_Pipe::kMGPipeClearKindDepthStencil;
|
||||||
|
record.DepthValue = depth;
|
||||||
|
record.StencilValue = stencil;
|
||||||
|
break;
|
||||||
|
default: UnmigratedVerbFatal(slot);
|
||||||
|
}
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
|
EmitF1Clear("ClearBufferfv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer,
|
||||||
|
MG_Pipe::kMGPipeClearValueClassFloat, value);
|
||||||
|
}
|
||||||
|
void EmitClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
|
EmitF1Clear("ClearNamedFramebufferfv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo),
|
||||||
|
buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
|
EmitF1Clear("ClearBufferiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer,
|
||||||
|
MG_Pipe::kMGPipeClearValueClassInt, value);
|
||||||
|
}
|
||||||
|
void EmitClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
|
EmitF1Clear("ClearNamedFramebufferiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo),
|
||||||
|
buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassInt, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
|
EmitF1Clear("ClearBufferuiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer,
|
||||||
|
MG_Pipe::kMGPipeClearValueClassUint, value);
|
||||||
|
}
|
||||||
|
void EmitClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
|
EmitF1Clear("ClearNamedFramebufferuiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo),
|
||||||
|
buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassUint, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
|
EmitF1Clear("ClearBufferfi", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer,
|
||||||
|
MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil);
|
||||||
|
}
|
||||||
|
void EmitClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo,
|
||||||
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
|
EmitF1Clear("ClearNamedFramebufferfi", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo),
|
||||||
|
buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil);
|
||||||
|
}
|
||||||
|
const SharedPtr<MG_State::GLState::ITextureObject>& F1BoundTexture(GLenum target) {
|
||||||
|
auto& ctx = *MG_State::pGLContext;
|
||||||
|
return ctx.GetTextureUnitObject(ctx.GetActiveTextureUnit())
|
||||||
|
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject();
|
||||||
|
}
|
||||||
|
void EmitF1Copy(GLenum target, GLint level, GLenum format, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height, GLint xoffset, GLint yoffset, Bool subImage) {
|
||||||
|
auto& session = RequireSession(subImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
MG_Pipe::MGPCopyFromFramebuffer record{};
|
||||||
|
record.Dst = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*F1BoundTexture(target));
|
||||||
|
record.Target = static_cast<Uint32>(target);
|
||||||
|
record.Level = level;
|
||||||
|
record.InternalFormat = format;
|
||||||
|
record.X = x; record.Y = y;
|
||||||
|
record.Width = width; record.Height = height;
|
||||||
|
record.XOffset = xoffset; record.YOffset = yoffset;
|
||||||
|
record.SubImage = subImage;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::CopyFramebufferToTexture, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
void EmitCopyTexImage2D(GLenum target, GLint level, GLenum format, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height, GLint) {
|
||||||
|
EmitF1Copy(target, level, format, x, y, width, height, 0, 0, false);
|
||||||
|
}
|
||||||
|
void EmitCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
||||||
|
GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
|
EmitF1Copy(target, level, 0, x, y, width, height, xoffset, yoffset, true);
|
||||||
|
}
|
||||||
|
void EmitGenerateMipmap(GLenum target) {
|
||||||
|
auto& session = RequireSession("GenerateMipmap");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
const auto& texture = F1BoundTexture(target);
|
||||||
|
MG_Pipe::MGPMipPlan record{};
|
||||||
|
record.Res = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*texture);
|
||||||
|
record.Target = static_cast<Uint16>(target);
|
||||||
|
record.BaseLevel = texture->GetLevelRange().x();
|
||||||
|
const auto* mipmap = dynamic_cast<const MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
||||||
|
record.LevelCount = mipmap ? mipmap->GetMipmapLevelCount() : 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) {
|
void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
ClientSession& session = RequireSession("DrawArrays");
|
ClientSession& session = RequireSession("DrawArrays");
|
||||||
BeforeDrawVerb();
|
BeforeDrawVerb();
|
||||||
@@ -850,6 +981,356 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
LogE2ControlLine(g_presentOrdinal);
|
LogE2ControlLine(g_presentOrdinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block
|
||||||
|
// (MG_Remote/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows.
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record
|
||||||
|
// carries the GL arguments verbatim - the enums as tokens, the GL names the backend
|
||||||
|
// keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's
|
||||||
|
// ServerVerbSink reproduces the backend call the monolith makes. The backend keeps
|
||||||
|
// reading the frontend state it reads today through the BARRIER-PULLED fields of its
|
||||||
|
// verb class, which the record's own verb stamp is what makes legal. So a migration is
|
||||||
|
// one emitter here plus one sink body there, and nothing in the backend moves.
|
||||||
|
//
|
||||||
|
// THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks
|
||||||
|
// below dispatch on the GL NAME - that is the whole point of carrying it - so a handle
|
||||||
|
// is carried for P7's sake only. FindByLifetimeId answers the handle the resource
|
||||||
|
// subsystem has already published for this object and kMGPipeNullHandle when it has
|
||||||
|
// published none; Acquire would MINT one here instead, at a call site that emits no
|
||||||
|
// create record, and the server would then be handed an identity it has never seen.
|
||||||
|
// A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a
|
||||||
|
// true statement, rather than a slot nobody allocated.
|
||||||
|
|
||||||
|
MG_Pipe::MGPipeHandle PublishedTextureHandle(
|
||||||
|
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||||
|
if (!texture) return MG_Pipe::kMGPipeNullHandle;
|
||||||
|
return MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture,
|
||||||
|
texture->GetLifetimeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// glBindImageTexture. Emitted AT THE CALL, after the frontend has written the unit's
|
||||||
|
// ImageTextureBinding and MGP_FILL(BindImageTexture) has run - the record's verb
|
||||||
|
// boundary is what makes the server's read of that binding legal. set_shader_images
|
||||||
|
// (the draw-prep set) still travels at the next validate, untouched: that record
|
||||||
|
// describes a resolved unit for the draw, this one reproduces a call.
|
||||||
|
//
|
||||||
|
// NO PRE-VERB HOOK, AND THAT IS DELIBERATE. b1's two hooks describe "the work the
|
||||||
|
// record is ABOUT TO START" - PushPersistentMapsBeforeVerb publishes bytes an
|
||||||
|
// application wrote through a coherent map, MarkGpuWrites* builds the GPU-write set.
|
||||||
|
// A bind starts no shader and reads no buffer; the dispatch that later reads this image
|
||||||
|
// is the verb that carries both hooks, and running them here as well would push the
|
||||||
|
// same maps twice per dispatch and inflate b1's per-row counters.
|
||||||
|
void EmitBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered,
|
||||||
|
GLint layer, GLenum access, GLenum format) {
|
||||||
|
ClientSession& session = RequireSession("BindImageTexture");
|
||||||
|
|
||||||
|
MG_Pipe::MGPImageBind record{};
|
||||||
|
// The unit's binding is the frontend's and has just been written by the caller, so
|
||||||
|
// the texture this record names is the one the server's SyncImageTextureBinding
|
||||||
|
// will pull for the same unit. Read from MG_State::pGLContext and NOT through
|
||||||
|
// MGB_CTX: on this side of a split MGB_CTX is gPipeInputs, which is the SERVER's
|
||||||
|
// view, and the client asking it a question is how the two halves come to disagree.
|
||||||
|
if (MG_State::pGLContext != nullptr) {
|
||||||
|
record.Res = PublishedTextureHandle(
|
||||||
|
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit)).Texture);
|
||||||
|
}
|
||||||
|
record.Unit = static_cast<Uint32>(unit);
|
||||||
|
// The GL name the application passed, verbatim - what the ES slot is handed as
|
||||||
|
// `texture` and currently ignores. Never an identity (ARCHITECTURE 4.2.1).
|
||||||
|
record.GlName = static_cast<Uint32>(texture);
|
||||||
|
record.Level = static_cast<Int32>(level);
|
||||||
|
record.Layer = static_cast<Int32>(layer);
|
||||||
|
// THE GL ACCESS TOKEN, not MGPImageView::Access's three-value encoding (table 0's
|
||||||
|
// MGPImageBind::Access row). Two records, two jobs.
|
||||||
|
record.Access = static_cast<Uint32>(access);
|
||||||
|
record.Format = static_cast<Uint32>(format);
|
||||||
|
record.Layered = layered != GL_FALSE ? 1 : 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::BindShaderImage, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glDispatchCompute. THE HOOK ORDER IS b1's AND IS INHERITED FROM THE CLASS-C STUB
|
||||||
|
// VERBATIM: push the persistent maps (they produce resource_subdata records that must
|
||||||
|
// precede the verb on SEG_CMD), then the dispatch mark walk, then the record. The stub
|
||||||
|
// carried both calls before its Fatal precisely so that the package which flipped this
|
||||||
|
// slot would inherit a call site that was already correct.
|
||||||
|
void EmitDispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||||
|
ClientSession& session = RequireSession("DispatchCompute");
|
||||||
|
PushPersistentMapsBeforeVerb();
|
||||||
|
MarkGpuWritesForDispatch();
|
||||||
|
|
||||||
|
MG_Pipe::MGPGridInfo record{};
|
||||||
|
record.GridX = static_cast<Uint32>(numGroupsX);
|
||||||
|
record.GridY = static_cast<Uint32>(numGroupsY);
|
||||||
|
record.GridZ = static_cast<Uint32>(numGroupsZ);
|
||||||
|
// Block* STAY 0 IN P5b (contract i1): the local size is a link artifact the backend
|
||||||
|
// reads from its own program, and a client-minted copy would be a second statement
|
||||||
|
// of it. P7's Magma may fill it from the reflection archive.
|
||||||
|
record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
record.IndirectOffset = 0;
|
||||||
|
record.IsIndirect = 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0,
|
||||||
|
nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitDispatchComputeIndirect(GLintptr indirect) {
|
||||||
|
ClientSession& session = RequireSession("DispatchComputeIndirect");
|
||||||
|
PushPersistentMapsBeforeVerb();
|
||||||
|
MarkGpuWritesForDispatch();
|
||||||
|
|
||||||
|
MG_Pipe::MGPGridInfo record{};
|
||||||
|
// The counts come from the GL_DISPATCH_INDIRECT_BUFFER, so the three grid fields are
|
||||||
|
// 0 and IsIndirect is what says so; the sink dispatches on it and calls
|
||||||
|
// glDispatchComputeIndirect with the offset the application spelled.
|
||||||
|
record.IsIndirect = 1;
|
||||||
|
record.IndirectOffset = static_cast<Uint64>(indirect);
|
||||||
|
record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
if (MG_State::pGLContext != nullptr) {
|
||||||
|
const auto& bound =
|
||||||
|
MG_State::pGLContext
|
||||||
|
->GetBufferBindingSlot(::MobileGL::BufferTarget::DispatchIndirect)
|
||||||
|
.GetBoundObject();
|
||||||
|
if (bound) {
|
||||||
|
record.IndirectBuffer = MG_Pipe::MGPipeSlots().FindByLifetimeId(
|
||||||
|
MG_Pipe::MGPipeKind::Buffer, bound->GetLifetimeId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0,
|
||||||
|
nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glMemoryBarrier / glMemoryBarrierByRegion. The bits cross VERBATIM: the frontend has
|
||||||
|
// already validated them and already folds glTextureBarrier onto the same field
|
||||||
|
// (GL_Drawing.cpp:920-937), and Espryt's atomic-counter lowering - the counter bit
|
||||||
|
// implying the storage bit - stays inside the backend where the reason for it lives
|
||||||
|
// (DirectGLES.cpp:8837). A client that pre-lowered would be answering a driver question
|
||||||
|
// from the wrong side and the two arms would stop being byte-identical.
|
||||||
|
//
|
||||||
|
// No pre-verb hook: a barrier orders memory the GPU already holds. It starts no shader
|
||||||
|
// and reads no mapped buffer.
|
||||||
|
void EmitMemoryBarrier(GLbitfield barriers) {
|
||||||
|
ClientSession& session = RequireSession("MemoryBarrier");
|
||||||
|
MG_Pipe::MGPMemoryBarrier record{};
|
||||||
|
record.Bits = static_cast<Uint32>(barriers);
|
||||||
|
record.ByRegion = 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr,
|
||||||
|
0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitMemoryBarrierByRegion(GLbitfield barriers) {
|
||||||
|
ClientSession& session = RequireSession("MemoryBarrierByRegion");
|
||||||
|
MG_Pipe::MGPMemoryBarrier record{};
|
||||||
|
record.Bits = static_cast<Uint32>(barriers);
|
||||||
|
record.ByRegion = 1;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr,
|
||||||
|
0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glCopyImageSubData -> resource_copy_region (53), which P5b rules is glCopyImageSubData
|
||||||
|
// ONLY (contract §6.4; the framebuffer-sourced copies are f1's row 76).
|
||||||
|
void EmitCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
|
||||||
|
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
|
const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget,
|
||||||
|
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
|
ClientSession& session = RequireSession("CopyImageSubData");
|
||||||
|
|
||||||
|
// ID-57's SHAPE: REFUSED BY NAME, BEFORE ANY EMISSION. GL 4.6 core 18.3.2 accepts
|
||||||
|
// GL_RENDERBUFFER as either endpoint, and an endpoint is a sum type for exactly that
|
||||||
|
// reason - but no sticky forward hands out a renderbuffer object, so the sink has no
|
||||||
|
// way to rebuild one from a name, and none was measured. P7 is where the backend
|
||||||
|
// takes handles and this arm becomes ordinary. The sink refuses the same shape by
|
||||||
|
// the same name if a record ever reaches it (defence on both sides of one wire).
|
||||||
|
if (src.IsRenderbuffer() || dst.IsRenderbuffer()) {
|
||||||
|
UnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER");
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Pipe::MGPCopyRegion record{};
|
||||||
|
record.Src = PublishedTextureHandle(src.Texture);
|
||||||
|
record.Dst = PublishedTextureHandle(dst.Texture);
|
||||||
|
// The GL names beside the handles: the key MGB_CTX->GetTextureObject(name) takes on
|
||||||
|
// the far side (a BARRIER-PULLED sticky forward, counted in `rsp`, retired by P7).
|
||||||
|
record.SrcGlName =
|
||||||
|
src.Texture ? static_cast<Uint32>(src.Texture->GetExternalIndex()) : 0u;
|
||||||
|
record.DstGlName =
|
||||||
|
dst.Texture ? static_cast<Uint32>(dst.Texture->GetExternalIndex()) : 0u;
|
||||||
|
// The GL targets verbatim, in a Uint16 - every GL texture target fits one. NOT
|
||||||
|
// MGPipeResourceTarget: the sink only ever forwards these to a slot that takes GL
|
||||||
|
// enums, and the tree has no resource-target -> GL-enum inverse to spend on them.
|
||||||
|
record.SrcTarget = static_cast<Uint16>(srcTarget);
|
||||||
|
record.DstTarget = static_cast<Uint16>(dstTarget);
|
||||||
|
record.SrcLevel = static_cast<Uint16>(srcLevel);
|
||||||
|
record.DstLevel = static_cast<Uint16>(dstLevel);
|
||||||
|
// SrcBox is {srcX, srcY, srcZ, w, h, d}: the source origin AND the extent, which is
|
||||||
|
// one extent for both endpoints (GL spells the copy's size once).
|
||||||
|
record.SrcBox = MG_Pipe::MGPBox{srcX, srcY, srcZ, static_cast<Uint32>(srcWidth),
|
||||||
|
static_cast<Uint32>(srcHeight),
|
||||||
|
static_cast<Uint32>(srcDepth)};
|
||||||
|
record.DstX = dstX;
|
||||||
|
record.DstY = dstY;
|
||||||
|
record.DstZ = dstZ;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCopyRegion, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glShaderStorageBlockBinding -> set_storage_block_binding (75), the ONE content-carrying
|
||||||
|
// row P5b adds. The block is NAMED, not indexed, because the application's index is the
|
||||||
|
// frontend interface-query enumeration's and no backend shares that index space
|
||||||
|
// (BackendObject.h:216-221) - the name is the one coordinate all three agree on.
|
||||||
|
void EmitShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName,
|
||||||
|
GLuint storageBlockBinding) {
|
||||||
|
ClientSession& session = RequireSession("ShaderStorageBlockBinding");
|
||||||
|
// The backend slot's own first line (DirectGLES.cpp:9201), kept here so a null name
|
||||||
|
// never becomes a zero-size blob - which rule A forbids spelling at all.
|
||||||
|
if (storageBlockName == nullptr) return;
|
||||||
|
|
||||||
|
MG_Pipe::MGPStorageBlockBinding record{};
|
||||||
|
record.GlName = static_cast<Uint32>(program);
|
||||||
|
record.Binding = static_cast<Uint32>(storageBlockBinding);
|
||||||
|
record.ShaderCso = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
if (MG_State::pGLContext != nullptr) {
|
||||||
|
const auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
if (programObject) {
|
||||||
|
record.ShaderCso = MG_Pipe::MGPipeSlots().FindByLifetimeId(
|
||||||
|
MG_Pipe::MGPipeKind::ShaderCso, programObject->GetLifetimeId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Size = strlen + 1: THE NUL TRAVELS (contract table 0's block-name row). The
|
||||||
|
// decoder re-terminates into a bounded local and refuses a run whose last byte is
|
||||||
|
// not NUL, so the two sides agree on where the name ends.
|
||||||
|
const Uint64 nameBytes = static_cast<Uint64>(std::strlen(storageBlockName)) + 1ull;
|
||||||
|
record.Name = session.Encoder().StageBytes(storageBlockName, nameBytes);
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::SetStorageBlockBinding, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the
|
||||||
|
// tessellation patch parameter (MG_Remote/CONTRACT-P5B.md §2 t2).
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a
|
||||||
|
// control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets
|
||||||
|
// the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(),
|
||||||
|
// which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no
|
||||||
|
// shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE
|
||||||
|
// TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span,
|
||||||
|
// after the record and before GLContext::EndTransformFeedback clears the live bindings,
|
||||||
|
// which is exactly where b1 already put it (GL_Drawing.cpp's
|
||||||
|
// MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same
|
||||||
|
// buffers twice and taking it INSTEAD of there would mark nothing.
|
||||||
|
//
|
||||||
|
// RULE D (CONTRACT-P5B.md §0): each record carries the GL arguments the frontend handed
|
||||||
|
// the backend slot and nothing that is a READING of them. The capture program, the
|
||||||
|
// capture-buffer bindings, the patch state and the bound XFB object all stay
|
||||||
|
// BARRIER-PULLED - the server's backend reads the client's gPipeInputs fill of the
|
||||||
|
// moment, which MGP_FILL at each call site has just written and the verb barrier holds
|
||||||
|
// still (R-1). That is why these are six two-line emitters and not an XFB protocol.
|
||||||
|
//
|
||||||
|
// WHAT MUST HAVE CROSSED BEFORE begin_stream_output, since it is the ordering question
|
||||||
|
// this package was asked: the capture buffers' own resource records (emitted at their
|
||||||
|
// own call sites through the resource family, long before this point), the program
|
||||||
|
// (the CSO/program family, likewise), and the buffer BINDINGS - which do not cross as a
|
||||||
|
// record at all, because set_stream_output_targets (39) has no producer and no consumer
|
||||||
|
// and CONTRACT-P5B.md §2 rules it NOT required for t2: under the barrier the server's
|
||||||
|
// StartPendingTransformFeedback reads them through the kXfbSpan/kDraw pulls
|
||||||
|
// (GetTransformFeedbackProgram, GetBufferBindingPoint). Producing that row is P9's.
|
||||||
|
|
||||||
|
void EmitBeginTransformFeedback(GLenum primitiveMode) {
|
||||||
|
ClientSession& session = RequireSession("BeginTransformFeedback");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPStreamOutputBegin record{};
|
||||||
|
// The GL token verbatim (contract table 0's "GL enums on the wire"): the sink hands
|
||||||
|
// it to the backend slot that takes it, and nothing between here and there reads it.
|
||||||
|
record.PrimitiveMode = static_cast<Uint32>(primitiveMode);
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::BeginStreamOutput, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitEndTransformFeedback() {
|
||||||
|
ClientSession& session = RequireSession("EndTransformFeedback");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPXfbAccounting record{};
|
||||||
|
// THE ACCOUNTING IS THE CLIENT'S OWN AND IT IS INFORMATIONAL ON THIS SIDE OF THE
|
||||||
|
// WIRE: end_stream_output's backend call takes no arguments, and the three numbers
|
||||||
|
// are what the frontend has counted over this span (CONTRACT-P5B.md §2 t2, the
|
||||||
|
// companions row). They travel because the row has carried them since P4a and
|
||||||
|
// because they are what a server-side scatter would need when P9 lands one; the
|
||||||
|
// sink today calls GL.EndTransformFeedback() and reads none of them. Read here,
|
||||||
|
// BEFORE GLContext::EndTransformFeedback resets the counters at the call site.
|
||||||
|
const auto& context = *MG_State::pGLContext;
|
||||||
|
record.CapturedVertices = context.GetTransformFeedbackCapturedVertices();
|
||||||
|
record.PrimitivesWritten = context.GetTransformFeedbackPrimitiveCounter();
|
||||||
|
record.PrimitiveMode = static_cast<Uint32>(context.GetTransformFeedbackPrimitiveMode());
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::EndStreamOutput, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitPauseTransformFeedback() {
|
||||||
|
ClientSession& session = RequireSession("PauseTransformFeedback");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
// Reserved IS zero and the contract says so (MGPStreamOutputControl{Reserved = 0}).
|
||||||
|
// The row exists to BE the verb boundary - the stamp the server puts up before the
|
||||||
|
// sink runs - not to carry anything.
|
||||||
|
MG_Pipe::MGPStreamOutputControl record{};
|
||||||
|
record.Reserved = 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::PauseStreamOutput, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitResumeTransformFeedback() {
|
||||||
|
ClientSession& session = RequireSession("ResumeTransformFeedback");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPStreamOutputControl record{};
|
||||||
|
record.Reserved = 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::ResumeStreamOutput, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitBindTransformFeedback(GLuint name) {
|
||||||
|
ClientSession& session = RequireSession("BindTransformFeedback");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPStreamOutputBind record{};
|
||||||
|
// THE GL NAME IS NOT AN IDENTITY (ARCHITECTURE 4.2.1) and is carried anyway, because
|
||||||
|
// it is the key the backend has always used: Espryt indexes its driver objects by it
|
||||||
|
// (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and generates the ES object on
|
||||||
|
// first bind. Name 0 is the default object, which is why the field is not a handle.
|
||||||
|
record.GlName = static_cast<Uint32>(name);
|
||||||
|
// Beside it, the identity that WILL dispatch: the frontend's per-object lifetime id,
|
||||||
|
// process-wide and never reused, which is what survives glGenTransformFeedbacks
|
||||||
|
// recycling a name. Read AFTER GLContext::BindTransformFeedbackObject at the call
|
||||||
|
// site, so it is the id of the object being bound and not of the previous one.
|
||||||
|
record.LifetimeId = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitPatchParameteri(GLenum pname, GLint value) {
|
||||||
|
ClientSession& session = RequireSession("PatchParameteri");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPPatchParameter record{};
|
||||||
|
// GL_PATCH_VERTICES is the only pname that reaches a backend slot - the frontend
|
||||||
|
// answers GL_PATCH_DEFAULT_*_LEVEL itself and bakes those into the synthesized
|
||||||
|
// control stage - and the frontend has already rejected every other pname with
|
||||||
|
// INVALID_ENUM before this call (GL_Drawing.cpp's PatchParameteri). Carried verbatim
|
||||||
|
// so the sink reproduces the call rather than a reading of it.
|
||||||
|
record.Pname = static_cast<Uint32>(pname);
|
||||||
|
record.Value = static_cast<Int32>(value);
|
||||||
|
// set_patch_state (43) STILL TRAVELS, at the next validate, and that is not a
|
||||||
|
// duplicate: it is the applier's working-block copy, this is the driver push Espryt
|
||||||
|
// does AT THE CALL (DirectGLES.cpp:8760), and both pushes happen today on the
|
||||||
|
// monolith path too (CONTRACT-P5B.md §2 t2).
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::PatchParameter, &record, sizeof(record),
|
||||||
|
nullptr, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER.
|
// CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -889,8 +1370,7 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// CLASS C - Fatal{UnmigratedVerb}. 45 slots on this head: 44 in GLFunctionsTable +
|
// CLASS C - 21 slots remain after d1/i1/t2/f1; each names its first blocker.
|
||||||
// SetSwapInterval (64 at the P5b contract commit; d1 v1 moved its 19 to class B).
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
//
|
//
|
||||||
// PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md,
|
// PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md,
|
||||||
@@ -925,43 +1405,24 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// below still reads 0 + 19 = 19 and a slot that fell back in would have to be added here.
|
// below still reads 0 + 19 = 19 and a slot that fell back in would have to be added here.
|
||||||
#define MGR_UNMIGRATED_D1_SLOTS(X)
|
#define MGR_UNMIGRATED_D1_SLOTS(X)
|
||||||
|
|
||||||
// DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below
|
// i1 HAS LANDED: the list is EMPTY and all seven slots are class B (the five emitters
|
||||||
// because they carry b1's dispatch hook before the Fatal.
|
// above plus the two compute ones). It is kept as an empty macro rather than deleted so
|
||||||
#define MGR_UNMIGRATED_I1_SLOTS(X) \
|
// that MGR_UNMIGRATED_GL_SLOTS' union, kUnmigratedI1's arithmetic and the ownership
|
||||||
X(BindImageTexture, void, (GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum)) \
|
// static_assert below all keep their shape - and so the next package to need a row here
|
||||||
X(CopyImageSubData, void, \
|
// (a P5b wave-3 image/compute slot) has the partition to put it in.
|
||||||
(const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, \
|
#define MGR_UNMIGRATED_I1_SLOTS(X)
|
||||||
const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, GLsizei, GLsizei, \
|
|
||||||
GLsizei)) \
|
|
||||||
X(MemoryBarrier, void, (GLbitfield)) \
|
|
||||||
X(MemoryBarrierByRegion, void, (GLbitfield)) \
|
|
||||||
X(ShaderStorageBlockBinding, void, (GLuint, const GLchar*, GLuint))
|
|
||||||
|
|
||||||
|
// t2 LANDED (CONTRACT-P5B.md §2 t2): the three measured slots - BeginTransformFeedback
|
||||||
|
// (95 lane entries), PatchParameteri (43), BindTransformFeedback (2) - and the three
|
||||||
|
// companions that share their rows are class B now and live in the block above.
|
||||||
|
// DeleteTransformFeedback is the one that stays: it has NO ROW in P5b, by ruling and
|
||||||
|
// not by omission (unmeasured; the driver object leaks on the server until P9's XFB
|
||||||
|
// namespace work, and a bind of name 0 is what the backend does on delete of the bound
|
||||||
|
// one, DirectGLES.cpp:1422). It therefore still aborts by its own name.
|
||||||
#define MGR_UNMIGRATED_T2_SLOTS(X) \
|
#define MGR_UNMIGRATED_T2_SLOTS(X) \
|
||||||
X(PatchParameteri, void, (GLenum, GLint)) \
|
|
||||||
X(BeginTransformFeedback, void, (GLenum)) \
|
|
||||||
X(EndTransformFeedback, void, ()) \
|
|
||||||
X(PauseTransformFeedback, void, ()) \
|
|
||||||
X(ResumeTransformFeedback, void, ()) \
|
|
||||||
X(BindTransformFeedback, void, (GLuint)) \
|
|
||||||
X(DeleteTransformFeedback, void, (GLuint))
|
X(DeleteTransformFeedback, void, (GLuint))
|
||||||
|
|
||||||
#define MGR_UNMIGRATED_F1_SLOTS(X) \
|
#define MGR_UNMIGRATED_F1_SLOTS(X)
|
||||||
X(ClearBufferfi, void, (GLenum, GLint, GLfloat, GLint)) \
|
|
||||||
X(ClearBufferfv, void, (GLenum, GLint, const GLfloat*)) \
|
|
||||||
X(ClearBufferuiv, void, (GLenum, GLint, const GLuint*)) \
|
|
||||||
X(ClearBufferiv, void, (GLenum, GLint, const GLint*)) \
|
|
||||||
X(ClearNamedFramebufferfv, void, \
|
|
||||||
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, const GLfloat*)) \
|
|
||||||
X(ClearNamedFramebufferfi, void, \
|
|
||||||
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, GLfloat, GLint)) \
|
|
||||||
X(ClearNamedFramebufferiv, void, \
|
|
||||||
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, const GLint*)) \
|
|
||||||
X(ClearNamedFramebufferuiv, void, \
|
|
||||||
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, const GLuint*)) \
|
|
||||||
X(CopyTexImage2D, void, (GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint)) \
|
|
||||||
X(CopyTexSubImage2D, void, (GLenum, GLint, GLint, GLint, GLint, GLint, GLsizei, GLsizei)) \
|
|
||||||
X(GenerateMipmap, void, (GLenum))
|
|
||||||
|
|
||||||
// The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot).
|
// The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot).
|
||||||
#define MGR_UNMIGRATED_TAIL_SLOTS(X) \
|
#define MGR_UNMIGRATED_TAIL_SLOTS(X) \
|
||||||
@@ -1014,31 +1475,18 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_DEFINE_UNMIGRATED)
|
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_DEFINE_UNMIGRATED)
|
||||||
#undef MGR_DEFINE_UNMIGRATED
|
#undef MGR_DEFINE_UNMIGRATED
|
||||||
|
|
||||||
// THE TWO COMPUTE SLOTS CARRY b1's DISPATCH HOOK BEFORE THE FATAL, and this is stated
|
// THE TWO COMPUTE SLOTS' class-C stubs are GONE (P5b i1): they carried b1's dispatch
|
||||||
// rather than hidden. MarkGpuWritesForDispatch() belongs immediately before the
|
// hook before their Fatal so that the package flipping them would inherit a call site
|
||||||
// dispatch record, and the dispatch record is class C until i1 lands - so the call site
|
// that was already correct, and EmitDispatchCompute / EmitDispatchComputeIndirect above
|
||||||
// is here, in the right place, and is UNREACHABLE-IN-EFFECT: the abort follows it. The
|
// are that inheritance - same two calls, same order, the record where the Fatal was.
|
||||||
// package that moves DispatchCompute into class B (i1: launch_grid, opcode 60) replaces
|
|
||||||
// the Fatal and inherits a call site that is already correct rather than discovering
|
|
||||||
// that the mark walk was never wired.
|
|
||||||
void DispatchCompute_Unmigrated(GLuint, GLuint, GLuint) {
|
|
||||||
PushPersistentMapsBeforeVerb();
|
|
||||||
MarkGpuWritesForDispatch();
|
|
||||||
UnmigratedVerbFatal("DispatchCompute");
|
|
||||||
}
|
|
||||||
void DispatchComputeIndirect_Unmigrated(GLintptr) {
|
|
||||||
PushPersistentMapsBeforeVerb();
|
|
||||||
MarkGpuWritesForDispatch();
|
|
||||||
UnmigratedVerbFatal("DispatchComputeIndirect");
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetSwapInterval_Unmigrated(Int) { UnmigratedVerbFatal("SetSwapInterval"); }
|
void SetSwapInterval_Unmigrated(Int) { UnmigratedVerbFatal("SetSwapInterval"); }
|
||||||
|
|
||||||
// The counts, as arithmetic. MGR_COUNT_ONE expands to `+ 1` per row.
|
// The counts, as arithmetic. MGR_COUNT_ONE expands to `+ 1` per row.
|
||||||
#define MGR_COUNT_ONE(Name, Ret, Sig) +1
|
#define MGR_COUNT_ONE(Name, Ret, Sig) +1
|
||||||
constexpr Uint32 kUnmigratedD1 = 0 MGR_UNMIGRATED_D1_SLOTS(MGR_COUNT_ONE);
|
constexpr Uint32 kUnmigratedD1 = 0 MGR_UNMIGRATED_D1_SLOTS(MGR_COUNT_ONE);
|
||||||
// + DispatchCompute, DispatchComputeIndirect, written out by hand.
|
// i1 landed: the list is empty and the two hand-written compute stubs are gone with it.
|
||||||
constexpr Uint32 kUnmigratedI1 = 0 MGR_UNMIGRATED_I1_SLOTS(MGR_COUNT_ONE) + 2;
|
constexpr Uint32 kUnmigratedI1 = 0 MGR_UNMIGRATED_I1_SLOTS(MGR_COUNT_ONE);
|
||||||
constexpr Uint32 kUnmigratedT2 = 0 MGR_UNMIGRATED_T2_SLOTS(MGR_COUNT_ONE);
|
constexpr Uint32 kUnmigratedT2 = 0 MGR_UNMIGRATED_T2_SLOTS(MGR_COUNT_ONE);
|
||||||
constexpr Uint32 kUnmigratedF1 = 0 MGR_UNMIGRATED_F1_SLOTS(MGR_COUNT_ONE);
|
constexpr Uint32 kUnmigratedF1 = 0 MGR_UNMIGRATED_F1_SLOTS(MGR_COUNT_ONE);
|
||||||
// + SetSwapInterval, written out by hand.
|
// + SetSwapInterval, written out by hand.
|
||||||
@@ -1050,10 +1498,10 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
|
|
||||||
// The emitted counts, PER OWNER. P5's five are c1's; each P5b package raises its own.
|
// The emitted counts, PER OWNER. P5's five are c1's; each P5b package raises its own.
|
||||||
constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present
|
constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present
|
||||||
constexpr Uint32 kEmittedSlotsD1 = 19; // the draw family, all on draw_vbo (d1 v1)
|
constexpr Uint32 kEmittedSlotsD1 = 19;
|
||||||
constexpr Uint32 kEmittedSlotsI1 = 0;
|
constexpr Uint32 kEmittedSlotsI1 = 7;
|
||||||
constexpr Uint32 kEmittedSlotsT2 = 0;
|
constexpr Uint32 kEmittedSlotsT2 = 6;
|
||||||
constexpr Uint32 kEmittedSlotsF1 = 0;
|
constexpr Uint32 kEmittedSlotsF1 = 11;
|
||||||
constexpr Uint32 kEmittedSlots =
|
constexpr Uint32 kEmittedSlots =
|
||||||
kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1;
|
kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1;
|
||||||
constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported
|
constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported
|
||||||
@@ -1067,12 +1515,7 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots");
|
static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots");
|
||||||
static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots");
|
static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots");
|
||||||
static_assert(kUnmigratedTail == 20, "the wave-3 tail is 20 slots and no P5b package owns one");
|
static_assert(kUnmigratedTail == 20, "the wave-3 tail is 20 slots and no P5b package owns one");
|
||||||
// 64 at the P5b contract commit; every P5b flip moves exactly one slot from C to B, so
|
static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots");
|
||||||
// the SUM is what stays pinned, and a package's flip changes its own count and nothing
|
|
||||||
// else on this line.
|
|
||||||
static_assert(kUnmigratedSlots + (kEmittedSlots - kEmittedSlotsP5) == 64,
|
|
||||||
"CONTRACT-P5.md §7 class C was 64 slots at the P5b contract commit; a P5b flip "
|
|
||||||
"moves one slot from C to B and the two counts must still sum to 64");
|
|
||||||
static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount,
|
static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount,
|
||||||
"the three classes no longer partition the 71 slots");
|
"the three classes no longer partition the 71 slots");
|
||||||
|
|
||||||
@@ -1089,8 +1532,6 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
MGR_UNMIGRATED_GL_SLOTS(MGR_ASSIGN_UNMIGRATED)
|
MGR_UNMIGRATED_GL_SLOTS(MGR_ASSIGN_UNMIGRATED)
|
||||||
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_ASSIGN_UNMIGRATED)
|
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_ASSIGN_UNMIGRATED)
|
||||||
#undef MGR_ASSIGN_UNMIGRATED
|
#undef MGR_ASSIGN_UNMIGRATED
|
||||||
table.GL.DispatchCompute = &DispatchCompute_Unmigrated;
|
|
||||||
table.GL.DispatchComputeIndirect = &DispatchComputeIndirect_Unmigrated;
|
|
||||||
table.SetSwapInterval = &SetSwapInterval_Unmigrated;
|
table.SetSwapInterval = &SetSwapInterval_Unmigrated;
|
||||||
|
|
||||||
// ---- class A
|
// ---- class A
|
||||||
@@ -1130,6 +1571,38 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
table.GL.ReadPixels = &EmitReadPixels;
|
table.GL.ReadPixels = &EmitReadPixels;
|
||||||
table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
|
table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
|
||||||
table.Present = &EmitPresent;
|
table.Present = &EmitPresent;
|
||||||
|
// ---- class B, P5b t2. Assigned AFTER the class-C block above, which is what makes
|
||||||
|
// the flip a single-line change per slot: the Fatal thunk is overwritten, and a slot
|
||||||
|
// whose row is removed from MGR_UNMIGRATED_T2_SLOTS but not assigned here would be
|
||||||
|
// NULL and caught by RemoteEmitTable.NoSlotIsNull rather than silently skipped.
|
||||||
|
table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback;
|
||||||
|
table.GL.EndTransformFeedback = &EmitEndTransformFeedback;
|
||||||
|
table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback;
|
||||||
|
table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback;
|
||||||
|
table.GL.BindTransformFeedback = &EmitBindTransformFeedback;
|
||||||
|
table.GL.PatchParameteri = &EmitPatchParameteri;
|
||||||
|
|
||||||
|
// ---- f1 ----
|
||||||
|
table.GL.ClearBufferfi = &EmitClearBufferfi;
|
||||||
|
table.GL.ClearBufferfv = &EmitClearBufferfv;
|
||||||
|
table.GL.ClearBufferiv = &EmitClearBufferiv;
|
||||||
|
table.GL.ClearBufferuiv = &EmitClearBufferuiv;
|
||||||
|
table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi;
|
||||||
|
table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv;
|
||||||
|
table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv;
|
||||||
|
table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv;
|
||||||
|
table.GL.CopyTexImage2D = &EmitCopyTexImage2D;
|
||||||
|
table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D;
|
||||||
|
table.GL.GenerateMipmap = &EmitGenerateMipmap;
|
||||||
|
|
||||||
|
// ---- class B, P5b package i1 (kEmittedSlotsI1 = 7)
|
||||||
|
table.GL.BindImageTexture = &EmitBindImageTexture;
|
||||||
|
table.GL.DispatchCompute = &EmitDispatchCompute;
|
||||||
|
table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect;
|
||||||
|
table.GL.MemoryBarrier = &EmitMemoryBarrier;
|
||||||
|
table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion;
|
||||||
|
table.GL.CopyImageSubData = &EmitCopyImageSubData;
|
||||||
|
table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding;
|
||||||
|
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,11 +75,30 @@
|
|||||||
// GL_Drawing.cpp:844 PatchParameteri. "Absent" means the patch size is never set and every
|
// GL_Drawing.cpp:844 PatchParameteri. "Absent" means the patch size is never set and every
|
||||||
// tessellation draw silently uses the previous one. Class C; it aborts by name.
|
// tessellation draw silently uses the previous one. Class C; it aborts by name.
|
||||||
//
|
//
|
||||||
|
// P5b t2 CLOSED THE FIRST OF THOSE TWO, AND THE OTHER SIX SITES NEEDED NOTHING (CONTRACT-P5B.md
|
||||||
|
// §2 t2, §6.5). The six SPAN sites - :1274 Begin, :1371 End, :1420 Pause, :1435 Resume, :1641
|
||||||
|
// Delete, :1673 Bind - are guards over a slot that is now class B in the client's table, so they
|
||||||
|
// simply call the emitter; their `if (const auto f = ...)` shape is left exactly as it was,
|
||||||
|
// because under split the slot is non-null and under monolith nothing moved. THE PROBE AT :1290
|
||||||
|
// IS THE ONE THAT HAD TO CHANGE, and it is the reason this header said the XFB span family would
|
||||||
|
// be the first to need a bit: it is not a guard on a call, it is a QUESTION ABOUT THE BACKEND
|
||||||
|
// asked of a table that under split belongs to the client. It now reads
|
||||||
|
// MGL_BACKEND_SLOT_CAP(EndTransformFeedback, kCapBackendOwnsXfbCapture), the bit the server sets
|
||||||
|
// from ITS table in MG_Backend/Init.cpp's InitSplitRoles. Espryt registers the slot and answers
|
||||||
|
// yes; Magma registers no XFB slot, answers no, and the client keeps reordering for it exactly
|
||||||
|
// as it does under monolith. GL_Drawing.cpp:844's PatchParameteri stays a plain guard for the
|
||||||
|
// same reason as the six: the slot is class B now, so "absent" never arises.
|
||||||
|
//
|
||||||
|
// The one XFB slot still class C is DeleteTransformFeedback, which CONTRACT-P5B.md gives no row
|
||||||
|
// (unmeasured); :1641's guard therefore still reaches Fatal{UnmigratedVerb} by name, which is
|
||||||
|
// the outcome R-4 asks for.
|
||||||
|
//
|
||||||
// WHAT THIS HEADER DELIBERATELY DOES NOT DO. It does not touch the 28 unguarded slots: those
|
// WHAT THIS HEADER DELIBERATELY DOES NOT DO. It does not touch the 28 unguarded slots: those
|
||||||
// have no probe to convert, and calling one reaches Fatal{UnmigratedVerb, "<slot>"} by name,
|
// have no probe to convert, and calling one reaches Fatal{UnmigratedVerb, "<slot>"} by name,
|
||||||
// which is R-4's intent. And it does not invent a cap bit - a new MGPCapBit is an
|
// which is R-4's intent. And it does not invent a cap bit - a new MGPCapBit is an
|
||||||
// MGPipeTypes.h edit and that file is c0's, so a family that needs one goes through the
|
// MGPipeTypes.h edit and that file is c0's, so a family that needs one goes through the
|
||||||
// integrator (the XFB span family is the first that will).
|
// integrator (the XFB span family is the first that will). It did: P5b's contract granted t2
|
||||||
|
// exactly that one bit, kCapBackendOwnsXfbCapture (CONTRACT-P5B.md §6.5, §8), and t2 added it.
|
||||||
//
|
//
|
||||||
// G1: in a build without MOBILEGL_BUILD_DISAGGREGATED both macros expand to the null check the
|
// G1: in a build without MOBILEGL_BUILD_DISAGGREGATED both macros expand to the null check the
|
||||||
// site already had, so the pull build's code generation is unchanged.
|
// site already had, so the pull build's code generation is unchanged.
|
||||||
|
|||||||
@@ -73,14 +73,16 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
if (table == nullptr) return false;
|
if (table == nullptr) return false;
|
||||||
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||||
|
|
||||||
// THE FBO HANDLE IS NOT RESOLVED HERE, AND THAT IS THE RULING RATHER THAN AN OMISSION.
|
// Named records precede the verb; bound-form backends re-sync the live draw binding.
|
||||||
// MGPClear::Fbo names the framebuffer the clear belongs to, but the BINDING is already
|
if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo) &&
|
||||||
// server state: set_framebuffer_state (op 33) arrives ahead of the clear and the
|
clear.Fbo != MG_Pipe::MGPipeApplier().BoundFramebuffer[0]) {
|
||||||
// applier has bound it. Re-resolving the handle to a frontend FramebufferObject here
|
const char* slot = clear.Kind == kMGPClearKindDepthStencil ? "ClearNamedFramebufferfi+UNBOUND" :
|
||||||
// would need the SharedPtr the four ClearNamedFramebuffer* entries take - a frontend
|
clear.ValueClass == kMGPClearValueClassInt ? "ClearNamedFramebufferiv+UNBOUND" :
|
||||||
// heap reference that table 2 lists as one of the six fields with no wire carrier. So
|
clear.ValueClass == kMGPClearValueClassUint ? "ClearNamedFramebufferuiv+UNBOUND" :
|
||||||
// P5 clears THE BOUND FRAMEBUFFER, which for the reduced path (default FBO) is exactly
|
"ClearNamedFramebufferfv+UNBOUND";
|
||||||
// right, and the named form is P7's along with the handle it needs.
|
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot);
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
switch (clear.Kind) {
|
switch (clear.Kind) {
|
||||||
case kMGPClearKindWhole:
|
case kMGPClearKindWhole:
|
||||||
if (gl.Clear == nullptr) return false;
|
if (gl.Clear == nullptr) return false;
|
||||||
@@ -526,72 +528,267 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// in the same words), the call, and a tally the lane can assert moved.
|
// in the same words), the call, and a tally the lane can assert moved.
|
||||||
// -----------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ---- i1 ----
|
// ---- i1 ---- (MG_Remote/CONTRACT-P5B.md §2 i1; landed by package p5b/i1)
|
||||||
|
//
|
||||||
|
// RULE D IN FIVE BODIES. Each reproduces the backend call the monolith makes, from the
|
||||||
|
// record and from server state, and NOTHING ELSE: the backend goes on reading the frontend
|
||||||
|
// fields it reads today through the BARRIER-PULLED entries of its verb class, which the
|
||||||
|
// verb stamp PipeApplier::ApplyOne put up before this sink ran is exactly what makes legal.
|
||||||
|
// That is why none of these touches a backend file and why the monolith path is byte
|
||||||
|
// identical - and it is also the honest statement of the debt, which `rsp` counts.
|
||||||
|
|
||||||
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
|
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
|
||||||
(void)grid;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
|
||||||
ServerUnmigratedVerbFatal(grid.IsIndirect ? "DispatchComputeIndirect" : "DispatchCompute");
|
if (table == nullptr) return false;
|
||||||
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||||
|
// The compute program is NOT named by this record and must not be: it is
|
||||||
|
// GetProgramForDispatch, GetProgramForDraw's twin, which the backend pulls inside its
|
||||||
|
// own PrepareForCompute (DirectGLES.cpp:5779). i1 is what puts compute on the path, so
|
||||||
|
// the field moves FATAL -> BARRIER_PULLED in FieldOwnership.def (contract §6.9, the
|
||||||
|
// one row this package is granted). Block* are 0 on the wire for the same reason: the
|
||||||
|
// local size is a link artifact the backend reads from its own program.
|
||||||
|
if (grid.IsIndirect != 0) {
|
||||||
|
if (gl.DispatchComputeIndirect == nullptr) return false;
|
||||||
|
// IndirectBuffer travels for P7's sake; the BINDING is server state, put there by
|
||||||
|
// the set_buffer_bindings record that preceded this one, exactly as OnClear's Fbo
|
||||||
|
// is not re-resolved here. glDispatchComputeIndirect takes only the offset.
|
||||||
|
gl.DispatchComputeIndirect(static_cast<GLintptr>(grid.IndirectOffset));
|
||||||
|
} else {
|
||||||
|
if (gl.DispatchCompute == nullptr) return false;
|
||||||
|
gl.DispatchCompute(static_cast<GLuint>(grid.GridX), static_cast<GLuint>(grid.GridY),
|
||||||
|
static_cast<GLuint>(grid.GridZ));
|
||||||
|
}
|
||||||
|
++m_dispatches;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) {
|
Bool ServerVerbSink::OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) {
|
||||||
ServerUnmigratedVerbFatal(barrier.ByRegion ? "MemoryBarrierByRegion" : "MemoryBarrier");
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("memory_barrier");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||||
|
// THE BITS GO OVER VERBATIM AND ARE LOWERED HERE BY NOBODY. Espryt's atomic-counter
|
||||||
|
// lowering - the counter bit implying the storage bit, because glslang lowers every
|
||||||
|
// atomic_uint onto a storage block - lives inside its own MemoryBarrier
|
||||||
|
// (DirectGLES.cpp:8837) and is a statement about the DRIVER. Repeating it on this side
|
||||||
|
// would make the split arm and the monolith arm two different calls.
|
||||||
|
if (barrier.ByRegion != 0) {
|
||||||
|
if (gl.MemoryBarrierByRegion == nullptr) return false;
|
||||||
|
gl.MemoryBarrierByRegion(static_cast<GLbitfield>(barrier.Bits));
|
||||||
|
} else {
|
||||||
|
if (gl.MemoryBarrier == nullptr) return false;
|
||||||
|
gl.MemoryBarrier(static_cast<GLbitfield>(barrier.Bits));
|
||||||
|
}
|
||||||
|
++m_memoryBarriers;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) {
|
Bool ServerVerbSink::OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) {
|
||||||
(void)copy;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resource_copy_region");
|
||||||
ServerUnmigratedVerbFatal("CopyImageSubData");
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.CopyImageSubData == nullptr) return false;
|
||||||
|
|
||||||
|
// REFUSED BY NAME ON BOTH SIDES OF ONE WIRE. The client refuses a renderbuffer endpoint
|
||||||
|
// before it emits (ID-57's shape, EmitTables.cpp), and this is the same refusal for a
|
||||||
|
// record that reached here anyway: no sticky forward hands out a RenderbufferObject, so
|
||||||
|
// there is no honest way to build the endpoint, and guessing an empty one would copy
|
||||||
|
// nothing and say it copied.
|
||||||
|
if (copy.SrcTarget == GL_RENDERBUFFER || copy.DstTarget == GL_RENDERBUFFER) {
|
||||||
|
ServerUnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER");
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE TWO ENDPOINTS ARE REBUILT FROM THE GL NAMES, through the BARRIER-PULLED sticky
|
||||||
|
// forward GetTextureObject(name) - `rsp` counts every one of these and P7 is what
|
||||||
|
// retires them by making the backend take the handles that travel beside the names.
|
||||||
|
MG_Backend::CopyImageEndpoint src{};
|
||||||
|
MG_Backend::CopyImageEndpoint dst{};
|
||||||
|
src.Texture = MG_Pipe::gPipeInputs.GetTextureObject(static_cast<Uint>(copy.SrcGlName));
|
||||||
|
dst.Texture = MG_Pipe::gPipeInputs.GetTextureObject(static_cast<Uint>(copy.DstGlName));
|
||||||
|
if (!src.Exists() || !dst.Exists()) {
|
||||||
|
// The monolith's own answer to this, in its own words (DirectGLES.cpp:9067
|
||||||
|
// "source or destination image failed to sync; declining the copy"): the frontend
|
||||||
|
// validator is what keeps it unreachable and what reports the INVALID_VALUE the
|
||||||
|
// application is owed. A decline here is a real answer, not a silent success.
|
||||||
|
MGLOG_E_ONCE("MG_Remote server: resource_copy_region named texture(s) %u -> %u that "
|
||||||
|
"the frontend no longer holds; declining the copy",
|
||||||
|
static_cast<unsigned>(copy.SrcGlName),
|
||||||
|
static_cast<unsigned>(copy.DstGlName));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SrcBox is {origin, extent} and the extent is the copy's, spelled once by GL for both
|
||||||
|
// endpoints; the destination contributes only its origin.
|
||||||
|
table->GL.CopyImageSubData(src, static_cast<GLenum>(copy.SrcTarget),
|
||||||
|
static_cast<GLint>(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y,
|
||||||
|
copy.SrcBox.Z, dst, static_cast<GLenum>(copy.DstTarget),
|
||||||
|
static_cast<GLint>(copy.DstLevel), copy.DstX, copy.DstY,
|
||||||
|
copy.DstZ, static_cast<GLsizei>(copy.SrcBox.W),
|
||||||
|
static_cast<GLsizei>(copy.SrcBox.H),
|
||||||
|
static_cast<GLsizei>(copy.SrcBox.D));
|
||||||
|
++m_imageCopies;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) {
|
Bool ServerVerbSink::OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) {
|
||||||
(void)bind;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_shader_image");
|
||||||
ServerUnmigratedVerbFatal("BindImageTexture");
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.BindImageTexture == nullptr) return false;
|
||||||
|
// THE SAME CALL IS RIGHT FOR BOTH BACKENDS, which is why the record carries the whole
|
||||||
|
// argument list although neither reads all of it today: Espryt ignores everything but
|
||||||
|
// Unit and syncs that unit from the barrier-pulled GetImageTextureBinding
|
||||||
|
// (DirectGLES.cpp:9154, :2471), and Magma's slot is a no-op (DirectVulkan.cpp:665). The
|
||||||
|
// arguments travel because rule D says a verb crosses as the CALL, and because P7 is
|
||||||
|
// what makes the backend read them instead of pulling.
|
||||||
|
table->GL.BindImageTexture(static_cast<GLuint>(bind.Unit), static_cast<GLuint>(bind.GlName),
|
||||||
|
static_cast<GLint>(bind.Level),
|
||||||
|
bind.Layered != 0 ? GL_TRUE : GL_FALSE,
|
||||||
|
static_cast<GLint>(bind.Layer),
|
||||||
|
static_cast<GLenum>(bind.Access),
|
||||||
|
static_cast<GLenum>(bind.Format));
|
||||||
|
++m_imageBinds;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
|
Bool ServerVerbSink::OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
|
||||||
const char* name) {
|
const char* name) {
|
||||||
(void)binding;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("set_storage_block_binding");
|
||||||
(void)name;
|
if (table == nullptr) return false;
|
||||||
ServerUnmigratedVerbFatal("ShaderStorageBlockBinding");
|
if (table->GL.ShaderStorageBlockBinding == nullptr) return false;
|
||||||
|
if (name == nullptr) return false;
|
||||||
|
// The NAME is the one coordinate the application, the frontend and both backends agree
|
||||||
|
// on (BackendObject.h:216-221), which is why the row carries a blob rather than the
|
||||||
|
// application's block INDEX. `name` points into the decoder's bounded local and is
|
||||||
|
// valid for this call only (rule C); the backend slot copies what it needs.
|
||||||
|
//
|
||||||
|
// Both backends resolve the PROGRAM through the barrier-pulled GetProgramObject(GlName)
|
||||||
|
// / TryGetDirectVulkanProgram - `rsp` again, retired by P9. ShaderCso travels beside the
|
||||||
|
// name for the phase that dispatches on it.
|
||||||
|
table->GL.ShaderStorageBlockBinding(static_cast<GLuint>(binding.GlName), name,
|
||||||
|
static_cast<GLuint>(binding.Binding));
|
||||||
|
++m_storageBlockBindings;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- t2 ----
|
// ---- t2 ---- (MG_Remote/CONTRACT-P5B.md §2 t2)
|
||||||
|
//
|
||||||
|
// SIX BODIES, SIX BACKEND CALLS, NO STATE OF THEIR OWN. Rule D: the record IS the call, and
|
||||||
|
// everything the backend reads around it - the capture program, the capture-buffer
|
||||||
|
// bindings, the bound XFB object, the patch state - it reads from gPipeInputs through its
|
||||||
|
// verb class's BARRIER-PULLED fields, which the client filled at the call site and the
|
||||||
|
// verb barrier holds still (R-1). That is why none of these touches m_backend beyond
|
||||||
|
// Table() and why not one of them caches anything across records.
|
||||||
|
//
|
||||||
|
// A NULL SLOT DECLINES, AND THE DECLINE IS THE MONOLITH'S ANSWER IN THE SAME WORDS. Magma
|
||||||
|
// (DirectVulkan) registers NO XFB slot and no PatchParameteri at all
|
||||||
|
// (BackendObject_DirectVulkan.cpp), and under monolith the frontend's own
|
||||||
|
// `if (const auto f = table.GL.X)` guard simply skips the call; `return false` here is that
|
||||||
|
// same skip, reported to DecodeAndApply as "this build did not apply it" rather than as a
|
||||||
|
// crash or as a silent success. Contract §2 t2 says so for PatchParameteri by name.
|
||||||
|
|
||||||
Bool ServerVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) {
|
Bool ServerVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) {
|
||||||
(void)begin;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("begin_stream_output");
|
||||||
ServerUnmigratedVerbFatal("BeginTransformFeedback");
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.BeginTransformFeedback == nullptr) return false;
|
||||||
|
// Espryt's Begin only ARMS the span (DirectGLES.cpp:1212-1220: primitiveMode, pending,
|
||||||
|
// targets cleared); the driver glBeginTransformFeedback happens in the tail of the next
|
||||||
|
// PrepareForDraw (StartPendingTransformFeedback, :1224), where the capture program and
|
||||||
|
// the buffer bindings are read through the pulls. So this record's effect is not
|
||||||
|
// visible until a DRAW crosses - which is why an XFB scenario whose draw is still class
|
||||||
|
// C moves its first blocker to that draw rather than rendering.
|
||||||
|
table->GL.BeginTransformFeedback(static_cast<GLenum>(begin.PrimitiveMode));
|
||||||
|
++m_streamOutputSpans;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) {
|
Bool ServerVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("end_stream_output");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.EndTransformFeedback == nullptr) return false;
|
||||||
|
// THE THREE ACCOUNTING FIELDS ARE NOT READ, AND THAT IS THE RULING RATHER THAN AN
|
||||||
|
// OMISSION. glEndTransformFeedback takes no arguments; the numbers are the CLIENT's own
|
||||||
|
// per-span accounting (contract §2 t2's companions row) and the client is where they are
|
||||||
|
// consumed - by the primitive queries and by the capture-capacity clamp. A server that
|
||||||
|
// second-guessed them from its own driver would be publishing a second answer to a
|
||||||
|
// question the frontend already answers, and the second answer is the one that goes
|
||||||
|
// stale. They cross because the row has carried them since P4a and because P9's
|
||||||
|
// server-side scatter is what will need them.
|
||||||
(void)accounting;
|
(void)accounting;
|
||||||
ServerUnmigratedVerbFatal("EndTransformFeedback");
|
table->GL.EndTransformFeedback();
|
||||||
|
++m_streamOutputSpans;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
Bool ServerVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
||||||
(void)control;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("pause_stream_output");
|
||||||
ServerUnmigratedVerbFatal("PauseTransformFeedback");
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.PauseTransformFeedback == nullptr) return false;
|
||||||
|
(void)control; // Reserved, and the contract says it is 0.
|
||||||
|
table->GL.PauseTransformFeedback();
|
||||||
|
++m_streamOutputControls;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
Bool ServerVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resume_stream_output");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.ResumeTransformFeedback == nullptr) return false;
|
||||||
(void)control;
|
(void)control;
|
||||||
ServerUnmigratedVerbFatal("ResumeTransformFeedback");
|
table->GL.ResumeTransformFeedback();
|
||||||
|
++m_streamOutputControls;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
|
Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
|
||||||
(void)bind;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output");
|
||||||
ServerUnmigratedVerbFatal("BindTransformFeedback");
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.BindTransformFeedback == nullptr) return false;
|
||||||
|
// THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver
|
||||||
|
// objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates
|
||||||
|
// the ES object on first bind; passing the lifetime id would index a map that has never
|
||||||
|
// heard of it and silently create a second driver object per bind. The lifetime id
|
||||||
|
// travels as the identity P7/P9 will dispatch on once the XFB namespace has a wire
|
||||||
|
// lifetime of its own - it has no reader on this side today, and pretending otherwise
|
||||||
|
// by folding it into the key is exactly the "a GL name is never an identity" confusion
|
||||||
|
// the contract's GlName row is written against.
|
||||||
|
table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
|
||||||
|
++m_streamOutputBinds;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
|
Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
|
||||||
(void)patch;
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter");
|
||||||
ServerUnmigratedVerbFatal("PatchParameteri");
|
if (table == nullptr) return false;
|
||||||
|
// Magma registers no PatchParameteri: it compiles the patch size into its synthesized
|
||||||
|
// control stage from set_patch_state instead, so the DECLINE below is the whole of the
|
||||||
|
// right answer for that backend and not a gap (contract §2 t2).
|
||||||
|
if (table->GL.PatchParameteri == nullptr) return false;
|
||||||
|
// Pname is GL_PATCH_VERTICES and the frontend has already rejected every other spelling
|
||||||
|
// with INVALID_ENUM before the record was built, so this is a forward and not a switch.
|
||||||
|
table->GL.PatchParameteri(static_cast<GLenum>(patch.Pname), static_cast<GLint>(patch.Value));
|
||||||
|
++m_patchParameters;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- f1 ----
|
// ---- f1 ----
|
||||||
Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) {
|
Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) {
|
||||||
(void)plan;
|
const auto* table = Table("GenerateMipmap");
|
||||||
ServerUnmigratedVerbFatal("GenerateMipmap");
|
if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false;
|
||||||
|
table->GL.GenerateMipmap(plan.Target);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) {
|
Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) {
|
||||||
ServerUnmigratedVerbFatal(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
|
const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
if (copy.SubImage) {
|
||||||
|
if (table->GL.CopyTexSubImage2D == nullptr) return false;
|
||||||
|
table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset,
|
||||||
|
copy.X, copy.Y, copy.Width, copy.Height);
|
||||||
|
} else {
|
||||||
|
if (table->GL.CopyTexImage2D == nullptr) return false;
|
||||||
|
table->GL.CopyTexImage2D(copy.Target, copy.Level, copy.InternalFormat,
|
||||||
|
copy.X, copy.Y, copy.Width, copy.Height, 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion
|
// i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion
|
||||||
// ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"),
|
// ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"),
|
||||||
// OnSetStorageBlockBinding ("ShaderStorageBlockBinding")
|
// OnSetStorageBlockBinding ("ShaderStorageBlockBinding")
|
||||||
// t2 OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput /
|
// t2 LANDED. OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput /
|
||||||
// OnResumeStreamOutput ("*TransformFeedback"), OnBindStreamOutput
|
// OnResumeStreamOutput / OnBindStreamOutput / OnPatchParameter are real bodies
|
||||||
// ("BindTransformFeedback"), OnPatchParameter ("PatchParameteri")
|
// now: the backend call the contract names, the null-slot DECLINE, a tally.
|
||||||
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" /
|
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" /
|
||||||
// "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already)
|
// "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already)
|
||||||
// d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the
|
// d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the
|
||||||
@@ -154,6 +154,16 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint64 Presents() const { return m_presents; }
|
Uint64 Presents() const { return m_presents; }
|
||||||
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
|
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
|
||||||
Uint64 ReadbackBytes() const { return m_readbackBytes; }
|
Uint64 ReadbackBytes() const { return m_readbackBytes; }
|
||||||
|
// ---- P5b package i1's tallies. R-16: a probe may not arm against a stub, and on a
|
||||||
|
// split build "the scenario passed" is also what a scenario that never left the
|
||||||
|
// monolith path looks like - so the lane asserts the number that only this sink can
|
||||||
|
// move. One per wire ROW, not per GL slot: launch_grid carries both dispatch entry
|
||||||
|
// points and memory_barrier both barrier ones, and the sink is where they separate.
|
||||||
|
Uint64 ImageBinds() const { return m_imageBinds; }
|
||||||
|
Uint64 Dispatches() const { return m_dispatches; }
|
||||||
|
Uint64 MemoryBarriers() const { return m_memoryBarriers; }
|
||||||
|
Uint64 ImageCopies() const { return m_imageCopies; }
|
||||||
|
Uint64 StorageBlockBindings() const { return m_storageBlockBindings; }
|
||||||
// ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal
|
// ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal
|
||||||
// the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from
|
// the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from
|
||||||
// DstSize is exactly the heap overflow codex 1 found, one field over.
|
// DstSize is exactly the heap overflow codex 1 found, one field over.
|
||||||
@@ -177,6 +187,16 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
const LastDrawRecord& LastDraw() const { return m_lastDraw; }
|
const LastDrawRecord& LastDraw() const { return m_lastDraw; }
|
||||||
// draw_vbo records seen, applied or declined; Draws() above counts only the applied.
|
// draw_vbo records seen, applied or declined; Draws() above counts only the applied.
|
||||||
Uint64 DrawRecords() const { return m_drawRecords; }
|
Uint64 DrawRecords() const { return m_drawRecords; }
|
||||||
|
// P5b t2's tallies, for the same reason the five above exist (R-16): under split "the
|
||||||
|
// scenario passed" is also what a scenario that ran entirely on the monolith path looks
|
||||||
|
// like, so a lane that wants to say the XFB spans CROSSED has to read a counter the
|
||||||
|
// server moved. Spans counts Begin and End together - they are one span and a lane that
|
||||||
|
// saw only one of them has a bug the two-counter version would have hidden behind a
|
||||||
|
// sum; controls counts Pause and Resume; binds and patch parameters count their own.
|
||||||
|
Uint64 StreamOutputSpans() const { return m_streamOutputSpans; }
|
||||||
|
Uint64 StreamOutputControls() const { return m_streamOutputControls; }
|
||||||
|
Uint64 StreamOutputBinds() const { return m_streamOutputBinds; }
|
||||||
|
Uint64 PatchParameters() const { return m_patchParameters; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
|
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
|
||||||
@@ -189,6 +209,15 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint64 m_presents = 0;
|
Uint64 m_presents = 0;
|
||||||
Uint64 m_lastPresentSerial = 0;
|
Uint64 m_lastPresentSerial = 0;
|
||||||
Uint64 m_readbackBytes = 0;
|
Uint64 m_readbackBytes = 0;
|
||||||
|
Uint64 m_imageBinds = 0;
|
||||||
|
Uint64 m_dispatches = 0;
|
||||||
|
Uint64 m_memoryBarriers = 0;
|
||||||
|
Uint64 m_imageCopies = 0;
|
||||||
|
Uint64 m_storageBlockBindings = 0;
|
||||||
|
Uint64 m_streamOutputSpans = 0;
|
||||||
|
Uint64 m_streamOutputControls = 0;
|
||||||
|
Uint64 m_streamOutputBinds = 0;
|
||||||
|
Uint64 m_patchParameters = 0;
|
||||||
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
||||||
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
||||||
// between them. Grown, never shrunk, and never handed out past the call.
|
// between them. Grown, never shrunk, and never handed out past the call.
|
||||||
|
|||||||
@@ -530,6 +530,7 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// a loop that applies and never retires ends the first MOBILEGL_IPC_STAGE_MB of
|
// a loop that applies and never retires ends the first MOBILEGL_IPC_STAGE_MB of
|
||||||
// staging in Fatal{RingOverrun, "SEG_STAGE"} (w1-v1 5). Once per drain batch, not
|
// staging in Fatal{RingOverrun, "SEG_STAGE"} (w1-v1 5). Once per drain batch, not
|
||||||
// once per record: retiring LATE is always legal, retiring EARLY never is.
|
// once per record: retiring LATE is always legal, retiring EARLY never is.
|
||||||
|
if (const auto hook = m_beforeRetireHook.load(std::memory_order_acquire)) hook();
|
||||||
consumer.RetireThrough(consumer.AppliedSeq());
|
consumer.RetireThrough(consumer.AppliedSeq());
|
||||||
// LEAVING THE APPLIER (p1's M-5) IS *NOT* DONE HERE. It is done inside
|
// LEAVING THE APPLIER (p1's M-5) IS *NOT* DONE HERE. It is done inside
|
||||||
// PipeApplier::ApplyOne, before s1's SessionConsumer::ApplyOne publishes appliedSeq
|
// PipeApplier::ApplyOne, before s1's SessionConsumer::ApplyOne publishes appliedSeq
|
||||||
|
|||||||
@@ -146,6 +146,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// fail for its own reason.
|
// fail for its own reason.
|
||||||
Uint64 DrainedRecords() const;
|
Uint64 DrainedRecords() const;
|
||||||
Uint64 ParkCount() const;
|
Uint64 ParkCount() const;
|
||||||
|
// Scheduling perturbation only: the hook runs after application, before retirement.
|
||||||
|
// Integration tests use it to observe real producer back-pressure from GL uploads.
|
||||||
|
void SetBeforeRetireHookForTesting(void (*hook)()) {
|
||||||
|
m_beforeRetireHook.store(hook, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
// C7 / ID-54 diagnostics, read by ServerLoopTest's C7 and N-3 controls. NativeBindCount is
|
// C7 / ID-54 diagnostics, read by ServerLoopTest's C7 and N-3 controls. NativeBindCount is
|
||||||
// how many times ApplyMakeCurrent FORWARDED a bind to the backend (a tuple it did not
|
// how many times ApplyMakeCurrent FORWARDED a bind to the backend (a tuple it did not
|
||||||
@@ -230,6 +235,7 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint64 m_affinityMask = 0;
|
Uint64 m_affinityMask = 0;
|
||||||
std::atomic<Uint64> m_drained{0};
|
std::atomic<Uint64> m_drained{0};
|
||||||
std::atomic<Uint64> m_parks{0};
|
std::atomic<Uint64> m_parks{0};
|
||||||
|
std::atomic<void (*)()> m_beforeRetireHook{nullptr};
|
||||||
|
|
||||||
// C7 / ID-54: the (dpy, draw, read, ctx) currently bound on the apply thread. Written and
|
// C7 / ID-54: the (dpy, draw, read, ctx) currently bound on the apply thread. Written and
|
||||||
// read ONLY on the apply thread inside ApplyMakeCurrent, so it needs no lock; the two
|
// read ONLY on the apply thread inside ApplyMakeCurrent, so it needs no lock; the two
|
||||||
|
|||||||
@@ -776,10 +776,12 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
std::abort();
|
std::abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
bool reclaimed = false;
|
||||||
|
bool waited = false;
|
||||||
|
for (;;) {
|
||||||
// THE WRAP SKIP MAY ONLY BE CHARGED AGAINST BYTES THAT ARE STILL IN FLIGHT. When
|
// THE WRAP SKIP MAY ONLY BE CHARGED AGAINST BYTES THAT ARE STILL IN FLIGHT. When
|
||||||
// there are none the allocator starts over at offset zero, so a blob the segment
|
// there are none the allocator starts over at offset zero, so a blob the segment
|
||||||
// can hold whole is never refused (see RebaseEmptyStage). On attempt 1 this runs
|
// can hold whole is never refused (see RebaseEmptyStage). On retry this runs
|
||||||
// AFTER ReclaimStagedBytes, which is the case the finding describes: 8 MiB
|
// AFTER ReclaimStagedBytes, which is the case the finding describes: 8 MiB
|
||||||
// allocated, then retired, then a 28 MiB request that used to abort.
|
// allocated, then retired, then a 28 MiB request that used to abort.
|
||||||
RebaseEmptyStage();
|
RebaseEmptyStage();
|
||||||
@@ -793,21 +795,28 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
m_stageHead += skip + need;
|
m_stageHead += skip + need;
|
||||||
return m_stageBase + at;
|
return m_stageBase + at;
|
||||||
}
|
}
|
||||||
if (attempt == 0) {
|
if (!reclaimed) {
|
||||||
// One try at reclaiming what the server has already retired. A second failure
|
// Lazy reclamation of already-retired bytes is NOT a producer wait.
|
||||||
// means the bytes genuinely do not fit, which R-10 says P5 does not chunk and
|
|
||||||
// must instead prove it never needs to.
|
|
||||||
//
|
|
||||||
// AND THIS IS P5'S ONE REAL BACK-PRESSURE EVENT, so it is counted here and
|
|
||||||
// published as `ringwaits=`. Reaching this line means the producer could not
|
|
||||||
// place a blob until the CONSUMER had retired earlier ones - the producer's
|
|
||||||
// progress depended on retiredSeq, which is exactly what R-9's "batching may
|
|
||||||
// only delay a watermark" is about. Exit gate E3(e)'s small-ring lane exists
|
|
||||||
// to make it happen at least once; a lane that never reaches it has a ring
|
|
||||||
// that is small only in its environment block.
|
|
||||||
++m_stageReclaimWaits;
|
|
||||||
ReclaimStagedBytes();
|
ReclaimStagedBytes();
|
||||||
|
reclaimed = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if (m_stageRetirementBell == nullptr || m_stageMarkFront == m_stageMarks.size()) break;
|
||||||
|
const Uint64 pending = m_stageMarks[m_stageMarkFront].Seq;
|
||||||
|
const auto ready = [&] {
|
||||||
|
return m_control->retiredSeq.load(std::memory_order_acquire) >= pending;
|
||||||
|
};
|
||||||
|
if (!ready()) {
|
||||||
|
// The allocation still cannot progress after reclamation. Count this
|
||||||
|
// blocked allocation once, not each watermark poll or each reclaimed mark.
|
||||||
|
if (!waited) { ++m_stageReclaimWaits; waited = true; }
|
||||||
|
if (!m_stageRetirementBell->Wait(m_control->producerParked, ready, 0, 5000)) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{RetirementWaitFailed, \"SEG_STAGE\"} producer wait "
|
||||||
|
"ended before the pending allocation retired (shutdown or timeout)");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReclaimStagedBytes();
|
||||||
}
|
}
|
||||||
MGLOG_F("MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob does not fit a %llu "
|
MGLOG_F("MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob does not fit a %llu "
|
||||||
"byte staging segment with %llu bytes still in flight (retiredSeq=%llu); P5 "
|
"byte staging segment with %llu bytes still in flight (retiredSeq=%llu); P5 "
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
#include "../Transport/Ring.h"
|
#include "../Transport/Ring.h"
|
||||||
|
#include "../Transport/Doorbell.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Wire {
|
namespace MobileGL::MG_Remote::Wire {
|
||||||
|
|
||||||
@@ -311,17 +312,16 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
// this number would have been red for the arithmetic of the record catalogue rather
|
// this number would have been red for the arithmetic of the record catalogue rather
|
||||||
// than for anything about the ring.
|
// than for anything about the ring.
|
||||||
//
|
//
|
||||||
// `StageReclaimWaits()` counts every SEG_STAGE allocation that did not fit until the
|
// `StageReclaimWaits()` counts allocations blocked on an outstanding retiredSeq
|
||||||
// encoder reclaimed the runs the server had already retired - i.e. every time the
|
// after immediate reclamation still left insufficient space. Reclaiming bytes
|
||||||
// producer's progress depended on the consumer's retiredSeq. That is the honest
|
// the consumer had already retired does not increment it. One allocation counts
|
||||||
// back-pressure reading in P5, and the reason the command ring has none: the verb
|
// once even if it waits for several marks; this is staging, not command-ring pressure.
|
||||||
// barrier makes EmitAndWait wait for appliedSeq after EVERY record (R-1), so at most
|
|
||||||
// one record is ever in flight on SEG_CMD and a full command ring is not a wait but a
|
|
||||||
// Fatal{RingOverrun} (ClientSession.cpp). Publishing a "command ring waits" counter
|
|
||||||
// that can only ever be zero-or-dead is the decoration this file's counters are not.
|
|
||||||
Uint64 CmdWraps() const;
|
Uint64 CmdWraps() const;
|
||||||
Uint64 CmdWrapPads() const;
|
Uint64 CmdWrapPads() const;
|
||||||
Uint64 StageReclaimWaits() const;
|
Uint64 StageReclaimWaits() const;
|
||||||
|
// The live session supplies its shutdown-aware producer doorbell. Standalone codecs
|
||||||
|
// without a consumer cannot wait for retirement and retain the named refusal.
|
||||||
|
void SetStageRetirementDoorbell(Transport::Doorbell* bell) { m_stageRetirementBell = bell; }
|
||||||
|
|
||||||
// Bytes this encoder has ever written into SEG_CMD, pad fillers included: the
|
// Bytes this encoder has ever written into SEG_CMD, pad fillers included: the
|
||||||
// producer's monotonic head cursor. It is the DENOMINATOR the wrap count only means
|
// producer's monotonic head cursor. It is the DENOMINATOR the wrap count only means
|
||||||
@@ -365,6 +365,7 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
Uint64 m_cmdWraps = 0;
|
Uint64 m_cmdWraps = 0;
|
||||||
Uint64 m_cmdWrapPads = 0;
|
Uint64 m_cmdWrapPads = 0;
|
||||||
Uint64 m_stageReclaimWaits = 0;
|
Uint64 m_stageReclaimWaits = 0;
|
||||||
|
Transport::Doorbell* m_stageRetirementBell = nullptr;
|
||||||
Vector<StageMark> m_stageMarks;
|
Vector<StageMark> m_stageMarks;
|
||||||
SizeT m_stageMarkFront = 0;
|
SizeT m_stageMarkFront = 0;
|
||||||
Uint8* m_stageBase = nullptr;
|
Uint8* m_stageBase = nullptr;
|
||||||
|
|||||||
@@ -254,13 +254,23 @@ TEST_F(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) {
|
|||||||
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1u),
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1u),
|
||||||
MGPipeFieldOwnership::kFatal);
|
MGPipeFieldOwnership::kFatal);
|
||||||
|
|
||||||
// The three off the reduced path, each for its own checkable reason.
|
// The two off the reduced path, each for its own checkable reason. THREE UNTIL P5b: the
|
||||||
|
// third was GetProgramForDispatch, FATAL because "there is no compute on the reduced path",
|
||||||
|
// and package i1 is what put compute on the path (CONTRACT-P5B.md §6.9). It is asserted
|
||||||
|
// below in its new class rather than deleted from this case, because a field that quietly
|
||||||
|
// left the FATAL list is exactly what this case exists to catch.
|
||||||
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBoundTransformFeedbackName),
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBoundTransformFeedbackName),
|
||||||
MGPipeFieldOwnership::kFatal);
|
MGPipeFieldOwnership::kFatal);
|
||||||
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter),
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter),
|
||||||
MGPipeFieldOwnership::kFatal);
|
MGPipeFieldOwnership::kFatal);
|
||||||
|
// P5b i1: launch_grid (60) crosses, the backend's PrepareForCompute pulls the compute
|
||||||
|
// program inside it (DirectGLES.cpp:5779), and the field takes GetProgramForDraw's class
|
||||||
|
// and its retiring phases - so it is a measured DEBT now, not a defect.
|
||||||
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch),
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch),
|
||||||
MGPipeFieldOwnership::kFatal);
|
MGPipeFieldOwnership::kBarrierPulled);
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch),
|
||||||
|
MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDraw))
|
||||||
|
<< "GetProgramForDispatch is GetProgramForDraw's twin and must share its class";
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) {
|
TEST_F(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) {
|
||||||
@@ -516,17 +526,43 @@ TEST_F(FieldOwnershipTest, StrictErrorsAlsoPromotesTheStickyForwards) {
|
|||||||
|
|
||||||
// A FATAL-class read aborts whatever the knob says: no carrier, and the reduced path never
|
// A FATAL-class read aborts whatever the knob says: no carrier, and the reduced path never
|
||||||
// reads it, so it is a real defect rather than a debt.
|
// reads it, so it is a real defect rather than a debt.
|
||||||
|
// P5b i1: the exemplar MOVED. This case used GetProgramForDispatch, which is BARRIER-PULLED
|
||||||
|
// from i1 on (a debt the server serves, not an abort), so it would now assert that a served
|
||||||
|
// read aborts - green for the wrong reason at best. GetTransformFeedbackPausedPrimitiveCounter
|
||||||
|
// is the same statement with a field that is still FATAL: reachable only from class kQuery,
|
||||||
|
// which the reduced path never enters.
|
||||||
|
// RED ONCE BY DOING X: put GetProgramForDispatch back in the FATAL block of FieldOwnership.def
|
||||||
|
// and TheFieldOwnershipTableIsTheContractsTableRow's new kBarrierPulled expectation goes red by
|
||||||
|
// name; swap the field below for GetProgramForDispatch and THIS case goes red instead, because
|
||||||
|
// a barrier-pulled read under a stamp does not abort.
|
||||||
TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) {
|
TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) {
|
||||||
const ChildResult r = RunInChild([] {
|
const ChildResult r = RunInChild([] {
|
||||||
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
||||||
(void)gPipeInputs.GetProgramForDispatch();
|
(void)gPipeInputs.GetTransformFeedbackPausedPrimitiveCounter();
|
||||||
});
|
});
|
||||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch@DrawArrays\"}"),
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, "
|
||||||
|
"\"GetTransformFeedbackPausedPrimitiveCounter@DrawArrays\"}"),
|
||||||
std::string::npos)
|
std::string::npos)
|
||||||
<< r.Log;
|
<< r.Log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// And the field that LEFT the FATAL class is served rather than fatal, under the verb that made
|
||||||
|
// it reachable. This is i1's half of the §6.9 grant made checkable: a dispatch stamp plus a read
|
||||||
|
// of the compute program must NOT abort, which is precisely the statement "compute is on the
|
||||||
|
// path now". RED ONCE BY DOING X: revert the FieldOwnership.def row to FATAL and this child
|
||||||
|
// aborts with Fatal{UnmigratedPipeInput, "GetProgramForDispatch@DispatchCompute"}.
|
||||||
|
TEST_F(FieldOwnershipTest, TheComputeProgramIsServedUnderADispatchStampFromP5bOn) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DispatchCompute);
|
||||||
|
(void)gPipeInputs.GetProgramForDispatch();
|
||||||
|
MGPipeServerClearVerbBoundary();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_EQ(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch"), std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
// The argument-keyed row: the pack half is answerable and the unpack half is not, and the
|
// The argument-keyed row: the pack half is answerable and the unpack half is not, and the
|
||||||
// difference is the accessor's own argument rather than a second field id. Splitting the field
|
// difference is the accessor's own argument rather than a second field id. Splitting the field
|
||||||
// into two ids would have made the unpack half BARRIER-PULLED - silently served - which is
|
// into two ids would have made the unpack half BARRIER-PULLED - silently served - which is
|
||||||
|
|||||||
@@ -1204,6 +1204,113 @@ TEST_F(PipeWireCodecTest, BindStreamOutputReachesTheSink) {
|
|||||||
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull);
|
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P5b t2 (MG_Remote/CONTRACT-P5B.md §2 t2). c0b's cases above round-trip each row once; these
|
||||||
|
// pin the fields t2's EMITTERS actually fill and the one ordering property the span family has.
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, EndStreamOutputCarriesAllThreeAccountingFieldsAndNotJustTheVertices) {
|
||||||
|
// t2's emitter fills all three from the frontend's own per-span accounting
|
||||||
|
// (GetTransformFeedbackCapturedVertices / GetTransformFeedbackPrimitiveCounter /
|
||||||
|
// GetTransformFeedbackPrimitiveMode) and the row above asserts only CapturedVertices, so a
|
||||||
|
// codec that dropped either of the other two - or an emitter that left them zero - reads
|
||||||
|
// green there. THE THREE ARE DELIBERATELY DIFFERENT NUMBERS: with 300/300 a swap of the two
|
||||||
|
// 64-bit fields is invisible.
|
||||||
|
//
|
||||||
|
// Red once by making the recorder above push a default-constructed MGPXfbAccounting{}
|
||||||
|
// instead of the one the decoder handed it - the shape of a seam that loses the payload:
|
||||||
|
// "end_stream_output lost the primitives-written half of its accounting".
|
||||||
|
Wire2 wire;
|
||||||
|
MGPXfbAccounting end{};
|
||||||
|
end.CapturedVertices = 21;
|
||||||
|
end.PrimitivesWritten = 7;
|
||||||
|
end.PrimitiveMode = 0x0000; // GL_POINTS - and 0 is a legal primitive mode, not "unset"
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::EndStreamOutput, &end, sizeof(end)), kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
ASSERT_EQ(wire.Sink().Ends.size(), 1u);
|
||||||
|
EXPECT_EQ(wire.Sink().Ends[0].CapturedVertices, 21u);
|
||||||
|
EXPECT_EQ(wire.Sink().Ends[0].PrimitivesWritten, 7u)
|
||||||
|
<< "end_stream_output lost the primitives-written half of its accounting";
|
||||||
|
EXPECT_EQ(wire.Sink().Ends[0].PrimitiveMode, 0u)
|
||||||
|
<< "GL_POINTS is 0 and a row that treats 0 as 'no mode' has made a legal capture "
|
||||||
|
"indistinguishable from an unfilled record";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, BindStreamOutputOfNameZeroIsTheDefaultObjectAndNotAnAbsentOne) {
|
||||||
|
// THE ONE NAME t2 CANNOT TREAT AS "NOTHING". CONTRACT-P5B.md gives DeleteTransformFeedback
|
||||||
|
// no row, and the backend's answer to deleting the bound object is a bind of NAME 0
|
||||||
|
// (DirectGLES.cpp:1422) - so a row that folded 0 into kMGPipeNullHandle, or a sink that read
|
||||||
|
// 0 as "no object", would silently stop rebinding the default object and leave the driver
|
||||||
|
// bound to a deleted one. The lifetime id beside it is 0 too here, which is the frontend's
|
||||||
|
// seed value for the default object, so this case also pins that a wholly-zero record is
|
||||||
|
// legal and applies rather than being refused as unfilled.
|
||||||
|
//
|
||||||
|
// Red once by making the sink's OnBindStreamOutput refuse GlName == 0 (return false): the
|
||||||
|
// EXPECT_TRUE(applied) below failed, which is the shape the wrong reading would take.
|
||||||
|
Wire2 wire;
|
||||||
|
MGPStreamOutputBind bind{};
|
||||||
|
bind.GlName = 0;
|
||||||
|
bind.LifetimeId = 0;
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindStreamOutput, &bind, sizeof(bind)), kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied) << "a bind of the default transform-feedback object did not apply";
|
||||||
|
ASSERT_EQ(wire.Sink().StreamOutputBinds.size(), 1u);
|
||||||
|
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].GlName, 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, AWholeCaptureSpanReachesTheSinkInTheOrderTheClientEmittedIt) {
|
||||||
|
// THE PROPERTY A PER-ROW ROUND TRIP CANNOT STATE. A capture span is five calls whose MEANING
|
||||||
|
// is their order - bind the object, open the span, pause it, resume it, close it - and every
|
||||||
|
// one of them rides its own row with no sequence field of its own to check. The decoder's
|
||||||
|
// ordering is the ring's, so this is a pin on the seam rather than a new guarantee: if a
|
||||||
|
// later change ever batches or reorders records per row, a capture reordered into
|
||||||
|
// bind/begin/end/pause/resume applies five records, sets `applied` five times, and leaves
|
||||||
|
// every per-row case above green while the capture is destroyed.
|
||||||
|
//
|
||||||
|
// Red once by encoding the Pause AFTER the End: the interleaved-order EXPECT below failed on
|
||||||
|
// Pauses being 0 at the point the End was seen.
|
||||||
|
Wire2 wire;
|
||||||
|
MGPStreamOutputBind bind{};
|
||||||
|
bind.GlName = 3;
|
||||||
|
bind.LifetimeId = 0x5150ull;
|
||||||
|
MGPStreamOutputBegin begin{};
|
||||||
|
begin.PrimitiveMode = 0x0004; // GL_TRIANGLES
|
||||||
|
MGPStreamOutputControl control{};
|
||||||
|
MGPXfbAccounting end{};
|
||||||
|
end.CapturedVertices = 9;
|
||||||
|
end.PrimitivesWritten = 3;
|
||||||
|
end.PrimitiveMode = 0x0004;
|
||||||
|
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindStreamOutput, &bind, sizeof(bind)), kInvalidSeq);
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BeginStreamOutput, &begin, sizeof(begin)), kInvalidSeq);
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::PauseStreamOutput, &control, sizeof(control)), kInvalidSeq);
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResumeStreamOutput, &control, sizeof(control)), kInvalidSeq);
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::EndStreamOutput, &end, sizeof(end)), kInvalidSeq);
|
||||||
|
|
||||||
|
// Pumped ONE AT A TIME with the sink read between pumps, which is what makes this a
|
||||||
|
// statement about order rather than about totals: after the third record the pause must
|
||||||
|
// have happened and the end must NOT have.
|
||||||
|
bool applied = false;
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_EQ(wire.Sink().StreamOutputBinds.size(), 1u) << "the object bind did not come first";
|
||||||
|
EXPECT_EQ(wire.Sink().Begins.size(), 0u);
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_EQ(wire.Sink().Begins.size(), 1u) << "the span did not open second";
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_EQ(wire.Sink().Pauses, 1u) << "the pause did not arrive third";
|
||||||
|
EXPECT_EQ(wire.Sink().Ends.size(), 0u)
|
||||||
|
<< "the span closed before it was paused - the capture's order did not survive the wire";
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_EQ(wire.Sink().Resumes, 1u);
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
ASSERT_EQ(wire.Sink().Ends.size(), 1u);
|
||||||
|
EXPECT_EQ(wire.Sink().Ends[0].PrimitivesWritten, 3u);
|
||||||
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), 5u);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(PipeWireCodecTest, SetStorageBlockBindingCarriesItsNameAsAStagedBlob) {
|
TEST_F(PipeWireCodecTest, SetStorageBlockBindingCarriesItsNameAsAStagedBlob) {
|
||||||
// i1: the ONE string on the wire. Size is strlen + 1 - the NUL travels - and the decoder
|
// i1: the ONE string on the wire. Size is strlen + 1 - the NUL travels - and the decoder
|
||||||
// hands the sink a pointer that dies with the call.
|
// hands the sink a pointer that dies with the call.
|
||||||
@@ -1595,6 +1702,20 @@ TEST_F(PipeWireCodecTest, ABigProgramArchiveDoesNotGrowItsRecordAtAll) {
|
|||||||
EXPECT_GE(wire.Encoder().StagedBytesInFlight(), archive.size());
|
EXPECT_GE(wire.Encoder().StagedBytesInFlight(), archive.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, AlreadyRetiredStagingReclamationIsNotAProducerWait) {
|
||||||
|
Wire2 wire;
|
||||||
|
std::vector<std::uint8_t> payload(Wire2::kStageBytes * 3 / 4, 0x5a);
|
||||||
|
wire.Encoder().StageBytes(payload.data(), payload.size());
|
||||||
|
MGPBindRenderState bind{};
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindRenderState, &bind, sizeof(bind)), kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
// Do not explicitly reclaim: the second real allocation must do that itself.
|
||||||
|
wire.Encoder().StageBytes(payload.data(), payload.size());
|
||||||
|
EXPECT_EQ(wire.Encoder().StageReclaimWaits(), 0u)
|
||||||
|
<< "already-retired lazy reclamation is not a producer wait";
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(PipeWireCodecTest, StagedBytesAreReclaimedOnlyBehindRetiredSeq) {
|
TEST_F(PipeWireCodecTest, StagedBytesAreReclaimedOnlyBehindRetiredSeq) {
|
||||||
Wire2 wire;
|
Wire2 wire;
|
||||||
const std::uint8_t payload[64] = {};
|
const std::uint8_t payload[64] = {};
|
||||||
|
|||||||
@@ -171,19 +171,102 @@ namespace {
|
|||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
|
|
||||||
TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
|
TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
|
||||||
// CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal. Read from the functions the
|
// P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots.
|
||||||
// table itself reports with - which is also what t1's arming condition reads - rather than
|
|
||||||
// recomputed here, so a table that lost an emitter cannot look like one that never had it.
|
|
||||||
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
|
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
|
||||||
// P5b d1 moved the nineteen draw slots from class C to class B (CONTRACT-P5B.md §7: B = 5
|
EXPECT_EQ(ImplementedVerbCount(), 48u);
|
||||||
// + the packages' flips, C = 64 - the same). Each P5b package raises B and lowers C by the
|
EXPECT_EQ(UnmigratedSlotCount(), 21u);
|
||||||
// same number, so the two numbers here move together and the sum below never does.
|
|
||||||
EXPECT_EQ(ImplementedVerbCount(), 5u + 19u);
|
|
||||||
EXPECT_EQ(UnmigratedSlotCount(), 64u - 19u);
|
|
||||||
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
|
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
|
||||||
kRemoteEmitSlotCount);
|
kRemoteEmitSlotCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, TheSixXfbAndPatchSlotsAreNonNullAndDistinct) {
|
||||||
|
// P5b t2 (CONTRACT-P5B.md §2 t2), the half that needs no fork: the six slots exist and are
|
||||||
|
// six DIFFERENT functions. Six identical pointers would be one emitter assigned six times,
|
||||||
|
// which is how a copy-paste flip loses five records and still passes every count.
|
||||||
|
//
|
||||||
|
// Red once by assigning `table.GL.PauseTransformFeedback = &EmitResumeTransformFeedback;`
|
||||||
|
// in BuildRemoteEmitTable - the exact copy-paste this guards: "t2 slots 2 and 3 are one
|
||||||
|
// function".
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
|
||||||
|
const void* const six[] = {
|
||||||
|
reinterpret_cast<const void*>(table.GL.BeginTransformFeedback),
|
||||||
|
reinterpret_cast<const void*>(table.GL.EndTransformFeedback),
|
||||||
|
reinterpret_cast<const void*>(table.GL.PauseTransformFeedback),
|
||||||
|
reinterpret_cast<const void*>(table.GL.ResumeTransformFeedback),
|
||||||
|
reinterpret_cast<const void*>(table.GL.BindTransformFeedback),
|
||||||
|
reinterpret_cast<const void*>(table.GL.PatchParameteri),
|
||||||
|
};
|
||||||
|
for (SizeT i = 0; i < 6; ++i) {
|
||||||
|
EXPECT_NE(six[i], nullptr) << "t2 slot " << i << " is null";
|
||||||
|
for (SizeT j = i + 1; j < 6; ++j) {
|
||||||
|
EXPECT_NE(six[i], six[j]) << "t2 slots " << i << " and " << j << " are one function";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And the one XFB slot t2 does NOT flip is still there to be Fatal - CONTRACT-P5B.md gives
|
||||||
|
// DeleteTransformFeedback no row (unmeasured), so it must not have been swept up.
|
||||||
|
EXPECT_NE(table.GL.DeleteTransformFeedback, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
TEST(RemoteEmitTable, EachXfbAndPatchSlotIsClassBAndDemandsASessionByItsOwnName) {
|
||||||
|
// P5b t2, THE HALF THAT DECIDES THE CLASS. A pointer comparison cannot tell a class-B
|
||||||
|
// emitter from a class-C thunk - each unmigrated slot gets its own generated function, so
|
||||||
|
// every slot in the table is already a distinct non-null address. What distinguishes them is
|
||||||
|
// WHAT THEY SAY when called with no ClientSession: an emitter reaches RequireSession and
|
||||||
|
// dies Fatal{NoClientSession, "<slot>"}; a thunk dies Fatal{UnmigratedVerb, "<slot>"}. Both
|
||||||
|
// strings are asserted, because a case that only looked for the first would be satisfied by
|
||||||
|
// a build where every one of these had been flipped by accident.
|
||||||
|
//
|
||||||
|
// Red once by SWAPPING the Pause and Resume assignments in BuildRemoteEmitTable - the two
|
||||||
|
// emitters with the same signature, so the swap compiles and neither is orphaned (the first
|
||||||
|
// attempt redirected one slot at another's emitter and the build failed on the signature
|
||||||
|
// and on -Wunused-function, which is a control that did not run). The child called through
|
||||||
|
// PauseTransformFeedback died Fatal{NoClientSession, "ResumeTransformFeedback"} and this
|
||||||
|
// case failed with "PauseTransformFeedback did not reach the class-B emitter's session
|
||||||
|
// demand", so the string really is the slot's own name and not a shared constant.
|
||||||
|
struct Slot {
|
||||||
|
const char* Name;
|
||||||
|
void (*Call)();
|
||||||
|
};
|
||||||
|
static const Slot kSlots[] = {
|
||||||
|
{"BeginTransformFeedback", [] { RemoteEmitTable().GL.BeginTransformFeedback(0x0004); }},
|
||||||
|
{"EndTransformFeedback", [] { RemoteEmitTable().GL.EndTransformFeedback(); }},
|
||||||
|
{"PauseTransformFeedback", [] { RemoteEmitTable().GL.PauseTransformFeedback(); }},
|
||||||
|
{"ResumeTransformFeedback", [] { RemoteEmitTable().GL.ResumeTransformFeedback(); }},
|
||||||
|
{"BindTransformFeedback", [] { RemoteEmitTable().GL.BindTransformFeedback(0); }},
|
||||||
|
{"PatchParameteri", [] { RemoteEmitTable().GL.PatchParameteri(0x8E72, 3); }},
|
||||||
|
};
|
||||||
|
for (const Slot& slot : kSlots) {
|
||||||
|
const ChildResult r = RunInChild([&slot] { slot.Call(); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << slot.Name << ": " << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find(std::string("Fatal{NoClientSession, \"") + slot.Name + "\"}"),
|
||||||
|
std::string::npos)
|
||||||
|
<< slot.Name << " did not reach the class-B emitter's session demand:\n"
|
||||||
|
<< r.Log;
|
||||||
|
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos)
|
||||||
|
<< slot.Name << " is still class C:\n"
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, DeleteTransformFeedbackHasNoRowAndStillAbortsByItsOwnName) {
|
||||||
|
// CONTRACT-P5B.md §2 t2 and c0b-v1.md §6: the seventh slot in t2's ownership block gets NO
|
||||||
|
// row in P5b - it is unmeasured, the driver object leaks on the server until P9's XFB
|
||||||
|
// namespace work, and a bind of name 0 is what the backend does on delete of the bound one.
|
||||||
|
// That is a RULING, so it is pinned rather than left to be re-derived from a count: a later
|
||||||
|
// round that gives it a row has to delete this case and say why.
|
||||||
|
//
|
||||||
|
// Red once by assigning `table.GL.DeleteTransformFeedback = &EmitBindTransformFeedback;` in
|
||||||
|
// BuildRemoteEmitTable - a package sweeping the whole XFB family into class B: the child
|
||||||
|
// died Fatal{NoClientSession, "BindTransformFeedback"} and the UnmigratedVerb expectation
|
||||||
|
// failed.
|
||||||
|
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.DeleteTransformFeedback(7); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DeleteTransformFeedback\"}"), std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
#endif // MGTEST_HAVE_FORK
|
||||||
|
|
||||||
TEST(RemoteEmitTable, NoSlotIsNull) {
|
TEST(RemoteEmitTable, NoSlotIsNull) {
|
||||||
// R-4's whole rule, asserted over the STRUCT rather than over the list that built it. 91
|
// R-4's whole rule, asserted over the STRUCT rather than over the list that built it. 91
|
||||||
// MG_Impl sites call through this table directly; a null slot is 91 potential null calls,
|
// MG_Impl sites call through this table directly; a null slot is 91 potential null calls,
|
||||||
@@ -280,6 +363,49 @@ TEST(RemoteEmitTable, SetSwapIntervalIsClassCAndSaysSo) {
|
|||||||
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log;
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // MGTEST_HAVE_FORK
|
||||||
|
|
||||||
|
// ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) -------------------------------------
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, TheSevenI1SlotsAreClassBAndAreNotTheFatalThunk) {
|
||||||
|
// The census's five measured slots plus the two companions that share their rows. Named
|
||||||
|
// rather than counted, so a table that flipped a DIFFERENT seven is red here and not only
|
||||||
|
// in the arithmetic. The comparison is against a slot that is still class C: a flipped slot
|
||||||
|
// and an unflipped one cannot be the same pointer, which is what a forgotten class-B
|
||||||
|
// assignment would look like (class C is assigned FIRST in BuildRemoteEmitTable precisely so
|
||||||
|
// that the mistake is loud rather than null).
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
|
||||||
|
const void* fatal = reinterpret_cast<const void*>(table.GL.GetTexImage); // wave-3 tail, class C
|
||||||
|
ASSERT_NE(fatal, nullptr);
|
||||||
|
const void* const i1[] = {
|
||||||
|
reinterpret_cast<const void*>(table.GL.BindImageTexture),
|
||||||
|
reinterpret_cast<const void*>(table.GL.DispatchCompute),
|
||||||
|
reinterpret_cast<const void*>(table.GL.DispatchComputeIndirect),
|
||||||
|
reinterpret_cast<const void*>(table.GL.MemoryBarrier),
|
||||||
|
reinterpret_cast<const void*>(table.GL.MemoryBarrierByRegion),
|
||||||
|
reinterpret_cast<const void*>(table.GL.CopyImageSubData),
|
||||||
|
reinterpret_cast<const void*>(table.GL.ShaderStorageBlockBinding),
|
||||||
|
};
|
||||||
|
static const char* const kNames[] = {"BindImageTexture", "DispatchCompute",
|
||||||
|
"DispatchComputeIndirect", "MemoryBarrier",
|
||||||
|
"MemoryBarrierByRegion", "CopyImageSubData",
|
||||||
|
"ShaderStorageBlockBinding"};
|
||||||
|
for (SizeT i = 0; i < sizeof(i1) / sizeof(i1[0]); ++i) {
|
||||||
|
EXPECT_NE(i1[i], nullptr) << kNames[i] << " is null";
|
||||||
|
EXPECT_NE(i1[i], fatal) << kNames[i]
|
||||||
|
<< " still points at an UnmigratedVerbFatal thunk; i1 flipped it "
|
||||||
|
"to class B";
|
||||||
|
}
|
||||||
|
// The two barrier slots and the two dispatch slots share a WIRE ROW but not an emitter: the
|
||||||
|
// discriminant (ByRegion / IsIndirect) is set by the emitter, so one thunk for both would
|
||||||
|
// carry the wrong one.
|
||||||
|
EXPECT_NE(i1[3], i1[4]) << "MemoryBarrier and MemoryBarrierByRegion share memory_barrier (61) "
|
||||||
|
"but must set opposite ByRegion values";
|
||||||
|
EXPECT_NE(i1[1], i1[2]) << "DispatchCompute and DispatchComputeIndirect share launch_grid (60) "
|
||||||
|
"but must set opposite IsIndirect values";
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) {
|
TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) {
|
||||||
// The other half of "no slot may fall through to the driver". With no ClientSession the
|
// The other half of "no slot may fall through to the driver". With no ClientSession the
|
||||||
// emitter has nowhere to put the record, and the one thing it may not do is return quietly:
|
// emitter has nowhere to put the record, and the one thing it may not do is return quietly:
|
||||||
@@ -288,6 +414,53 @@ TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) {
|
|||||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"Clear\"}"), std::string::npos) << r.Log;
|
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"Clear\"}"), std::string::npos) << r.Log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, EachI1SlotReachesRequireSessionUnderItsOwnName) {
|
||||||
|
// The behavioural half: an i1 slot is class B, so with no ClientSession it reaches
|
||||||
|
// RequireSession and aborts Fatal{NoClientSession, "<slot>"} - NOT Fatal{UnmigratedVerb}
|
||||||
|
// (which would mean the flip never happened) and NOT quietly (which is the split lane
|
||||||
|
// running monolith and going green, R-4). The name in the message is the slot's own, which
|
||||||
|
// is the half a single case could not state.
|
||||||
|
//
|
||||||
|
// RED ONCE BY DOING X: put `X(MemoryBarrier, void, (GLbitfield))` back in
|
||||||
|
// MGR_UNMIGRATED_I1_SLOTS and drop `table.GL.MemoryBarrier = &EmitMemoryBarrier;` - the
|
||||||
|
// MemoryBarrier arm below then finds Fatal{UnmigratedVerb, "MemoryBarrier"} instead.
|
||||||
|
const ChildResult barrier = RunInChild([] { RemoteEmitTable().GL.MemoryBarrier(0x2000); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(barrier)) << DescribeStatus(barrier) << "\n" << barrier.Log;
|
||||||
|
EXPECT_NE(barrier.Log.find("Fatal{NoClientSession, \"MemoryBarrier\"}"), std::string::npos)
|
||||||
|
<< barrier.Log;
|
||||||
|
EXPECT_EQ(barrier.Log.find("Fatal{UnmigratedVerb"), std::string::npos)
|
||||||
|
<< "MemoryBarrier is class B from P5b i1 on:\n"
|
||||||
|
<< barrier.Log;
|
||||||
|
|
||||||
|
const ChildResult dispatch = RunInChild([] { RemoteEmitTable().GL.DispatchCompute(1, 1, 1); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(dispatch)) << DescribeStatus(dispatch) << "\n" << dispatch.Log;
|
||||||
|
EXPECT_NE(dispatch.Log.find("Fatal{NoClientSession, \"DispatchCompute\"}"), std::string::npos)
|
||||||
|
<< dispatch.Log;
|
||||||
|
|
||||||
|
const ChildResult bind = RunInChild(
|
||||||
|
[] { RemoteEmitTable().GL.BindImageTexture(0, 1, 0, GL_FALSE, 0, 0x88BA, 0x8058); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(bind)) << DescribeStatus(bind) << "\n" << bind.Log;
|
||||||
|
EXPECT_NE(bind.Log.find("Fatal{NoClientSession, \"BindImageTexture\"}"), std::string::npos)
|
||||||
|
<< bind.Log;
|
||||||
|
|
||||||
|
const ChildResult ssbo = RunInChild(
|
||||||
|
[] { RemoteEmitTable().GL.ShaderStorageBlockBinding(1, "Blk", 2); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(ssbo)) << DescribeStatus(ssbo) << "\n" << ssbo.Log;
|
||||||
|
EXPECT_NE(ssbo.Log.find("Fatal{NoClientSession, \"ShaderStorageBlockBinding\"}"),
|
||||||
|
std::string::npos)
|
||||||
|
<< ssbo.Log;
|
||||||
|
|
||||||
|
const ChildResult copy = RunInChild([] {
|
||||||
|
const MG_Backend::CopyImageEndpoint src{};
|
||||||
|
const MG_Backend::CopyImageEndpoint dst{};
|
||||||
|
RemoteEmitTable().GL.CopyImageSubData(src, 0x0DE1, 0, 0, 0, 0, dst, 0x0DE1, 0, 0, 0, 0, 1,
|
||||||
|
1, 1);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(copy)) << DescribeStatus(copy) << "\n" << copy.Log;
|
||||||
|
EXPECT_NE(copy.Log.find("Fatal{NoClientSession, \"CopyImageSubData\"}"), std::string::npos)
|
||||||
|
<< copy.Log;
|
||||||
|
}
|
||||||
#endif // MGTEST_HAVE_FORK
|
#endif // MGTEST_HAVE_FORK
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
@@ -1042,6 +1215,300 @@ TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "RemoteClientControls.inc"
|
#include "RemoteClientControls.inc"
|
||||||
|
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||||
|
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||||
|
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||||
|
|
||||||
|
// f1: the installed emitters are decoded by a peer on the apply thread.
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
namespace {
|
||||||
|
struct F1Peer : Codec::WireVerbSink {
|
||||||
|
MGPClear clear{};
|
||||||
|
MGPCopyFromFramebuffer copy{};
|
||||||
|
MGPMipPlan mip{};
|
||||||
|
unsigned calls = 0;
|
||||||
|
Bool OnClear(const MGPClear& v) override { clear = v; ++calls; return true; }
|
||||||
|
Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& v) override { copy = v; ++calls; return true; }
|
||||||
|
Bool OnGenerateMipmap(const MGPMipPlan& v) override { mip = v; ++calls; return true; }
|
||||||
|
void Install() {
|
||||||
|
if (Srv::ServerLoopInstance().RunOnApplyThread([](void* self) {
|
||||||
|
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
|
||||||
|
decoder.SetVerbSink(static_cast<F1Peer*>(self));
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}, this) != MOBILEGL_OK) ::_exit(82);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearBufferfvFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferfv.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession();
|
||||||
|
F1Peer peer; peer.Install();
|
||||||
|
const GLfloat value[4] = {1, 7, 13, 23};
|
||||||
|
|
||||||
|
RemoteEmitTable().GL.ClearBufferfv(GL_COLOR, 3, value);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassFloat ||
|
||||||
|
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
|
||||||
|
!MGPipeHandleIsNull(r.Fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearBufferfv.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearNamedFramebufferfvFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferfv.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession();
|
||||||
|
F1Peer peer; peer.Install();
|
||||||
|
const GLfloat value[4] = {1, 7, 13, 23};
|
||||||
|
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
|
||||||
|
RemoteEmitTable().GL.ClearNamedFramebufferfv(fbo, GL_COLOR, 3, value);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassFloat ||
|
||||||
|
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
|
||||||
|
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearNamedFramebufferfv.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearBufferivFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferiv.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession();
|
||||||
|
F1Peer peer; peer.Install();
|
||||||
|
const GLint value[4] = {1, 7, 13, 23};
|
||||||
|
|
||||||
|
RemoteEmitTable().GL.ClearBufferiv(GL_COLOR, 3, value);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassInt ||
|
||||||
|
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
|
||||||
|
!MGPipeHandleIsNull(r.Fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearBufferiv.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearNamedFramebufferivFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferiv.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession();
|
||||||
|
F1Peer peer; peer.Install();
|
||||||
|
const GLint value[4] = {1, 7, 13, 23};
|
||||||
|
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
|
||||||
|
RemoteEmitTable().GL.ClearNamedFramebufferiv(fbo, GL_COLOR, 3, value);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassInt ||
|
||||||
|
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
|
||||||
|
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearNamedFramebufferiv.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearBufferuivFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferuiv.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession();
|
||||||
|
F1Peer peer; peer.Install();
|
||||||
|
const GLuint value[4] = {1, 7, 13, 23};
|
||||||
|
|
||||||
|
RemoteEmitTable().GL.ClearBufferuiv(GL_COLOR, 3, value);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassUint ||
|
||||||
|
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
|
||||||
|
!MGPipeHandleIsNull(r.Fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearBufferuiv.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearNamedFramebufferuivFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferuiv.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession();
|
||||||
|
F1Peer peer; peer.Install();
|
||||||
|
const GLuint value[4] = {1, 7, 13, 23};
|
||||||
|
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
|
||||||
|
RemoteEmitTable().GL.ClearNamedFramebufferuiv(fbo, GL_COLOR, 3, value);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassUint ||
|
||||||
|
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
|
||||||
|
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearNamedFramebufferuiv.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearBufferfiFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferfi.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession(); F1Peer peer; peer.Install();
|
||||||
|
|
||||||
|
RemoteEmitTable().GL.ClearBufferfi(GL_DEPTH_STENCIL, 0, 0.375f, 91);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindDepthStencil || r.DrawBufferIndex != 0 ||
|
||||||
|
r.DepthValue != 0.375f || r.StencilValue != 91 ||
|
||||||
|
!MGPipeHandleIsNull(r.Fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearBufferfi.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, ClearNamedFramebufferfiFieldsCross) {
|
||||||
|
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferfi.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession(); F1Peer peer; peer.Install();
|
||||||
|
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
|
||||||
|
RemoteEmitTable().GL.ClearNamedFramebufferfi(fbo, GL_DEPTH_STENCIL, 0, 0.375f, 91);
|
||||||
|
const auto& r = peer.clear;
|
||||||
|
if (peer.calls != 1 || r.Kind != kMGPipeClearKindDepthStencil || r.DrawBufferIndex != 0 ||
|
||||||
|
r.DepthValue != 0.375f || r.StencilValue != 91 ||
|
||||||
|
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.ClearNamedFramebufferfi.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
SharedPtr<MG_State::GLState::TextureObject2D> F1Texture() {
|
||||||
|
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||||
|
auto tex = MakeShared<MG_State::GLState::TextureObject2D>(91);
|
||||||
|
tex->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||||
|
for (Uint level = 0; level != 3; ++level) {
|
||||||
|
const Int size = 8 >> level;
|
||||||
|
tex->AllocateStorage(TextureUploadTarget::Texture2D, level,
|
||||||
|
MG_State::GLState::MipmapInput{IntVec3{size, size, 1}, static_cast<SizeT>(size * size * 4)});
|
||||||
|
}
|
||||||
|
tex->SetBaseLevel(1);
|
||||||
|
MGPipeTextureEmitterInstance().AcquireTexture(tex->GetLifetimeId(), tex.get());
|
||||||
|
MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(tex);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, CopyTexImage2DFieldsCross) {
|
||||||
|
// Red once (executed, reverted): increment the emitted copy level; F1.CopyTexImage2D.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession(); F1Peer peer; peer.Install(); const auto tex = F1Texture();
|
||||||
|
RemoteEmitTable().GL.CopyTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, -3, 4, 11, 13, 0);
|
||||||
|
const auto& r = peer.copy;
|
||||||
|
if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*tex) ||
|
||||||
|
MGPipeHandleIsNull(r.Dst) || r.Target != GL_TEXTURE_2D || r.Level != 2 ||
|
||||||
|
r.InternalFormat != GL_RGBA8 || r.X != -3 || r.Y != 4 || r.Width != 11 || r.Height != 13 ||
|
||||||
|
r.XOffset != 0 || r.YOffset != 0 || r.SubImage != 0) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.CopyTexImage2D.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, CopyTexSubImage2DFieldsCross) {
|
||||||
|
// Red once (executed, reverted): increment the emitted copy level; F1.CopyTexSubImage2D.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession(); F1Peer peer; peer.Install(); const auto tex = F1Texture();
|
||||||
|
RemoteEmitTable().GL.CopyTexSubImage2D(GL_TEXTURE_2D, 2, 5, 7, -3, 4, 11, 13);
|
||||||
|
const auto& r = peer.copy;
|
||||||
|
if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*tex) ||
|
||||||
|
MGPipeHandleIsNull(r.Dst) || r.Target != GL_TEXTURE_2D || r.Level != 2 ||
|
||||||
|
r.InternalFormat != 0 || r.X != -3 || r.Y != 4 || r.Width != 11 || r.Height != 13 ||
|
||||||
|
r.XOffset != 5 || r.YOffset != 7 || r.SubImage != 1) ::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.CopyTexSubImage2D.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, GenerateMipmapFieldsCross) {
|
||||||
|
// Red once (executed, reverted): increment the emitted base level; F1.GenerateMipmap.fields fails.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
StartControlSession(); F1Peer peer; peer.Install(); const auto tex = F1Texture();
|
||||||
|
RemoteEmitTable().GL.GenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
const auto& r = peer.mip;
|
||||||
|
if (peer.calls != 1 || r.Res != MGPipeTextureEmitterInstance().FindTexture(*tex) ||
|
||||||
|
MGPipeHandleIsNull(r.Res) || r.Target != GL_TEXTURE_2D || r.BaseLevel != 1 || r.LevelCount != 3)
|
||||||
|
::_exit(101);
|
||||||
|
ClientSessionInstance().Stop();
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
|
||||||
|
<< "F1.GenerateMipmap.fields: " << DescribeStatus(child) << child.Log;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
|
||||||
|
TEST(RemoteF1, UnboundNamedfvRefusesByName) {
|
||||||
|
// Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
CapsPeer backend;
|
||||||
|
Srv::ServerVerbSink sink;
|
||||||
|
sink.SetBackend(&backend);
|
||||||
|
MGPClear r{};
|
||||||
|
r.Fbo = {701, 1};
|
||||||
|
r.Kind = kMGPipeClearKindColor;
|
||||||
|
r.ValueClass = kMGPipeClearValueClassFloat;
|
||||||
|
sink.OnClear(r);
|
||||||
|
});
|
||||||
|
ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferfv+UNBOUND\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, UnboundNamedivRefusesByName) {
|
||||||
|
// Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
CapsPeer backend;
|
||||||
|
Srv::ServerVerbSink sink;
|
||||||
|
sink.SetBackend(&backend);
|
||||||
|
MGPClear r{};
|
||||||
|
r.Fbo = {701, 1};
|
||||||
|
r.Kind = kMGPipeClearKindColor;
|
||||||
|
r.ValueClass = kMGPipeClearValueClassInt;
|
||||||
|
sink.OnClear(r);
|
||||||
|
});
|
||||||
|
ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferiv+UNBOUND\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, UnboundNameduivRefusesByName) {
|
||||||
|
// Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
CapsPeer backend;
|
||||||
|
Srv::ServerVerbSink sink;
|
||||||
|
sink.SetBackend(&backend);
|
||||||
|
MGPClear r{};
|
||||||
|
r.Fbo = {701, 1};
|
||||||
|
r.Kind = kMGPipeClearKindColor;
|
||||||
|
r.ValueClass = kMGPipeClearValueClassUint;
|
||||||
|
sink.OnClear(r);
|
||||||
|
});
|
||||||
|
ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferuiv+UNBOUND\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteF1, UnboundNamedfiRefusesByName) {
|
||||||
|
// Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears.
|
||||||
|
const auto child = RunInChild([] {
|
||||||
|
CapsPeer backend;
|
||||||
|
Srv::ServerVerbSink sink;
|
||||||
|
sink.SetBackend(&backend);
|
||||||
|
MGPClear r{};
|
||||||
|
r.Fbo = {701, 1};
|
||||||
|
r.Kind = kMGPipeClearKindDepthStencil;
|
||||||
|
r.ValueClass = kMGPipeClearValueClassFloat;
|
||||||
|
sink.OnClear(r);
|
||||||
|
});
|
||||||
|
ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferfi+UNBOUND\"}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ def cases():
|
|||||||
[SUITE+'BoundPackBufferOffsetReadRefusesByName'], 'RemoteClientTest'),
|
[SUITE+'BoundPackBufferOffsetReadRefusesByName'], 'RemoteClientTest'),
|
||||||
('codex12-repeat-skip', BACKEND,
|
('codex12-repeat-skip', BACKEND,
|
||||||
replace('if (draw != EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {', 'if (false) {'),
|
replace('if (draw != EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {', 'if (false) {'),
|
||||||
[SUITE+'RepeatedMakeCurrentAdoptsRepublishedCapsWithoutAPumpOrPresent'], 'RemoteClientTest'),
|
[SUITE+'ADifferentTupleMakeCurrentIsAdoptedWithoutAPumpOrPresent'], 'RemoteClientTest'),
|
||||||
('BlobMissing-optional-to-required', WIRE,
|
('BlobMissing-optional-to-required', WIRE,
|
||||||
replace('record.Blob = StageOptional(session, blobBytes, blobByteCount);',
|
replace('record.Blob = StageOptional(session, blobBytes, blobByteCount);',
|
||||||
'record.Blob = StageRequired(session, "SetDynamicState", blobBytes, blobByteCount);'),
|
'record.Blob = StageRequired(session, "SetDynamicState", blobBytes, blobByteCount);'),
|
||||||
@@ -122,13 +122,20 @@ def main():
|
|||||||
binary = ROOT / 'build-split/MobileGL/MG_Test' / ('Pipe' if target == 'PipeCatalogueTest' else 'Wire') / target
|
binary = ROOT / 'build-split/MobileGL/MG_Test' / ('Pipe' if target == 'PipeCatalogueTest' else 'Wire') / target
|
||||||
build = ['cmake', '--build', 'build-split', '-j', '24', '--target', target]
|
build = ['cmake', '--build', 'build-split', '-j', '24', '--target', target]
|
||||||
run = [str(binary), '--gtest_filter='+':'.join(names)]
|
run = [str(binary), '--gtest_filter='+':'.join(names)]
|
||||||
|
# ID-67: both replacement cases must execute green before/after this mutation.
|
||||||
|
# Suppressing client adoption only reddens the different-tuple case; an identical
|
||||||
|
# tuple correctly republishes nothing and must remain green under the perturbation.
|
||||||
|
green_names = names
|
||||||
|
if label == 'codex12-repeat-skip':
|
||||||
|
green_names = names + [SUITE+'AnIdenticalRepeatedMakeCurrentRepublishesNothing']
|
||||||
|
green_run = [str(binary), '--gtest_filter='+':'.join(green_names)]
|
||||||
print('\n=== '+label+' ===', flush=True)
|
print('\n=== '+label+' ===', flush=True)
|
||||||
try:
|
try:
|
||||||
brc, out = command(build)
|
brc, out = command(build)
|
||||||
if brc:
|
if brc:
|
||||||
raise RuntimeError('baseline build failed\n'+out)
|
raise RuntimeError('baseline build failed\n'+out)
|
||||||
rc, out = command(run)
|
rc, out = command(green_run)
|
||||||
if rc or any('[ OK ] '+name+' (' not in out for name in names):
|
if rc or any('[ OK ] '+name+' (' not in out for name in green_names):
|
||||||
raise RuntimeError('baseline not green\n'+out)
|
raise RuntimeError('baseline not green\n'+out)
|
||||||
path.write_text(mutate(original.decode()))
|
path.write_text(mutate(original.decode()))
|
||||||
brc, out = command(build)
|
brc, out = command(build)
|
||||||
@@ -146,12 +153,12 @@ def main():
|
|||||||
finally:
|
finally:
|
||||||
path.write_bytes(original)
|
path.write_bytes(original)
|
||||||
brc, out = command(build)
|
brc, out = command(build)
|
||||||
rc, out = command(run) if brc == 0 else (brc, out)
|
rc, out = command(green_run) if brc == 0 else (brc, out)
|
||||||
if rc or any('[ OK ] '+name+' (' not in out for name in names):
|
if rc or any('[ OK ] '+name+' (' not in out for name in green_names):
|
||||||
print('RESTORE-FAIL\n'+out, flush=True)
|
print('RESTORE-FAIL\n'+out, flush=True)
|
||||||
failures.append(label+' restore')
|
failures.append(label+' restore')
|
||||||
else:
|
else:
|
||||||
print('RESTORED GREEN: '+', '.join(names), flush=True)
|
print('RESTORED GREEN: '+', '.join(green_names), flush=True)
|
||||||
print('FAILED_CONTROLS='+repr(failures), flush=True)
|
print('FAILED_CONTROLS='+repr(failures), flush=True)
|
||||||
return bool(failures)
|
return bool(failures)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Summarize the indebted broad lane without turning debt into a hard gate."""
|
||||||
|
import collections
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
cases = ET.parse(sys.argv[1]).getroot().findall('.//testcase')
|
||||||
|
if not cases:
|
||||||
|
sys.exit('Census FAILED: no executed test records')
|
||||||
|
counts = collections.Counter()
|
||||||
|
for case in cases:
|
||||||
|
failure = case.find('failure')
|
||||||
|
if failure is not None:
|
||||||
|
reason = failure.get('message', '')
|
||||||
|
status = 'aborted' if 'aborted' in reason.lower() else 'failed'
|
||||||
|
elif case.find('skipped') is not None:
|
||||||
|
status = 'skipped'
|
||||||
|
else:
|
||||||
|
status = 'passed'
|
||||||
|
counts[status] += 1
|
||||||
|
print('### Broad inproc census (ID-65: recorded debt, not a gate)')
|
||||||
|
print(f'CTest exit: {sys.argv[2]}; total: {len(cases)}')
|
||||||
|
print('\n| passed | skipped | aborted | failed |\n|---:|---:|---:|---:|')
|
||||||
|
print('| ' + ' | '.join(str(counts[k]) for k in ('passed', 'skipped', 'aborted', 'failed')) + ' |')
|
||||||
@@ -56,10 +56,15 @@ run_split() { # $1 = STUB_MODE
|
|||||||
run_retrace() { # $1 = STUB_MODE
|
run_retrace() { # $1 = STUB_MODE
|
||||||
cd "${WORK}" || return 127
|
cd "${WORK}" || return 127
|
||||||
mkdir -p "${WORK}/OpenRA"
|
mkdir -p "${WORK}/OpenRA"
|
||||||
|
local rc=0
|
||||||
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" \
|
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" \
|
||||||
CTEST=ctest CONTROL_TMPDIR="${WORK}/tmp-$1" \
|
CTEST=ctest CONTROL_TMPDIR="${WORK}/tmp-$1" \
|
||||||
PULL_LIBRARY="${WORK}/pull.so" FROZEN_LIBRARY="${WORK}/frozen.so" \
|
PULL_LIBRARY="${WORK}/pull.so" FROZEN_LIBRARY="${WORK}/frozen.so" \
|
||||||
bash "${HERE}/retrace_pull_library_control.sh" OpenRA DirectGLES
|
bash "${HERE}/retrace_pull_library_control.sh" OpenRA DirectGLES || rc=$?
|
||||||
|
cmp -s "${WORK}/frozen.so" "${WORK}/split.so" || {
|
||||||
|
echo 'F6 FAILED: pull control did not restore the split library'; return 1;
|
||||||
|
}
|
||||||
|
return "${rc}"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_drop_draw() { # $1 = STUB_MODE
|
run_drop_draw() { # $1 = STUB_MODE
|
||||||
@@ -67,7 +72,7 @@ run_drop_draw() { # $1 = STUB_MODE
|
|||||||
mkdir -p "${WORK}/OpenRA"
|
mkdir -p "${WORK}/OpenRA"
|
||||||
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" \
|
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" \
|
||||||
CTEST=ctest CONTROL_TMPDIR="${WORK}/tmp-$1" \
|
CTEST=ctest CONTROL_TMPDIR="${WORK}/tmp-$1" \
|
||||||
LIBRARY_LOG="${WORK}/tmp-$1/mobilegl.log" \
|
FROZEN_LIBRARY="${WORK}/frozen.so" LIBRARY_LOG="${WORK}/tmp-$1/mobilegl.log" \
|
||||||
bash "${HERE}/retrace_drop_draw_control.sh" OpenRA DirectGLES
|
bash "${HERE}/retrace_drop_draw_control.sh" OpenRA DirectGLES
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +85,8 @@ expect PASSED "the scenarios' own diagnostic" -- run_split evidenc
|
|||||||
expect FAILED "the knob leaves the selection green" -- run_split green
|
expect FAILED "the knob leaves the selection green" -- run_split green
|
||||||
# The arming counter's half of the finding: a baseline that is already red cannot arm anything.
|
# The arming counter's half of the finding: a baseline that is already red cannot arm anything.
|
||||||
expect FAILED "the baseline is already red" -- run_split red-baseline
|
expect FAILED "the baseline is already red" -- run_split red-baseline
|
||||||
# The disarmed lane, which is a legitimate exit 0 while c1/s1/v1 are landing.
|
# P5 is complete: losing the runtime implementation must no longer disarm the gate.
|
||||||
expect PASSED "every split entry skipped (lane not armed)" -- run_split all-skipped
|
expect FAILED "every split entry skipped (implementation lost)" -- run_split all-skipped
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=== the retrace lane's pull-library control (scripts/ci/retrace_pull_library_control.sh)"
|
echo "=== the retrace lane's pull-library control (scripts/ci/retrace_pull_library_control.sh)"
|
||||||
@@ -93,7 +98,9 @@ else
|
|||||||
echo "no cc available; the retrace half of this smoke test needs one" >&2
|
echo "no cc available; the retrace half of this smoke test needs one" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
: > "${WORK}/frozen.so"
|
printf '%s\n' 'int MG_Remote_stub(void) { return 1; }' > "${WORK}/split.c"
|
||||||
|
cc -shared -fPIC -o "${WORK}/frozen.so" "${WORK}/split.c" || exit 1
|
||||||
|
cp "${WORK}/frozen.so" "${WORK}/split.so"
|
||||||
|
|
||||||
# THE FINDING, part (b): a regex matching no tests. --no-tests=error exits non-zero and the old
|
# THE FINDING, part (b): a regex matching no tests. --no-tests=error exits non-zero and the old
|
||||||
# control read that as "the pull library turned it red".
|
# control read that as "the pull library turned it red".
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import sys
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
def tally(path):
|
def tally(path, split_only=False):
|
||||||
passed = failed = skipped = 0
|
passed = failed = skipped = 0
|
||||||
for case in ET.parse(path).getroot().iter('testcase'):
|
for case in ET.parse(path).getroot().iter('testcase'):
|
||||||
|
if split_only and not case.get('name', '').startswith('DirectGLES.Split.'):
|
||||||
|
continue
|
||||||
if case.find('failure') is not None or case.find('error') is not None:
|
if case.find('failure') is not None or case.find('error') is not None:
|
||||||
failed += 1
|
failed += 1
|
||||||
elif case.find('skipped') is not None or case.get('status') in ('notrun', 'disabled'):
|
elif case.find('skipped') is not None or case.get('status') in ('notrun', 'disabled'):
|
||||||
@@ -33,15 +35,18 @@ def tally(path):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) not in (2, 3) or (len(sys.argv) == 3 and sys.argv[2] != '--require-split-ran'):
|
||||||
print("usage: junit_tally.py <junit.xml>", file=sys.stderr)
|
print("usage: junit_tally.py <junit.xml> [--require-split-ran]", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
try:
|
try:
|
||||||
passed, failed, skipped = tally(sys.argv[1])
|
passed, failed, skipped = tally(sys.argv[1], split_only=len(sys.argv) == 3)
|
||||||
except Exception as exc: # a malformed file is not "zero of everything"
|
except Exception as exc: # a malformed file is not "zero of everything"
|
||||||
print(f"junit_tally: cannot parse {sys.argv[1]}: {exc}", file=sys.stderr)
|
print(f"junit_tally: cannot parse {sys.argv[1]}: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
print(f"{passed} {failed} {skipped}")
|
print(f"{passed} {failed} {skipped}")
|
||||||
|
if len(sys.argv) == 3 and (passed == 0 or failed):
|
||||||
|
print('split baseline FAILED: no successful split runtime entries or an already-red selection', file=sys.stderr)
|
||||||
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ split, retrace, dropdraw = sys.argv[1], sys.argv[2], sys.argv[3]
|
|||||||
# other two catching the smoke test's "unrelated failure" case, and the case never flips - which
|
# other two catching the smoke test's "unrelated failure" case, and the case never flips - which
|
||||||
# is what this script measured the first time the perturbation actually applied.
|
# is what this script measured the first time the perturbation actually applied.
|
||||||
rules = [
|
rules = [
|
||||||
(split, [('LINE', 'grep -qE "${evidence}"', ('if ! ', 'elif ! ')),
|
(split, [('LINE', '"${log_helper}" assertion', ('if ! ', 'elif ! ')),
|
||||||
('SUBST', '"[A-Za-z_][A-Za-z_0-9]*"\\}\' || exit 1', '"[A-Za-z_][A-Za-z_0-9]*"\\}\' || true'),
|
('SUBST', '"[A-Za-z_][A-Za-z_0-9]*"\\}\' || exit 1', '"[A-Za-z_][A-Za-z_0-9]*"\\}\' || true'),
|
||||||
('SUBST', '"${private_evidence}" "${name}" || exit 1',
|
('SUBST', '"${private_evidence}" "${name}" || exit 1',
|
||||||
'"${private_evidence}" "${name}" || true')]),
|
'"${private_evidence}" "${name}" || true')]),
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ CTEST="${CTEST:-ctest}"
|
|||||||
CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
|
CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
|
||||||
LIBRARY_LOG="${LIBRARY_LOG:-${CASE}/${BACKEND}/output/mobilegl.log}"
|
LIBRARY_LOG="${LIBRARY_LOG:-${CASE}/${BACKEND}/output/mobilegl.log}"
|
||||||
mkdir -p "${CONTROL_TMPDIR}"
|
mkdir -p "${CONTROL_TMPDIR}"
|
||||||
|
FROZEN_LIBRARY="${FROZEN_LIBRARY:?FROZEN_LIBRARY must name the split library the replay loads}"
|
||||||
|
symbols=$(nm --defined-only "${FROZEN_LIBRARY}") || exit 1
|
||||||
|
remote_count=$(printf '%s\n' "${symbols}" | grep -ic MG_Remote || true)
|
||||||
|
echo "draw-drop control library: ${FROZEN_LIBRARY}: MG_Remote=${remote_count}"
|
||||||
|
if [ "${remote_count}" -lt 1 ]; then
|
||||||
|
echo '::error::draw-drop control requires a split library: MG_Remote=0'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
selector="^MobileGLTraceReplay\.${CASE}\.${BACKEND}$"
|
selector="^MobileGLTraceReplay\.${CASE}\.${BACKEND}$"
|
||||||
|
|
||||||
@@ -66,6 +74,8 @@ restore_good_output() {
|
|||||||
echo "restored the verified run's output over the control's"
|
echo "restored the verified run's output over the control's"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
trap restore_good_output EXIT
|
||||||
|
trap 'exit 130' INT TERM
|
||||||
|
|
||||||
matched=$("${CTEST}" -N -R "${selector}" | grep -cE '^ *Test *#[0-9]+:')
|
matched=$("${CTEST}" -N -R "${selector}" | grep -cE '^ *Test *#[0-9]+:')
|
||||||
if [ "${matched}" -lt 1 ]; then
|
if [ "${matched}" -lt 1 ]; then
|
||||||
|
|||||||
@@ -69,6 +69,18 @@ restore_good_output() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Restore the exact split library on success, failure, and interruption. The following
|
||||||
|
# draw-drop control uses this same frozen path.
|
||||||
|
saved_library=$(mktemp "${CONTROL_TMPDIR}/split-library.XXXXXX") || exit 1
|
||||||
|
cp -p "${FROZEN_LIBRARY}" "${saved_library}" || exit 1
|
||||||
|
restore_control() {
|
||||||
|
cp -p "${saved_library}" "${FROZEN_LIBRARY}"
|
||||||
|
rm -f "${saved_library}"
|
||||||
|
restore_good_output
|
||||||
|
}
|
||||||
|
trap restore_control EXIT
|
||||||
|
trap 'exit 130' INT TERM
|
||||||
|
|
||||||
# HOLE 1: COUNT THE SELECTION FIRST. `--no-tests=error` turns an empty selection into a non-zero
|
# HOLE 1: COUNT THE SELECTION FIRST. `--no-tests=error` turns an empty selection into a non-zero
|
||||||
# exit, which is indistinguishable from a working control unless the selection is counted.
|
# exit, which is indistinguishable from a working control unless the selection is counted.
|
||||||
matched=$("${CTEST}" -N -R "${selector}" | grep -cE '^ *Test *#[0-9]+:')
|
matched=$("${CTEST}" -N -R "${selector}" | grep -cE '^ *Test *#[0-9]+:')
|
||||||
@@ -82,8 +94,11 @@ fi
|
|||||||
# in. It defines no MG_Remote symbol, so ConfigLoader has no transport parser and
|
# in. It defines no MG_Remote symbol, so ConfigLoader has no transport parser and
|
||||||
# MOBILEGL_TRANSPORT=inproc is accepted and ignored - the exact shape of "the split lane ran
|
# MOBILEGL_TRANSPORT=inproc is accepted and ignored - the exact shape of "the split lane ran
|
||||||
# monolith".
|
# monolith".
|
||||||
cp "${PULL_LIBRARY}" "${FROZEN_LIBRARY}"
|
cp "${PULL_LIBRARY}" "${FROZEN_LIBRARY}" || exit 1
|
||||||
if nm --defined-only "${FROZEN_LIBRARY}" | grep -q -i MG_Remote; then
|
symbols=$(nm --defined-only "${FROZEN_LIBRARY}") || exit 1
|
||||||
|
remote_count=$(printf '%s\n' "${symbols}" | grep -ic MG_Remote || true)
|
||||||
|
echo "pull control library: ${FROZEN_LIBRARY}: MG_Remote=${remote_count}"
|
||||||
|
if [ "${remote_count}" -ne 0 ]; then
|
||||||
restore_good_output
|
restore_good_output
|
||||||
echo "::error::the control's own library defines MG_Remote symbols, so it is not a pull build and this control would prove nothing"
|
echo "::error::the control's own library defines MG_Remote symbols, so it is not a pull build and this control would prove nothing"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
|
|||||||
mkdir -p "${CONTROL_TMPDIR}"
|
mkdir -p "${CONTROL_TMPDIR}"
|
||||||
|
|
||||||
junit="${CONTROL_TMPDIR}/isplit.xml"
|
junit="${CONTROL_TMPDIR}/isplit.xml"
|
||||||
|
rm -f "${junit}"
|
||||||
log_helper="$(dirname "$0")/../../MobileGL/MG_IntegrationTest/Harness/split_log_paths.py"
|
log_helper="$(dirname "$0")/../../MobileGL/MG_IntegrationTest/Harness/split_log_paths.py"
|
||||||
|
|
||||||
# Check ownership even while the runtime lane is disarmed and will skip.
|
# Check ownership even while the runtime lane is disarmed and will skip.
|
||||||
@@ -87,14 +88,14 @@ echo "split entries - passed: ${baseline_passed}, failed: ${baseline_failed}, sk
|
|||||||
# A RED BASELINE DISARMS THE CONTROLS RATHER THAN ARMING THEM (review finding 8, second half).
|
# A RED BASELINE DISARMS THE CONTROLS RATHER THAN ARMING THEM (review finding 8, second half).
|
||||||
# `|| true` plus a "not skipped" counter used to treat a case that ran and FAILED as evidence the
|
# `|| true` plus a "not skipped" counter used to treat a case that ran and FAILED as evidence the
|
||||||
# lane was live. Turning an already-red entry red is not a measurement.
|
# lane was live. Turning an already-red entry red is not a measurement.
|
||||||
if [ "${baseline_failed}" -gt 0 ]; then
|
if [ "${baseline_failed}" -gt 0 ] || [ "${baseline_rc}" -ne 0 ]; then
|
||||||
echo "::error::${baseline_failed} DirectGLES.Split. entries are ALREADY RED with both knobs at their defaults, so neither negative control below can attribute its red to the knob it turns. Fix the lane first; a control measured against a red baseline is not a control. (This used to be swallowed by an unconditional '|| true' and counted as 'the lane is armed'.)"
|
echo "::error::${baseline_failed} DirectGLES.Split. entries are ALREADY RED with both knobs at their defaults, so neither negative control below can attribute its red to the knob it turns. Fix the lane first; a control measured against a red baseline is not a control. (This used to be swallowed by an unconditional '|| true' and counted as 'the lane is armed'.)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${baseline_passed}" -lt 1 ]; then
|
if [ "${baseline_passed}" -lt 1 ]; then
|
||||||
echo "::warning::every DirectGLES.Split. entry SKIPPED, so neither negative control can fire. The arming condition is a runtime fact - MG_Config::Transport, ClientSession::Active() and ImplementedVerbCount(), read by Harness/SplitRuntimePeek - and it becomes true on the commit that lands the last of c1/s1/v1. This step becomes a gate then, with no edit; it is not a green that asserted anything today."
|
echo "::error::split baseline FAILED: every DirectGLES.Split. entry SKIPPED; the split implementation did not execute"
|
||||||
exit 0
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- the controls ---------------------------------------------------------------------------
|
# ---- the controls ---------------------------------------------------------------------------
|
||||||
@@ -145,7 +146,7 @@ run_control() {
|
|||||||
if [ "${evidence}" = "private-barrier-fatal" ]; then
|
if [ "${evidence}" = "private-barrier-fatal" ]; then
|
||||||
python3 "${log_helper}" evidence "${manifest}" "${filter}" \
|
python3 "${log_helper}" evidence "${manifest}" "${filter}" \
|
||||||
'Fatal\{BarrierViolation, "[A-Za-z_][A-Za-z_0-9]*"\}' || exit 1
|
'Fatal\{BarrierViolation, "[A-Za-z_][A-Za-z_0-9]*"\}' || exit 1
|
||||||
elif ! tr -s '[:space:]' ' ' < "${out}" | grep -qE "${evidence}"; then
|
elif ! python3 "${log_helper}" assertion "${manifest}" "${filter}" "${result}" "${evidence}"; then
|
||||||
echo "::error::${name} FAILED: red lacks its persistent-map push diagnostic. Required: ${evidence}"
|
echo "::error::${name} FAILED: red lacks its persistent-map push diagnostic. Required: ${evidence}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -179,8 +180,10 @@ run_control "negative control E1 (MOBILEGL_IPC_VERB_BARRIER=0)" \
|
|||||||
# * the LIBRARY's own line in the entry's private file, saying the push was disabled by this
|
# * the LIBRARY's own line in the entry's private file, saying the push was disabled by this
|
||||||
# knob. It did not exist until ID-65 assigned it (joint-v1.md 3), which is why this control
|
# knob. It did not exist until ID-65 assigned it (joint-v1.md 3), which is why this control
|
||||||
# used to rest on the pixels alone.
|
# used to rest on the pixels alone.
|
||||||
|
# TheMapLandsInTheArmItsLaneDeclares skips by design outside PersistentMapArm.
|
||||||
|
# Select only the pixel cases; a pre-flight skip in either remains a hard failure.
|
||||||
run_control "negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0)" \
|
run_control "negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0)" \
|
||||||
'DirectGLES\.Split\.(SmallRing\.)?PersistentCoherentMapScenario' \
|
'DirectGLES\.Split\.(SmallRing\.)?PersistentCoherentMapScenario\.(TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw|AWriteAfterAFrameBoundaryReachesTheNextFramesDraw)$' \
|
||||||
"the SECOND write through the same mapping, announced by nothing|frame 1's write through the SAME mapping, after a Present" \
|
"the SECOND write through the same mapping, announced by nothing|frame 1's write through the SAME mapping, after a Present" \
|
||||||
'MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0' \
|
'MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0' \
|
||||||
MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0
|
MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,7 @@ trap 'rm -rf "${WORK}"' EXIT
|
|||||||
cp "${HERE}/testdata/stub_ctest.sh" "${WORK}/ctest"
|
cp "${HERE}/testdata/stub_ctest.sh" "${WORK}/ctest"
|
||||||
chmod +x "${WORK}/ctest"
|
chmod +x "${WORK}/ctest"
|
||||||
passes=0
|
passes=0
|
||||||
for mode in missing-fatal stdout-fatal stale-fatal evidence e3-unrelated e3-no-private skipped-selection notrun-selection missing-selection partial-fatal wrong-fatal; do
|
for mode in missing-fatal stdout-fatal stale-fatal evidence e3-unrelated e3-no-private skipped-selection e3-skipped-selection notrun-selection missing-selection partial-fatal wrong-fatal; do
|
||||||
mkdir -p "${WORK}/${mode}"
|
mkdir -p "${WORK}/${mode}"
|
||||||
rc=0
|
rc=0
|
||||||
STUB_MODE="${mode}" CTEST="${WORK}/ctest" CONTROL_TMPDIR="${WORK}/${mode}" \
|
STUB_MODE="${mode}" CTEST="${WORK}/ctest" CONTROL_TMPDIR="${WORK}/${mode}" \
|
||||||
@@ -25,6 +25,7 @@ for mode in missing-fatal stdout-fatal stale-fatal evidence e3-unrelated e3-no-p
|
|||||||
case "${mode}" in
|
case "${mode}" in
|
||||||
e3-no-private) message='no selected private log carries /MGPipe: persistent-map push disabled' ;;
|
e3-no-private) message='no selected private log carries /MGPipe: persistent-map push disabled' ;;
|
||||||
skipped-selection) message='SplitLogPaths FAILED: E1 control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
|
skipped-selection) message='SplitLogPaths FAILED: E1 control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
|
||||||
|
e3-skipped-selection) message='SplitLogPaths FAILED: E3(a) control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
|
||||||
notrun-selection|missing-selection) message='SplitLogPaths FAILED: E1 control: 1 selected entries did not run' ;;
|
notrun-selection|missing-selection) message='SplitLogPaths FAILED: E1 control: 1 selected entries did not run' ;;
|
||||||
esac
|
esac
|
||||||
if [[ "${mode}" = *-selection ]]; then
|
if [[ "${mode}" = *-selection ]]; then
|
||||||
|
|||||||
Vendored
+19
-2
@@ -24,7 +24,7 @@
|
|||||||
# never says the push was disabled (the half ID-65 added)
|
# never says the push was disabled (the half ID-65 added)
|
||||||
# green baseline green; the control's own run PASSES (the knob is not load-bearing)
|
# green baseline green; the control's own run PASSES (the knob is not load-bearing)
|
||||||
# red-baseline the baseline itself has a failed entry
|
# red-baseline the baseline itself has a failed entry
|
||||||
# all-skipped the baseline is entirely skipped (the disarmed lane, a legitimate exit 0)
|
# all-skipped the baseline is entirely skipped (lost implementation, a hard failure)
|
||||||
# retrace-noselect `ctest -N` matches nothing; the run exits 8 the way --no-tests=error does
|
# retrace-noselect `ctest -N` matches nothing; the run exits 8 the way --no-tests=error does
|
||||||
# retrace-unrelated one match; the run fails without naming the transport
|
# retrace-unrelated one match; the run fails without naming the transport
|
||||||
# retrace-evidence one match; the run fails with run_trace_case.cmake's own sentence
|
# retrace-evidence one match; the run fails with run_trace_case.cmake's own sentence
|
||||||
@@ -76,10 +76,20 @@ write_junit() {
|
|||||||
entry=DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels
|
entry=DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels
|
||||||
[ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" != 0 ] || entry=DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw
|
[ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" != 0 ] || entry=DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw
|
||||||
body="<testcase name=\"${entry}\" status=\"fail\"><failure message=\"control red\"/></testcase>"
|
body="<testcase name=\"${entry}\" status=\"fail\"><failure message=\"control red\"/></testcase>"
|
||||||
|
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
|
||||||
|
case "${mode}" in
|
||||||
|
evidence|e3-no-private)
|
||||||
|
body="<testcase name=\"${entry}\" status=\"fail\"><failure/><system-out>the SECOND write through the same mapping, announced by nothing</system-out></testcase>" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
if [ "${mode}" = partial-fatal ] && [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
|
if [ "${mode}" = partial-fatal ] && [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
|
||||||
body="${body}<testcase name=\"DirectGLES.Split.TriangleScenario.SecondEntry\" status=\"fail\"><failure/></testcase>"
|
body="${body}<testcase name=\"DirectGLES.Split.TriangleScenario.SecondEntry\" status=\"fail\"><failure/></testcase>"
|
||||||
fi
|
fi
|
||||||
case "${mode}" in
|
case "${mode}" in
|
||||||
|
e3-skipped-selection)
|
||||||
|
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
|
||||||
|
body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>"
|
||||||
|
fi ;;
|
||||||
skipped-selection) body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>" ;;
|
skipped-selection) body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>" ;;
|
||||||
notrun-selection) body="<testcase name=\"${entry}\" status=\"notrun\"/>" ;;
|
notrun-selection) body="<testcase name=\"${entry}\" status=\"notrun\"/>" ;;
|
||||||
missing-selection) body='' ;;
|
missing-selection) body='' ;;
|
||||||
@@ -130,7 +140,7 @@ fi
|
|||||||
# The control's own run.
|
# The control's own run.
|
||||||
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
|
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
|
||||||
case "${mode}" in
|
case "${mode}" in
|
||||||
evidence|e3-unrelated|e3-no-private|skipped-selection|notrun-selection|missing-selection|partial-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}" ;;
|
evidence|e3-unrelated|e3-no-private|skipped-selection|e3-skipped-selection|notrun-selection|missing-selection|partial-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}" ;;
|
||||||
wrong-fatal) echo 'Fatal{ReplyMissing, "DrawVbo"}' > "${log}" ;;
|
wrong-fatal) echo 'Fatal{ReplyMissing, "DrawVbo"}' > "${log}" ;;
|
||||||
missing-fatal) echo "library setup only; no fatal" > "${log}" ;;
|
missing-fatal) echo "library setup only; no fatal" > "${log}" ;;
|
||||||
stdout-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' ;;
|
stdout-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' ;;
|
||||||
@@ -144,6 +154,13 @@ if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ] && [ "${mode}" = evidence ]
|
|||||||
> "${CONTROL_TMPDIR}/pmap.log"
|
> "${CONTROL_TMPDIR}/pmap.log"
|
||||||
fi
|
fi
|
||||||
case "${mode}" in
|
case "${mode}" in
|
||||||
|
e3-skipped-selection)
|
||||||
|
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
|
||||||
|
echo 'selected E3 entry ... ***Skipped'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 8
|
||||||
|
;;
|
||||||
skipped-selection|notrun-selection|missing-selection)
|
skipped-selection|notrun-selection|missing-selection)
|
||||||
echo '1/1 Test #1: selected entry ... ***Skipped'
|
echo '1/1 Test #1: selected entry ... ***Skipped'
|
||||||
echo '100% tests passed, 0 tests failed out of 1'
|
echo '100% tests passed, 0 tests failed out of 1'
|
||||||
|
|||||||
@@ -532,7 +532,8 @@ if [ "${1:-}" = "--self-test" ]; then
|
|||||||
printf '%s %s\n' \
|
printf '%s %s\n' \
|
||||||
"0000000000000000000000000000000000000000000000000000000000000000" "$target" \
|
"0000000000000000000000000000000000000000000000000000000000000000" "$target" \
|
||||||
>> "$WORK_DIR/pinprec.sha"
|
>> "$WORK_DIR/pinprec.sha"
|
||||||
apply_pinned_shas "$WORK_DIR/pinprec.sha" || exit 2
|
# Drive production extraction on a real historical ref whose body differs from the pin.
|
||||||
|
extract_baseline ff2994d9 pinprec || exit 2
|
||||||
got=$(awk -v n="$target" '$2 == n { print $1 }' "$WORK_DIR/pinprec.sha")
|
got=$(awk -v n="$target" '$2 == n { print $1 }' "$WORK_DIR/pinprec.sha")
|
||||||
if [ "$got" != "$pinned" ]; then
|
if [ "$got" != "$pinned" ]; then
|
||||||
say "PIN CONTROL FAILED: a baseline that carried a DIFFERENT sha for $target came out as"
|
say "PIN CONTROL FAILED: a baseline that carried a DIFFERENT sha for $target came out as"
|
||||||
@@ -552,7 +553,7 @@ if [ "${1:-}" = "--self-test" ]; then
|
|||||||
python3 "$PY" extract "$WORK_DIR/pinperturbed.cpp" "$ALL_FUNCTIONS" \
|
python3 "$PY" extract "$WORK_DIR/pinperturbed.cpp" "$ALL_FUNCTIONS" \
|
||||||
> "$WORK_DIR/pinperturbed.sha" || exit 2
|
> "$WORK_DIR/pinperturbed.sha" || exit 2
|
||||||
cp -f "$WORK_DIR/pristine.sha" "$WORK_DIR/pinbase.sha" || exit 2
|
cp -f "$WORK_DIR/pristine.sha" "$WORK_DIR/pinbase.sha" || exit 2
|
||||||
apply_pinned_shas "$WORK_DIR/pinbase.sha" || exit 2
|
extract_baseline HEAD pinbase || exit 2
|
||||||
if compare_lists "$WORK_DIR/pinbase.sha" "$WORK_DIR/pinperturbed.sha" \
|
if compare_lists "$WORK_DIR/pinbase.sha" "$WORK_DIR/pinperturbed.sha" \
|
||||||
"PIN($PINNED_BASELINE_REF)" "one-token-perturbed" 2> "$WORK_DIR/pinperturbed.err"; then
|
"PIN($PINNED_BASELINE_REF)" "one-token-perturbed" 2> "$WORK_DIR/pinperturbed.err"; then
|
||||||
say "NEGATIVE CONTROL DID NOT TRIP: one token was inserted into $target's body and the"
|
say "NEGATIVE CONTROL DID NOT TRIP: one token was inserted into $target's body and the"
|
||||||
|
|||||||
@@ -766,7 +766,8 @@ if [ "${1:-}" = "--self-test" ]; then
|
|||||||
printf '%s %s\n' \
|
printf '%s %s\n' \
|
||||||
"0000000000000000000000000000000000000000000000000000000000000000" "$target" \
|
"0000000000000000000000000000000000000000000000000000000000000000" "$target" \
|
||||||
>> "$WORK_DIR/pinprec.sha"
|
>> "$WORK_DIR/pinprec.sha"
|
||||||
apply_pinned_shas "$WORK_DIR/pinprec.sha" || exit 2
|
# Drive production extraction on a real historical ref whose body differs from the pin.
|
||||||
|
extract_baseline 37da3c3a pinprec || exit 2
|
||||||
got=$(awk -v n="$target" '$2 == n { print $1 }' "$WORK_DIR/pinprec.sha")
|
got=$(awk -v n="$target" '$2 == n { print $1 }' "$WORK_DIR/pinprec.sha")
|
||||||
if [ "$got" != "$pinned" ]; then
|
if [ "$got" != "$pinned" ]; then
|
||||||
say "PIN CONTROL FAILED: a baseline that carried a DIFFERENT sha for $target came out as"
|
say "PIN CONTROL FAILED: a baseline that carried a DIFFERENT sha for $target came out as"
|
||||||
@@ -789,7 +790,7 @@ if [ "${1:-}" = "--self-test" ]; then
|
|||||||
"$WORK_DIR/pinperturbed/$(blob_name "$targetSource")" token || exit 2
|
"$WORK_DIR/pinperturbed/$(blob_name "$targetSource")" token || exit 2
|
||||||
python3 "$PY" extract "$WORK_DIR/pinperturbed.spec" > "$WORK_DIR/pinperturbed.sha" || exit 2
|
python3 "$PY" extract "$WORK_DIR/pinperturbed.spec" > "$WORK_DIR/pinperturbed.sha" || exit 2
|
||||||
cp -f "$WORK_DIR/pristine.sha" "$WORK_DIR/pinbase.sha" || exit 2
|
cp -f "$WORK_DIR/pristine.sha" "$WORK_DIR/pinbase.sha" || exit 2
|
||||||
apply_pinned_shas "$WORK_DIR/pinbase.sha" || exit 2
|
extract_baseline HEAD pinbase || exit 2
|
||||||
if compare_lists "$WORK_DIR/pinbase.sha" "$WORK_DIR/pinperturbed.sha" \
|
if compare_lists "$WORK_DIR/pinbase.sha" "$WORK_DIR/pinperturbed.sha" \
|
||||||
"PIN($PINNED_BASELINE_REF)" "one-token-perturbed" 2> "$WORK_DIR/pinperturbed.err"; then
|
"PIN($PINNED_BASELINE_REF)" "one-token-perturbed" 2> "$WORK_DIR/pinperturbed.err"; then
|
||||||
say "NEGATIVE CONTROL DID NOT TRIP: one token was inserted into $target's body and the"
|
say "NEGATIVE CONTROL DID NOT TRIP: one token was inserted into $target's body and the"
|
||||||
|
|||||||
Reference in New Issue
Block a user