Compare commits

..
2 Commits
Author SHA1 Message Date
swung0x48 9eae98581f [Test] (SelfTest, POST): probe upload/draw ordering in persistently mapped vertex arenas - native GLES controls distinguish mapped destination corruption from staging and synchronization failures
Add the persistent buffer ordering probe to the GLES POST's known-driver-bug inventory. Queue updates and draws into independent FBOs before reading them back, covering SubData and copies from both coherent persistent and ordinary staging buffers. Retry fresh mapped allocations for intermittent corruption.

Require passing never-mapped and fully serialized controls before reporting a finding. Include finish-before, map-then-unmap and barrier diagnostics, preserve caller GL state, and treat setup, allocation and GL errors as inconclusive. This adds detection and reporting only; no rendering workaround is enabled.

Validation: all 46 DriverBugProbesTest tests pass, including 11 new ordering, control, collector and cleanup cases. The Android API 26 / NDK 27 native probe detects all three upload paths on Mali-G1-Ultra r54p1 with clean controls and no GL errors. llvmpipe reports no finding after 240 mapped FBO checks per upload path.
2026-09-07 00:44:08 -04:00
swung0x48 d7655247f7 [Fix, Test] (DirectGLES, Integration): rebind VAOs when an adopted buffer is respecified - the immediate retire path forgot the buffer-id generation, so cached vertex and element bindings kept the deleted store
Advance the buffer-id generation when Ops_Respecify retires immutable storage on the context thread, matching the existing adoption and deferred-retirement paths.

Add pixel regression coverage for unchanged VBO/IBO attachments across same-size redefinition, growth and shrinkage, including bound and unbound VAOs sharing a vertex arena and an index arena returning to shadow storage.
2026-09-06 22:55:06 -04:00
117 changed files with 1219 additions and 20190 deletions
-81
View File
@@ -295,33 +295,6 @@ jobs:
path: /tmp/core.*
if-no-files-found: ignore
# MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and
# flatc is deliberately absent from the default build graph (a codegen step in
# the graph is how the earlier branch ended up cross-compiling an arm64 flatc
# and trying to run it on the host). This job is what keeps the committed
# header honest: build the pinned flatc, regenerate, and fail on any diff.
# It needs no MobileGL build, so it does not depend on build-linux.
flatc-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Check out the FlatBuffers submodule only
# Just this one: the schema check has nothing to do with glslang,
# SPIRV-Cross or the trace fixtures.
run: git submodule update --init 3rdparty/flatbuffers
- name: Regenerate protocol_generated.h
run: python3 scripts/gen_protocol.py --build-dir "${{ runner.temp }}/flatc-build"
- name: Fail if the committed header is stale
run: git diff --exit-code -- MobileGL/MG_Remote/Protocol/generated/protocol_generated.h
benchmark:
runs-on: ubuntu-latest
needs: build-linux
@@ -805,57 +778,3 @@ jobs:
)
echo "Deleted ${deleted} intermediate Linux artifact(s); retained ${retained} failed-retrace fixture(s)."
pipe-gates:
name: MGPipe generators and hygiene gates
runs-on: ubuntu-latest
# Deliberately independent of build-linux: these are source-level gates, they take
# seconds, and a broken build must not hide a drifted interface.
steps:
- name: Checkout repo
uses: actions/checkout@v6
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
# keeps the two interface tables, the wire records, the verify comparators, the
# PipeInputs field ids, the read-inventory coverage and the render-state member list
# from drifting apart from the catalogue. The generated files are committed
# deliberately: the build must not depend on python.
- name: Regenerate the MGPipe interface (G1-G7)
run: |
python3 scripts/gen_pipe.py
git diff --exit-code -- MobileGL/MG_Pipe/generated
# Per-draw fprintf/printf instrumentation has repeatedly been committed by accident,
# once inside a mutex critical section. Nothing under these two trees prints to a
# stdio stream today - MGLOG_D compiles out in INFO builds and is the only channel
# they are allowed to use - so this gate starts with no exceptions, and any addition
# to it needs a reason in the pull request rather than a quiet whitelist entry. The
# alternation names every stdio spelling, not just the two that were committed:
# fprintf to either stream, printf, puts, and the iostream pair.
- name: No stdio instrumentation in MG_Backend or MG_State
run: |
if grep -rnE 'fprintf[[:space:]]*\((stderr|stdout)|(^|[^[:alnum:]_>.])printf[[:space:]]*\(|(^|[^[:alnum:]_>.:])puts[[:space:]]*\(|std::(cout|cerr)' \
MobileGL/MG_Backend MobileGL/MG_State; then
echo "::error::stdio instrumentation found; use MGLOG_D (compiled out in INFO builds)"
exit 1
fi
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
# Informational: the frontend mutation surface an MGPipe aggregate generation has to
# cover. It becomes a gate in P1, when the mapping file exists to diff against.
- name: MGPipe dirty-surface report
run: python3 scripts/gen_pipe_dirty_surface.py --summary
# Warning only for now: the disaggregation documents are still being written, and a
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
# --strict when the documents settle.
- name: Documentation citation lint
run: |
shopt -s nullglob
documents=(docs/Disaggregated/*.md)
if [ ${#documents[@]} -eq 0 ]; then
echo "no disaggregation documents to check"
exit 0
fi
python3 scripts/check_doc_citations.py "${documents[@]}" || true
-3
View File
@@ -34,6 +34,3 @@
[submodule "include/ska"]
path = include/ska
url = https://github.com/MobileGL-Dev/flat_hash_map.git
[submodule "3rdparty/flatbuffers"]
path = 3rdparty/flatbuffers
url = https://github.com/google/flatbuffers.git
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
+1 -108
View File
@@ -14,14 +14,6 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
# That emptiness is one of the two byte-level equalities the plan's validation
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
@@ -246,8 +238,6 @@ set(SOURCE_FILES
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
MobileGL/MG_Util/Metrics/PipeStats.cpp
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
@@ -323,6 +313,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
@@ -427,49 +418,6 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
)
# ---------------------------------------------------------------------------
# MG_Remote (disaggregated transport). Everything below is gated: with the
# option OFF not one file here is compiled and no include path is added.
# ---------------------------------------------------------------------------
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
# a checkout without the submodule must configure and build, just without the
# disaggregated shape, rather than fail with a missing-header error a hundred
# lines later. Note this only checks for the RUNTIME headers - flatc is never
# built here (see scripts/gen_protocol.py).
if (MOBILEGL_BUILD_DISAGGREGATED AND
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
message(WARNING
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
"disaggregated shape for this configure; the cached ON takes effect once the "
"submodule is present.")
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
# made the plain re-configure after `git submodule update` stay OFF with no message at
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
# was, so the next configure - with the submodule there - honours it.
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
endif()
if (MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Remote/Transport/Ring.cpp
MobileGL/MG_Remote/Transport/Doorbell.cpp
MobileGL/MG_Remote/Transport/ShmSegment.cpp
# Both platform halves are listed unconditionally and each is empty on
# the other OS, so neither can rot behind an `if (WIN32)` nobody
# configures.
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
MobileGL/MG_Remote/Transport/FdPassing.cpp
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
)
endif()
if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
@@ -520,19 +468,11 @@ set(MOBILEGL_COMPILE_DEF
-DASIO_NO_DEPRECATED
)
if (MOBILEGL_BUILD_DISAGGREGATED)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
endif()
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
set(MOBILEGL_INCLUDE_DIR
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/MobileGL
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
# line above too; this entry lets the client, the backends and MG_Remote spell them
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
${spirv-tools_SOURCE_DIR}
${spirv-tools_SOURCE_DIR}/include
${spirv-tools_BINARY_DIR}
@@ -543,13 +483,6 @@ set(MOBILEGL_INCLUDE_DIR
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# Header-only runtime: an include path, no add_subdirectory, no link
# target, and above all no flatc in the build graph. protocol_generated.h
# is committed and regenerated by scripts/gen_protocol.py.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
endif()
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
@@ -766,43 +699,3 @@ endif()
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# ---------------------------------------------------------------------------
# P0 spike A: the Android delivery chain for a second native executable.
#
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
# so a second executable has to be built with an .so name and exec'd out of
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
# chain end to end: it is packaged like a library, exec'd from the app's own
# untrusted_app process, and writes a marker the parent reads back.
#
# Off by default and ANDROID-only, so no shipping configuration builds it. The
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
# ---------------------------------------------------------------------------
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
add_executable(MobileGLServer
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
# An executable that is named like a shared library still has to be a real
# PIE executable: Android has refused non-PIE executables since API 21, and
# the name alone does not change what the loader demands of the file.
set_target_properties(MobileGLServer PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServer"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLServer PRIVATE -fPIE)
target_link_options(MobileGLServer PRIVATE -pie)
# AGP packages what the external native build drops into the per-ABI output
# directory, and it selects by the .so extension. CMake puts executables in
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
# the library directory when the generator gave us one.
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
-40
View File
@@ -316,46 +316,6 @@ namespace MobileGL::MG_Config {
// immune to the probe's verdict moving), and ForceOff is the negative control that
// replays the driver's silence.
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
// 0 - the default and the only shipped value until the migration lands - is "pull
// everything", i.e. exactly today's behaviour. One bit of it also turns OFF
// client-side content addressing of CSOs, which is the negative control the CSO
// design is measured against. Accepts decimal or 0x-prefixed hex.
Uint64 PipePush = 0;
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
// against a snapshot taken from GLContext the old way, printing the first field
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
// the semantic gate that replaces byte identity, and it catches the dangerous
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
Bool PipeVerify = false;
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
// alive so the first handle waves have a real old-versus-new arm to be compared
// against. ON by default for the whole migration window, deleted with the pull
// path itself.
Bool PipeLegacyMemos = true;
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
// holds a complete CPU shadow, so this cache buys latency, never correctness.
Uint32 PipeTexelRetainMb = 0;
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
// the server without shipping index bytes per draw. Over budget it degrades to
// per-draw staging, counted separately in the stats.
Uint32 PipeIndexMirrorMb = 64;
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
// steady-state cadence; the device retrace harness never reaches the teardown dump
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
// needs its numbers at all sets this low enough to land at least one window.
Uint32 PipeStatsPeriod = 120;
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
// Empty (the default) means no dump; the per-120-frame summary line still goes to
// the log whenever PipeStats is on, so a device run needs no writable path.
String PipeStatsFile;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
-47
View File
@@ -159,38 +159,6 @@ namespace MobileGL::MG_ConfigLoader {
return static_cast<Uint32>(parsedValue);
}
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return defaultValue;
}
const String& value = it->second;
const char* text = value.c_str();
int base = 10;
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
text += 2;
base = 16;
}
char* parseEnd = nullptr;
errno = 0;
const bool negative = value.find('-') != String::npos;
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
return defaultValue;
}
return static_cast<Uint64>(parsedValue);
}
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
@@ -239,21 +207,6 @@ namespace MobileGL::MG_ConfigLoader {
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
// that starts with MOBILEGL_ is visible to these queries by construction.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
// truthy check: unset must keep the memos, and only an explicitly falsy value may
// drop them.
features.PipeLegacyMemos =
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
}
inline void InitBackendType() {
-10
View File
@@ -17,7 +17,6 @@
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
@@ -43,11 +42,6 @@ namespace MobileGL {
if (logLifecycle) {
MGLOG_I("MobileGL closing...");
}
// Before any subsystem the counters name goes away, and before the last frame's
// numbers can be lost: emits the final summary line and, when
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
// off, and idempotent.
MG_Util::PipeStats::Shutdown();
// First, before anything else is torn down. In-flight compile/link jobs own
// their own inputs and are safe against everything below EXCEPT glslang's
// process globals and the TShader/TProgram objects hanging off pGLContext,
@@ -108,10 +102,6 @@ namespace MobileGL {
MGLOG_I("Initializing MobileGL...");
MG_ConfigLoader::Init();
MGLOG_I("Config loaded");
// Immediately after the config load and before anything can count: the MGPipe
// boundary counters latch their enable flag here, so every counting site in the
// two backends is a load of an already-settled global for the rest of the run.
MG_Util::PipeStats::Init();
MG_State::Init();
MGLOG_D("MG_State initialized");
MG_Backend::Init();
+2 -29
View File
@@ -192,23 +192,9 @@ namespace MobileGL {
void (*MemoryBarrierByRegion)(GLbitfield barriers);
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
GLenum access, GLenum format);
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
// 64-bit and float/double widths are derived there from the same answer, which is why
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
// case for at all.
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
// size - all of which are frontend link artifacts on ProgramObject, and
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
// one, or a SPIR-V module), in a namespace the application never sees.
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
// a backend query: it describes the program the application wrote, in the
// application's namespace, which neither backend program is in. It is answered
@@ -378,19 +364,6 @@ namespace MobileGL {
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
// axis. These six, with the invocations limit above, are the only indexed limits a
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
// answers, like the invocations limit: GL_Getter and the compile environment floor
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
// MaxClipDistances' does.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
@@ -1255,6 +1255,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi;
@@ -1415,13 +1417,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
// The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
}
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
+129 -84
View File
@@ -28,7 +28,6 @@
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/Metrics/BufferMetrics.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
#include <Config.h>
#include <atomic>
@@ -1413,21 +1412,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) {
const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
if (MG_Util::PipeStats::Enabled()) {
// Two accessor calls whichever way the shutter goes; only the unit WALK is
// gated, and that walk reads no GLContext accessor of its own.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2);
}
if (g_observedUnitBindingsContextId == contextId && g_observedUnitBindingsMaxUnit == maxTouchedUnit &&
g_observedUnitBindingsGeneration == bindGeneration) {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/true);
}
return g_unitBindingsEpoch;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/false);
}
if (g_observedUnitBindingsContextId != contextId || g_observedUnitBindingsMaxUnit != maxTouchedUnit ||
!UnitBindingsUnchanged(maxTouchedUnit, g_observedUnitBindings)) {
CaptureUnitBindings(maxTouchedUnit, g_observedUnitBindings);
@@ -1517,10 +1505,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
if (MG_Util::PipeStats::Enabled()) {
// The three reads above; CurrentUnitBindingsEpoch counts its own two.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
return keys;
}
@@ -1550,11 +1534,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
PairingsIntact(g_unitTextureSyncList)) {
if (MG_Util::PipeStats::Enabled()) {
// Gate 2 of section 2.3.1. The served path walks the memoised entries
// and reads no GLContext accessor at all.
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true);
}
for (const auto& entry : g_unitTextureSyncList) {
// Aggregate gate == the conjunction of the three callees' own
// early-outs (see IsDrawSyncClean); skipping on true is
@@ -1567,13 +1546,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
entry.backend->SyncMipmapsToBackend(*entry.slot);
}
} else {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false);
// One GetTextureUnitObject per touched unit in the rebuild walk below.
MG_Util::PipeStats::AddCalls(
MG_Util::PipeStats::CallClass::AccessorCalls,
maxTouchedUnit >= 0 ? static_cast<Uint64>(maxTouchedUnit) + 1u : 0u);
}
g_unitTextureSyncListValid = false;
g_unitTextureSyncList.clear();
for (Int index = 0; index <= maxTouchedUnit; ++index) {
@@ -2034,22 +2006,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask;
if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState &&
currentRenderStateVersion == g_syncedRenderStateVersion) {
// Gate 1 of section 2.3.1: the steady-state cost of this whole function is
// the one Uint16 read above plus this compare.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/false);
// The version read above, the parameter-block fetch and the viewport fetch
// below - the three accessor calls this function makes unconditionally on a
// miss. The conditional sRGB capability read further down is deliberately
// NOT counted (see the inventory in PipeStats.cpp).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
@@ -2966,14 +2924,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// cross-TU call with a guarded static inside - repeating it per stage showed
// up in draw-loop profiles.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (MG_Util::PipeStats::Enabled()) {
// THE per-draw denominator for Espryt, plus this function's own two accessor
// calls (the VAO and the draw program). Everything the callees below read is
// counted by the callees that are instrumented; the rest is not counted (see
// the inventory in PipeStats.cpp).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2);
}
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion,
@@ -3420,10 +3370,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (BufferImpl::UboRingAllocate(bindSize, offset)) {
std::memcpy(static_cast<Uint8*>(BufferImpl::UboRingMappedPtr()) + offset,
currentProgram->MapUBO(), uboSize);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(uboSize));
}
ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial,
offset};
slotValid = true;
@@ -3443,11 +3389,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
currentProgram->MapUBO());
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(
MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(currentProgram->GetUBOSize()));
}
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
}
@@ -4365,12 +4306,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_restartIndices.capacity = capacity;
if (data != nullptr && bytes != 0) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
// Rewritten index list staged on the draw path: in a split build these
// bytes are the index-mirror-versus-ship decision of section 8.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
static_cast<Uint64>(bytes));
}
}
return true;
}
@@ -7317,22 +7252,138 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::SyncImageTextureBinding(unit);
}
// Only the pnames MG_Impl/GLImpl/Getter/GL_Getter.cpp has no case for reach here. Every
// indexed pname naming FRONTEND state - the indexed buffer bindings, the per-unit
// texture/sampler bindings, the image-unit bindings, the viewport rectangles, the indexed
// capabilities - is answered there and returns before the table is consulted, so the arms
// this function used to carry for GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were
// unreachable duplicates of the frontend's, and they did not even agree with it (the
// frontend reports the range glBindBufferRange was ASKED for, verbatim and unclamped; these
// clamped it to the buffer's current storage). In practice what arrives is
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE, which the driver owns.
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
if (!data) return;
if (g_GLESFuncs.glGetIntegeri_v) {
g_GLESFuncs.glGetIntegeri_v(target, index, data);
} else {
*data = 0;
switch (target) {
case GL_SHADER_STORAGE_BUFFER_BINDING: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint>(end - start);
return;
}
case GL_IMAGE_BINDING_NAME: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
return;
}
case GL_IMAGE_BINDING_LEVEL: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Level;
return;
}
case GL_IMAGE_BINDING_LAYERED: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Layered;
return;
}
case GL_IMAGE_BINDING_LAYER: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Layer;
return;
}
case GL_IMAGE_BINDING_ACCESS: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = static_cast<GLint>(imageBinding.Access);
return;
}
case GL_IMAGE_BINDING_FORMAT: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = static_cast<GLint>(imageBinding.Format);
return;
}
default:
if (g_GLESFuncs.glGetIntegeri_v) {
g_GLESFuncs.glGetIntegeri_v(target, index, data);
} else {
*data = 0;
}
return;
}
}
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
if (!data) return;
switch (target) {
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint64>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint64>(end - start);
return;
}
default:
if (g_GLESFuncs.glGetInteger64i_v) {
g_GLESFuncs.glGetInteger64i_v(target, index, data);
} else {
*data = 0;
}
return;
}
}
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
if (!params) return;
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) {
params[0] = 0;
return;
}
g_GLESFuncs.glGetProgramiv(backendProgramId, pname, params);
}
// NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry
@@ -10587,12 +10638,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::UnpackRingOnPresent();
BufferImpl::UploadRingOnPresent();
BufferImpl::TrimBufferPool();
// THE frame boundary for the MGPipe counters: publish this frame's plots, fold the
// frame into the run totals and, every 120th frame, emit the summary line.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::OnPresent();
}
}
void DestroyEGLContext() {
@@ -92,6 +92,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
+4 -76
View File
@@ -15,7 +15,6 @@
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
@@ -780,12 +779,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* initialData =
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
if (MG_Util::PipeStats::Enabled() && initialData != nullptr) {
// An ORPHANING respecify passes NULL and moves nothing, which is exactly
// why the test is on initialData rather than on size.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource.storageSize = size;
resource.storageInitialized = true;
resource.pendingRespecify = false;
@@ -893,13 +886,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT start = std::min(range.start, end);
const SizeT size = end - start;
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
// Counted once per queued range, before the three delivery shapes
// below diverge: all three move exactly these bytes, and it is the
// byte count - not the shape - that sizes SEG_STAGE.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
// The invalidating map's fast path is SHAPE-dependent on this Mali
// driver: a whole-buffer invalidation renames the store outright,
// and a large range gets fresh pages - but a small unaligned range
@@ -967,10 +953,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (write.offset >= limit) continue;
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
@@ -1099,6 +1081,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (resource->id != 0 && CanTouchGLNow() &&
resource->contextGeneration == g_bufferContextGeneration) {
NoteBufferIdDeleted(resource->id);
// Frontend VAO bindings survive respecification; force their
// backend twins to bind the replacement buffer name.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
@@ -1368,7 +1353,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
// See the declaration: re-mints of a live resource's driver id. Written only on
// the context thread (both re-mint sites run there), read only by the VAO sync.
// the context thread (all re-mint sites run there), read only by the VAO sync.
Uint64 g_bufferBackendIdGeneration = 0;
void RegisterBufferBackendOps() {
@@ -1538,13 +1523,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindBufferId(TempBufferTarget, reused);
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize,
bufferObject->MappedData());
if (MG_Util::PipeStats::Enabled()) {
// The pool-recycle reseed is a whole-buffer upload on the hot path,
// not a bookkeeping detail: it moves the same bytes a fresh
// glBufferData would.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(poolSize));
}
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
@@ -2570,10 +2548,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
// GL ignores `normalized` for floating-point array types, so it is not
// forwarded here either.
g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
@@ -2604,10 +2578,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(uploadSize), clientData,
GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(uploadSize));
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
@@ -2702,12 +2672,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
// The VBO-backed half of the 64-bit narrowing. Same population as the
// client-array half above: a stream the backend synthesises per draw.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
stream.valid = true;
stream.sourceLifetimeId = sourceLifetimeId;
stream.sourceChangeSerial = sourceChangeSerial;
@@ -4430,42 +4394,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (ringStaged) {
BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId());
}
if (MG_Util::PipeStats::Enabled()) {
// One emission per (upload target, level) that ships texels;
// the switch below turns it into either one union-box job or
// dirtyRectCount rect jobs. The box/rect split is counted
// separately from the bytes on purpose: SSIM is blind to it
// and the +6 ms/frame Mali cliff was a shape regression, not
// a byte regression (plan section 7.3).
const Bool rectShape = subRectEligible && dirtyRectCount >= 2;
Uint64 shippedBytes = 0;
if (rectShape) {
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
shippedBytes += static_cast<Uint64>(rect.hi.x() - rect.lo.x()) *
static_cast<Uint64>(rect.hi.y() - rect.lo.y()) *
static_cast<Uint64>(std::max(rect.hi.z() - rect.lo.z(), 1)) *
static_cast<Uint64>(bpp);
}
} else if (subRectEligible) {
shippedBytes = static_cast<Uint64>(regionSize.x()) *
static_cast<Uint64>(regionSize.y()) *
static_cast<Uint64>(std::max(regionSize.z(), 1)) *
static_cast<Uint64>(bpp);
} else {
shippedBytes = static_cast<Uint64>(byteSize);
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
shippedBytes);
MG_Util::PipeStats::AddCalls(
MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1);
MG_Util::PipeStats::AddCalls(
rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions
: MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions,
1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs,
rectShape ? static_cast<Uint64>(dirtyRectCount) : 1u);
}
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
+6 -25
View File
@@ -9,7 +9,6 @@
#include "MultiDraw.h"
#include "Managers.h"
#include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstring>
#include <limits>
@@ -157,10 +156,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
// orphans the previous store, so the upload never waits on a dispatch still reading
// the old contents out of the same name.
// statsClass: which MGPipe byte population these bytes belong to. Counted here
// rather than at the four call sites so a new tier cannot forget it.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
MG_Util::PipeStats::ByteClass statsClass) {
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
@@ -173,9 +169,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
buffer.cursor = 0;
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
}
}
return true;
}
@@ -190,8 +183,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
constexpr SizeT kMinRingBytes = 1u << 16;
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
outOffset = 0;
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
@@ -215,9 +207,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
}
}
buffer.cursor += aligned;
return true;
@@ -428,8 +417,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
SizeT commandBase = 0;
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
return false;
}
@@ -544,8 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
}
SizeT indexBase = 0;
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
return false;
}
@@ -750,16 +737,10 @@ void main() {
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
if (!EnsureComputeProgram()) return;
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
return;
}
// data == nullptr: pure respecify, the compute pass writes the contents, so no
// host bytes cross here and nothing is counted.
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
return;
}
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
@@ -740,6 +740,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
@@ -937,15 +939,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
// them. Not clamped: unlike the block counts these are not amounts an application
// allocates, and the frontend already raises them to the GL minimum.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
}
m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks);
+117 -24
View File
@@ -13,7 +13,6 @@
#include "MG_State/GLState/ErrorState/ErrorInfo.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/Miscellany/IndexGenerator.h"
#include <atomic>
@@ -78,6 +77,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 blockBindingVersion = 0;
Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables;
GLint computeWorkGroupSize[3] = {1, 1, 1};
};
struct DrawElementsIndirectCommand {
@@ -208,6 +208,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
for (auto& module : modules) {
for (Uint32 entryIndex = 0; entryIndex < module.entry_point_count; ++entryIndex) {
const auto& entryPoint = module.entry_points[entryIndex];
if ((entryPoint.shader_stage & SPV_REFLECT_SHADER_STAGE_COMPUTE_BIT) == 0) {
continue;
}
cache.computeWorkGroupSize[0] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.x, 1));
cache.computeWorkGroupSize[1] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.y, 1));
cache.computeWorkGroupSize[2] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.z, 1));
}
uint32_t bindingCount = 0;
SpvReflectResult result = spvReflectEnumerateDescriptorBindings(&module, &bindingCount, nullptr);
if (result != SPV_REFLECT_RESULT_SUCCESS || bindingCount == 0) {
@@ -672,40 +682,130 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(void)format;
}
// The two compute limits are the only indexed pnames a backend genuinely owns: they come
// from the physical device, and MG_Impl/GLImpl/Getter/GL_Getter.cpp asks for them here so it
// can raise the answer to the GL required minimum. The same six numbers are carried in
// DynamicBackendParameters::MaxComputeWorkGroupCount/Size (filled at capability init from
// the same limits), which is their MGPCaps carrier once this entry retires - the
// AdvertisedLimitsScenario pins the two against each other. Every other indexed pname names FRONTEND
// state (the indexed buffer bindings, the per-unit texture/sampler bindings, the image-unit
// bindings, the viewport rectangles, the indexed capabilities) and is answered there before
// the table is consulted, so the arms this function used to carry for
// GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were unreachable duplicates - and not
// even faithful ones: the frontend reports the range glBindBufferRange was ASKED for,
// verbatim, while these clamped it to the buffer's current storage.
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
if (!data) return;
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetIntegeri_v called with null VulkanRenderer");
if (index >= 3) {
*data = 0;
return;
}
switch (target) {
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
if (index >= 3) {
*data = 0;
return;
}
*data = static_cast<GLint>(
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupCount[index]);
return;
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
if (index >= 3) {
*data = 0;
return;
}
*data = static_cast<GLint>(
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupSize[index]);
return;
case GL_SHADER_STORAGE_BUFFER_BINDING: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint>(end - start);
return;
}
case GL_IMAGE_BINDING_NAME:
case GL_IMAGE_BINDING_LEVEL:
case GL_IMAGE_BINDING_LAYERED:
case GL_IMAGE_BINDING_LAYER:
case GL_IMAGE_BINDING_ACCESS:
case GL_IMAGE_BINDING_FORMAT: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
if (target == GL_IMAGE_BINDING_NAME) {
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
} else if (target == GL_IMAGE_BINDING_LEVEL) {
*data = imageBinding.Level;
} else if (target == GL_IMAGE_BINDING_LAYERED) {
*data = imageBinding.Layered;
} else if (target == GL_IMAGE_BINDING_LAYER) {
*data = imageBinding.Layer;
} else if (target == GL_IMAGE_BINDING_ACCESS) {
*data = static_cast<GLint>(imageBinding.Access);
} else {
*data = static_cast<GLint>(imageBinding.Format);
}
return;
}
default:
*data = 0;
return;
}
}
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
if (!data) return;
switch (target) {
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint64>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint64>(end - start);
return;
}
default:
*data = 0;
return;
}
}
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
if (!params) return;
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) {
params[0] = 0;
return;
}
switch (pname) {
case GL_COMPUTE_WORK_GROUP_SIZE: {
auto& cache = GetProgramResourceCache(*programObject);
params[0] = cache.computeWorkGroupSize[0];
params[1] = cache.computeWorkGroupSize[1];
params[2] = cache.computeWorkGroupSize[2];
return;
}
default:
params[0] = 0;
return;
}
}
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || storageBlockName == nullptr) return;
@@ -1330,12 +1430,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Present() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
pVulkanRenderer->Present();
// THE frame boundary for the MGPipe counters, at the backend entry point rather
// than inside VulkanRenderer::Present: that function has an early return for the
// no-usable-swapchain case, and a suspended frame is still a frame the counters
// must close.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::OnPresent();
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -95,6 +95,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
@@ -20,7 +20,6 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
@@ -2077,16 +2076,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
out.dynamicOffset = rangeStart;
}
}
if (MG_Util::PipeStats::Enabled() && !out.directBindable) {
// D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host
// payload a split build would have to ship with set_shader_buffers. Espryt binds
// the frontend buffer to the driver and contributes nothing here, which is why
// the class is named for the payload and not for the call. Counted AFTER the
// zero-copy direct-bind decision: a direct bind repacks nothing, and counting it
// here reported a copy that never happened.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
static_cast<Uint64>(outSize));
}
return true;
}
@@ -2294,13 +2283,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outBuffer = slice.buffer;
outRange = ubo.payloadSize;
outDynamicOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo && MG_Util::PipeStats::Enabled()) {
// Magma's half of stage-ubo-global, so the class means the same on both
// backends. The memo hit above returns before this, so a frame that reuses the
// slice correctly contributes nothing.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(ubo.payloadSize));
}
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] =
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
@@ -10,8 +10,6 @@
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"
#include "MG_Util/Metrics/PipeStats.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
@@ -231,38 +229,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
return false;
}
if (MG_Util::PipeStats::Enabled()) {
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
// absent: its bytes are counted by the caller, which is the only place that
// knows whether the payload is the default block (stage-ubo-global) or a named
// one repacked into the ring (stage-ubo-named), and counting here as well would
// double every uniform byte.
switch (kind) {
case BufferKind::Vertex:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(size));
break;
case BufferKind::Index:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
static_cast<Uint64>(size));
break;
case BufferKind::Indirect:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
static_cast<Uint64>(size));
break;
case BufferKind::TextureBuffer:
case BufferKind::ShaderStorage:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
break;
case BufferKind::Uniform:
break;
}
}
return true;
(void)kind;
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
}
Bool VkBufferManager::InitializeTransientArenas() {
@@ -371,9 +339,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.pendingFullUpload = true;
return false;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource.pendingFullUpload = false;
return true;
}
@@ -388,11 +353,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), 16, staging)) {
return false;
}
if (MG_Util::PipeStats::Enabled()) {
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
// half of the same bytes and is not counted twice.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
if (commandBuffer == VK_NULL_HANDLE) {
return false;
@@ -462,8 +422,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
}
@@ -489,9 +447,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
return;
}
@@ -529,9 +484,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
return;
}
@@ -602,13 +554,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint8* seed = bufferObject.MappedData();
if (seed != nullptr) {
resource->buffer.Upload(seed, size, 0);
if (MG_Util::PipeStats::Enabled()) {
// The one-time seed of a persistent map. Everything the app writes AFTER
// this goes straight through the mapping and is persistent-map-push
// territory (unwired, D4/D-B4), not this class.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
}
resource->persistentMapped = true;
resource->pendingFullUpload = false;
@@ -657,10 +602,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->usageFlags = 0;
return false;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource->pendingFullUpload = false;
}
@@ -740,9 +681,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outSlice)) {
return false;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource->transientSlice = outSlice;
resource->transientFrameSerial = m_frameSerial;
resource->transientChangeSerial = changeSerial;
@@ -13,7 +13,6 @@
#include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include <Config.h>
#include <algorithm>
@@ -3151,32 +3150,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
packBox(dst, item.regionLo, item.regionSize);
}
if (MG_Util::PipeStats::Enabled()) {
// Same shape split as Espryt's: one union box per item, or one job per rect of
// a refined rect list. The box/rect decision is invisible to SSIM and is what
// the +6 ms/frame Mali cliff of section 7.3 was, so it is counted apart from
// the bytes.
Uint64 boxEmissions = 0;
Uint64 rectEmissions = 0;
Uint64 jobs = 0;
for (const auto& item : uploadItems) {
if (item.rects.empty()) {
++boxEmissions;
jobs += isCombinedDepthStencil ? 2u : 1u;
} else {
++rectEmissions;
jobs += static_cast<Uint64>(item.rects.size());
}
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
static_cast<Uint64>(stagingSize));
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadEmissions,
static_cast<Uint64>(uploadItems.size()));
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, boxEmissions);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadRectEmissions, rectEmissions);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, jobs);
}
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags uploadSrcAccessMask = 0;
@@ -27,7 +27,6 @@
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h"
#include "MG_Util/Texture/PixelStoreProcessor.h"
@@ -3267,9 +3266,8 @@ void main() {
}
m_vertexInputStateFactory.reset();
m_xfbCounterBuffer.Destroy();
m_xfbCounterSlotOwner.fill(0);
m_xfbCounterSlotLastUse.fill(0);
m_xfbCounterSlotUseSerial = 0;
m_xfbCounterSlotByObject.clear();
m_xfbNextCounterSlot = 0;
m_xfbCountersValid.fill(false);
m_xfbLastSeenGeneration.fill(0);
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
@@ -5001,19 +4999,9 @@ void main() {
entry.pipelineStateHash == pipelineStateHash &&
entry.primitiveRestartEnable == primitiveRestartEnable &&
entry.transformFlags == transformFlags) {
if (MG_Util::PipeStats::Enabled()) {
// Gate 5 of section 2.3.1. On a hit this whole function cost the one
// GetPipelineStateVersion read above plus this value compare.
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaPipelineMemo, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return entry.pipeline;
}
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaPipelineMemo, /*hit=*/false);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
// Shape gate. Behind the memo probe deliberately: only a pipeline that was created
// successfully is ever memoized, so a program refused here can never be sitting in the
@@ -5254,24 +5242,6 @@ void main() {
return VK_NULL_HANDLE;
}
if (MG_Util::PipeStats::Enabled()) {
// THE payload-builder walk section 2.3.1 says only runs on a pipeline memo miss.
// Counted as a constant, and counted HERE rather than at the top of the walk:
// the list-topology primitive-restart refusal above returns VK_NULL_HANDLE after
// only ten of these reads have run, and a tally that fires before an early return
// is an OVER-count, which breaks the lower-bound contract every other tally keeps.
//
// The 15 are: the six capability reads (cull face, depth test, polygon offset
// fill, rasterizer discard, colour logic op, stencil test), the draw-FBO slot
// read that gates depth/stencil, the two stencil face states, the polygon mode,
// the min sample shading value, the patch vertex count, the depth mask, the depth
// func, and the second draw-FBO slot read below. The sample-shading CAPABILITY
// read is the one excluded: it sits behind && on m_sampleRateShadingFeatureEnabled
// and does not run on a device without the feature. The other conditional reads -
// the cull-mode ternary, the logic-op fetch, the two tessellation default-level
// reads - are excluded for the same reason, so this stays a LOWER bound.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 15);
}
PipelineFactory::PipelineCreatePayload payload {
.programHash = programObj.hash,
.vertexInputHash = vertexLayoutHash,
@@ -5920,18 +5890,8 @@ void main() {
if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion &&
shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() &&
shadow.dynamicTailIsDefaultFbo == isDefaultFbo) {
// Gate 6 of section 2.3.1: one version read plus a four-integer compare.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDynamicTail, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return;
}
if (MG_Util::PipeStats::Enabled()) {
// The version read above and the bulk parameter fetch that builds the value key.
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDynamicTail, /*hit=*/false);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2);
}
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
// Second-level VALUE gate: the version moved, but RenderState's version counts
// every parameter, most of which this tail never reads. Build the key over
@@ -6405,15 +6365,6 @@ void main() {
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
}
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount);
if (MG_Util::PipeStats::Enabled()) {
// The six accessor reads this function makes unconditionally on the path that
// reaches here: the draw program, the VAO, the draw-FBO slot, the pipeline
// state version, the texture bind generation and the sampling-resolution
// generation. The XFB-active probe is elided on a device without the feature
// and the parameter-block fetch only runs when the pipeline state version
// moved, so neither is counted (lower bound, as everywhere else).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 6);
}
return true;
}
@@ -6439,27 +6390,9 @@ void main() {
return false;
}
}
if (MG_Util::PipeStats::Enabled()) {
// THE per-draw denominator for Magma, plus the draw-program read above. Placed
// here rather than inside TrySetupDrawFastPath because the fast path has 27
// decline returns and one success return: counting the gate from the caller is
// the only shape that cannot miss one.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDrawFastPath, /*hit=*/true);
}
return true;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDrawFastPath, /*hit=*/false);
// The three reads the full path makes immediately below (draw FBO, VAO,
// program). The accessor reads the declined fast path had already made before
// it turned back are NOT counted.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) {
@@ -11200,66 +11133,16 @@ void main() {
}
}
// Keyed on the frontend's never-reused lifetime id, NOT on the GL name. The name is
// recycled the moment glDeleteTransformFeedbacks gives it back, so a name-keyed slot
// handed a brand-new object the counter group - and the m_xfbCountersValid /
// m_xfbLastSeenGeneration entries - of the object that died under that name.
//
// Slots are never handed back (there is no backend entry telling this renderer that a span
// closed - registering the EndTransformFeedback one would flip the "captures through its own
// driver" test FixupGsStripCaptureOrder makes of it), so once all sixteen are owned a new
// object has to take one over. The victim is chosen among owners with NO OPEN SPAN: an object
// whose span is closed, or which no longer exists at all, can never resume, so its counter
// bytes are dead. Least-recently-used ALONE would be exactly the wrong rule - GL only permits
// another object to capture while this one is PAUSED, so the paused span whose counters the
// slots exist to protect is by construction the least recently used entry. Taking a group over
// resets its counter state, because those bytes describe the previous owner's span.
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
constexpr Uint32 kNoSlot = static_cast<Uint32>(kXfbCounterObjectSlots);
const Uint64 identity = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
MOBILEGL_ASSERT(identity != 0,
"transform feedback object reported the free-slot sentinel (0) as its identity - "
"every slot would then read as 'mine' without ever being claimed");
Uint32 freeSlot = kNoSlot;
for (Uint32 slot = 0; slot < kNoSlot; ++slot) {
if (m_xfbCounterSlotOwner[slot] == identity) {
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
return slot;
}
if (m_xfbCounterSlotOwner[slot] == 0 && freeSlot == kNoSlot) {
freeSlot = slot;
}
const Uint name = MG_State::pGLContext->GetBoundTransformFeedbackName();
const auto it = m_xfbCounterSlotByObject.find(name);
if (it != m_xfbCounterSlotByObject.end()) {
return it->second;
}
Uint32 slot = freeSlot;
if (slot == kNoSlot) {
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
if (MG_State::pGLContext->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
continue;
}
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
slot = candidate;
}
}
}
if (slot == kNoSlot) {
// Sixteen capture spans open at once. Whatever is taken loses its resume offset and
// restarts at byte 0 of its capture buffers, which is a wrong picture rather than a
// slow one - hence a report rather than a silent choice.
MGLOG_E_ONCE("CurrentXfbCounterSlot: all %zu counter groups belong to transform feedback objects "
"with an open capture span; the least recently used one is taken over and that span "
"will restart at offset 0 instead of appending",
kXfbCounterObjectSlots);
slot = 0;
for (Uint32 candidate = 1; candidate < kNoSlot; ++candidate) {
if (m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
slot = candidate;
}
}
}
m_xfbCounterSlotOwner[slot] = identity;
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
m_xfbCountersValid[slot] = false;
m_xfbLastSeenGeneration[slot] = 0;
// Past the tracked set every object shares slot group 0. Only concurrently-paused
// spans need distinct groups, and applications do not keep sixteen of those open.
const Uint32 slot = m_xfbNextCounterSlot < kXfbCounterObjectSlots ? m_xfbNextCounterSlot++ : 0;
m_xfbCounterSlotByObject[name] = slot;
return slot;
}
@@ -675,18 +675,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// per object: one group of four slots each, handed out on first use.
static constexpr SizeT kXfbCounterObjectSlots = 16;
VkBufferObject m_xfbCounterBuffer;
// Which transform feedback object owns each slot group, by the frontend's never-reused
// lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
// inherited the dead one's slot, and since nothing ever removed an entry the map also
// grew for the life of the context. A fixed table cannot do either: a group is taken over
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
// counters can still be resumed never loses them, and a dead object's group comes back.
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
// Tie-break among reclaimable groups only; never on its own, because the paused span the
// groups exist for is by construction the least recently used one.
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
Uint64 m_xfbCounterSlotUseSerial = 0;
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
Uint32 m_xfbNextCounterSlot = 0;
// Set for a slot once a captured draw has been recorded into its span; selects
// counter-buffer resume on the next captured draw of the same span.
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
@@ -51,7 +51,6 @@ endif()
add_executable(MobileGLIntegrationTest
Main.cpp
Harness/HeadlessGL.cpp
Harness/BackendCapsPeek.cpp
Scenarios/OrientationScenario.cpp
Scenarios/CrossFrameBufferScenario.cpp
Scenarios/ResidentIndexScenario.cpp
@@ -1,42 +0,0 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "BackendCapsPeek.h"
#if !defined(__ANDROID__)
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Backend {
// Declared in MG_Backend/BackendObjects.h, which also pulls in both backends' headers
// and, through them, their loaders; the reference alone is all that is needed here.
extern UniquePtr<BackendObject>& pActiveBackendObject;
} // namespace MobileGL::MG_Backend
#endif
namespace MGITest {
bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]) {
#if defined(__ANDROID__)
(void)outCount;
(void)outSize;
return false;
#else
const auto& backend = MobileGL::MG_Backend::pActiveBackendObject;
if (!backend) {
return false;
}
const MobileGL::MG_Backend::DynamicBackendParameters& caps = backend->GetDynamicParameters();
for (int axis = 0; axis < 3; ++axis) {
outCount[axis] = caps.MaxComputeWorkGroupCount[axis];
outSize[axis] = caps.MaxComputeWorkGroupSize[axis];
}
return true;
#endif
}
} // namespace MGITest
@@ -1,29 +0,0 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// The one place this module looks past the GL API into the active backend's caps block.
//
// It exists for exactly one assertion: that the six per-axis compute limits the MGPipe
// caps block carries (DynamicBackendParameters::MaxComputeWorkGroupCount/Size, plan B
// section 4.4.1) are the same numbers glGetIntegeri_v answers today, since P0.5 retires
// the getter in favour of the caps. A separate translation unit, because the scenario
// sources include the GL headers with prototypes and MobileGL's umbrella header is not
// meant to meet them in one file.
#pragma once
namespace MGITest {
// Copies the active backend's MaxComputeWorkGroupCount / MaxComputeWorkGroupSize into the
// two arrays and returns true. Returns false, touching nothing, where the caps block is
// out of reach: on Android this module links the SHIPPING libMobileGL.so, built
// -fvisibility=hidden, so no internal symbol resolves; on desktop it links MobileGL_s and
// the read is direct.
bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]);
} // namespace MGITest
@@ -26,11 +26,9 @@
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
// fails on both is a table bug.
#include <algorithm>
#include <string>
#include <vector>
#include "../Harness/BackendCapsPeek.h"
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
@@ -380,250 +378,5 @@ namespace MGITest {
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
}
// THE INDEXED AND PER-PROGRAM QUERIES THAT NAME FRONTEND STATE, pinned on both lanes.
//
// Both backends used to carry their own arms for GL_SHADER_STORAGE_BUFFER_* and
// GL_IMAGE_BINDING_* inside GLFunctionsTable::GetIntegeri_v, and their own
// GetInteger64i_v / GetProgramiv table entries. None of it was reachable: GL_Getter and
// GL_Program answer every one of these pnames from the frontend's own state and return
// before the table is consulted. The duplicates did not even agree - the backend arms
// clamped a bound range to the buffer's current storage, which GL 4.6 core tables
// 23.4/23.5 do not permit - so the code was one refactor away from becoming the answer.
// These cases pin what the frontend actually reports, so a future move of any of it back
// behind the interface has to keep saying the same thing.
TEST_F(AdvertisedLimitsScenario, IndexedBufferBindingsAreReportedVerbatimOnBothWidths) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
// A range that is NOT the whole buffer, so a clamp to the store would be visible.
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, 256, 512);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
GLint binding32 = -1;
GLint start32 = -1;
GLint size32 = -1;
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, 1, &binding32);
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start32);
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size32);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_EQ(binding32, static_cast<GLint>(buffer));
EXPECT_EQ(start32, 256);
EXPECT_EQ(size32, 512);
// The 64-bit width has to agree pname for pname. It has no backend entry of its own
// and derives everything from the 32-bit answer above plus its own buffer arm.
GLint64 binding64 = -1;
GLint64 start64 = -1;
GLint64 size64 = -1;
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_BINDING, 1, &binding64);
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start64);
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size64);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_EQ(binding64, static_cast<GLint64>(buffer));
EXPECT_EQ(start64, static_cast<GLint64>(256));
EXPECT_EQ(size64, static_cast<GLint64>(512));
// An unbound index answers zero rather than erroring or leaking the driver's answer.
GLint unbound = -1;
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, 0, &unbound);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_EQ(unbound, 0);
// THE ARM THAT SEPARATES VERBATIM FROM CLAMPED. GL 4.6 core tables 23.4/23.5 report
// the size glBindBufferRange was ASKED for; it does not follow the buffer, so
// shrinking the store underneath the binding must not move it. A clamp to the
// current storage - which is exactly what both backends' deleted arms did - answers
// 128 here, and answers 0 for the bind-then-allocate shape
// KHR-GL43.shader_storage_buffer_object.basic-binding uses.
glBufferData(GL_SHADER_STORAGE_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
GLint startAfterShrink = -1;
GLint sizeAfterShrink = -1;
GLint64 sizeAfterShrink64 = -1;
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &startAfterShrink);
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &sizeAfterShrink);
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &sizeAfterShrink64);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_EQ(startAfterShrink, 256)
<< "the bound range's start followed the buffer through a re-specification";
EXPECT_EQ(sizeAfterShrink, 512)
<< "the bound range's size was clamped to the buffer's current 128-byte storage; the range is "
"state of the BINDING POINT and is reported verbatim";
EXPECT_EQ(sizeAfterShrink64, static_cast<GLint64>(512))
<< "the 64-bit width disagreed with the 32-bit one about the same pname";
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glDeleteBuffers(1, &buffer);
(void)FirstGLError();
}
TEST_F(AdvertisedLimitsScenario, ImageUnitBindingsAreReportedFromTheFrontendState) {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
(void)FirstGLError();
if (maxImageUnits < 2) GTEST_SKIP() << "no image units to bind on this lane";
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 8, 8);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glBindImageTexture(1, texture, 1, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
struct Expectation {
GLenum pname;
const char* name;
GLint expected;
};
const Expectation expectations[] = {
{GL_IMAGE_BINDING_NAME, "GL_IMAGE_BINDING_NAME", static_cast<GLint>(texture)},
{GL_IMAGE_BINDING_LEVEL, "GL_IMAGE_BINDING_LEVEL", 1},
{GL_IMAGE_BINDING_LAYERED, "GL_IMAGE_BINDING_LAYERED", GL_FALSE},
{GL_IMAGE_BINDING_LAYER, "GL_IMAGE_BINDING_LAYER", 0},
{GL_IMAGE_BINDING_ACCESS, "GL_IMAGE_BINDING_ACCESS", GL_READ_ONLY},
{GL_IMAGE_BINDING_FORMAT, "GL_IMAGE_BINDING_FORMAT", GL_RGBA8},
};
for (const Expectation& expectation : expectations) {
GLint value = -424242;
glGetIntegeri_v(expectation.pname, 1, &value);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << expectation.name;
EXPECT_EQ(value, expectation.expected) << expectation.name;
// Same pname through the wide width - it must not fall through to a driver that
// knows nothing about MobileGL's image-unit state.
GLint64 wide = -424242;
glGetInteger64i_v(expectation.pname, 1, &wide);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << expectation.name << " (64-bit)";
EXPECT_EQ(wide, static_cast<GLint64>(expectation.expected)) << expectation.name << " (64-bit)";
}
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glDeleteTextures(1, &texture);
(void)FirstGLError();
}
// glGetProgramiv(GL_COMPUTE_WORK_GROUP_SIZE) is a LINK ARTIFACT of the program the
// application wrote. DirectVulkan used to answer it from its own spirv-reflect cache and
// DirectGLES by forwarding to the driver's ESSL program - neither of which the
// application ever named - while GL_Program.cpp has always answered it from
// ProgramObject::GetComputeLocalSize. This pins the declared local size on both lanes.
TEST_F(AdvertisedLimitsScenario, ComputeLocalSizeComesFromTheLinkedProgram) {
static const char* kSource = R"(#version 430 core
layout(local_size_x = 4, local_size_y = 3, local_size_z = 2) in;
layout(std430, binding = 0) buffer Output { uint g_data[]; };
void main() { g_data[gl_LocalInvocationIndex] = 1u; }
)";
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &kSource, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
glDeleteShader(shader);
(void)FirstGLError();
GTEST_SKIP() << "no compute shader support on this lane: " << log;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
glDeleteProgram(program);
(void)FirstGLError();
GTEST_SKIP() << "the compute program did not link on this lane: " << log;
}
GLint localSize[3] = {-1, -1, -1};
glGetProgramiv(program, GL_COMPUTE_WORK_GROUP_SIZE, localSize);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_EQ(localSize[0], 4);
EXPECT_EQ(localSize[1], 3);
EXPECT_EQ(localSize[2], 2);
// A program with no compute stage must answer INVALID_OPERATION, not a stale or
// defaulted (1, 1, 1) - the frontend's rule, and the one a backend that answers from
// its own reflection cache cannot express.
const GLuint empty = glCreateProgram();
GLint ignored[3] = {0, 0, 0};
glGetProgramiv(empty, GL_COMPUTE_WORK_GROUP_SIZE, ignored);
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_OPERATION))
<< "GL 4.6 core 7.13: the query is only defined for a linked program with a compute shader";
glDeleteProgram(empty);
glDeleteProgram(program);
(void)FirstGLError();
}
// THE SIX COMPUTE LIMITS THAT OUTLIVE THE GETTER. GL_MAX_COMPUTE_WORK_GROUP_COUNT and
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each, are the only indexed pnames the
// DEVICE answers rather than the frontend (glGetIntegeri_v on Espryt, VkPhysicalDevice-
// Limits on Magma), and therefore the only ones that have to cross the MGPipe boundary
// once GetIntegeri_v is retired (plan B section 4.4.6 / P0.5). They ride in MGPCaps by
// inclusion, as DynamicBackendParameters::MaxComputeWorkGroupCount/Size, filled by both
// backends at capability init. This case pins that the caps copy and the live getter
// answer are one number - the getter floors the backend's raw answer at the GL 4.3
// minimum, so the comparison is against the floored caps value - and pins the
// GL-visible half on every lane: answerability, the floors, vector/indexed agreement
// and the index bound. On a lane where the caps block is out of reach (Android links
// the shipping .so) only the GL-visible half runs.
TEST_F(AdvertisedLimitsScenario, ComputeWorkGroupLimitsAreTheCapsBlocksAnswer) {
struct Axis {
GLenum pname;
const char* name;
GLint minimum[3]; // GL 4.3 core table 23.60
};
const Axis axes[] = {
{GL_MAX_COMPUTE_WORK_GROUP_COUNT, "GL_MAX_COMPUTE_WORK_GROUP_COUNT", {65535, 65535, 65535}},
{GL_MAX_COMPUTE_WORK_GROUP_SIZE, "GL_MAX_COMPUTE_WORK_GROUP_SIZE", {1024, 1024, 64}},
};
int capsCount[3] = {0, 0, 0};
int capsSize[3] = {0, 0, 0};
const bool capsVisible = PeekComputeWorkGroupCaps(capsCount, capsSize);
for (const Axis& axis : axes) {
GLint indexed[3] = {-1, -1, -1};
for (GLuint i = 0; i < 3; ++i) {
glGetIntegeri_v(axis.pname, i, &indexed[i]);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name << "[" << i << "]";
EXPECT_GE(indexed[i], axis.minimum[i])
<< axis.name << "[" << i << "] = " << indexed[i]
<< " is below the GL 4.3 core table 23.60 minimum " << axis.minimum[i];
}
GLint vector[3] = {-1, -1, -1};
glGetIntegerv(axis.pname, vector);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name;
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(vector[i], indexed[i])
<< axis.name << "[" << i << "]: the vector query and the indexed query disagree";
}
GLint outOfRange = -424242;
glGetIntegeri_v(axis.pname, 3, &outOfRange);
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_VALUE))
<< axis.name << "[3]: an index past the three axes is INVALID_VALUE (GL 4.6 core 22.1)";
if (!capsVisible) continue;
const int* capsAxis = axis.pname == GL_MAX_COMPUTE_WORK_GROUP_COUNT ? capsCount : capsSize;
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(std::max(capsAxis[i], axis.minimum[i]), indexed[i])
<< axis.name << "[" << i << "]: MGPCaps carries " << capsAxis[i]
<< " but glGetIntegeri_v answers " << indexed[i]
<< " - the caps block and the getter path must be one number, because P0.5 retires "
"the getter in favour of the caps";
}
}
}
} // namespace
} // namespace MGITest
@@ -102,6 +102,12 @@ void main() { word = 0xC0FFEEu; }
// The NULL-data definition is the adoption point (and Minecraft's
// arena-creation idiom).
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
ConfigureVertexArray(m_vao);
}
void ConfigureVertexArray(GLuint vao) {
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
@@ -173,12 +179,12 @@ void main() { word = 0xC0FFEEu; }
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
}
void DrawQuad() {
void DrawQuad(GLuint vao = 0) {
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glBindVertexArray(vao != 0 ? vao : m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
@@ -222,6 +228,97 @@ void main() { word = 0xC0FFEEu; }
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
}
// Respecifying a frontend buffer preserves its VAO attachments even when the
// backend replaces the adopted store's GL name. Keep every attribute binding
// unchanged so a stale backend VAO cannot be repaired by a frontend rebind.
TEST_F(LargeArenaAdoptionScenario, RespecifiedVertexArenaKeepsVaoBindings) {
if (!Ready() || IsSkipped()) return;
UploadQuad(1.f, 0.f, 0.f);
DrawQuad();
ASSERT_GT(CenterPixel()[0], 200);
ASSERT_EQ(FirstGLError(), 0u);
GLuint otherVao = 0;
glGenVertexArrays(1, &otherVao);
ConfigureVertexArray(otherVao);
DrawQuad(otherVao);
EXPECT_GT(CenterPixel()[0], 200);
EXPECT_EQ(FirstGLError(), 0u);
constexpr std::array<GLsizeiptr, 3> sizes = {
kArenaBytes, kArenaBytes + 4096, kArenaBytes - 4096,
};
constexpr std::array<std::array<float, 3>, 3> colors = {{
{0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, {1.f, 0.f, 0.f},
}};
for (std::size_t i = 0; i < sizes.size(); ++i) {
SCOPED_TRACE(sizes[i]);
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glBufferData(GL_ARRAY_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
UploadQuad(colors[i][0], colors[i][1], colors[i][2]);
// The unbound VAO can retain the deleted store; the current VAO's
// attachments can be cleared by deletion. Both must be repaired.
for (GLuint vao : {m_vao, otherVao}) {
SCOPED_TRACE(vao);
DrawQuad(vao);
const auto px = CenterPixel();
EXPECT_EQ(FirstGLError(), 0u);
for (std::size_t channel = 0; channel < 3; ++channel) {
if (colors[i][channel] != 0.f) {
EXPECT_GT(px[channel], 200) << "VAO did not fetch the replacement vertex store";
} else {
EXPECT_LT(px[channel], 50) << "VAO still fetched the previous vertex store";
}
}
}
}
glDeleteVertexArrays(1, &otherVao);
}
TEST_F(LargeArenaAdoptionScenario, RespecifiedIndexArenaKeepsVaoBinding) {
if (!Ready() || IsSkipped()) return;
auto vertices = QuadVertices(1.f, 0.f, 0.f);
const auto green = QuadVertices(0.f, 1.f, 0.f);
vertices.insert(vertices.end(), green.begin(), green.end());
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
GLuint indices = 0;
glGenBuffers(1, &indices);
glBindVertexArray(m_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices);
// Redefine through COPY_WRITE_BUFFER so the element binding slot never
// changes. The small final store also exercises returning to shadow storage.
glBindBuffer(GL_COPY_WRITE_BUFFER, indices);
constexpr std::array<GLsizeiptr, 4> sizes = {
kArenaBytes, kArenaBytes, kArenaBytes + 4096, 4096,
};
for (std::size_t i = 0; i < sizes.size(); ++i) {
SCOPED_TRACE(sizes[i]);
const GLuint first = (i % 2) == 0 ? 0u : 6u;
const std::array<GLuint, 6> elements = {
first, first + 1, first + 2, first + 3, first + 4, first + 5,
};
glBufferData(GL_COPY_WRITE_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, sizeof(elements), elements.data());
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_program);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
const auto px = CenterPixel();
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_GT(px[first == 0 ? 0 : 1], 200) << "VAO did not fetch the replacement index store";
EXPECT_LT(px[first == 0 ? 1 : 0], 50) << "VAO still fetched the previous index store";
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
glDeleteBuffers(1, &indices);
}
// The shadow IS the mapping: a readback straight after a CPU write must hand
// back exactly those bytes.
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
-106
View File
@@ -1,106 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/Coverage.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The hand-maintained half of G6 (plan B section 4.7, gate 10.3-5): which MGPipe call
// answers each backend read point in scripts/data/backend_read_inventory.md (477 rows, 57
// files, generated from the backends by MobileGL-CS's extract_backend_read_inventory.py).
//
// gen_pipe.py joins the inventory's `member` column against MGP_COVERAGE_ACCESSOR_LIST and
// its `delta` column against MGP_COVERAGE_DELTA_LIST, then writes generated/PipeCoverage.inc
// with the per-accessor table and prints the coverage summary. Rows matching neither are
// UNMAPPED: allowed in P0 and merely counted, ZERO from P5 onward, when the gate becomes
// "regenerate and git diff --exit-code with 0 UNMAPPED".
//
// Three pseudo-calls stand for read points that do NOT become a forward call:
// kClientResolved - the frontend answers it itself; the server is never asked
// (section 4.4.6: "the server answers nothing the client can answer").
// kReverseChannel - it becomes one of the ten MGPipeCallbacks (section 7.1).
// kStructuralHandle - the row is a SIGNATURE carrying SharedPtr<MG_State...>, which
// becomes an MGPipeHandle parameter; there is no single call to name.
//
// clang-format off
// X(Accessor, PipeCall)
#define MGP_COVERAGE_ACCESSOR_LIST(X) \
X(GetActiveTextureUnit, SetSamplerViews) \
X(GetBlendColor, SetDynamicState) \
X(GetBlendEquationIndexed, CreateRenderState) \
X(GetBlendFuncIndexed, CreateRenderState) \
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
X(GetBoundVertexArray, BindVertexElements) \
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
/* set_index_buffer, set_indirect_buffers and set_shader_buffers once the */ \
/* inventory carries the target argument (P1). Named for the plan's explicit */ \
/* replacement of the DrawIndirect/Parameter pair. */ \
X(GetBufferBindingSlot, SetIndirectBuffers) \
X(GetBufferBindingPoint, SetShaderBuffers) \
X(GetBufferBindingPointCount, SetShaderBuffers) \
X(GetTouchedBufferBindingPointCount, SetShaderBuffers) \
X(GetClampReadColor, SetDynamicState) \
X(GetClearColor, SetDynamicState) \
X(GetClearDepth, SetDynamicState) \
X(GetClearStencil, SetDynamicState) \
X(GetColorMaskIndexed, CreateRenderState) \
X(GetCullFaceMode, CreateRenderState) \
X(GetCurrentVertexAttribute, SetVertexAttribDefaults) \
X(GetDepthFunc, CreateRenderState) \
X(GetDepthMask, CreateRenderState) \
X(GetDepthRangeIndexed, SetDynamicState) \
X(GetFramebufferBindingSlot, SetFramebufferState) \
X(GetImageTextureBinding, SetShaderImages) \
X(GetLineWidth, SetDynamicState) \
X(GetLogicOp, CreateRenderState) \
X(GetMaxTouchedTextureUnit, SetSamplerViews) \
X(GetMinSampleShadingValue, CreateRenderState) \
X(GetPatchDefaultInnerLevel, SetPatchState) \
X(GetPatchDefaultOuterLevel, SetPatchState) \
X(GetPatchVertices, SetPatchState) \
X(GetPipelineStateVersion, BindRenderState) \
X(GetPixelStoreParameters, SetPixelPackState) \
X(GetPolygonModeFront, CreateRenderState) \
X(GetPolygonOffsetFactor, SetDynamicState) \
X(GetPolygonOffsetUnits, SetDynamicState) \
X(GetPrimitiveRestartIndex, DrawVbo) \
X(GetProgramForDispatch, SetDispatchProgram) \
X(GetProgramForDraw, SetDrawProgram) \
X(GetProgramObject, CreateShaderState) \
/* Not in ComputePipelineStateHash today even though Vulkan makes it pipeline */ \
/* state; recorded here so the G7 chunk table has to answer for it before it */ \
/* freezes (section 10.3-5). */ \
X(GetProvokingVertexMode, CreateRenderState) \
X(GetRenderStateParameters, CreateRenderState) \
X(GetRenderStateParametersVersion, BindRenderState) \
X(GetSamplingResolutionGeneration, SetSamplerViews) \
X(GetScissorBox, SetDynamicState) \
X(GetStencilState, CreateRenderState) \
X(GetTextureBindGeneration, SetSamplerViews) \
X(GetTextureContextId, SetSamplerViews) \
X(GetTextureObject, SetSamplerViews) \
X(GetTextureUnitObject, SetSamplerViews) \
X(GetTransformFeedbackCapturedVertices, DrawVbo) \
X(GetTransformFeedbackGeneration, SetStreamOutputTargets) \
X(GetTransformFeedbackPausedPrimitiveCounter, EndStreamOutput) \
X(GetTransformFeedbackProgram, SetStreamOutputTargets) \
X(GetViewport, SetDynamicState) \
X(GetViewportIndexed, SetDynamicState) \
X(IsCapabilityEnabled, CreateRenderState) \
X(IsCapabilityEnabledIndexed, CreateRenderState) \
X(IsTransformFeedbackActive, BeginStreamOutput) \
X(IsTransformFeedbackPaused, PauseStreamOutput) \
X(InvalidateCompileEnv, kClientResolved) \
X(ValidateProgramName, kClientResolved) \
X(RecordError, kReverseChannel)
// X(DeltaKind, PipeCall) - for inventory rows with no accessor in the member column.
// Read by gen_pipe.py ONLY, never by the C++ preprocessor: the delta kinds are the
// inventory's own free-text labels, not C tokens.
#define MGP_COVERAGE_DELTA_LIST(X) \
X(handle-ify (wire handle), kStructuralHandle) \
X(Buffer ops delta, ResourceRespecify)
// clang-format on
-93
View File
@@ -1,93 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/MGPipe.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "MGPipeCallbacks.h"
#include "MGPipeHandles.h"
#include "MGPipeHostSpan.h"
#include "MGPipeTypes.h"
// The MGPipe boundary (plan B section 4).
//
// The two interface tables are FUNCTION-POINTER STRUCTS, not virtual bases. Three reasons
// out of this repository rather than out of gallium: the boundary already is a
// function-pointer struct sitting on one hook point in MG_Backend/Init.cpp; a nullptr entry
// already means "not implemented, frontend falls back", which is exactly what a
// not-yet-migrated subsystem needs to say while it keeps pulling; and MG_Test already
// substitutes this table to mock a backend. The rare EGL and caps surface stays on
// pActiveBackendObject's virtual functions.
namespace MobileGL::MG_Pipe {
// Unscoped on purpose: PipeCalls.def spells these as bare tokens so the same file can
// be read by the C++ preprocessor and by scripts/gen_pipe.py.
enum MGPipeCallClass : Uint8 {
kScreen,
kCtxCso,
kCtxState,
kCtxObject,
kCtxVerb,
kCtxQuery,
kCallClassCount,
};
enum MGPipeCallFlags : Uint32 {
kNone = 0,
// The caller must not proceed until the server has acknowledged. Rare by design.
kNeedsAck = 1u << 0,
// Carries an MGPBlobRef.
kHasBlob = 1u << 1,
// Carries a variable-length array after the fixed payload.
kVarTail = 1u << 2,
// Carries an MGHostSpan - the one shape that changes with the transport.
kHostSpan = 1u << 3,
// Answers into an MGPReplySlot; never blocks.
kReplySlot = 1u << 4,
// May be null in a backend's table. A null entry is a real answer ("this backend
// does not implement it"), not an error: DirectVulkan deliberately leaves
// buffer_subdata_resident unregistered, and SetSwapInterval likewise.
kOptional = 1u << 5,
};
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
// 4.5.2). Generated by G7 from the field list ComputePipelineStateHash already hashes;
// MGPipeRenderStateSpans.cpp and the setter-consistency test land with P2, which is
// when the chunk table can be filled with real offsets.
struct MGPipeRenderStateSpans;
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
#include "PipeCalls.def"
// G1: the two interface tables. A null entry means "not implemented" (section 4.1).
#include "generated/PipeTables.inc"
// The installed tables. Zero-initialized, so an un-installed MGPipe is every entry
// null - which is precisely the pre-migration state.
inline MGPipeScreen gMGPipeScreen{};
inline MGPipeContext gMGPipeContext{};
// G2: monolith thunks. These are what MG_Impl call sites move onto, replacing
// gBackendFunctionsTable.GL.* one name at a time.
#include "generated/PipeThunks.inc"
// G3: wire records, their size assertions, and the applier's bounds precondition.
#include "generated/PipeWire.inc"
// G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.
#include "generated/PipeVerify.inc"
// G5: PipeInputs field ids and the per-verb poison generations.
#include "generated/PipeFilled.inc"
// G6: the backend read inventory's coverage table.
#include "generated/PipeCoverage.inc"
// G7: the render-state pipeline subset, by member name.
#include "generated/PipeSpanTable.inc"
} // namespace MobileGL::MG_Pipe
-61
View File
@@ -1,61 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeCallbacks.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "MGPipeHandles.h"
#include "MGPipeTypes.h"
// The backend -> frontend reverse channel, named (plan B section 7.1).
//
// Today this traffic is 95 call sites across 17 methods poked directly into frontend
// objects. gallium has no vocabulary for shadow writeback, GPU-write notification, texture
// re-send requests or default-framebuffer geometry, because in Mesa the state tracker and
// the driver share an address space. Naming them as ten callbacks plus one forward
// terminator (MGPipeContext::ResourceSubDataComplete) is the deliberate deviation (D8).
//
// Installed at context creation. In a monolith these are direct calls; under split they are
// records on the reverse channel, and their ORDER is a correctness requirement rather than
// an optimization (section 7.4).
namespace MobileGL::MG_Pipe {
struct MGPipeCallbacks {
// A driver-detected GL error that only the server could have seen.
void (*OnGlError)(Uint32 code);
// Ranges of a resource the GPU wrote; retires MarkGpuWritten.
void (*OnGpuWritten)(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges);
void (*OnBufferWriteback)(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes);
void (*OnTextureWriteback)(MGPipeHandle res, const MGPBox* box, MGPBlobRef bytes);
// The one new stall class in this design (D-B6): the server recast a texture and
// needs its texels back. The client answers with zero or more ResourceSubData
// records terminated by ResourceSubDataComplete carrying the same pullSerial.
void (*OnTexturePullRequest)(MGPipeHandle res, Uint16 target, Uint16 firstLevel, Uint16 levelCount,
Uint64 pullSerial);
// SHAPE ONLY, never bytes: the client owns the CPU shadow and allocates the levels
// itself.
void (*OnMipLevelsGenerated)(MGPipeHandle res, Uint16 base, Uint16 count);
// Retires the layering inversion where the swapchain writes into MG_Impl's
// pDefaultFramebufferInfo.
void (*OnSurfaceChanged)(const MGPSurfaceInfo* info);
void (*OnCapsInvalidated)();
// <= WARN is lossy, >= ERROR is lossless and rate limited.
void (*OnLog)(Uint8 level, const char* text);
// The XFB scatter is a read-modify-write of the CLIENT's shadow, so the server
// hands back the packed scratch and the client scatters (section 7.2.1).
void (*OnXfbScatterReady)(MGPipeHandle scratch, Uint64 packedStride, Uint64 vertices);
};
// Ten, and the count is asserted so an eleventh cannot be added without touching the
// transport's reverse-channel record table.
inline constexpr SizeT kMGPipeCallbackCount = 10;
static_assert(sizeof(MGPipeCallbacks) == kMGPipeCallbackCount * sizeof(void (*)()),
"MGPipeCallbacks gained or lost a callback");
// Null-initialized: a backend that installs nothing sends nothing.
inline MGPipeCallbacks gMGPipeCallbacks{};
} // namespace MobileGL::MG_Pipe
-98
View File
@@ -1,98 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeHandles.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
// MGPipe object identity (plan B section 4.2).
//
// A handle is a {slot, gen} pair minted by the CLIENT and never by the server: no create_*
// call in the catalogue returns a server-cast handle, which is the deliberate deviation
// from gallium (D1) that lets the whole catalogue be remoted with ZERO creation round
// trips.
//
// Slots are dense and allocated PER KIND, so the server's object table is an array rather
// than a hash map. The allocator is a free list plus a high-water mark and has nothing to
// do with MG_State's IndexGenerator - that container's LIFO name reuse is the very problem
// {slot, gen} exists to close.
namespace MobileGL::MG_Pipe {
enum class MGPipeKind : Uint8 {
None = 0,
Buffer = 1,
Texture,
Renderbuffer,
Framebuffer,
Xfb,
RenderStateCso,
VertexElementsCso,
SamplerCso,
SamplerViewCso,
ShaderCso,
Fence,
Query,
Context,
KindCount,
};
// 8 bytes, POD, passed by value in a register pair.
//
// Gen increments only when a SLOT IS REUSED - never on a respecify - so {slot, gen} is
// unique until the same slot has been recycled 2^32 times. That bound is documented
// rather than defended at runtime in release builds: at one recycle per frame at
// 1000 fps a single slot would take ~50 days of continuous churn to wrap, and the
// debug allocator asserts on the wrap.
//
// Two generations exist in this design and they are strictly separate (section 4.2.2):
// this one is the CLIENT's answer to "is this still the same GL object", while MGGen is
// the SERVER's own epoch for "did I recast my driver object". Interface rule: no MGPipe
// call may require the client to supply or know MGGen.
struct MGPipeHandle {
Uint32 Slot;
Uint32 Gen;
friend constexpr Bool operator==(const MGPipeHandle& a, const MGPipeHandle& b) {
return a.Slot == b.Slot && a.Gen == b.Gen;
}
};
static_assert(sizeof(MGPipeHandle) == 8, "MGPipeHandle is the 8-byte {slot, gen} pair");
static_assert(alignof(MGPipeHandle) == 4, "MGPipeHandle must not gain padding on the wire");
static_assert(std::is_trivially_copyable_v<MGPipeHandle>);
// Reserved handles (section 4.2.1).
// {0, 0} is null for every kind.
// {0, 1} of kind Framebuffer is the DEFAULT framebuffer. It exists so the four
// pDefaultFramebufferInfo->defaultFBO identity comparisons in DirectGLES retire into
// an ordinary handle compare.
inline constexpr MGPipeHandle kMGPipeNullHandle{0, 0};
inline constexpr MGPipeHandle kMGPipeDefaultFramebuffer{0, 1};
inline constexpr Bool MGPipeHandleIsNull(const MGPipeHandle& handle) {
return handle.Slot == 0 && handle.Gen == 0;
}
// Slot 0 of every kind is reserved (null, and the default framebuffer for kind
// Framebuffer), so a real allocation starts at 1.
inline constexpr Uint32 kMGPipeFirstAllocatableSlot = 1;
// ShaderCso slot space. The top 1/16 of it is reserved for PROGRAM PIPELINE COMPOSITES
// (section 5.6.3): a composite is minted client-side out of the stage programs bound to
// a pipeline object, and the server never learns it is a composite - it is just another
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
// lifetime bookkeeping out of the ordinary program slot allocator.
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
inline constexpr Bool MGPipeIsCompositeShaderSlot(Uint32 slot) {
return slot >= kMGPipeShaderCsoCompositeSlotBase && slot < kMGPipeShaderCsoSlotLimit;
}
static_assert(kMGPipeShaderCsoCompositeSlotBase > kMGPipeFirstAllocatableSlot,
"the composite band must not swallow the ordinary program slots");
} // namespace MobileGL::MG_Pipe
-57
View File
@@ -1,57 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeHostSpan.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
// The ONE thing in MGPipe whose shape changes with the transport (plan B section 4.5.7).
//
// Monolith: Ptr addresses the frontend shadow or the application's own memory and the
// accessor is one predictable branch. Split: Ptr is null and the bytes live in a staging
// segment named by Seg/Offset, or - for the index bytes a server-side primitive-restart
// rewrite or multi-draw flattening consumes - in the server's own index host mirror, which
// costs no wire traffic at all (D-B7).
namespace MobileGL::MG_Pipe {
// Seg sentinels. Anything else is a real SEG_STAGE id assigned by the transport.
inline constexpr Uint32 kMGHostSpanSegNone = 0;
// "The bytes are already on your side": the server reads them out of the index host
// mirror it maintains for every resource created with the ELEMENT_ARRAY bind bit while
// kCapNeedsHostIndexBytes is set. When the mirror is over budget the tracker degrades
// to per-draw staging and counts the bytes in index-bytes-shipped.
inline constexpr Uint32 kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFFu;
struct MGHostSpan {
// Field order is chosen so the struct is 32 bytes with natural alignment on both a
// 64-bit and a 32-bit host: the pointer and the two 32-bit words fill the first
// 16-byte block either way.
const void* Ptr;
Uint32 Seg;
Uint32 Pad0;
Uint64 Size;
Uint64 Offset;
};
static_assert(sizeof(MGHostSpan) == 32, "MGHostSpan is the 32-byte host-bytes descriptor");
static_assert(std::is_trivially_copyable_v<MGHostSpan>);
// Split-mode resolution needs the transport's segment table, which does not exist in a
// monolith build; the hook is a weak-ish indirection installed by MG_Remote when it is
// compiled in. In P0 there is no transport, so a span that names a segment resolves to
// null and every caller is still on the monolith branch.
using MGPipeSegmentResolver = const void* (*)(Uint32 seg, Uint64 offset, Uint64 size);
inline MGPipeSegmentResolver gMGPipeSegmentResolver = nullptr;
// One predictable branch on the hot path.
inline const void* MGPipeHostBytes(const MGHostSpan& span) {
if (span.Ptr != nullptr) {
return static_cast<const Uint8*>(span.Ptr) + span.Offset;
}
if (gMGPipeSegmentResolver == nullptr) return nullptr;
return gMGPipeSegmentResolver(span.Seg, span.Offset, span.Size);
}
} // namespace MobileGL::MG_Pipe
-806
View File
@@ -1,806 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeTypes.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "MGPipeHandles.h"
#include "MGPipeHostSpan.h"
// Every MGPipe payload (plan B section 4.5). Each one is a flat POD with explicit padding,
// carries a static_assert on trivial copyability and one on its exact size, and never
// contains a pointer: MGHostSpan, the one shape that changes with the transport, only ever
// rides in a variable tail (draw_vbo's user indices, set_shader_buffers' named-UBO bytes),
// never inline in a fixed payload.
//
// Sizes are asserted rather than merely documented because the wire records generated from
// these structs (generated/PipeWire.inc) are memcpy'd; a field silently changing width is a
// protocol break that no test would otherwise see.
//
// P0.5 DEBT, recorded here so it is impossible to miss: two payloads reach into headers
// this directory is eventually forbidden to see - MGPCaps embeds MG_Backend's
// DynamicBackendParameters, and ResidualValueBlock embeds MG_State's RenderStateParameters
// and PixelStoreParameters. Both are deliberate: the caps block IS that struct (section
// 4.4.1) and the residual block is the migration carrier for Track V (section 6.3). P0.5
// extracts MGPipeValueTypes.h and both includes below go away; until then purity gate A
// (section 10.3) cannot be armed for this header.
#include <MG_Backend/BackendObject.h>
#include <MG_State/GLState/RenderState/RenderState.h>
namespace MobileGL::MG_Pipe {
using MG_Backend::DynamicBackendParameters;
// Both live directly in namespace MobileGL today; P0.5 moves them into
// MG_Pipe/MGPipeValueTypes.h.
using MobileGL::PixelStoreParameters;
using MobileGL::RenderStateParameters;
// A payload must be memcpy-able and its size must be an exact, stated number.
#define MGP_ASSERT_POD(T, Size) \
static_assert(std::is_trivially_copyable_v<T>, #T " must be trivially copyable"); \
static_assert(sizeof(T) == (Size), #T " changed size; update the wire format and this assertion")
// ---------------------------------------------------------------------------------
// Shared primitives
// ---------------------------------------------------------------------------------
// A run of bytes in the command stream's blob area. Monolith: Seg is
// kMGHostSpanSegNone and Offset is an address into the caller's staging arena. Split:
// Seg names a transport segment.
struct MGPBlobRef {
Uint64 Offset;
Uint64 Size;
Uint32 Seg;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPBlobRef, 24);
struct MGPRange {
Uint64 Offset;
Uint64 Size;
};
MGP_ASSERT_POD(MGPRange, 16);
// Destination box in the level's own coordinate system (section 4.5.6).
struct MGPBox {
Int32 X, Y, Z;
Uint32 W, H, D;
};
MGP_ASSERT_POD(MGPBox, 24);
// Where an asynchronous answer lands. Every server query in this catalogue is
// async-with-handle; none of them blocks (section 4.4.6, "the total rule").
struct MGPReplySlot {
Uint64 Id;
};
MGP_ASSERT_POD(MGPReplySlot, 8);
// One contiguous run of RenderStateParameters bytes. The pipeline/dynamic split is
// defined exactly once, in MGPipeRenderStateSpans, and generated by G7 from the field
// list VulkanRenderer::ComputePipelineStateHash already hashes (section 4.5.2).
struct MGPStateChunk {
Uint16 Offset;
Uint16 Length;
};
MGP_ASSERT_POD(MGPStateChunk, 4);
// The payload of every call that carries nothing but an object identity.
struct MGPHandleOnly {
MGPipeHandle Handle;
Uint32 Kind; // MGPipeKind, widened for a stable wire size
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPHandleOnly, 16);
// ---------------------------------------------------------------------------------
// Screen: caps, resources, fences
// ---------------------------------------------------------------------------------
// Capability bits that replace "is this table slot null" as an implicit feature probe
// (section 4.4.1). The five ownership-switch bits of v1 are deliberately absent: what
// they tried to express - who performs primitive-restart rewriting and multi-draw
// flattening - is not expressible as a capability (D-B7).
enum MGPCapBit : Uint64 {
kCapNone = 0,
kCapViewportArray = 1ull << 0,
kCapFloat64VertexAttrib = 1ull << 1,
kCapResidentSubData = 1ull << 2,
kCapCpuXfbPrimitiveAccounting = 1ull << 3,
kCapTimerQuery = 1ull << 4,
kCapOcclusionQuery = 1ull << 5,
kCapXfbPrimitivesQuery = 1ull << 6,
// The server rewrites restart indices / flattens multi-draws itself and therefore
// needs the index bytes on its side: under split this arms the index host mirror
// (D-B7).
kCapNeedsHostIndexBytes = 1ull << 7,
// 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).
kCapNeedsHostUboBytes = 1ull << 8,
};
struct MGPCaps {
// The ~90 flat scalars the backends already publish, by inclusion rather than by
// restatement: a caps field added there must not need a second edit here. This is
// also where the six per-axis compute limits (MaxComputeWorkGroupCount/Size) ride -
// the only indexed answers the device owns, and therefore the only ones that outlive
// the GetIntegeri_v table entry (see the PipeCalls.def footer).
DynamicBackendParameters Dynamic;
Uint64 CallMask; // MGPCapBit
// The two halves that are not flat PODs travel as blobs: the format capability
// cache holds Vector<Int> sample-count lists, and the renderer strings are
// Strings. Their serializers land with the transport (P5).
MGPBlobRef FormatCapabilities;
MGPBlobRef RendererInfo;
};
static_assert(std::is_trivially_copyable_v<MGPCaps>, "MGPCaps must be trivially copyable");
// Stated as a COMPOSITION rather than a literal: DynamicBackendParameters still carries
// SizeT fields, so its literal size is ABI-dependent until P0.5 moves the caps block
// into MGPipeValueTypes.h with fixed-width members. The assertion still fires on any
// padding introduced between the members below.
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
"MGPCaps gained padding or a member; update the wire format");
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
// share one create/respecify shape (section 4.5.1).
struct MGPResourceDesc {
MGPipeHandle Resource;
Uint8 Target; // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The ELEMENT_ARRAY bit is the
// D-B7 switch: with kCapNeedsHostIndexBytes set the server mirrors this resource.
Uint16 BindMask;
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
Uint32 Width, Height, Depth;
Uint16 ArrayLayers, Levels, Samples;
Uint8 FixedSampleLocations, Immutable;
Uint32 Usage; // BufferUsage
Uint32 StorageFlags; // glBufferStorage flags
Uint8 HasDefinedContent; // false after a NULL-data respecify
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
Uint16 Pad0;
// Diagnostics only. A GL name is NEVER an identity, never a memo key and never part
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
// cannot hold one.
Uint32 GlNameForDiag;
Uint32 Pad1;
MGPipeHandle ViewOf; // storage owner for a texture view
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
};
MGP_ASSERT_POD(MGPResourceDesc, 88);
inline constexpr Uint64 kMGPipeWholeBuffer = ~0ull;
struct MGPFenceWait {
MGPipeHandle Fence;
Uint64 TimeoutNs;
};
MGP_ASSERT_POD(MGPFenceWait, 16);
struct MGPQueryDesc {
MGPipeHandle Query;
Uint32 Kind; // GL query target
Uint32 Stream; // indexed query stream, 0 otherwise
};
MGP_ASSERT_POD(MGPQueryDesc, 16);
struct MGPQueryResultRequest {
MGPipeHandle Query;
Uint8 Wait; // the two-value contract of GetSyncStatus is preserved verbatim
Uint8 Pad0[3];
Uint32 Pad1;
};
MGP_ASSERT_POD(MGPQueryResultRequest, 16);
// query_timestamp: glGetInteger64v(GL_TIMESTAMP), the synchronous "what time is it on the
// GPU" GLFunctionsTable::GetGpuTimestampNs answers today. The request names nothing; the
// Int64 nanosecond stamp comes back through the reply slot.
struct MGPTimestampRequest {
Uint32 Reserved;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPTimestampRequest, 8);
// ---------------------------------------------------------------------------------
// CSOs
// ---------------------------------------------------------------------------------
// create_render_state carries ONLY the pipeline subset's chunk bytes. chunkMask lets an
// incremental create send just the chunks that moved, against baseCso (section 4.5.2).
struct MGPRenderStateDesc {
MGPipeHandle Cso;
MGPipeHandle BaseCso;
Uint32 ChunkMask; // all ones for a brand new CSO
Uint32 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPRenderStateDesc, 48);
// Steady state: 12 bytes on the wire, no hashing, no blob.
struct MGPBindRenderState {
MGPipeHandle Cso;
Uint16 Version;
Uint16 PipelineVersion;
};
MGP_ASSERT_POD(MGPBindRenderState, 12);
// The half of the render state that must NOT mint a CSO: viewport, scissor, depth
// range, blend colour, line width, polygon offset, stencil ref/write mask, clear
// values, sample coverage, hints and the point-size family. This is what keeps
// glViewport from evicting Magma's pipeline memo (D-B1).
struct MGPDynamicState {
Uint32 ChunkMask;
Uint16 Version;
Uint16 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPDynamicState, 32);
// Both views travel, and neither is derivable from the other: the resolved
// VertexAttribute[32] AND the binding points, because a pointer-call stride of 0 means
// "element size" while a binding-model stride of 0 means "every vertex reads the same
// element" (section 4.5.3). IsLong and Type == Float64 are carried separately.
struct MGPVertexElements {
MGPipeHandle Cso;
Uint32 AttributeCount;
Uint32 BindingPointCount;
MGPBlobRef Blob; // VertexAttribute[] followed by VertexBufferBindingPoint[]
};
MGP_ASSERT_POD(MGPVertexElements, 40);
// SamplerParameters crosses byte for byte INCLUDING borderColorForm: without it the
// backend cannot choose between glSamplerParameterIiv and fv, or between the
// VkBorderColor families, because all three representations are always numerically
// populated (section 4.5.4). Carried as a blob until P0.5 gives it a value header.
struct MGPSamplerDesc {
MGPipeHandle Cso;
MGPBlobRef Parameters;
};
MGP_ASSERT_POD(MGPSamplerDesc, 32);
// = pipe_sampler_view, and ONLY the view restrictions. Everything a glTexParameter
// writes lives on set_texture_params instead, because a texture that is only an FBO
// attachment, only an image binding or only a glCopyImageSubData endpoint has no
// sampler view to hang it on (section 4.4.3).
struct MGPSamplerView {
MGPipeHandle Cso;
MGPipeHandle Texture;
Uint32 InternalFormat; // aliasing format for glTextureView
Uint8 Target;
Uint8 Pad0[3];
Uint16 MinLevel, NumLevels, MinLayer, NumLayers;
Uint16 Samples;
Uint8 FixedSampleLocations;
Uint8 Pad1;
};
MGP_ASSERT_POD(MGPSamplerView, 36);
// Per texture OBJECT, independent of any view.
struct MGPTextureParams {
MGPipeHandle Res;
Uint16 BaseLevel, MaxLevel;
Uint8 Swizzle[4];
Uint8 DepthStencilMode;
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
// override that the frontend params version does not move for.
Uint8 ForceResync;
Uint8 Pad0[2];
Float MinLod, MaxLod, LodBias;
};
MGP_ASSERT_POD(MGPTextureParams, 32);
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
// deserialize into them without dragging in glslang (section 4.5.5).
struct MGPProgramDesc {
MGPipeHandle Cso;
Uint32 StageMask; // == GetLinkedShaderStages()
Uint32 GlobalUboSize;
Uint32 ReservedNumSamplesOffset;
Uint8 SpirvStatus;
Uint8 NativeFloat64;
Uint8 PointSizeDemoted;
Uint8 EnableSpirvValidation;
MGPBlobRef Spirv[6]; // per stage
MGPBlobRef Reflection;
};
MGP_ASSERT_POD(MGPProgramDesc, 192);
// ---------------------------------------------------------------------------------
// set_*
// ---------------------------------------------------------------------------------
// = pipe_surface. internalFormat is INLINE so the four cross-object masks fall out at
// push time with no lookup (section 4.5.6).
struct MGPSurface {
MGPipeHandle Res;
Uint32 InternalFormat;
Uint8 Kind; // Texture | Renderbuffer | None
Uint8 Layered;
Uint16 Level;
Uint32 Layer;
Uint16 UploadTarget;
Uint16 Pad0;
};
MGP_ASSERT_POD(MGPSurface, 24);
struct MGPFramebufferState {
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
MGPSurface Color[8];
MGPSurface Depth, Stencil;
// The RESOLVED read surface, not an index. This is what structurally closes the
// read-buffer-shared-FBO defect class.
MGPSurface ReadSurface;
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
Uint16 Width, Height, Layers, Samples;
Uint8 FixedSampleLocations, IsDefault, Complete, Pad0;
Uint32 Pad1;
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
// emission suppressor - an unchanged hash means this record is not sent at all.
// The same pattern is mandatory for every kVarTail set_* below, or 26.2's
// redundant glBindSampler traffic reappears as a variable-length record per batch.
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPFramebufferState, 304);
struct MGPVertexBuffer {
MGPipeHandle Res;
Uint64 Offset;
Uint32 Stride;
Uint32 Divisor;
Uint32 BindingIndex;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPVertexBuffer, 32);
// Var-tail header: MGPVertexBuffer[Count] follows.
struct MGPVertexBuffers {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPVertexBuffers, 16);
// An independent call, NOT a subset of the VAO configuration version (D5).
struct MGPIndexBuffer {
MGPipeHandle Res;
Uint64 Offset;
Uint32 IndexSize;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPIndexBuffer, 24);
struct MGPIndirectBuffers {
MGPipeHandle DrawIndirect;
MGPipeHandle Parameter;
};
MGP_ASSERT_POD(MGPIndirectBuffers, 16);
// One entry of set_sampler_views. No stage dimension: MobileGL's texture unit space is
// MERGED (TextureState::m_textureUnits is one Array of MAX_TEXTURE_IMAGE_UNITS = 192),
// and the same unit may be sampled from two stages (section 4.4.3).
struct MGPBoundView {
MGPipeHandle View;
MGPipeHandle Texture;
Uint32 Unit;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPBoundView, 24);
struct MGPSamplerViews {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPSamplerViews, 16);
// Var-tail header: MGPipeHandle[Count] of sampler CSOs follows.
struct MGPSamplerStates {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPSamplerStates, 16);
struct MGPImageView {
MGPipeHandle Res;
Uint32 Unit;
Uint32 InternalFormat;
Uint32 Layer;
Uint16 Level;
Uint8 Layered;
Uint8 Access;
};
MGP_ASSERT_POD(MGPImageView, 24);
struct MGPShaderImages {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPShaderImages, 16);
// One bound buffer range: 24 bytes, no inline host span. The named-UBO host bytes a
// backend needs under kCapNeedsHostUboBytes (D-B8) travel as an OPTIONAL second var-tail,
// MGHostSpan[HostSpanCount] behind the ranges, announced by MGPShaderBuffers below. An
// inline span would have cost every SSBO, atomic-counter and XFB range 32 dead bytes, and
// D-B8 says not to freeze that payload's shape before the stage-ubo-named counter has
// produced numbers.
struct MGPBufferRange {
MGPipeHandle Res;
Uint64 Offset;
Uint64 Size;
};
MGP_ASSERT_POD(MGPBufferRange, 24);
// Var-tail header: MGPBufferRange[Count], then MGHostSpan[HostSpanCount]. HostSpanCount is
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (a range with nothing to
// ship carries an empty span, so the two arrays stay index-aligned).
struct MGPShaderBuffers {
Uint32 Class; // Uniform | ShaderStorage | AtomicCounter
Uint32 Start;
Uint32 Count;
Uint32 WritableMask;
Uint32 HostSpanCount; // 0, or Count when the kHostSpan tail is present (D-B8)
Uint32 Pad0;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPShaderBuffers, 32);
// Var-tail header: MGPBufferRange[Count] then Uint32 offsets[Count].
struct MGPStreamOutputTargets {
Uint32 Count;
Uint32 Pad0;
Uint64 Generation;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPStreamOutputTargets, 24);
// Covers the DEFAULT UNIFORM BLOCK only (D6).
struct MGPGlobalConstants {
MGPipeHandle ShaderCso;
Uint32 Version;
Uint32 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPGlobalConstants, 40);
// The float/int/uint view is resolved on the CLIENT by ClassifyVertexAttribType.
struct MGPAttribValue {
Uint32 Location;
Uint8 ValueClass; // Float | Int | Uint | Double
Uint8 Pad0[3];
Uint32 Data[4];
};
MGP_ASSERT_POD(MGPAttribValue, 24);
// Var-tail header: MGPAttribValue[popcount(Mask)] follows.
struct MGPVertexAttribDefaults {
Uint32 Mask;
Uint32 Count;
};
MGP_ASSERT_POD(MGPVertexAttribDefaults, 8);
// PACK only. There is deliberately no unpack counterpart: nothing on the far side of
// the boundary reads unpack state (section 4.6 D5), and the staged-repack upload path
// does not even issue glPixelStorei.
struct MGPPixelPackState {
PixelStoreParameters Pack;
};
static_assert(std::is_trivially_copyable_v<MGPPixelPackState>);
// 28 is what PixelStoreParameters measures: two Bools, two bytes of padding, six Ints.
// Asserting against sizeof(PixelStoreParameters) itself was a tautology that could not
// notice the value struct changing width under the wire format.
static_assert(sizeof(MGPPixelPackState) == 28,
"MGPPixelPackState changed size; update the wire format and this assertion");
// Also a shader-variant input: both backends bake these into the synthesized
// pass-through control stage.
struct MGPPatchState {
Uint32 Vertices;
Uint32 Pad0;
Float Outer[4];
Float Inner[2];
Uint32 Pad1[2];
};
MGP_ASSERT_POD(MGPPatchState, 40);
// Migration-only (section 6.3). Every stage removes fields and lowers
// MGL_RESIDUAL_BLOCK_SIZE; P13 asserts it is zero, which is the retirement trip wire.
//
// Layout must be asserted MEMBER BY MEMBER, not only by sizeof: a heterogeneous POD
// union is where padding differs across ABIs, and the monolith verify harness is blind
// to it because both sides are the same translation unit. G3 emits the offsetof
// assertions; under split the block is serialized field-wise rather than memcpy'd.
struct ResidualValueBlock {
RenderStateParameters RenderState; // until create/bind_render_state + set_dynamic_state land
PixelStoreParameters Pack; // until set_pixel_pack_state lands
Uint64 CapabilityBits;
Uint32 PatchVertices;
Uint32 Pad0;
Float PatchOuter[4];
Float PatchInner[2];
Uint32 Pad1[2];
};
static_assert(std::is_trivially_copyable_v<ResidualValueBlock>);
// The retirement ratchet. This number only ever goes DOWN: every stage that lands a real
// set_* call deletes fields here and lowers it, and P13 replaces it with
// static_assert(sizeof(ResidualValueBlock) == 0), which stays red until the last field is
// gone. Shrinking the block without lowering the number, or growing it at all, is a build
// break - which is the point.
//
// Stable across the ABIs MobileGL ships on: every member of RenderStateParameters and
// PixelStoreParameters is a fixed-width scalar or an array of one, with no pointer and no
// SizeT.
#define MGL_RESIDUAL_BLOCK_SIZE 1248
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
"retired, and do not raise it");
struct MGPResidualValueState {
Uint32 Version;
Uint32 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPResidualValueState, 32);
// ---------------------------------------------------------------------------------
// Transfer
// ---------------------------------------------------------------------------------
// Shape copied from the unpack ring's existing UnpackStagingBlock. The source strides
// are CARRIED, not inferred from a pointer comparison: the old
// `uploadData == mipData` test cannot survive a split, where the client neither ships
// the whole level nor keeps a server-side mirror of it (section 4.5.6).
struct MGPSubRegion {
Int32 X, Y, Z;
Uint32 W, H, D;
Uint64 SrcOffset; // into the blob
Uint32 SrcRowStride; // bytes; 0 = tightly packed (w * bpp)
Uint32 SrcSliceStride; // bytes; 0 = tightly packed
};
MGP_ASSERT_POD(MGPSubRegion, 40);
// Carries the union box AND the region list so the SERVER picks the upload shape - the
// decision belongs on the side that pays the GPU cost. Mali prices texture upload by
// JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame.
//
// THE BUFFER HALF. With Target == Buffer there is no level and no box, so the destination
// byte range rides in the box's first coordinate and first extent: UnionBox.X is the byte
// offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0, and
// Blob holds exactly Size source bytes. That caps ONE record at a 2^31-1 offset and a
// 2^32-1 size; a range beyond either is split by the emitter - the same rule, and at
// SEG_STAGE's 32 MiB the far tighter one, that the ring's half-capacity bound already
// imposes on it. MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size below are
// the only spelling of this convention; nothing else reads the box for a buffer.
struct MGPSubData {
MGPipeHandle Res;
Uint16 Target, Level;
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
// bytes an untransformed level shadow?
Uint8 SourceIsVerbatimLevelShadow;
Uint8 Pad0[3];
MGPBox UnionBox;
Uint32 RegionCount; // MGPSubRegion[] in the variable tail
Uint32 Pad1;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPSubData, 72);
// Encodes a buffer byte range into the record's box. False, with the record untouched,
// when the range does not fit one record: the emitter has to split it.
inline Bool MGPipeSetSubDataBufferRange(MGPSubData& record, Uint64 offset, Uint64 size) {
if (offset > 0x7FFFFFFFull || size > 0xFFFFFFFFull) {
return false;
}
record.UnionBox = MGPBox{static_cast<Int32>(offset), 0, 0, static_cast<Uint32>(size), 1, 1};
record.Level = 0;
record.RegionCount = 0;
return true;
}
inline Uint64 MGPipeSubDataBufferOffset(const MGPSubData& record) {
// A negative X is a corrupt record (the encoder never writes one); read as unsigned
// it lands above the encodable bound, which the applier's bounds gate refuses.
return static_cast<Uint64>(static_cast<Uint32>(record.UnionBox.X));
}
inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; }
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
// zero regions - that is how a pull that needs nothing is answered.
struct MGPSubDataComplete {
MGPipeHandle Res;
Uint16 Target, FirstLevel, LevelCount, Pad0;
Uint64 PullSerial;
};
MGP_ASSERT_POD(MGPSubDataComplete, 24);
// Carries the application's REAL access flags, not a normalized subset.
struct MGPFlushRange {
MGPipeHandle Res;
Uint64 Offset, Size;
Uint32 AccessFlags; // Flags<BufferMappingAccessBit>
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPFlushRange, 32);
struct MGPReadback {
MGPipeHandle Res;
Uint64 Offset, Size;
};
MGP_ASSERT_POD(MGPReadback, 24);
struct MGPCopyRegion {
MGPipeHandle Src, Dst;
MGPBox SrcBox;
Int32 DstX, DstY, DstZ;
Uint16 SrcTarget, DstTarget;
Uint16 SrcLevel, DstLevel;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPCopyRegion, 64);
struct MGPBlit {
MGPipeHandle ReadFbo, DrawFbo;
Int32 SrcX0, SrcY0, SrcX1, SrcY1;
Int32 DstX0, DstY0, DstX1, DstY1;
Uint32 Mask;
Uint32 Filter;
};
MGP_ASSERT_POD(MGPBlit, 56);
// One discriminated record replacing glClear, the four glClearBuffer* and the four
// glClearNamedFramebuffer* entry points (section 4.4.4).
struct MGPClear {
MGPipeHandle Fbo;
Uint32 Kind; // Whole | Color | Depth | Stencil | DepthStencil
Int32 DrawBufferIndex;
Uint32 BufferMask; // GL_COLOR_BUFFER_BIT etc. for the whole-framebuffer form
Uint32 ValueClass; // Float | Int | Uint
Uint32 ColorValue[4];
Float DepthValue;
Int32 StencilValue;
};
MGP_ASSERT_POD(MGPClear, 48);
struct MGPMipPlan {
MGPipeHandle Res;
Uint16 Target, BaseLevel, LevelCount, Pad0;
};
MGP_ASSERT_POD(MGPMipPlan, 16);
// read_pixels and get_texture_image share one shape; both answer into a reply slot.
struct MGPReadbackInfo {
MGPipeHandle Res; // null for read_pixels: the bound read surface answers
MGPBox Box;
Uint32 Format, Type;
Uint16 Target, Level;
Uint32 Pad0;
Uint64 DstOffset, DstSize;
};
MGP_ASSERT_POD(MGPReadbackInfo, 64);
// ---------------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------------
enum MGPDrawFlagBit : Uint8 {
kDrawHasUserIndices = 1u << 0,
kDrawPrimitiveRestart = 1u << 1,
kDrawIndicesAreClient = 1u << 2,
kDrawHasIndexRange = 1u << 3,
kDrawHasXfbCount = 1u << 4,
};
// = pipe_draw_info. Today's twenty draw entry points collapse onto this one call, with
// MGPDrawRange[] holding exactly the shape the glMultiDraw* family already has.
//
// minIndex/maxIndex are computed only on the client-memory array path today, and
// xfbCpuCapturedVertices only on the XFB scatter path, so Flags gates the WORK. They
// stay in the fixed head; moving them into the variable tail is a wire-format decision
// that belongs with the transport (P5), where per-draw byte histograms exist to size
// it. userIndices is in the variable tail already, so the VBO path - every Minecraft
// and Sodium draw - never pays the 32 bytes of an MGHostSpan.
struct MGPDrawInfo {
Uint32 Mode;
Uint8 IndexSize; // 0 = arrays, else 1 / 2 / 4
Uint8 Flags; // MGPDrawFlagBit
Uint16 Pad0;
Uint32 InstanceCount, StartInstance;
Uint32 RestartIndex;
Uint32 DrawIdOffset;
MGPipeHandle IndexResource;
Uint32 MinIndex, MaxIndex; // ~0 = unknown
Uint64 XfbCpuCapturedVertices;
Uint32 NumDraws; // MGPDrawRange[] in the variable tail
Uint32 Pad1;
};
MGP_ASSERT_POD(MGPDrawInfo, 56);
// = pipe_draw_start_count_bias.
struct MGPDrawRange {
Uint32 Start, Count;
Int32 IndexBias;
};
MGP_ASSERT_POD(MGPDrawRange, 12);
// Present when the draw is indirect. The client resolves the COUNT itself, so the
// server never reads an indirect command block to learn how many draws there are.
struct MGPDrawIndirect {
MGPipeHandle Buffer;
MGPipeHandle ParameterBuffer;
Uint64 Offset, ParameterOffset;
Uint32 Stride, DrawCount;
};
MGP_ASSERT_POD(MGPDrawIndirect, 40);
struct MGPGridInfo {
Uint32 GridX, GridY, GridZ;
Uint32 BlockX, BlockY, BlockZ;
MGPipeHandle IndirectBuffer;
Uint64 IndirectOffset;
Uint8 IsIndirect;
Uint8 Pad0[7];
};
MGP_ASSERT_POD(MGPGridInfo, 48);
struct MGPMemoryBarrier {
Uint32 Bits; // GLbitfield
Uint8 ByRegion;
Uint8 Pad0[3];
};
MGP_ASSERT_POD(MGPMemoryBarrier, 8);
struct MGPStreamOutputBegin {
Uint32 PrimitiveMode;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPStreamOutputBegin, 8);
// end_stream_output carries the accounting the client owns; the scatter itself is a
// read-modify-write of the client's shadow and lives there (section 7.2.1).
struct MGPXfbAccounting {
Uint64 CapturedVertices;
Uint64 PrimitivesWritten;
Uint32 PrimitiveMode;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPXfbAccounting, 24);
struct MGPStreamOutputControl {
Uint32 Reserved;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPStreamOutputControl, 8);
struct MGPFlush {
Uint32 Flags;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPFlush, 8);
struct MGPPresent {
Uint64 FrameSerial;
};
MGP_ASSERT_POD(MGPPresent, 8);
struct MGPSwapInterval {
Int32 Interval;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPSwapInterval, 8);
// ---------------------------------------------------------------------------------
// Reverse channel payloads (section 7.1)
// ---------------------------------------------------------------------------------
struct MGPSurfaceInfo {
Uint32 Width, Height;
Uint32 InternalFormat;
Uint16 Samples, Layers;
Uint8 IsDefault;
Uint8 Pad0[7];
};
MGP_ASSERT_POD(MGPSurfaceInfo, 24);
#undef MGP_ASSERT_POD
} // namespace MobileGL::MG_Pipe
-177
View File
@@ -1,177 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/PipeCalls.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The single source of truth for the MGPipe call catalogue (plan B section 4.1 / 4.4 /
// appendix A). One line per call; seven generators consume this file
// (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test
// (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic.
//
// X(Name, PayloadStruct, Class, Flags)
// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
// kScreen lands in struct MGPipeScreen, every other class in struct
// MGPipeContext (plan section 4.3).
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
//
// RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their
// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new
// call is APPENDED to its group; a retired call keeps its slot with a comment. The wire
// opcode is the 1-based position in this list, so reordering is a protocol break.
//
// ---------------------------------------------------------------------------------------
// COUNTS. MGP_CALL_LIST_DOCUMENTED_COUNT below is the authority; PipeCatalogueTest asserts
// that the expansion, the two generated tables and this number agree.
//
// class entries group (as the plan tabulates it)
// kScreen 11 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
// appended server-side fence wait 1
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
// kCtxCso 13 CSO create/bind/delete
// kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state
// kCtxObject 9 set_texture_params (the 17th set_*) + 8 object-scoped transfers
// kCtxVerb 13 3 context-reading transfer calls + the 10 commands
// total 71
//
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
// do not add up to a set of UNIQUE records and this file has to hold unique records:
// - "screen 14" tabulates the fence and query families together with the screen block.
// Section 4.3 assigns the query NAMESPACE to the context ("VAO / FBO / XFB object /
// query namespaces, the command stream, present"), so the six query calls carry
// kCtxQuery and live in MGPipeContext. Screen keeps 10 of the plan's (11 with the appended
// FenceWaitServer, below). The eight EGL lifecycle entry points stay virtual functions on
// pActiveBackendObject and are deliberately NOT calls here (section 4.4.1, last row).
// - "CSO 15" is create/bind/delete x 5 kinds. Two of those binds are ALSO named in the
// set_* catalogue as their array forms - bind_sampler_states and set_sampler_views
// (section 4.4.3) - and a call may only exist once, so they are emitted under
// kCtxState and the CSO group holds 13: create/delete x 5 plus the three remaining
// binds (render state, vertex elements, shader).
// - "transfer 12" enumerates 11 calls in section 4.4.4 plus appendix A
// (resource_subdata, buffer_subdata_resident, resource_flush_range, resource_readback,
// resource_copy_region, blit, clear, generate_mipmap, read_pixels, get_texture_image,
// resource_subdata_complete). Eleven is what is emitted; the twelfth is not named
// anywhere in the plan.
// - "about 74 items" in section 4.1 is the sum of those headline numbers, so it inherits
// the same double counting. 68 unique records was the honest total of the plan's own
// catalogue.
// - Three LIVE GLFunctionsTable entries had no carrier in it at all: GetGpuTimestampNs
// (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp
// (glQueryCounter, a one-shot stamp rather than a begin/end pair) and WaitSync (the
// GPU-side wait, which FenceWait's client-side wait does not express). They are
// QueryTimestamp, QueryCounter and FenceWaitServer, APPENDED at the end of the list -
// not slotted into their groups - because the wire opcode is the position, so a record
// that arrives late goes last. 71 unique records.
// ---------------------------------------------------------------------------------------
#define MGP_CALL_LIST_DOCUMENTED_COUNT 71
// clang-format off
#define MGP_CALL_LIST(X) \
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNone) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
X(FenceCreate, MGPHandleOnly, kScreen, kNone) \
X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot) \
X(FenceWait, MGPFenceWait, kScreen, kReplySlot) \
X(FenceDestroy, MGPHandleOnly, kScreen, kNone) \
/* ---- context: query objects (plan 4.3 gives the namespace to the context) ---- */ \
X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot) \
X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot) \
X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone) \
/* ---- context: CSO create/bind/delete (plan 4.4.2, 4.5.2-4.5.5) ---- */ \
X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob) \
X(BindRenderState, MGPBindRenderState, kCtxCso, kNone) \
X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone) \
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kNone) \
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob) \
X(BindShaderState, MGPHandleOnly, kCtxCso, kNone) \
X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone) \
/* ---- context: set_* (plan 4.4.3) ---- */ \
X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob) \
X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone) \
X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail) \
X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone) \
X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone) \
X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail) \
X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail) \
X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail) \
X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan) \
X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail) \
X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob) \
X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail) \
X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone) \
X(SetPatchState, MGPPatchState, kCtxState, kNone) \
X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone) \
X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone) \
/* Migration-only carrier for Track V, retired field by field across P2..P13. Its */ \
/* retirement is a compile error: MGL_RESIDUAL_BLOCK_SIZE only ever goes DOWN and the */ \
/* final step asserts sizeof(ResidualValueBlock) == 0 (plan 6.3). */ \
X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob) \
/* ---- context: per-object state and transfer (plan 4.4.3 set_texture_params, 4.4.4) ---- */ \
X(SetTextureParams, MGPTextureParams, kCtxObject, kNone) \
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail) \
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone) \
X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot) \
/* ---- context: transfer calls that read whole-context state, and the commands ---- */ \
X(Blit, MGPBlit, kCtxVerb, kNone) \
X(Clear, MGPClear, kCtxVerb, kNone) \
X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot) \
X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail) \
X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone) \
X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone) \
X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone) \
X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone) \
X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
X(Flush, MGPFlush, kCtxVerb, kNone) \
X(Present, MGPPresent, kCtxVerb, kNone) \
X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional) \
/* ---- APPENDED. Opcodes are positional, so a late arrival goes at the END, never into ---- */ \
/* ---- its group: three live GLFunctionsTable entries the catalogue had no carrier for. ---- */ \
/* glGetInteger64v(GL_TIMESTAMP) - GetGpuTimestampNs, a synchronous server answer, which */ \
/* the reply slot carries. The query namespace is the context's (plan 4.3). */ \
X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot) \
/* glQueryCounter(GL_TIMESTAMP) - QueryCounterTimestamp, a one-shot stamp into a query */ \
/* object, NOT a begin/end pair. Kind carries GL_TIMESTAMP. */ \
X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone) \
/* glWaitSync - WaitSync, the GPU-side wait, distinct from FenceWait's client-side one. */ \
/* TimeoutNs is GL_TIMEOUT_IGNORED by contract. */ \
X(FenceWaitServer, MGPFenceWait, kScreen, kNone)
// clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
// - GetIntegeri_v / GetInteger64i_v. The six backend-owned answers they carry -
// GL_MAX_COMPUTE_WORK_GROUP_COUNT and GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each,
// the only indexed pnames the device rather than the frontend answers - live in MGPCaps
// as DynamicBackendParameters::MaxComputeWorkGroupCount / MaxComputeWorkGroupSize, filled
// by both backends at capability init (DirectGLES from glGetIntegeri_v, DirectVulkan from
// VkPhysicalDeviceLimits) and floored by the frontend. Every other indexed pname names
// frontend state and is answered before any table is consulted.
// - GetProgramiv. GL_COMPUTE_WORK_GROUP_SIZE is a FRONTEND link artifact
// (ProgramObject::GetComputeLocalSize, what GL_Program.cpp has always answered from), not
// a backend answer at all; nothing a backend knows about a program crosses this way.
// - ShaderStorageBlockBinding (folded into MGPProgramDesc's reflection archive),
// set_pixel_unpack_state (no such state crosses the line - plan 4.6 D5), a
// compressed-format concept, pipe_transfer, and the stage dimension of set_sampler_views
// (MobileGL's texture unit space is merged, not per stage - plan 4.4.3).
-238
View File
@@ -1,238 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/PipeFields.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Field lists for the G4 shadow comparator (plan B section 10.3-2). One macro per payload
// in MGPipeTypes.h, listing the fields that carry MEANING - padding is deliberately absent,
// because MOBILEGL_PIPE_VERIFY has to have ZERO false positives and a padding byte is
// exactly what makes a memcmp of RenderStateParameters false-DIFFER
// (DirectGLES.cpp documents that behaviour where it does the same comparison itself).
//
// Hand maintained alongside MGPipeTypes.h. Adding a field to a payload without adding it
// here makes the comparator blind to it; that gap closes in P1, when the verify harness
// goes live and the comparator's coverage is itself asserted.
//
// clang-format off
#define MGP_FIELDS_MGPBlobRef(F) \
F(Offset) F(Size) F(Seg)
#define MGP_FIELDS_MGPRange(F) \
F(Offset) F(Size)
#define MGP_FIELDS_MGPBox(F) \
F(X) F(Y) F(Z) F(W) F(H) F(D)
#define MGP_FIELDS_MGPReplySlot(F) \
F(Id)
#define MGP_FIELDS_MGPStateChunk(F) \
F(Offset) F(Length)
#define MGP_FIELDS_MGPHandleOnly(F) \
F(Handle) F(Kind)
#define MGP_FIELDS_MGPCaps(F) \
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
#define MGP_FIELDS_MGPResourceDesc(F) \
F(Resource) F(Target) F(StorageKind) F(BindMask) F(InternalFormat) F(Width) F(Height) F(Depth) \
F(ArrayLayers) F(Levels) F(Samples) F(FixedSampleLocations) F(Immutable) F(Usage) F(StorageFlags) \
F(HasDefinedContent) F(ImageBindableHint) F(GlNameForDiag) F(ViewOf) F(BufferForTexBuffer) \
F(BufOffset) F(BufSize)
#define MGP_FIELDS_MGPFenceWait(F) \
F(Fence) F(TimeoutNs)
#define MGP_FIELDS_MGPQueryDesc(F) \
F(Query) F(Kind) F(Stream)
#define MGP_FIELDS_MGPQueryResultRequest(F) \
F(Query) F(Wait)
#define MGP_FIELDS_MGPTimestampRequest(F) \
F(Reserved)
#define MGP_FIELDS_MGPRenderStateDesc(F) \
F(Cso) F(BaseCso) F(ChunkMask) F(Blob)
#define MGP_FIELDS_MGPBindRenderState(F) \
F(Cso) F(Version) F(PipelineVersion)
#define MGP_FIELDS_MGPDynamicState(F) \
F(ChunkMask) F(Version) F(Blob)
#define MGP_FIELDS_MGPVertexElements(F) \
F(Cso) F(AttributeCount) F(BindingPointCount) F(Blob)
#define MGP_FIELDS_MGPSamplerDesc(F) \
F(Cso) F(Parameters)
#define MGP_FIELDS_MGPSamplerView(F) \
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
F(Samples) F(FixedSampleLocations)
#define MGP_FIELDS_MGPTextureParams(F) \
F(Res) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) F(MinLod) F(MaxLod) \
F(LodBias)
#define MGP_FIELDS_MGPProgramDesc(F) \
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
#define MGP_FIELDS_MGPSurface(F) \
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget)
#define MGP_FIELDS_MGPFramebufferState(F) \
F(Fbo) F(Color) F(Depth) F(Stencil) F(ReadSurface) F(DrawBuffers) F(Width) F(Height) F(Layers) \
F(Samples) F(FixedSampleLocations) F(IsDefault) F(Complete) F(ContentHash)
#define MGP_FIELDS_MGPVertexBuffer(F) \
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
#define MGP_FIELDS_MGPVertexBuffers(F) \
F(Start) F(Count) F(ContentHash)
#define MGP_FIELDS_MGPIndexBuffer(F) \
F(Res) F(Offset) F(IndexSize)
#define MGP_FIELDS_MGPIndirectBuffers(F) \
F(DrawIndirect) F(Parameter)
#define MGP_FIELDS_MGPBoundView(F) \
F(View) F(Texture) F(Unit)
#define MGP_FIELDS_MGPSamplerViews(F) \
F(Start) F(Count) F(ContentHash)
#define MGP_FIELDS_MGPSamplerStates(F) \
F(Start) F(Count) F(ContentHash)
#define MGP_FIELDS_MGPImageView(F) \
F(Res) F(Unit) F(InternalFormat) F(Layer) F(Level) F(Layered) F(Access)
#define MGP_FIELDS_MGPShaderImages(F) \
F(Start) F(Count) F(ContentHash)
#define MGP_FIELDS_MGPBufferRange(F) \
F(Res) F(Offset) F(Size)
#define MGP_FIELDS_MGPShaderBuffers(F) \
F(Class) F(Start) F(Count) F(WritableMask) F(HostSpanCount) F(ContentHash)
#define MGP_FIELDS_MGPStreamOutputTargets(F) \
F(Count) F(Generation) F(ContentHash)
#define MGP_FIELDS_MGPGlobalConstants(F) \
F(ShaderCso) F(Version) F(Blob)
#define MGP_FIELDS_MGPAttribValue(F) \
F(Location) F(ValueClass) F(Data)
#define MGP_FIELDS_MGPVertexAttribDefaults(F) \
F(Mask) F(Count)
#define MGP_FIELDS_MGPPixelPackState(F) \
F(Pack)
#define MGP_FIELDS_MGPPatchState(F) \
F(Vertices) F(Outer) F(Inner)
#define MGP_FIELDS_ResidualValueBlock(F) \
F(RenderState) F(Pack) F(CapabilityBits) F(PatchVertices) F(PatchOuter) F(PatchInner)
#define MGP_FIELDS_MGPResidualValueState(F) \
F(Version) F(Blob)
#define MGP_FIELDS_MGPSubRegion(F) \
F(X) F(Y) F(Z) F(W) F(H) F(D) F(SrcOffset) F(SrcRowStride) F(SrcSliceStride)
#define MGP_FIELDS_MGPSubData(F) \
F(Res) F(Target) F(Level) F(SourceIsVerbatimLevelShadow) F(UnionBox) F(RegionCount) F(Blob)
#define MGP_FIELDS_MGPSubDataComplete(F) \
F(Res) F(Target) F(FirstLevel) F(LevelCount) F(PullSerial)
#define MGP_FIELDS_MGPFlushRange(F) \
F(Res) F(Offset) F(Size) F(AccessFlags)
#define MGP_FIELDS_MGPReadback(F) \
F(Res) F(Offset) F(Size)
#define MGP_FIELDS_MGPCopyRegion(F) \
F(Src) F(Dst) F(SrcBox) F(DstX) F(DstY) F(DstZ) F(SrcTarget) F(DstTarget) F(SrcLevel) F(DstLevel)
#define MGP_FIELDS_MGPBlit(F) \
F(ReadFbo) F(DrawFbo) F(SrcX0) F(SrcY0) F(SrcX1) F(SrcY1) F(DstX0) F(DstY0) F(DstX1) F(DstY1) \
F(Mask) F(Filter)
#define MGP_FIELDS_MGPClear(F) \
F(Fbo) F(Kind) F(DrawBufferIndex) F(BufferMask) F(ValueClass) F(ColorValue) F(DepthValue) \
F(StencilValue)
#define MGP_FIELDS_MGPMipPlan(F) \
F(Res) F(Target) F(BaseLevel) F(LevelCount)
#define MGP_FIELDS_MGPReadbackInfo(F) \
F(Res) F(Box) F(Format) F(Type) F(Target) F(Level) F(DstOffset) F(DstSize)
#define MGP_FIELDS_MGPDrawInfo(F) \
F(Mode) F(IndexSize) F(Flags) F(InstanceCount) F(StartInstance) F(RestartIndex) F(DrawIdOffset) \
F(IndexResource) F(MinIndex) F(MaxIndex) F(XfbCpuCapturedVertices) F(NumDraws)
#define MGP_FIELDS_MGPDrawRange(F) \
F(Start) F(Count) F(IndexBias)
#define MGP_FIELDS_MGPDrawIndirect(F) \
F(Buffer) F(ParameterBuffer) F(Offset) F(ParameterOffset) F(Stride) F(DrawCount)
#define MGP_FIELDS_MGPGridInfo(F) \
F(GridX) F(GridY) F(GridZ) F(BlockX) F(BlockY) F(BlockZ) F(IndirectBuffer) F(IndirectOffset) \
F(IsIndirect)
#define MGP_FIELDS_MGPMemoryBarrier(F) \
F(Bits) F(ByRegion)
#define MGP_FIELDS_MGPStreamOutputBegin(F) \
F(PrimitiveMode)
#define MGP_FIELDS_MGPXfbAccounting(F) \
F(CapturedVertices) F(PrimitivesWritten) F(PrimitiveMode)
#define MGP_FIELDS_MGPStreamOutputControl(F) \
F(Reserved)
#define MGP_FIELDS_MGPFlush(F) \
F(Flags)
#define MGP_FIELDS_MGPPresent(F) \
F(FrameSerial)
#define MGP_FIELDS_MGPSwapInterval(F) \
F(Interval)
#define MGP_FIELDS_MGPSurfaceInfo(F) \
F(Width) F(Height) F(InternalFormat) F(Samples) F(Layers) F(IsDefault)
// Every payload above, in the order the comparator is generated. Keep in sync with the
// macros; gen_pipe.py reads THIS list to know what to emit.
#define MGP_VERIFY_PAYLOAD_LIST(P) \
P(MGPBlobRef) P(MGPRange) P(MGPBox) P(MGPReplySlot) P(MGPStateChunk) P(MGPHandleOnly) P(MGPCaps) \
P(MGPResourceDesc) P(MGPFenceWait) P(MGPQueryDesc) P(MGPQueryResultRequest) P(MGPTimestampRequest) P(MGPRenderStateDesc) \
P(MGPBindRenderState) P(MGPDynamicState) P(MGPVertexElements) P(MGPSamplerDesc) P(MGPSamplerView) \
P(MGPTextureParams) P(MGPProgramDesc) P(MGPSurface) P(MGPFramebufferState) P(MGPVertexBuffer) \
P(MGPVertexBuffers) P(MGPIndexBuffer) P(MGPIndirectBuffers) P(MGPBoundView) P(MGPSamplerViews) \
P(MGPSamplerStates) P(MGPImageView) P(MGPShaderImages) P(MGPBufferRange) P(MGPShaderBuffers) \
P(MGPStreamOutputTargets) P(MGPGlobalConstants) P(MGPAttribValue) P(MGPVertexAttribDefaults) \
P(MGPPixelPackState) P(MGPPatchState) P(ResidualValueBlock) P(MGPResidualValueState) \
P(MGPSubRegion) P(MGPSubData) P(MGPSubDataComplete) P(MGPFlushRange) P(MGPReadback) \
P(MGPCopyRegion) P(MGPBlit) P(MGPClear) P(MGPMipPlan) P(MGPReadbackInfo) P(MGPDrawInfo) \
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
P(MGPSurfaceInfo)
// clang-format on
-108
View File
@@ -1,108 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeCoverage.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G6: backend read inventory -> MGPipe call coverage.
//
// GENERATED by scripts/gen_pipe.py from Coverage.def and scripts/data/backend_read_inventory.md - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// The acceptance rule (plan B section 10.3-5): regenerate, `git diff --exit-code`, and
// ZERO unmapped rows. P0 permits unmapped rows and only counts them; the count below is
// the number the later gate has to drive to zero.
//
// Three pseudo-calls stand for read points that never become a forward record:
// kClientResolved (the frontend answers it), kReverseChannel (it becomes one of the ten
// MGPipeCallbacks) and kStructuralHandle (the row is a signature carrying a
// SharedPtr<MG_State...> that becomes an MGPipeHandle parameter).
struct MGPipeCoverageEntry {
const char* Accessor;
const char* Call;
Uint32 ReadPoints;
};
inline constexpr MGPipeCoverageEntry kMGPipeCoverage[] = {
{"Buffer ops delta", "ResourceRespecify", 17},
{"GetActiveTextureUnit", "SetSamplerViews", 8},
{"GetBlendColor", "SetDynamicState", 1},
{"GetBlendEquationIndexed", "CreateRenderState", 1},
{"GetBlendFuncIndexed", "CreateRenderState", 1},
{"GetBoundTransformFeedbackName", "SetStreamOutputTargets", 1},
{"GetBoundVertexArray", "BindVertexElements", 12},
{"GetBufferBindingPoint", "SetShaderBuffers", 19},
{"GetBufferBindingPointCount", "SetShaderBuffers", 3},
{"GetBufferBindingSlot", "SetIndirectBuffers", 29},
{"GetClampReadColor", "SetDynamicState", 1},
{"GetClearColor", "SetDynamicState", 1},
{"GetClearDepth", "SetDynamicState", 1},
{"GetClearStencil", "SetDynamicState", 1},
{"GetColorMaskIndexed", "CreateRenderState", 6},
{"GetCullFaceMode", "CreateRenderState", 1},
{"GetCurrentVertexAttribute", "SetVertexAttribDefaults", 2},
{"GetDepthFunc", "CreateRenderState", 1},
{"GetDepthMask", "CreateRenderState", 5},
{"GetDepthRangeIndexed", "SetDynamicState", 1},
{"GetFramebufferBindingSlot", "SetFramebufferState", 19},
{"GetImageTextureBinding", "SetShaderImages", 14},
{"GetLineWidth", "SetDynamicState", 1},
{"GetLogicOp", "CreateRenderState", 1},
{"GetMaxTouchedTextureUnit", "SetSamplerViews", 1},
{"GetMinSampleShadingValue", "CreateRenderState", 1},
{"GetPatchDefaultInnerLevel", "SetPatchState", 3},
{"GetPatchDefaultOuterLevel", "SetPatchState", 3},
{"GetPatchVertices", "SetPatchState", 3},
{"GetPipelineStateVersion", "BindRenderState", 3},
{"GetPixelStoreParameters", "SetPixelPackState", 6},
{"GetPolygonModeFront", "CreateRenderState", 1},
{"GetPolygonOffsetFactor", "SetDynamicState", 1},
{"GetPolygonOffsetUnits", "SetDynamicState", 1},
{"GetPrimitiveRestartIndex", "DrawVbo", 3},
{"GetProgramForDispatch", "SetDispatchProgram", 3},
{"GetProgramForDraw", "SetDrawProgram", 7},
{"GetProgramObject", "CreateShaderState", 3},
{"GetProvokingVertexMode", "CreateRenderState", 1},
{"GetRenderStateParameters", "CreateRenderState", 11},
{"GetRenderStateParametersVersion", "BindRenderState", 2},
{"GetSamplingResolutionGeneration", "SetSamplerViews", 9},
{"GetScissorBox", "SetDynamicState", 3},
{"GetStencilState", "CreateRenderState", 8},
{"GetTextureBindGeneration", "SetSamplerViews", 5},
{"GetTextureContextId", "SetSamplerViews", 6},
{"GetTextureObject", "SetSamplerViews", 1},
{"GetTextureUnitObject", "SetSamplerViews", 19},
{"GetTouchedBufferBindingPointCount", "SetShaderBuffers", 2},
{"GetTransformFeedbackCapturedVertices", "DrawVbo", 1},
{"GetTransformFeedbackGeneration", "SetStreamOutputTargets", 1},
{"GetTransformFeedbackPausedPrimitiveCounter", "EndStreamOutput", 2},
{"GetTransformFeedbackProgram", "SetStreamOutputTargets", 3},
{"GetViewport", "SetDynamicState", 1},
{"GetViewportIndexed", "SetDynamicState", 1},
{"InvalidateCompileEnv", "kClientResolved", 2},
{"IsCapabilityEnabled", "CreateRenderState", 29},
{"IsCapabilityEnabledIndexed", "CreateRenderState", 1},
{"IsTransformFeedbackActive", "BeginStreamOutput", 5},
{"IsTransformFeedbackPaused", "PauseStreamOutput", 2},
{"RecordError", "kReverseChannel", 6},
{"ValidateProgramName", "kClientResolved", 3},
{"handle-ify (wire handle)", "kStructuralHandle", 167},
};
inline constexpr SizeT kMGPipeCoverageEntryCount = 63;
inline constexpr Uint32 kMGPipeInventoryReadPoints = 477;
inline constexpr Uint32 kMGPipeInventoryMappedToCall = 299;
inline constexpr Uint32 kMGPipeInventoryClientResolved = 5;
inline constexpr Uint32 kMGPipeInventoryReverseChannel = 6;
inline constexpr Uint32 kMGPipeInventoryStructuralHandle = 167;
inline constexpr Uint32 kMGPipeInventoryUnmapped = 0;
static_assert(kMGPipeCoverageEntryCount == sizeof(kMGPipeCoverage) / sizeof(kMGPipeCoverage[0]));
static_assert(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +
kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +
kMGPipeInventoryUnmapped ==
kMGPipeInventoryReadPoints,
"every inventory row must land in exactly one bucket");
-308
View File
@@ -1,308 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeFilled.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G5: PipeInputs field ids and the per-verb poison generations.
//
// GENERATED by scripts/gen_pipe.py from Coverage.def and PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// One field id per GLContext accessor the backends actually read (plan B section 6.2:
// PipeInputs is organized by MEMO KEY, not by read point, which is why the field set is
// small and stable across the whole migration).
//
// The poison is a per-verb GENERATION, not a bit. A bitmap cannot see the dangerous case:
// a field filled by the previous DRAW and then read by the glTexSubImage that follows is
// stale, and its bit is already set. So every verb bumps CurrentVerbSerial, filling a
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
enum class MGPipeInputField : Uint16 {
GetActiveTextureUnit,
GetBlendColor,
GetBlendEquationIndexed,
GetBlendFuncIndexed,
GetBoundTransformFeedbackName,
GetBoundVertexArray,
GetBufferBindingSlot,
GetBufferBindingPoint,
GetBufferBindingPointCount,
GetTouchedBufferBindingPointCount,
GetClampReadColor,
GetClearColor,
GetClearDepth,
GetClearStencil,
GetColorMaskIndexed,
GetCullFaceMode,
GetCurrentVertexAttribute,
GetDepthFunc,
GetDepthMask,
GetDepthRangeIndexed,
GetFramebufferBindingSlot,
GetImageTextureBinding,
GetLineWidth,
GetLogicOp,
GetMaxTouchedTextureUnit,
GetMinSampleShadingValue,
GetPatchDefaultInnerLevel,
GetPatchDefaultOuterLevel,
GetPatchVertices,
GetPipelineStateVersion,
GetPixelStoreParameters,
GetPolygonModeFront,
GetPolygonOffsetFactor,
GetPolygonOffsetUnits,
GetPrimitiveRestartIndex,
GetProgramForDispatch,
GetProgramForDraw,
GetProgramObject,
GetProvokingVertexMode,
GetRenderStateParameters,
GetRenderStateParametersVersion,
GetSamplingResolutionGeneration,
GetScissorBox,
GetStencilState,
GetTextureBindGeneration,
GetTextureContextId,
GetTextureObject,
GetTextureUnitObject,
GetTransformFeedbackCapturedVertices,
GetTransformFeedbackGeneration,
GetTransformFeedbackPausedPrimitiveCounter,
GetTransformFeedbackProgram,
GetViewport,
GetViewportIndexed,
IsCapabilityEnabled,
IsCapabilityEnabledIndexed,
IsTransformFeedbackActive,
IsTransformFeedbackPaused,
InvalidateCompileEnv,
ValidateProgramName,
RecordError,
kFieldCount,
};
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
static_assert(kMGPipeInputFieldCount == 61, "the PipeInputs field set moved");
inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
"GetActiveTextureUnit",
"GetBlendColor",
"GetBlendEquationIndexed",
"GetBlendFuncIndexed",
"GetBoundTransformFeedbackName",
"GetBoundVertexArray",
"GetBufferBindingSlot",
"GetBufferBindingPoint",
"GetBufferBindingPointCount",
"GetTouchedBufferBindingPointCount",
"GetClampReadColor",
"GetClearColor",
"GetClearDepth",
"GetClearStencil",
"GetColorMaskIndexed",
"GetCullFaceMode",
"GetCurrentVertexAttribute",
"GetDepthFunc",
"GetDepthMask",
"GetDepthRangeIndexed",
"GetFramebufferBindingSlot",
"GetImageTextureBinding",
"GetLineWidth",
"GetLogicOp",
"GetMaxTouchedTextureUnit",
"GetMinSampleShadingValue",
"GetPatchDefaultInnerLevel",
"GetPatchDefaultOuterLevel",
"GetPatchVertices",
"GetPipelineStateVersion",
"GetPixelStoreParameters",
"GetPolygonModeFront",
"GetPolygonOffsetFactor",
"GetPolygonOffsetUnits",
"GetPrimitiveRestartIndex",
"GetProgramForDispatch",
"GetProgramForDraw",
"GetProgramObject",
"GetProvokingVertexMode",
"GetRenderStateParameters",
"GetRenderStateParametersVersion",
"GetSamplingResolutionGeneration",
"GetScissorBox",
"GetStencilState",
"GetTextureBindGeneration",
"GetTextureContextId",
"GetTextureObject",
"GetTextureUnitObject",
"GetTransformFeedbackCapturedVertices",
"GetTransformFeedbackGeneration",
"GetTransformFeedbackPausedPrimitiveCounter",
"GetTransformFeedbackProgram",
"GetViewport",
"GetViewportIndexed",
"IsCapabilityEnabled",
"IsCapabilityEnabledIndexed",
"IsTransformFeedbackActive",
"IsTransformFeedbackPaused",
"InvalidateCompileEnv",
"ValidateProgramName",
"RecordError",
};
// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each
// true has to be argued for in P1 when the fillers land: a sticky field is a field
// the poison cannot protect.
inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetActiveTextureUnit
false, // GetBlendColor
false, // GetBlendEquationIndexed
false, // GetBlendFuncIndexed
false, // GetBoundTransformFeedbackName
false, // GetBoundVertexArray
false, // GetBufferBindingSlot
false, // GetBufferBindingPoint
false, // GetBufferBindingPointCount
false, // GetTouchedBufferBindingPointCount
false, // GetClampReadColor
false, // GetClearColor
false, // GetClearDepth
false, // GetClearStencil
false, // GetColorMaskIndexed
false, // GetCullFaceMode
false, // GetCurrentVertexAttribute
false, // GetDepthFunc
false, // GetDepthMask
false, // GetDepthRangeIndexed
false, // GetFramebufferBindingSlot
false, // GetImageTextureBinding
false, // GetLineWidth
false, // GetLogicOp
false, // GetMaxTouchedTextureUnit
false, // GetMinSampleShadingValue
false, // GetPatchDefaultInnerLevel
false, // GetPatchDefaultOuterLevel
false, // GetPatchVertices
false, // GetPipelineStateVersion
false, // GetPixelStoreParameters
false, // GetPolygonModeFront
false, // GetPolygonOffsetFactor
false, // GetPolygonOffsetUnits
false, // GetPrimitiveRestartIndex
false, // GetProgramForDispatch
false, // GetProgramForDraw
false, // GetProgramObject
false, // GetProvokingVertexMode
false, // GetRenderStateParameters
false, // GetRenderStateParametersVersion
false, // GetSamplingResolutionGeneration
false, // GetScissorBox
false, // GetStencilState
false, // GetTextureBindGeneration
false, // GetTextureContextId
false, // GetTextureObject
false, // GetTextureUnitObject
false, // GetTransformFeedbackCapturedVertices
false, // GetTransformFeedbackGeneration
false, // GetTransformFeedbackPausedPrimitiveCounter
false, // GetTransformFeedbackProgram
false, // GetViewport
false, // GetViewportIndexed
false, // IsCapabilityEnabled
false, // IsCapabilityEnabledIndexed
false, // IsTransformFeedbackActive
false, // IsTransformFeedbackPaused
false, // InvalidateCompileEnv
false, // ValidateProgramName
false, // RecordError
};
// Which call is expected to have filled a field by the time a verb reads it. Names
// come from Coverage.def, so this table and the coverage table cannot disagree.
inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] = {
"SetSamplerViews",
"SetDynamicState",
"CreateRenderState",
"CreateRenderState",
"SetStreamOutputTargets",
"BindVertexElements",
"SetIndirectBuffers",
"SetShaderBuffers",
"SetShaderBuffers",
"SetShaderBuffers",
"SetDynamicState",
"SetDynamicState",
"SetDynamicState",
"SetDynamicState",
"CreateRenderState",
"CreateRenderState",
"SetVertexAttribDefaults",
"CreateRenderState",
"CreateRenderState",
"SetDynamicState",
"SetFramebufferState",
"SetShaderImages",
"SetDynamicState",
"CreateRenderState",
"SetSamplerViews",
"CreateRenderState",
"SetPatchState",
"SetPatchState",
"SetPatchState",
"BindRenderState",
"SetPixelPackState",
"CreateRenderState",
"SetDynamicState",
"SetDynamicState",
"DrawVbo",
"SetDispatchProgram",
"SetDrawProgram",
"CreateShaderState",
"CreateRenderState",
"CreateRenderState",
"BindRenderState",
"SetSamplerViews",
"SetDynamicState",
"CreateRenderState",
"SetSamplerViews",
"SetSamplerViews",
"SetSamplerViews",
"SetSamplerViews",
"DrawVbo",
"SetStreamOutputTargets",
"EndStreamOutput",
"SetStreamOutputTargets",
"SetDynamicState",
"SetDynamicState",
"CreateRenderState",
"CreateRenderState",
"BeginStreamOutput",
"PauseStreamOutput",
"kClientResolved", // pseudo-call: not filled by a forward record
"kClientResolved", // pseudo-call: not filled by a forward record
"kReverseChannel", // pseudo-call: not filled by a forward record
};
struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
Uint64 FilledGen[kMGPipeInputFieldCount];
};
[[noreturn]] inline void MGPipeInputPoisonFatal(MGPipeInputField field, const char* verb) {
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"}",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], verb);
std::abort();
}
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
: state.FilledGen[index] == state.CurrentVerbSerial;
}
@@ -1,67 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeSpanTable.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G7: the render-state pipeline subset, by member name.
//
// GENERATED by scripts/gen_pipe.py from the field list in scripts/gen_pipe.py - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
// derived from a list that was reviewed rather than invented.
//
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
// table freezes:
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
// backend read points are constant false today. Pipeline state or dead capability?
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
// are handled elsewhere in the payload path rather than in the memo word.
//
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
// glViewport must not mint a new CSO.
inline constexpr const char* const kMGPipePipelineStateMembers[] = {
"CullFaceEnabled",
"DepthTestEnabled",
"PolygonOffsetFillEnabled",
"RasterizerDiscardEnabled",
"ColorLogicOpEnabled",
"StencilTestEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"DepthMask",
"SampleShadingEnabled",
"MultisampleEnabled",
"SampleMaskEnabled",
"SampleMaskValue",
"MinSampleShadingValue",
"PatchVertices",
"PatchDefaultOuterLevel",
"PatchDefaultInnerLevel",
"PolygonModeFront",
"CullFaceModeSetting",
"DepthFunc",
"LogicOp",
"StencilStates",
"BlendStates",
"ColorMasks",
};
inline constexpr SizeT kMGPipePipelineStateMemberCount = 24;
static_assert(kMGPipePipelineStateMemberCount ==
sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));
// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets
// in C++ with offsetof rather than guessing them in python.
extern const MGPStateChunk kMGPipePipelineChunks[];
extern const MGPStateChunk kMGPipeDynamicChunks[];
-108
View File
@@ -1,108 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeTables.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G1: the two MGPipe interface tables.
//
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// share group: 11 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeScreen {
void (*GetCaps)(const MGPCaps* payload, MGPReplySlot* reply);
void (*ResourceCreate)(const MGPResourceDesc* payload);
void (*ResourceRespecify)(const MGPResourceDesc* payload);
void (*ResourceDestroy)(const MGPHandleOnly* payload);
void (*MapPersistent)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*UnmapPersistent)(const MGPHandleOnly* payload);
void (*FenceCreate)(const MGPHandleOnly* payload);
void (*FenceStatus)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply);
void (*FenceDestroy)(const MGPHandleOnly* payload);
void (*FenceWaitServer)(const MGPFenceWait* payload);
};
// context: 60 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeContext {
void (*QueryCreate)(const MGPQueryDesc* payload);
void (*QueryBegin)(const MGPQueryDesc* payload);
void (*QueryEnd)(const MGPQueryDesc* payload);
void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply);
void (*QueryDestroy)(const MGPHandleOnly* payload);
void (*CreateRenderState)(const MGPRenderStateDesc* payload);
void (*BindRenderState)(const MGPBindRenderState* payload);
void (*DeleteRenderState)(const MGPHandleOnly* payload);
void (*CreateVertexElements)(const MGPVertexElements* payload);
void (*BindVertexElements)(const MGPHandleOnly* payload);
void (*DeleteVertexElements)(const MGPHandleOnly* payload);
void (*CreateSamplerState)(const MGPSamplerDesc* payload);
void (*DeleteSamplerState)(const MGPHandleOnly* payload);
void (*CreateSamplerView)(const MGPSamplerView* payload);
void (*DeleteSamplerView)(const MGPHandleOnly* payload);
void (*CreateShaderState)(const MGPProgramDesc* payload);
void (*BindShaderState)(const MGPHandleOnly* payload);
void (*DeleteShaderState)(const MGPHandleOnly* payload);
void (*SetDynamicState)(const MGPDynamicState* payload);
void (*SetFramebufferState)(const MGPFramebufferState* payload);
void (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount);
void (*SetIndexBuffer)(const MGPIndexBuffer* payload);
void (*SetIndirectBuffers)(const MGPIndirectBuffers* payload);
void (*SetSamplerViews)(const MGPSamplerViews* payload, const void* varTail, Uint32 varTailCount);
void (*BindSamplerStates)(const MGPSamplerStates* payload, const void* varTail, Uint32 varTailCount);
void (*SetShaderImages)(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount);
void (*SetShaderBuffers)(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount);
void (*SetStreamOutputTargets)(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount);
void (*SetGlobalConstants)(const MGPGlobalConstants* payload);
void (*SetVertexAttribDefaults)(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount);
void (*SetPixelPackState)(const MGPPixelPackState* payload);
void (*SetPatchState)(const MGPPatchState* payload);
void (*SetDrawProgram)(const MGPHandleOnly* payload);
void (*SetDispatchProgram)(const MGPHandleOnly* payload);
void (*SetResidualValueState)(const MGPResidualValueState* payload);
void (*SetTextureParams)(const MGPTextureParams* payload);
void (*ResourceSubData)(const MGPSubData* payload, const void* varTail, Uint32 varTailCount);
void (*BufferSubDataResident)(const MGPSubData* payload);
void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload);
void (*ResourceFlushRange)(const MGPFlushRange* payload);
void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply);
void (*ResourceCopyRegion)(const MGPCopyRegion* payload);
void (*GenerateMipmap)(const MGPMipPlan* payload);
void (*GetTextureImage)(const MGPReadbackInfo* payload, MGPReplySlot* reply);
void (*Blit)(const MGPBlit* payload);
void (*Clear)(const MGPClear* payload);
void (*ReadPixels)(const MGPReadbackInfo* payload, MGPReplySlot* reply);
void (*DrawVbo)(const MGPDrawInfo* payload, const void* varTail, Uint32 varTailCount);
void (*LaunchGrid)(const MGPGridInfo* payload);
void (*MemoryBarrier)(const MGPMemoryBarrier* payload);
void (*BeginStreamOutput)(const MGPStreamOutputBegin* payload);
void (*EndStreamOutput)(const MGPXfbAccounting* payload);
void (*PauseStreamOutput)(const MGPStreamOutputControl* payload);
void (*ResumeStreamOutput)(const MGPStreamOutputControl* payload);
void (*Flush)(const MGPFlush* payload);
void (*Present)(const MGPPresent* payload);
void (*SetSwapInterval)(const MGPSwapInterval* payload);
void (*QueryTimestamp)(const MGPTimestampRequest* payload, MGPReplySlot* reply);
void (*QueryCounter)(const MGPQueryDesc* payload);
};
inline constexpr SizeT kMGPipeScreenCallCount = 11;
inline constexpr SizeT kMGPipeContextCallCount = 60;
inline constexpr SizeT kMGPipeCallCount = 71;
// A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about.
static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()),
"MGPipeScreen is not exactly its catalogue's function pointers");
static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()),
"MGPipeContext is not exactly its catalogue's function pointers");
static_assert(kMGPipeScreenCallCount + kMGPipeContextCallCount == kMGPipeCallCount);
static_assert(kMGPipeCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT,
"the catalogue and its documented count disagree");
-302
View File
@@ -1,302 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeThunks.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G2: monolith thunks over the two tables.
//
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// One inline call through the installed table. These are the names MG_Impl call
// sites move onto, replacing gBackendFunctionsTable.GL.* one at a time. An
// unimplemented (null) entry is the caller's business to check, exactly as it is
// with the table this replaces.
inline void MGP_GetCaps(const MGPCaps* payload, MGPReplySlot* reply) {
gMGPipeScreen.GetCaps(payload, reply);
}
inline void MGP_ResourceCreate(const MGPResourceDesc* payload) {
gMGPipeScreen.ResourceCreate(payload);
}
inline void MGP_ResourceRespecify(const MGPResourceDesc* payload) {
gMGPipeScreen.ResourceRespecify(payload);
}
inline void MGP_ResourceDestroy(const MGPHandleOnly* payload) {
gMGPipeScreen.ResourceDestroy(payload);
}
inline void MGP_MapPersistent(const MGPHandleOnly* payload, MGPReplySlot* reply) {
gMGPipeScreen.MapPersistent(payload, reply);
}
inline void MGP_UnmapPersistent(const MGPHandleOnly* payload) {
gMGPipeScreen.UnmapPersistent(payload);
}
inline void MGP_FenceCreate(const MGPHandleOnly* payload) {
gMGPipeScreen.FenceCreate(payload);
}
inline void MGP_FenceStatus(const MGPHandleOnly* payload, MGPReplySlot* reply) {
gMGPipeScreen.FenceStatus(payload, reply);
}
inline void MGP_FenceWait(const MGPFenceWait* payload, MGPReplySlot* reply) {
gMGPipeScreen.FenceWait(payload, reply);
}
inline void MGP_FenceDestroy(const MGPHandleOnly* payload) {
gMGPipeScreen.FenceDestroy(payload);
}
inline void MGP_QueryCreate(const MGPQueryDesc* payload) {
gMGPipeContext.QueryCreate(payload);
}
inline void MGP_QueryBegin(const MGPQueryDesc* payload) {
gMGPipeContext.QueryBegin(payload);
}
inline void MGP_QueryEnd(const MGPQueryDesc* payload) {
gMGPipeContext.QueryEnd(payload);
}
inline void MGP_QueryAvailable(const MGPHandleOnly* payload, MGPReplySlot* reply) {
gMGPipeContext.QueryAvailable(payload, reply);
}
inline void MGP_QueryResult(const MGPQueryResultRequest* payload, MGPReplySlot* reply) {
gMGPipeContext.QueryResult(payload, reply);
}
inline void MGP_QueryDestroy(const MGPHandleOnly* payload) {
gMGPipeContext.QueryDestroy(payload);
}
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload) {
gMGPipeContext.CreateRenderState(payload);
}
inline void MGP_BindRenderState(const MGPBindRenderState* payload) {
gMGPipeContext.BindRenderState(payload);
}
inline void MGP_DeleteRenderState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteRenderState(payload);
}
inline void MGP_CreateVertexElements(const MGPVertexElements* payload) {
gMGPipeContext.CreateVertexElements(payload);
}
inline void MGP_BindVertexElements(const MGPHandleOnly* payload) {
gMGPipeContext.BindVertexElements(payload);
}
inline void MGP_DeleteVertexElements(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteVertexElements(payload);
}
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload) {
gMGPipeContext.CreateSamplerState(payload);
}
inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteSamplerState(payload);
}
inline void MGP_CreateSamplerView(const MGPSamplerView* payload) {
gMGPipeContext.CreateSamplerView(payload);
}
inline void MGP_DeleteSamplerView(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteSamplerView(payload);
}
inline void MGP_CreateShaderState(const MGPProgramDesc* payload) {
gMGPipeContext.CreateShaderState(payload);
}
inline void MGP_BindShaderState(const MGPHandleOnly* payload) {
gMGPipeContext.BindShaderState(payload);
}
inline void MGP_DeleteShaderState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteShaderState(payload);
}
inline void MGP_SetDynamicState(const MGPDynamicState* payload) {
gMGPipeContext.SetDynamicState(payload);
}
inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) {
gMGPipeContext.SetFramebufferState(payload);
}
inline void MGP_SetVertexBuffers(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetVertexBuffers(payload, varTail, varTailCount);
}
inline void MGP_SetIndexBuffer(const MGPIndexBuffer* payload) {
gMGPipeContext.SetIndexBuffer(payload);
}
inline void MGP_SetIndirectBuffers(const MGPIndirectBuffers* payload) {
gMGPipeContext.SetIndirectBuffers(payload);
}
inline void MGP_SetSamplerViews(const MGPSamplerViews* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetSamplerViews(payload, varTail, varTailCount);
}
inline void MGP_BindSamplerStates(const MGPSamplerStates* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.BindSamplerStates(payload, varTail, varTailCount);
}
inline void MGP_SetShaderImages(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetShaderImages(payload, varTail, varTailCount);
}
inline void MGP_SetShaderBuffers(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetShaderBuffers(payload, varTail, varTailCount);
}
inline void MGP_SetStreamOutputTargets(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount);
}
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload) {
gMGPipeContext.SetGlobalConstants(payload);
}
inline void MGP_SetVertexAttribDefaults(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetVertexAttribDefaults(payload, varTail, varTailCount);
}
inline void MGP_SetPixelPackState(const MGPPixelPackState* payload) {
gMGPipeContext.SetPixelPackState(payload);
}
inline void MGP_SetPatchState(const MGPPatchState* payload) {
gMGPipeContext.SetPatchState(payload);
}
inline void MGP_SetDrawProgram(const MGPHandleOnly* payload) {
gMGPipeContext.SetDrawProgram(payload);
}
inline void MGP_SetDispatchProgram(const MGPHandleOnly* payload) {
gMGPipeContext.SetDispatchProgram(payload);
}
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload) {
gMGPipeContext.SetResidualValueState(payload);
}
inline void MGP_SetTextureParams(const MGPTextureParams* payload) {
gMGPipeContext.SetTextureParams(payload);
}
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.ResourceSubData(payload, varTail, varTailCount);
}
inline void MGP_BufferSubDataResident(const MGPSubData* payload) {
gMGPipeContext.BufferSubDataResident(payload);
}
inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) {
gMGPipeContext.ResourceSubDataComplete(payload);
}
inline void MGP_ResourceFlushRange(const MGPFlushRange* payload) {
gMGPipeContext.ResourceFlushRange(payload);
}
inline void MGP_ResourceReadback(const MGPReadback* payload, MGPReplySlot* reply) {
gMGPipeContext.ResourceReadback(payload, reply);
}
inline void MGP_ResourceCopyRegion(const MGPCopyRegion* payload) {
gMGPipeContext.ResourceCopyRegion(payload);
}
inline void MGP_GenerateMipmap(const MGPMipPlan* payload) {
gMGPipeContext.GenerateMipmap(payload);
}
inline void MGP_GetTextureImage(const MGPReadbackInfo* payload, MGPReplySlot* reply) {
gMGPipeContext.GetTextureImage(payload, reply);
}
inline void MGP_Blit(const MGPBlit* payload) {
gMGPipeContext.Blit(payload);
}
inline void MGP_Clear(const MGPClear* payload) {
gMGPipeContext.Clear(payload);
}
inline void MGP_ReadPixels(const MGPReadbackInfo* payload, MGPReplySlot* reply) {
gMGPipeContext.ReadPixels(payload, reply);
}
inline void MGP_DrawVbo(const MGPDrawInfo* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.DrawVbo(payload, varTail, varTailCount);
}
inline void MGP_LaunchGrid(const MGPGridInfo* payload) {
gMGPipeContext.LaunchGrid(payload);
}
inline void MGP_MemoryBarrier(const MGPMemoryBarrier* payload) {
gMGPipeContext.MemoryBarrier(payload);
}
inline void MGP_BeginStreamOutput(const MGPStreamOutputBegin* payload) {
gMGPipeContext.BeginStreamOutput(payload);
}
inline void MGP_EndStreamOutput(const MGPXfbAccounting* payload) {
gMGPipeContext.EndStreamOutput(payload);
}
inline void MGP_PauseStreamOutput(const MGPStreamOutputControl* payload) {
gMGPipeContext.PauseStreamOutput(payload);
}
inline void MGP_ResumeStreamOutput(const MGPStreamOutputControl* payload) {
gMGPipeContext.ResumeStreamOutput(payload);
}
inline void MGP_Flush(const MGPFlush* payload) {
gMGPipeContext.Flush(payload);
}
inline void MGP_Present(const MGPPresent* payload) {
gMGPipeContext.Present(payload);
}
inline void MGP_SetSwapInterval(const MGPSwapInterval* payload) {
gMGPipeContext.SetSwapInterval(payload);
}
inline void MGP_QueryTimestamp(const MGPTimestampRequest* payload, MGPReplySlot* reply) {
gMGPipeContext.QueryTimestamp(payload, reply);
}
inline void MGP_QueryCounter(const MGPQueryDesc* payload) {
gMGPipeContext.QueryCounter(payload);
}
inline void MGP_FenceWaitServer(const MGPFenceWait* payload) {
gMGPipeScreen.FenceWaitServer(payload);
}
-573
View File
@@ -1,573 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeVerify.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.
//
// GENERATED by scripts/gen_pipe.py from PipeFields.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// Field by field, never memcmp over a whole payload: RenderStateParameters is documented
// in DirectGLES.cpp to false-DIFFER on padding under memcmp (harmlessly there, fatally
// here - a comparator with false positives is a comparator nobody reads). Each function
// reports the FIRST differing field by name, which with the draw serial is what the verify
// harness prints.
//
// Floating-point fields are compared by BITS, so a NaN patch level - which
// glPatchParameterfv accepts and ComputePipelineStateHash already hashes bitwise - equals
// itself instead of tripping every draw.
#include "../PipeFields.def"
template <class T>
struct MGPipeHasFieldVerifier : std::false_type {};
inline Bool MGPipeVerify(const MGPBlobRef& a, const MGPBlobRef& b, const char** outField);
inline Bool MGPipeVerify(const MGPRange& a, const MGPRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPBox& a, const MGPBox& b, const char** outField);
inline Bool MGPipeVerify(const MGPReplySlot& a, const MGPReplySlot& b, const char** outField);
inline Bool MGPipeVerify(const MGPStateChunk& a, const MGPStateChunk& b, const char** outField);
inline Bool MGPipeVerify(const MGPHandleOnly& a, const MGPHandleOnly& b, const char** outField);
inline Bool MGPipeVerify(const MGPCaps& a, const MGPCaps& b, const char** outField);
inline Bool MGPipeVerify(const MGPResourceDesc& a, const MGPResourceDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPFenceWait& a, const MGPFenceWait& b, const char** outField);
inline Bool MGPipeVerify(const MGPQueryDesc& a, const MGPQueryDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPQueryResultRequest& a, const MGPQueryResultRequest& b, const char** outField);
inline Bool MGPipeVerify(const MGPTimestampRequest& a, const MGPTimestampRequest& b, const char** outField);
inline Bool MGPipeVerify(const MGPRenderStateDesc& a, const MGPRenderStateDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPBindRenderState& a, const MGPBindRenderState& b, const char** outField);
inline Bool MGPipeVerify(const MGPDynamicState& a, const MGPDynamicState& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexElements& a, const MGPVertexElements& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerDesc& a, const MGPSamplerDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerView& a, const MGPSamplerView& b, const char** outField);
inline Bool MGPipeVerify(const MGPTextureParams& a, const MGPTextureParams& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramDesc& a, const MGPProgramDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPSurface& a, const MGPSurface& b, const char** outField);
inline Bool MGPipeVerify(const MGPFramebufferState& a, const MGPFramebufferState& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexBuffer& a, const MGPVertexBuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexBuffers& a, const MGPVertexBuffers& b, const char** outField);
inline Bool MGPipeVerify(const MGPIndexBuffer& a, const MGPIndexBuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPIndirectBuffers& a, const MGPIndirectBuffers& b, const char** outField);
inline Bool MGPipeVerify(const MGPBoundView& a, const MGPBoundView& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerViews& a, const MGPSamplerViews& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerStates& a, const MGPSamplerStates& b, const char** outField);
inline Bool MGPipeVerify(const MGPImageView& a, const MGPImageView& b, const char** outField);
inline Bool MGPipeVerify(const MGPShaderImages& a, const MGPShaderImages& b, const char** outField);
inline Bool MGPipeVerify(const MGPBufferRange& a, const MGPBufferRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPShaderBuffers& a, const MGPShaderBuffers& b, const char** outField);
inline Bool MGPipeVerify(const MGPStreamOutputTargets& a, const MGPStreamOutputTargets& b, const char** outField);
inline Bool MGPipeVerify(const MGPGlobalConstants& a, const MGPGlobalConstants& b, const char** outField);
inline Bool MGPipeVerify(const MGPAttribValue& a, const MGPAttribValue& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexAttribDefaults& a, const MGPVertexAttribDefaults& b, const char** outField);
inline Bool MGPipeVerify(const MGPPixelPackState& a, const MGPPixelPackState& b, const char** outField);
inline Bool MGPipeVerify(const MGPPatchState& a, const MGPPatchState& b, const char** outField);
inline Bool MGPipeVerify(const ResidualValueBlock& a, const ResidualValueBlock& b, const char** outField);
inline Bool MGPipeVerify(const MGPResidualValueState& a, const MGPResidualValueState& b, const char** outField);
inline Bool MGPipeVerify(const MGPSubRegion& a, const MGPSubRegion& b, const char** outField);
inline Bool MGPipeVerify(const MGPSubData& a, const MGPSubData& b, const char** outField);
inline Bool MGPipeVerify(const MGPSubDataComplete& a, const MGPSubDataComplete& b, const char** outField);
inline Bool MGPipeVerify(const MGPFlushRange& a, const MGPFlushRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPReadback& a, const MGPReadback& b, const char** outField);
inline Bool MGPipeVerify(const MGPCopyRegion& a, const MGPCopyRegion& b, const char** outField);
inline Bool MGPipeVerify(const MGPBlit& a, const MGPBlit& b, const char** outField);
inline Bool MGPipeVerify(const MGPClear& a, const MGPClear& b, const char** outField);
inline Bool MGPipeVerify(const MGPMipPlan& a, const MGPMipPlan& b, const char** outField);
inline Bool MGPipeVerify(const MGPReadbackInfo& a, const MGPReadbackInfo& b, const char** outField);
inline Bool MGPipeVerify(const MGPDrawInfo& a, const MGPDrawInfo& b, const char** outField);
inline Bool MGPipeVerify(const MGPDrawRange& a, const MGPDrawRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPDrawIndirect& a, const MGPDrawIndirect& b, const char** outField);
inline Bool MGPipeVerify(const MGPGridInfo& a, const MGPGridInfo& b, const char** outField);
inline Bool MGPipeVerify(const MGPMemoryBarrier& a, const MGPMemoryBarrier& b, const char** outField);
inline Bool MGPipeVerify(const MGPStreamOutputBegin& a, const MGPStreamOutputBegin& b, const char** outField);
inline Bool MGPipeVerify(const MGPXfbAccounting& a, const MGPXfbAccounting& b, const char** outField);
inline Bool MGPipeVerify(const MGPStreamOutputControl& a, const MGPStreamOutputControl& b, const char** outField);
inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outField);
inline Bool MGPipeVerify(const MGPPresent& a, const MGPPresent& b, const char** outField);
inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, const char** outField);
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
template <>
struct MGPipeHasFieldVerifier<MGPBlobRef> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBox> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPReplySlot> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStateChunk> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPHandleOnly> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPCaps> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPResourceDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFenceWait> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPQueryDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPQueryResultRequest> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPTimestampRequest> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPRenderStateDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBindRenderState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDynamicState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexElements> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerView> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPTextureParams> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSurface> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFramebufferState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexBuffer> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexBuffers> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPIndexBuffer> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPIndirectBuffers> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBoundView> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerViews> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerStates> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPImageView> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPShaderImages> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBufferRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPShaderBuffers> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStreamOutputTargets> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPGlobalConstants> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPAttribValue> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexAttribDefaults> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPPixelPackState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPPatchState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<ResidualValueBlock> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPResidualValueState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSubRegion> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSubData> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSubDataComplete> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFlushRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPReadback> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPCopyRegion> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBlit> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPClear> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPMipPlan> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPReadbackInfo> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDrawInfo> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDrawRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDrawIndirect> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPGridInfo> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPMemoryBarrier> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStreamOutputBegin> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPXfbAccounting> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStreamOutputControl> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFlush> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPPresent> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSwapInterval> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSurfaceInfo> : std::true_type {};
template <class T>
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
if constexpr (MGPipeHasFieldVerifier<T>::value) {
const char* unusedField = nullptr;
return MGPipeVerify(a, b, &unusedField);
} else if constexpr (std::is_floating_point_v<T>) {
return std::memcmp(&a, &b, sizeof(T)) == 0;
} else if constexpr (std::is_scalar_v<T> || std::is_enum_v<T>) {
return a == b;
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
return a == b;
} else {
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
// and this branch stops being reachable from any payload.
return std::memcmp(&a, &b, sizeof(T)) == 0;
}
}
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
for (SizeT i = 0; i < N; ++i) {
if (!MGPipeFieldEqual(a[i], b[i])) return false;
}
return true;
}
#define MGP_VERIFY_FIELD(FieldName) \
if (!MGPipeFieldEqual(a.FieldName, b.FieldName)) { \
if (outField != nullptr) *outField = #FieldName; \
return false; \
}
inline Bool MGPipeVerify(const MGPBlobRef& a, const MGPBlobRef& b, const char** outField) {
MGP_FIELDS_MGPBlobRef(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPRange& a, const MGPRange& b, const char** outField) {
MGP_FIELDS_MGPRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBox& a, const MGPBox& b, const char** outField) {
MGP_FIELDS_MGPBox(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPReplySlot& a, const MGPReplySlot& b, const char** outField) {
MGP_FIELDS_MGPReplySlot(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStateChunk& a, const MGPStateChunk& b, const char** outField) {
MGP_FIELDS_MGPStateChunk(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPHandleOnly& a, const MGPHandleOnly& b, const char** outField) {
MGP_FIELDS_MGPHandleOnly(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPCaps& a, const MGPCaps& b, const char** outField) {
MGP_FIELDS_MGPCaps(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPResourceDesc& a, const MGPResourceDesc& b, const char** outField) {
MGP_FIELDS_MGPResourceDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFenceWait& a, const MGPFenceWait& b, const char** outField) {
MGP_FIELDS_MGPFenceWait(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPQueryDesc& a, const MGPQueryDesc& b, const char** outField) {
MGP_FIELDS_MGPQueryDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPQueryResultRequest& a, const MGPQueryResultRequest& b, const char** outField) {
MGP_FIELDS_MGPQueryResultRequest(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPTimestampRequest& a, const MGPTimestampRequest& b, const char** outField) {
MGP_FIELDS_MGPTimestampRequest(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPRenderStateDesc& a, const MGPRenderStateDesc& b, const char** outField) {
MGP_FIELDS_MGPRenderStateDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBindRenderState& a, const MGPBindRenderState& b, const char** outField) {
MGP_FIELDS_MGPBindRenderState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDynamicState& a, const MGPDynamicState& b, const char** outField) {
MGP_FIELDS_MGPDynamicState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexElements& a, const MGPVertexElements& b, const char** outField) {
MGP_FIELDS_MGPVertexElements(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerDesc& a, const MGPSamplerDesc& b, const char** outField) {
MGP_FIELDS_MGPSamplerDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerView& a, const MGPSamplerView& b, const char** outField) {
MGP_FIELDS_MGPSamplerView(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPTextureParams& a, const MGPTextureParams& b, const char** outField) {
MGP_FIELDS_MGPTextureParams(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPProgramDesc& a, const MGPProgramDesc& b, const char** outField) {
MGP_FIELDS_MGPProgramDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSurface& a, const MGPSurface& b, const char** outField) {
MGP_FIELDS_MGPSurface(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFramebufferState& a, const MGPFramebufferState& b, const char** outField) {
MGP_FIELDS_MGPFramebufferState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexBuffer& a, const MGPVertexBuffer& b, const char** outField) {
MGP_FIELDS_MGPVertexBuffer(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexBuffers& a, const MGPVertexBuffers& b, const char** outField) {
MGP_FIELDS_MGPVertexBuffers(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPIndexBuffer& a, const MGPIndexBuffer& b, const char** outField) {
MGP_FIELDS_MGPIndexBuffer(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPIndirectBuffers& a, const MGPIndirectBuffers& b, const char** outField) {
MGP_FIELDS_MGPIndirectBuffers(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBoundView& a, const MGPBoundView& b, const char** outField) {
MGP_FIELDS_MGPBoundView(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerViews& a, const MGPSamplerViews& b, const char** outField) {
MGP_FIELDS_MGPSamplerViews(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerStates& a, const MGPSamplerStates& b, const char** outField) {
MGP_FIELDS_MGPSamplerStates(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPImageView& a, const MGPImageView& b, const char** outField) {
MGP_FIELDS_MGPImageView(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPShaderImages& a, const MGPShaderImages& b, const char** outField) {
MGP_FIELDS_MGPShaderImages(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBufferRange& a, const MGPBufferRange& b, const char** outField) {
MGP_FIELDS_MGPBufferRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPShaderBuffers& a, const MGPShaderBuffers& b, const char** outField) {
MGP_FIELDS_MGPShaderBuffers(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStreamOutputTargets& a, const MGPStreamOutputTargets& b, const char** outField) {
MGP_FIELDS_MGPStreamOutputTargets(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPGlobalConstants& a, const MGPGlobalConstants& b, const char** outField) {
MGP_FIELDS_MGPGlobalConstants(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPAttribValue& a, const MGPAttribValue& b, const char** outField) {
MGP_FIELDS_MGPAttribValue(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexAttribDefaults& a, const MGPVertexAttribDefaults& b, const char** outField) {
MGP_FIELDS_MGPVertexAttribDefaults(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPPixelPackState& a, const MGPPixelPackState& b, const char** outField) {
MGP_FIELDS_MGPPixelPackState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPPatchState& a, const MGPPatchState& b, const char** outField) {
MGP_FIELDS_MGPPatchState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const ResidualValueBlock& a, const ResidualValueBlock& b, const char** outField) {
MGP_FIELDS_ResidualValueBlock(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPResidualValueState& a, const MGPResidualValueState& b, const char** outField) {
MGP_FIELDS_MGPResidualValueState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSubRegion& a, const MGPSubRegion& b, const char** outField) {
MGP_FIELDS_MGPSubRegion(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSubData& a, const MGPSubData& b, const char** outField) {
MGP_FIELDS_MGPSubData(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSubDataComplete& a, const MGPSubDataComplete& b, const char** outField) {
MGP_FIELDS_MGPSubDataComplete(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFlushRange& a, const MGPFlushRange& b, const char** outField) {
MGP_FIELDS_MGPFlushRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPReadback& a, const MGPReadback& b, const char** outField) {
MGP_FIELDS_MGPReadback(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPCopyRegion& a, const MGPCopyRegion& b, const char** outField) {
MGP_FIELDS_MGPCopyRegion(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBlit& a, const MGPBlit& b, const char** outField) {
MGP_FIELDS_MGPBlit(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPClear& a, const MGPClear& b, const char** outField) {
MGP_FIELDS_MGPClear(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPMipPlan& a, const MGPMipPlan& b, const char** outField) {
MGP_FIELDS_MGPMipPlan(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPReadbackInfo& a, const MGPReadbackInfo& b, const char** outField) {
MGP_FIELDS_MGPReadbackInfo(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDrawInfo& a, const MGPDrawInfo& b, const char** outField) {
MGP_FIELDS_MGPDrawInfo(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDrawRange& a, const MGPDrawRange& b, const char** outField) {
MGP_FIELDS_MGPDrawRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDrawIndirect& a, const MGPDrawIndirect& b, const char** outField) {
MGP_FIELDS_MGPDrawIndirect(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPGridInfo& a, const MGPGridInfo& b, const char** outField) {
MGP_FIELDS_MGPGridInfo(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPMemoryBarrier& a, const MGPMemoryBarrier& b, const char** outField) {
MGP_FIELDS_MGPMemoryBarrier(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStreamOutputBegin& a, const MGPStreamOutputBegin& b, const char** outField) {
MGP_FIELDS_MGPStreamOutputBegin(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPXfbAccounting& a, const MGPXfbAccounting& b, const char** outField) {
MGP_FIELDS_MGPXfbAccounting(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStreamOutputControl& a, const MGPStreamOutputControl& b, const char** outField) {
MGP_FIELDS_MGPStreamOutputControl(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outField) {
MGP_FIELDS_MGPFlush(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPPresent& a, const MGPPresent& b, const char** outField) {
MGP_FIELDS_MGPPresent(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, const char** outField) {
MGP_FIELDS_MGPSwapInterval(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField) {
MGP_FIELDS_MGPSurfaceInfo(MGP_VERIFY_FIELD)
return true;
}
#undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 63;
-931
View File
@@ -1,931 +0,0 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeWire.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G3: wire records, size assertions and the applier's bounds gate.
//
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// Every record is a fixed header plus its payload, padded to the stream's 8-byte
// granularity. The size assertion is stated as a COMPOSITION so it fires on any padding
// the compiler inserts between the header and the payload while staying honest about the
// tail padding the alignment requires.
//
// The applier's precondition is checked BEFORE dispatch, on every record, in every build:
// a record that is shorter than its own type, longer than what is left in the buffer, or
// not a multiple of 8 is protocol corruption and is fatal. There is no recovery path -
// silently applying a truncated record is how a corrupt stream becomes a wrong picture.
//
// OVERSIZED PAYLOADS ARE CHUNKED, NEVER EMITTED WHOLE (plan section 8.2: G3 has to define
// the path for a record larger than the segment). The bound is the ring's,
// RingProducer::MaxRecordBytes() == Capacity()/2, and it is exact rather than
// conservative: a record has to be placeable at every head offset of an empty ring, the
// wrap pad in front of it costs up to total-8 bytes, and only a record of at most half the
// ring survives that at every offset. An emitter holding more than Capacity()/2 bytes of
// record (a large resource_subdata, a create_shader_state archive) splits it into several
// records of at most that size; the transport refuses a bigger one outright - nullptr plus
// an MGLOG_E - rather than let the producer wait on free bytes that can never suffice.
struct MGPWireRecHeader {
Uint16 Op; // MGPWireOp
Uint16 Flags; // MGPipeCallFlags of the call, for asserts and tracing
Uint32 Size; // bytes of this record including the header and the variable tail
};
static_assert(sizeof(MGPWireRecHeader) == 8, "the wire header is 8 bytes");
static_assert(std::is_trivially_copyable_v<MGPWireRecHeader>);
// The opcode is the call's position in PipeCalls.def. Reordering that file is a protocol
// break; appending to it is not.
enum class MGPWireOp : Uint16 {
kInvalid = 0,
GetCaps = 1,
ResourceCreate = 2,
ResourceRespecify = 3,
ResourceDestroy = 4,
MapPersistent = 5,
UnmapPersistent = 6,
FenceCreate = 7,
FenceStatus = 8,
FenceWait = 9,
FenceDestroy = 10,
QueryCreate = 11,
QueryBegin = 12,
QueryEnd = 13,
QueryAvailable = 14,
QueryResult = 15,
QueryDestroy = 16,
CreateRenderState = 17,
BindRenderState = 18,
DeleteRenderState = 19,
CreateVertexElements = 20,
BindVertexElements = 21,
DeleteVertexElements = 22,
CreateSamplerState = 23,
DeleteSamplerState = 24,
CreateSamplerView = 25,
DeleteSamplerView = 26,
CreateShaderState = 27,
BindShaderState = 28,
DeleteShaderState = 29,
SetDynamicState = 30,
SetFramebufferState = 31,
SetVertexBuffers = 32,
SetIndexBuffer = 33,
SetIndirectBuffers = 34,
SetSamplerViews = 35,
BindSamplerStates = 36,
SetShaderImages = 37,
SetShaderBuffers = 38,
SetStreamOutputTargets = 39,
SetGlobalConstants = 40,
SetVertexAttribDefaults = 41,
SetPixelPackState = 42,
SetPatchState = 43,
SetDrawProgram = 44,
SetDispatchProgram = 45,
SetResidualValueState = 46,
SetTextureParams = 47,
ResourceSubData = 48,
BufferSubDataResident = 49,
ResourceSubDataComplete = 50,
ResourceFlushRange = 51,
ResourceReadback = 52,
ResourceCopyRegion = 53,
GenerateMipmap = 54,
GetTextureImage = 55,
Blit = 56,
Clear = 57,
ReadPixels = 58,
DrawVbo = 59,
LaunchGrid = 60,
MemoryBarrier = 61,
BeginStreamOutput = 62,
EndStreamOutput = 63,
PauseStreamOutput = 64,
ResumeStreamOutput = 65,
Flush = 66,
Present = 67,
SetSwapInterval = 68,
QueryTimestamp = 69,
QueryCounter = 70,
FenceWaitServer = 71,
kOpCount = 72,
};
struct alignas(8) MGPWireRec_GetCaps {
MGPWireRecHeader Header;
MGPCaps Payload;
};
static_assert(sizeof(MGPWireRec_GetCaps) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPCaps) + 7u) & ~SizeT(7u)),
"MGPWireRec_GetCaps gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceCreate {
MGPWireRecHeader Header;
MGPResourceDesc Payload;
};
static_assert(sizeof(MGPWireRec_ResourceCreate) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPResourceDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceCreate gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceRespecify {
MGPWireRecHeader Header;
MGPResourceDesc Payload;
};
static_assert(sizeof(MGPWireRec_ResourceRespecify) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPResourceDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceRespecify gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceDestroy {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_ResourceDestroy) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceDestroy gained padding; the wire format moved");
struct alignas(8) MGPWireRec_MapPersistent {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_MapPersistent) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_MapPersistent gained padding; the wire format moved");
struct alignas(8) MGPWireRec_UnmapPersistent {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_UnmapPersistent) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_UnmapPersistent gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceCreate {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_FenceCreate) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceCreate gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceStatus {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_FenceStatus) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceStatus gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceWait {
MGPWireRecHeader Header;
MGPFenceWait Payload;
};
static_assert(sizeof(MGPWireRec_FenceWait) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFenceWait) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceWait gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceDestroy {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_FenceDestroy) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceDestroy gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryCreate {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryCreate) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryCreate gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryBegin {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryBegin) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryBegin gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryEnd {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryEnd) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryEnd gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryAvailable {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_QueryAvailable) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryAvailable gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryResult {
MGPWireRecHeader Header;
MGPQueryResultRequest Payload;
};
static_assert(sizeof(MGPWireRec_QueryResult) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryResultRequest) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryResult gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryDestroy {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_QueryDestroy) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryDestroy gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateRenderState {
MGPWireRecHeader Header;
MGPRenderStateDesc Payload;
};
static_assert(sizeof(MGPWireRec_CreateRenderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPRenderStateDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateRenderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindRenderState {
MGPWireRecHeader Header;
MGPBindRenderState Payload;
};
static_assert(sizeof(MGPWireRec_BindRenderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPBindRenderState) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindRenderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteRenderState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteRenderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteRenderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateVertexElements {
MGPWireRecHeader Header;
MGPVertexElements Payload;
};
static_assert(sizeof(MGPWireRec_CreateVertexElements) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexElements) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateVertexElements gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindVertexElements {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_BindVertexElements) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindVertexElements gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteVertexElements {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteVertexElements) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteVertexElements gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateSamplerState {
MGPWireRecHeader Header;
MGPSamplerDesc Payload;
};
static_assert(sizeof(MGPWireRec_CreateSamplerState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateSamplerState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteSamplerState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteSamplerState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteSamplerState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateSamplerView {
MGPWireRecHeader Header;
MGPSamplerView Payload;
};
static_assert(sizeof(MGPWireRec_CreateSamplerView) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerView) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateSamplerView gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteSamplerView {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteSamplerView) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteSamplerView gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateShaderState {
MGPWireRecHeader Header;
MGPProgramDesc Payload;
};
static_assert(sizeof(MGPWireRec_CreateShaderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPProgramDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateShaderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindShaderState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_BindShaderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindShaderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteShaderState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteShaderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteShaderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetDynamicState {
MGPWireRecHeader Header;
MGPDynamicState Payload;
};
static_assert(sizeof(MGPWireRec_SetDynamicState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPDynamicState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetDynamicState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetFramebufferState {
MGPWireRecHeader Header;
MGPFramebufferState Payload;
};
static_assert(sizeof(MGPWireRec_SetFramebufferState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFramebufferState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetFramebufferState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetVertexBuffers {
MGPWireRecHeader Header;
MGPVertexBuffers Payload;
};
static_assert(sizeof(MGPWireRec_SetVertexBuffers) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexBuffers) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetVertexBuffers gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetIndexBuffer {
MGPWireRecHeader Header;
MGPIndexBuffer Payload;
};
static_assert(sizeof(MGPWireRec_SetIndexBuffer) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPIndexBuffer) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetIndexBuffer gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetIndirectBuffers {
MGPWireRecHeader Header;
MGPIndirectBuffers Payload;
};
static_assert(sizeof(MGPWireRec_SetIndirectBuffers) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPIndirectBuffers) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetIndirectBuffers gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetSamplerViews {
MGPWireRecHeader Header;
MGPSamplerViews Payload;
};
static_assert(sizeof(MGPWireRec_SetSamplerViews) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerViews) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetSamplerViews gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindSamplerStates {
MGPWireRecHeader Header;
MGPSamplerStates Payload;
};
static_assert(sizeof(MGPWireRec_BindSamplerStates) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerStates) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindSamplerStates gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetShaderImages {
MGPWireRecHeader Header;
MGPShaderImages Payload;
};
static_assert(sizeof(MGPWireRec_SetShaderImages) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPShaderImages) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetShaderImages gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetShaderBuffers {
MGPWireRecHeader Header;
MGPShaderBuffers Payload;
};
static_assert(sizeof(MGPWireRec_SetShaderBuffers) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPShaderBuffers) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetShaderBuffers gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetStreamOutputTargets {
MGPWireRecHeader Header;
MGPStreamOutputTargets Payload;
};
static_assert(sizeof(MGPWireRec_SetStreamOutputTargets) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputTargets) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetStreamOutputTargets gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetGlobalConstants {
MGPWireRecHeader Header;
MGPGlobalConstants Payload;
};
static_assert(sizeof(MGPWireRec_SetGlobalConstants) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPGlobalConstants) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetGlobalConstants gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetVertexAttribDefaults {
MGPWireRecHeader Header;
MGPVertexAttribDefaults Payload;
};
static_assert(sizeof(MGPWireRec_SetVertexAttribDefaults) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexAttribDefaults) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetVertexAttribDefaults gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetPixelPackState {
MGPWireRecHeader Header;
MGPPixelPackState Payload;
};
static_assert(sizeof(MGPWireRec_SetPixelPackState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPPixelPackState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetPixelPackState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetPatchState {
MGPWireRecHeader Header;
MGPPatchState Payload;
};
static_assert(sizeof(MGPWireRec_SetPatchState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPPatchState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetPatchState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetDrawProgram {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_SetDrawProgram) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetDrawProgram gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetDispatchProgram {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_SetDispatchProgram) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetDispatchProgram gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetResidualValueState {
MGPWireRecHeader Header;
MGPResidualValueState Payload;
};
static_assert(sizeof(MGPWireRec_SetResidualValueState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPResidualValueState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetResidualValueState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetTextureParams {
MGPWireRecHeader Header;
MGPTextureParams Payload;
};
static_assert(sizeof(MGPWireRec_SetTextureParams) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPTextureParams) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetTextureParams gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceSubData {
MGPWireRecHeader Header;
MGPSubData Payload;
};
static_assert(sizeof(MGPWireRec_ResourceSubData) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSubData) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceSubData gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BufferSubDataResident {
MGPWireRecHeader Header;
MGPSubData Payload;
};
static_assert(sizeof(MGPWireRec_BufferSubDataResident) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSubData) + 7u) & ~SizeT(7u)),
"MGPWireRec_BufferSubDataResident gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceSubDataComplete {
MGPWireRecHeader Header;
MGPSubDataComplete Payload;
};
static_assert(sizeof(MGPWireRec_ResourceSubDataComplete) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSubDataComplete) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceSubDataComplete gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceFlushRange {
MGPWireRecHeader Header;
MGPFlushRange Payload;
};
static_assert(sizeof(MGPWireRec_ResourceFlushRange) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFlushRange) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceFlushRange gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceReadback {
MGPWireRecHeader Header;
MGPReadback Payload;
};
static_assert(sizeof(MGPWireRec_ResourceReadback) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadback) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceReadback gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceCopyRegion {
MGPWireRecHeader Header;
MGPCopyRegion Payload;
};
static_assert(sizeof(MGPWireRec_ResourceCopyRegion) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyRegion) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceCopyRegion gained padding; the wire format moved");
struct alignas(8) MGPWireRec_GenerateMipmap {
MGPWireRecHeader Header;
MGPMipPlan Payload;
};
static_assert(sizeof(MGPWireRec_GenerateMipmap) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPMipPlan) + 7u) & ~SizeT(7u)),
"MGPWireRec_GenerateMipmap gained padding; the wire format moved");
struct alignas(8) MGPWireRec_GetTextureImage {
MGPWireRecHeader Header;
MGPReadbackInfo Payload;
};
static_assert(sizeof(MGPWireRec_GetTextureImage) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_GetTextureImage gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Blit {
MGPWireRecHeader Header;
MGPBlit Payload;
};
static_assert(sizeof(MGPWireRec_Blit) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPBlit) + 7u) & ~SizeT(7u)),
"MGPWireRec_Blit gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Clear {
MGPWireRecHeader Header;
MGPClear Payload;
};
static_assert(sizeof(MGPWireRec_Clear) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPClear) + 7u) & ~SizeT(7u)),
"MGPWireRec_Clear gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ReadPixels {
MGPWireRecHeader Header;
MGPReadbackInfo Payload;
};
static_assert(sizeof(MGPWireRec_ReadPixels) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_ReadPixels gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DrawVbo {
MGPWireRecHeader Header;
MGPDrawInfo Payload;
};
static_assert(sizeof(MGPWireRec_DrawVbo) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPDrawInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_DrawVbo gained padding; the wire format moved");
struct alignas(8) MGPWireRec_LaunchGrid {
MGPWireRecHeader Header;
MGPGridInfo Payload;
};
static_assert(sizeof(MGPWireRec_LaunchGrid) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPGridInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_LaunchGrid gained padding; the wire format moved");
struct alignas(8) MGPWireRec_MemoryBarrier {
MGPWireRecHeader Header;
MGPMemoryBarrier Payload;
};
static_assert(sizeof(MGPWireRec_MemoryBarrier) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPMemoryBarrier) + 7u) & ~SizeT(7u)),
"MGPWireRec_MemoryBarrier gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BeginStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputBegin Payload;
};
static_assert(sizeof(MGPWireRec_BeginStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputBegin) + 7u) & ~SizeT(7u)),
"MGPWireRec_BeginStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_EndStreamOutput {
MGPWireRecHeader Header;
MGPXfbAccounting Payload;
};
static_assert(sizeof(MGPWireRec_EndStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPXfbAccounting) + 7u) & ~SizeT(7u)),
"MGPWireRec_EndStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_PauseStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputControl Payload;
};
static_assert(sizeof(MGPWireRec_PauseStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputControl) + 7u) & ~SizeT(7u)),
"MGPWireRec_PauseStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResumeStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputControl Payload;
};
static_assert(sizeof(MGPWireRec_ResumeStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputControl) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResumeStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Flush {
MGPWireRecHeader Header;
MGPFlush Payload;
};
static_assert(sizeof(MGPWireRec_Flush) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFlush) + 7u) & ~SizeT(7u)),
"MGPWireRec_Flush gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Present {
MGPWireRecHeader Header;
MGPPresent Payload;
};
static_assert(sizeof(MGPWireRec_Present) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPPresent) + 7u) & ~SizeT(7u)),
"MGPWireRec_Present gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetSwapInterval {
MGPWireRecHeader Header;
MGPSwapInterval Payload;
};
static_assert(sizeof(MGPWireRec_SetSwapInterval) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSwapInterval) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetSwapInterval gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryTimestamp {
MGPWireRecHeader Header;
MGPTimestampRequest Payload;
};
static_assert(sizeof(MGPWireRec_QueryTimestamp) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPTimestampRequest) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryTimestamp gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryCounter {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryCounter) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryCounter gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceWaitServer {
MGPWireRecHeader Header;
MGPFenceWait Payload;
};
static_assert(sizeof(MGPWireRec_FenceWaitServer) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFenceWait) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceWaitServer gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
std::abort();
}
#define MGP_WIRE_CHECK_BOUNDS(RecType, CallName) \
do { \
if (!(size >= sizeof(RecType) && size <= remaining && (size % 8) == 0)) { \
MGPipeWireProtocolFatal(CallName, size, remaining); \
} \
} while (0)
// Returns whether the record was applied. P0 is a SKELETON: every case validates its
// bounds and then reports "not applied", because no applier exists until P5 wires
// MG_Remote/Server/PipeApplier.cpp to the real backend tables. The switch and the opcode
// enum come from the same list, so a call added to the catalogue cannot be forgotten here;
// the default arm is for the opcode that never came from this catalogue at all - a byte
// off a corrupt stream - and it is fatal for the same reason the bounds check is.
inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, Uint64 remaining) {
(void)record;
switch (op) {
case MGPWireOp::GetCaps:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetCaps, "GetCaps");
return false;
case MGPWireOp::ResourceCreate:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceCreate, "ResourceCreate");
return false;
case MGPWireOp::ResourceRespecify:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceRespecify, "ResourceRespecify");
return false;
case MGPWireOp::ResourceDestroy:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceDestroy, "ResourceDestroy");
return false;
case MGPWireOp::MapPersistent:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_MapPersistent, "MapPersistent");
return false;
case MGPWireOp::UnmapPersistent:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_UnmapPersistent, "UnmapPersistent");
return false;
case MGPWireOp::FenceCreate:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceCreate, "FenceCreate");
return false;
case MGPWireOp::FenceStatus:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceStatus, "FenceStatus");
return false;
case MGPWireOp::FenceWait:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceWait, "FenceWait");
return false;
case MGPWireOp::FenceDestroy:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceDestroy, "FenceDestroy");
return false;
case MGPWireOp::QueryCreate:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryCreate, "QueryCreate");
return false;
case MGPWireOp::QueryBegin:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryBegin, "QueryBegin");
return false;
case MGPWireOp::QueryEnd:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryEnd, "QueryEnd");
return false;
case MGPWireOp::QueryAvailable:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryAvailable, "QueryAvailable");
return false;
case MGPWireOp::QueryResult:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryResult, "QueryResult");
return false;
case MGPWireOp::QueryDestroy:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryDestroy, "QueryDestroy");
return false;
case MGPWireOp::CreateRenderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateRenderState, "CreateRenderState");
return false;
case MGPWireOp::BindRenderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindRenderState, "BindRenderState");
return false;
case MGPWireOp::DeleteRenderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteRenderState, "DeleteRenderState");
return false;
case MGPWireOp::CreateVertexElements:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateVertexElements, "CreateVertexElements");
return false;
case MGPWireOp::BindVertexElements:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindVertexElements, "BindVertexElements");
return false;
case MGPWireOp::DeleteVertexElements:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteVertexElements, "DeleteVertexElements");
return false;
case MGPWireOp::CreateSamplerState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateSamplerState, "CreateSamplerState");
return false;
case MGPWireOp::DeleteSamplerState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteSamplerState, "DeleteSamplerState");
return false;
case MGPWireOp::CreateSamplerView:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateSamplerView, "CreateSamplerView");
return false;
case MGPWireOp::DeleteSamplerView:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteSamplerView, "DeleteSamplerView");
return false;
case MGPWireOp::CreateShaderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateShaderState, "CreateShaderState");
return false;
case MGPWireOp::BindShaderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindShaderState, "BindShaderState");
return false;
case MGPWireOp::DeleteShaderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteShaderState, "DeleteShaderState");
return false;
case MGPWireOp::SetDynamicState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDynamicState, "SetDynamicState");
return false;
case MGPWireOp::SetFramebufferState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetFramebufferState, "SetFramebufferState");
return false;
case MGPWireOp::SetVertexBuffers:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetVertexBuffers, "SetVertexBuffers");
return false;
case MGPWireOp::SetIndexBuffer:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetIndexBuffer, "SetIndexBuffer");
return false;
case MGPWireOp::SetIndirectBuffers:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetIndirectBuffers, "SetIndirectBuffers");
return false;
case MGPWireOp::SetSamplerViews:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetSamplerViews, "SetSamplerViews");
return false;
case MGPWireOp::BindSamplerStates:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindSamplerStates, "BindSamplerStates");
return false;
case MGPWireOp::SetShaderImages:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetShaderImages, "SetShaderImages");
return false;
case MGPWireOp::SetShaderBuffers:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetShaderBuffers, "SetShaderBuffers");
return false;
case MGPWireOp::SetStreamOutputTargets:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetStreamOutputTargets, "SetStreamOutputTargets");
return false;
case MGPWireOp::SetGlobalConstants:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetGlobalConstants, "SetGlobalConstants");
return false;
case MGPWireOp::SetVertexAttribDefaults:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetVertexAttribDefaults, "SetVertexAttribDefaults");
return false;
case MGPWireOp::SetPixelPackState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetPixelPackState, "SetPixelPackState");
return false;
case MGPWireOp::SetPatchState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetPatchState, "SetPatchState");
return false;
case MGPWireOp::SetDrawProgram:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDrawProgram, "SetDrawProgram");
return false;
case MGPWireOp::SetDispatchProgram:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDispatchProgram, "SetDispatchProgram");
return false;
case MGPWireOp::SetResidualValueState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetResidualValueState, "SetResidualValueState");
return false;
case MGPWireOp::SetTextureParams:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetTextureParams, "SetTextureParams");
return false;
case MGPWireOp::ResourceSubData:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceSubData, "ResourceSubData");
return false;
case MGPWireOp::BufferSubDataResident:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BufferSubDataResident, "BufferSubDataResident");
return false;
case MGPWireOp::ResourceSubDataComplete:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceSubDataComplete, "ResourceSubDataComplete");
return false;
case MGPWireOp::ResourceFlushRange:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceFlushRange, "ResourceFlushRange");
return false;
case MGPWireOp::ResourceReadback:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceReadback, "ResourceReadback");
return false;
case MGPWireOp::ResourceCopyRegion:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceCopyRegion, "ResourceCopyRegion");
return false;
case MGPWireOp::GenerateMipmap:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GenerateMipmap, "GenerateMipmap");
return false;
case MGPWireOp::GetTextureImage:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImage, "GetTextureImage");
return false;
case MGPWireOp::Blit:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Blit, "Blit");
return false;
case MGPWireOp::Clear:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Clear, "Clear");
return false;
case MGPWireOp::ReadPixels:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ReadPixels, "ReadPixels");
return false;
case MGPWireOp::DrawVbo:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DrawVbo, "DrawVbo");
return false;
case MGPWireOp::LaunchGrid:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_LaunchGrid, "LaunchGrid");
return false;
case MGPWireOp::MemoryBarrier:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_MemoryBarrier, "MemoryBarrier");
return false;
case MGPWireOp::BeginStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BeginStreamOutput, "BeginStreamOutput");
return false;
case MGPWireOp::EndStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_EndStreamOutput, "EndStreamOutput");
return false;
case MGPWireOp::PauseStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_PauseStreamOutput, "PauseStreamOutput");
return false;
case MGPWireOp::ResumeStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResumeStreamOutput, "ResumeStreamOutput");
return false;
case MGPWireOp::Flush:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Flush, "Flush");
return false;
case MGPWireOp::Present:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Present, "Present");
return false;
case MGPWireOp::SetSwapInterval:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetSwapInterval, "SetSwapInterval");
return false;
case MGPWireOp::QueryTimestamp:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryTimestamp, "QueryTimestamp");
return false;
case MGPWireOp::QueryCounter:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryCounter, "QueryCounter");
return false;
case MGPWireOp::FenceWaitServer:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceWaitServer, "FenceWaitServer");
return false;
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
MGPipeWireProtocolFatal("<unknown opcode>", size, remaining);
}
}
#undef MGP_WIRE_CHECK_BOUNDS
File diff suppressed because it is too large Load Diff
@@ -1,120 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Protocol/mg_protocol_base.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Shared vocabulary of the MG_Remote wire contracts (transport, framing, ring,
// shm). Inherited from the earlier `Feat/CS-Delta-IPC` branch
// (MobileGL/Protocol/mg_protocol_base.h) and cut down to what plan B's
// transport actually needs: result codes, byte spans, a shm region reference
// and the id typedefs.
//
// Deliberately NOT inherited: MobileGLObjectKind / MobileGLObjectScope /
// MobileGLObjectHandle. Plan B does not put GL object identity on the wire at
// all - the frontend allocates {slot, generation} handles in MG_Pipe
// (PLAN-B.md section 4.2.1) and those are the only identity the backend ever
// sees, so a second object-identity vocabulary here would be a drift surface
// with no reader.
//
// This header must stay:
// - pure C (compilable from C and C++, no MG C++ types, no exceptions/RTTI),
// - dependency-free (only <stdbool.h>/<stddef.h>/<stdint.h>),
// - append-only within an ABI major (see versioning rules below).
//
// Versioning rules (contract-wide):
// - Every versioned struct starts with uint32_t structSize.
// - Appending fields at the tail is a MINOR bump; receivers must ignore
// bytes beyond the structSize they know.
// - Changing/removing/reordering existing fields is a MAJOR bump.
// - A major mismatch is a hard, structured failure, never an exception.
// (Plan B keeps the structSize-first discipline as the answer to risk B-R10,
// PLAN-B.md section 14.2.)
#ifndef MOBILEGL_REMOTE_PROTOCOL_BASE_H
#define MOBILEGL_REMOTE_PROTOCOL_BASE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// ---------------------------------------------------------------------------
// ABI versions
// ---------------------------------------------------------------------------
#define MOBILEGL_PROTOCOL_ABI_MAJOR 1
#define MOBILEGL_PROTOCOL_ABI_MINOR 0
#define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor))
#define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16)
#define MOBILEGL_ABI_MINOR_OF(version) ((uint32_t)(version) & 0xFFFFu)
// ---------------------------------------------------------------------------
// Ids
// ---------------------------------------------------------------------------
typedef uint64_t MobileGLSessionId; // one client GL context flow
typedef uint64_t MobileGLRequestSeq; // matches a request to its reply
typedef uint32_t MobileGLSegmentId; // shm segment id within a connection
// ---------------------------------------------------------------------------
// Spans / regions
// ---------------------------------------------------------------------------
// Borrowed, read-only byte span. The pointee is owned by the producing side
// and is only valid for the duration documented at the consuming call site.
typedef struct MobileGLByteSpan {
const void* data;
uint64_t size;
} MobileGLByteSpan;
typedef struct MobileGLMutableByteSpan {
void* data;
uint64_t size;
} MobileGLMutableByteSpan;
// A byte range inside an already-established shm segment. Segments are
// announced out of band (the SegmentRef table on the control channel, with the
// fd itself passed by SCM_RIGHTS) and stay stable for their declared lifetime;
// offsets are segment-relative.
typedef struct MobileGLShmRegion {
MobileGLSegmentId segmentId;
uint32_t reserved;
uint64_t offset;
uint64_t size;
} MobileGLShmRegion;
// ---------------------------------------------------------------------------
// Result codes (structured errors across every contract boundary)
// ---------------------------------------------------------------------------
typedef enum MobileGLResult {
MOBILEGL_OK = 0,
MOBILEGL_ERR_NOT_INITIALIZED = 1,
MOBILEGL_ERR_INVALID_ARGUMENT = 2,
MOBILEGL_ERR_UNSUPPORTED = 3,
MOBILEGL_ERR_OUT_OF_MEMORY = 4,
MOBILEGL_ERR_PROTOCOL_MISMATCH = 5, // ABI/wire major mismatch, bad framing
MOBILEGL_ERR_TRANSPORT_CLOSED = 6, // peer gone / EOF
MOBILEGL_ERR_TIMEOUT = 7, // nothing arrived within the deadline
MOBILEGL_ERR_SHM_EXHAUSTED = 8,
MOBILEGL_ERR_SESSION_UNKNOWN = 9,
MOBILEGL_ERR_HANDLE_UNKNOWN = 10,
// The caller's buffer is smaller than the pending message. The message is
// NOT consumed and the required size is reported back; see
// ITransport::ReceiveFrame.
MOBILEGL_ERR_BUFFER_TOO_SMALL = 11,
MOBILEGL_ERR_FORCE_U32 = 0x7FFFFFFF
} MobileGLResult;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // MOBILEGL_REMOTE_PROTOCOL_BASE_H
-235
View File
@@ -1,235 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Protocol/protocol.fbs
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// MobileGL disaggregated wire protocol - CONTROL PLANE ONLY.
//
// Plan B (docs plan "MGPipe") section 8.1 inherits the transport design of the
// earlier plan verbatim, and its section 7.1 splits the schema in two:
//
// - rare / variable-length / must-evolve messages -> FlatBuffers *tables*,
// carried as complete framed messages over the control channel. That is
// everything in this file.
// - the hot path -> FlatBuffers *structs* (fixed layout, no vtable, no
// offset indirection) written straight into the SEG_CMD ring. Those
// records are generated from MG_Pipe/PipeCalls.def and are deliberately
// NOT in this schema yet: the call catalogue is a separate P0 deliverable
// and record numbering must never churn.
//
// Regeneration: scripts/gen_protocol.py (flatc is NOT part of the default
// build graph). generated/protocol_generated.h is committed and CI's
// flatc-check regenerates it and runs `git diff --exit-code`.
namespace MobileGL.Wire;
// ---------------------------------------------------------------------------
// Segments
// ---------------------------------------------------------------------------
// Segment layout is inherited unchanged (earlier plan section 6.1):
// SEG_CMD 8MiB / SEG_STAGE 32MiB+ / SEG_REPLY 8MiB / SEG_EVENT 256KiB /
// SEG_SHADOW[n] / SEG_ADOPT[n].
enum SegmentKind : ubyte {
None = 0,
Cmd = 1, // client-owned command ring (RingControl + records)
Stage = 2, // client-owned bulk staging
Reply = 3, // server-owned reply pool
Event = 4, // server-owned event ring
Shadow = 5, // client-owned per-object shadow (P4.5+)
Adopt = 6, // server-owned adopted store, client RW (>= 16MiB)
}
// The fd itself never travels in a message: POSIX passes it with SCM_RIGHTS on
// the aux socket (ITransport::ShareFd), Windows resolves `name`.
table SegmentRef {
id: uint;
kind: SegmentKind;
sizeBytes: ulong;
name: string;
}
// ---------------------------------------------------------------------------
// Handshake
// ---------------------------------------------------------------------------
table Hello {
abiMajor: uint;
abiMinor: uint;
buildFingerprint: string;
backendType: uint;
pid: uint;
configBlob: [ubyte];
}
table Welcome {
abiMajor: uint;
abiMinor: uint;
serverPid: uint;
cmdRing: SegmentRef;
stageRing: SegmentRef;
replyPool: SegmentRef;
eventRing: SegmentRef;
}
// ---------------------------------------------------------------------------
// Capabilities
// ---------------------------------------------------------------------------
// Replaces the 40 `pActiveBackendObject->` reads plus the 89 caps read sites
// (plan B appendix A, `get_caps`). The three blobs are byte-for-byte images of
// the corresponding POD structs; they are versioned by structSize-first
// discipline, not by this schema.
table CapsSnapshot {
dynamicParameters: [ubyte];
rendererInfo: [ubyte];
formatCaps: [ubyte];
extensions: [string];
apiVersion: string;
maxComputeWorkGroupCount: [int]; // 3 entries
maxComputeWorkGroupSize: [int]; // 3 entries
tableSlotMask: ulong; // which GLFunctionsTable slots the peer registered
prefersCpuXfbPrimitiveAccounting: bool;
}
table DefaultFramebufferInfo {
width: int;
height: int;
colorFormat: uint;
depthFormat: uint;
stencilFormat: uint;
}
// ---------------------------------------------------------------------------
// Surface / EGL lifecycle
// ---------------------------------------------------------------------------
enum SurfaceOpKind : ubyte {
None = 0,
InitializeDisplay = 1,
CreateWindowSurface = 2,
CreatePbufferSurface = 3,
ResizeWindowSurface = 4,
ReleaseSurface = 5,
MakeCurrent = 6,
ReleaseCurrent = 7,
}
enum WindowKind : ubyte {
None = 0,
AndroidNativeWindow = 1,
X11 = 2,
Win32Hwnd = 3,
Surfaceless = 4,
Pbuffer = 5,
}
table SurfaceOp {
seq: ulong;
kind: SurfaceOpKind;
display: ulong;
surface: ulong;
windowKind: WindowKind;
nativeToken: ulong; // X11 XID / HWND; Android transfers the window out of band
width: int;
height: int;
swapInterval: int;
}
table SurfaceReply {
seq: ulong;
ok: bool;
eglMajor: int;
eglMinor: int;
defaultFb: DefaultFramebufferInfo;
}
// ---------------------------------------------------------------------------
// Resync / aux / diagnostics
// ---------------------------------------------------------------------------
// Sent by the client after it observes a serverEpoch bump (context lost or
// server restart): every cached ring offset and every server-side object is
// gone and the whole pushed state has to be replayed.
table ResyncRequest {
serverEpoch: uint;
}
table ResyncDone {}
enum AuxRequestKind : ubyte {
None = 0,
FenceClientWait = 1,
QueryResult = 2,
ScalarGet = 3,
}
// Requests issued from a thread that is not the ring producer (foreign-thread
// sync / query polling), so they cannot take the SPSC ring.
table AuxRequest {
seq: ulong;
kind: AuxRequestKind;
payload: [ubyte];
}
enum FatalCode : uint {
None = 0,
ProtocolCorruption = 1, // record bounds / self-describing length violated
RingOverrun = 2,
SegmentMismatch = 3,
DeviceLost = 4,
ServerCrashed = 5,
AbiMismatch = 6,
}
table Fatal {
code: FatalCode;
message: string;
}
// Severity-graded per plan B section 8.2: <= Warn is lossy, >= Error is
// lossless and rate limited.
enum LogLevel : ubyte {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Fatal = 4,
}
table LogLine {
level: LogLevel;
text: string;
}
// ---------------------------------------------------------------------------
// Envelope
// ---------------------------------------------------------------------------
// Union tags are wire values: only ever APPEND to this list.
// ProgramReflection from the earlier plan's section 7.1 is intentionally
// absent - plan B ships program artifacts inside the create_shader_state CSO
// blob, so if a control-plane reflection message is ever needed it appends
// here rather than reserving a tag today.
union CtrlMsg {
Hello,
Welcome,
CapsSnapshot,
SurfaceOp,
SurfaceReply,
ResyncRequest,
ResyncDone,
AuxRequest,
Fatal,
LogLine,
}
table CtrlEnvelope {
msg: CtrlMsg;
}
root_type CtrlEnvelope;
file_identifier "MGLC";
-259
View File
@@ -1,259 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "Doorbell.h"
#include <MG_Util/Debug/Log.h>
#include <condition_variable>
#include <mutex>
#if !defined(_WIN32)
#include <cerrno>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
// Same fallback as FdPassing.cpp: on macOS / BSD the protection is SO_NOSIGPIPE on the
// socket, set in SocketDoorbell's constructor, not a per-send flag.
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
#define MSG_NOSIGNAL 0
#endif
namespace MobileGL::MG_Remote::Transport {
// -----------------------------------------------------------------------
// CondVarDoorbell
// -----------------------------------------------------------------------
struct CondVarDoorbell::Impl {
std::mutex mutex;
std::condition_variable cv;
// Counted, not a flag: a wakeup that arrives while nobody is parked
// must still be observed by the next Park.
std::uint32_t signals = 0;
};
CondVarDoorbell::CondVarDoorbell() : m_impl(new Impl()) {}
CondVarDoorbell::~CondVarDoorbell() { delete m_impl; }
void CondVarDoorbell::Notify() {
{
std::lock_guard<std::mutex> lock(m_impl->mutex);
++m_impl->signals;
}
m_impl->cv.notify_one();
}
bool CondVarDoorbell::Park(std::uint32_t timeoutMs) {
std::unique_lock<std::mutex> lock(m_impl->mutex);
// The death latch is tested under the same mutex Kill sets it under, so
// a Kill cannot slip between this test and the wait below: it either
// returns here or wakes the predicate.
if (m_dead.load(std::memory_order_relaxed)) {
return false;
}
if (m_impl->signals != 0) {
--m_impl->signals;
return true;
}
if (timeoutMs == 0) {
return false;
}
const auto woken = [this] {
return m_impl->signals != 0 || m_dead.load(std::memory_order_relaxed);
};
if (timeoutMs == kWaitForever) {
m_impl->cv.wait(lock, woken);
} else if (!m_impl->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), woken)) {
return false;
}
if (m_dead.load(std::memory_order_relaxed)) {
// Woken by Kill, not by an event. The caller re-tests its condition
// regardless (Doorbell::Wait always does) and then sees Dead().
return false;
}
--m_impl->signals;
return true;
}
void CondVarDoorbell::Kill() {
{
std::lock_guard<std::mutex> lock(m_impl->mutex);
m_dead.store(true, std::memory_order_release);
}
// notify_all, not notify_one: both a raw Park and a Doorbell::Wait may
// be parked here, and after this nobody will ring again.
m_impl->cv.notify_all();
}
void CondVarDoorbell::Reset() {
std::lock_guard<std::mutex> lock(m_impl->mutex);
m_impl->signals = 0;
}
#if !defined(_WIN32)
// -----------------------------------------------------------------------
// SocketDoorbell
// -----------------------------------------------------------------------
SocketDoorbell::SocketDoorbell(int fd, std::uint8_t code, bool ownsFd)
: m_fd(fd), m_code(code), m_ownsFd(ownsFd) {
#if defined(SO_NOSIGPIPE)
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one:
// a Notify to a hung-up peer must come back as EPIPE, not as a fatal signal.
if (m_fd >= 0) {
const int one = 1;
(void)::setsockopt(m_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
}
#endif
}
SocketDoorbell::~SocketDoorbell() {
if (m_ownsFd && m_fd >= 0) {
::close(m_fd);
}
}
void SocketDoorbell::Notify() {
if (m_fd < 0) {
return;
}
const std::uint8_t byte = m_code;
for (;;) {
const ssize_t written = ::send(m_fd, &byte, 1, MSG_DONTWAIT | MSG_NOSIGNAL);
if (written == 1) {
return;
}
if (written < 0 && errno == EINTR) {
continue;
}
if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// The socket buffer already holds unread wakeups: the peer has
// one pending, which is all a doorbell promises.
return;
}
if (written < 0 && (errno == EPIPE || errno == ECONNRESET)) {
// The peer is gone: it can never ring back either, so latch it
// here too rather than waiting for a Park to discover it.
m_dead = true;
return;
}
MGLOG_D("MG_Remote doorbell: send failed (errno=%d)", errno);
return;
}
}
bool SocketDoorbell::Park(std::uint32_t timeoutMs) {
if (m_fd < 0 || m_dead) {
return false;
}
const auto start = std::chrono::steady_clock::now();
for (;;) {
int pollTimeout = -1;
if (timeoutMs != kWaitForever) {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
}
struct pollfd pfd{};
pfd.fd = m_fd;
pfd.events = POLLIN;
const int ready = ::poll(&pfd, 1, pollTimeout);
if (ready < 0) {
if (errno == EINTR) {
continue; // a signal is not a wakeup; keep the deadline
}
MGLOG_D("MG_Remote doorbell: poll failed (errno=%d)", errno);
return false;
}
if (ready == 0) {
return false; // timed out
}
// revents has to be inspected, not just `ready > 0`. Once the peer
// closes its end the descriptor is permanently poll-ready with
// nothing to read (measured on Linux: revents=POLLIN|POLLHUP,
// recv()==0), so treating any readiness as a wakeup turns every
// park on a dead peer into a 100% CPU spin - unbounded, because
// Doorbell::Wait re-parks until its deadline and kWaitForever has
// none.
if ((pfd.revents & (POLLERR | POLLNVAL)) != 0) {
MGLOG_D("MG_Remote doorbell: fd %d unusable (revents=0x%X)", m_fd,
static_cast<unsigned>(pfd.revents));
m_dead = true;
return false;
}
if ((pfd.revents & POLLIN) != 0) {
if (Drain() != 0) {
return true; // a real wakeup byte
}
if (m_dead) {
return false; // EOF, not an event
}
// Ready but empty and still alive: someone else drained it.
// Report the wakeup and let the caller re-test its condition.
return true;
}
if ((pfd.revents & POLLHUP) != 0) {
m_dead = true;
return false;
}
// Readiness with no bit we requested or recognise: there is
// nothing to consume and no way to make progress, so refuse to
// poll this descriptor again.
MGLOG_D("MG_Remote doorbell: fd %d ready with revents=0x%X", m_fd,
static_cast<unsigned>(pfd.revents));
m_dead = true;
return false;
}
}
std::uint64_t SocketDoorbell::Drain() {
// Level-triggered to edge-triggered: swallow every queued byte so one
// stale wakeup cannot make later Parks return without an event.
std::uint64_t consumed = 0;
std::uint8_t scratch[64];
for (;;) {
const ssize_t got = ::recv(m_fd, scratch, sizeof(scratch), MSG_DONTWAIT);
if (got > 0) {
consumed += static_cast<std::uint64_t>(got);
continue;
}
if (got == 0) {
// Orderly shutdown on a stream socket: the peer is gone and
// will never ring again.
m_dead = true;
return consumed;
}
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return consumed; // drained
}
MGLOG_D("MG_Remote doorbell: recv failed (errno=%d)", errno);
m_dead = true;
return consumed;
}
}
void SocketDoorbell::Reset() {
if (m_fd < 0 || m_dead) {
return;
}
(void)Drain();
}
#endif // !_WIN32
} // namespace MobileGL::MG_Remote::Transport
-268
View File
@@ -1,268 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The bidirectional doorbell: spin briefly, then park.
//
// Both directions exist, and that is the point (inherited design, earlier plan
// section 6.2a):
// - client -> server: the consumer spins, sets consumerParked, then blocks;
// the producer rings only when consumerParked is set.
// - server -> client: the client spins MOBILEGL_IPC_SPIN_US (default 50us),
// sets producerParked, then blocks; the server rings after advancing any
// watermark, only when producerParked is set.
// Without the second direction every client wait - present credit, a blocking
// kNeedsAck request, a full ring - degenerates into a cross-process spin on
// one shared cache line: up to a whole frame of a big core at full clock on a
// phone, fighting the GPU and the game's JVM for it. MobileGL has no affinity
// control anywhere in the tree, so it cannot even be pushed to a little core.
//
// Two implementations, no platform-specific wakeup primitive (no futex, no
// eventfd, no named event):
// - CondVarDoorbell for `inproc` (one process, two threads),
// - SocketDoorbell for `spawn` (one byte on a socket; POSIX only).
//
// The lost-wakeup window is closed by two seq_cst FENCES, not by the ordering
// of the park flag's own load and store:
// - the waiter sets the flag, executes std::atomic_thread_fence(seq_cst),
// and THEN re-tests the condition (Doorbell::Wait);
// - the notifier publishes its watermark, executes the same fence, and THEN
// reads the flag (NotifyIfParked).
// Both fences sit in the single seq_cst total order, so one precedes the
// other, and [atomics.order] then forces at least one side to observe the
// other's store. The flag's own accesses may be relaxed: they are not what
// closes the window.
//
// A seq_cst store paired with a seq_cst load would NOT be enough, which is
// why the fences are here and why neither may be removed. That Dekker
// argument needs all FOUR accesses in the total order, and the other two are
// not: the watermark publish is a release store (RingProducer::Publish) and
// the condition re-test is an acquire load. On x86 the gap is concrete rather
// than theoretical - a release store is a plain MOV that can still sit in the
// store buffer while the load of the park flag, also a plain MOV, reads 0, so
// the notifier skips the ring and the waiter parks on a stale watermark
// forever. (ARMv8 survives it only because STLR->LDAR is RCsc, i.e. by luck.)
//
// The other half of the contract is ordering between the caller and the
// fence: NotifyIfParked must be called AFTER the watermark is published. A
// fence only orders what precedes it.
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
#if defined(__x86_64__) || defined(__i386__)
#include <immintrin.h>
#endif
namespace MobileGL::MG_Remote::Transport {
// MOBILEGL_IPC_SPIN_US default.
inline constexpr std::uint32_t kDefaultSpinUs = 50;
// Park with no deadline.
inline constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
// Wire codes, so a shared socket can carry both directions distinguishably.
inline constexpr std::uint8_t kDoorbellRingAdvanced = 0x01; // client -> server
inline constexpr std::uint8_t kDoorbellWatermarkAdvanced = 0x02; // server -> client
inline void CpuRelax() {
#if defined(__x86_64__) || defined(__i386__)
_mm_pause();
#elif defined(__aarch64__) || defined(__arm__)
__asm__ __volatile__("yield" ::: "memory");
#else
std::atomic_signal_fence(std::memory_order_seq_cst);
#endif
}
class Doorbell {
public:
virtual ~Doorbell() = default;
Doorbell(const Doorbell&) = delete;
Doorbell& operator=(const Doorbell&) = delete;
// Wakes a parked peer. Cheap and idempotent: a wakeup that arrives when
// nobody is parked is remembered, so the next Park returns immediately
// rather than sleeping through an event that already happened.
virtual void Notify() = 0;
// Blocks until notified or the deadline passes. Returns true when a
// wakeup was consumed. timeoutMs == 0 polls; kWaitForever never times
// out.
virtual bool Park(std::uint32_t timeoutMs) = 0;
// Drops pending wakeups. Used when a waiter gives up, so a stale byte
// does not make the next Park return spuriously forever.
virtual void Reset() = 0;
// True once the wakeup channel is permanently unusable: the peer closed
// its end of the socket, or the inproc channel was shut down. A dead
// doorbell can never deliver another wakeup, and Wait must stop
// re-parking on it - for the socket because its descriptor is
// permanently poll-ready and a waiter with no deadline would burn a
// big core at full clock, for the condvar because Park would otherwise
// block forever and Shutdown could never join the waiter. Every
// implementation has a death state; the base default is only for a
// bell that cannot die.
virtual bool Dead() const { return false; }
// Spin `spinUs`, then park until `ready()` or the deadline.
// `parked` is the RingControl flag the peer tests before ringing.
template <class Ready>
bool Wait(std::atomic<std::uint32_t>& parked, Ready&& ready, std::uint32_t spinUs,
std::uint32_t timeoutMs) {
if (ready()) {
return true;
}
const auto start = std::chrono::steady_clock::now();
const auto deadline = timeoutMs == kWaitForever
? std::chrono::steady_clock::time_point::max()
: start + std::chrono::milliseconds(timeoutMs);
const auto spinEnd = start + std::chrono::microseconds(spinUs);
while (std::chrono::steady_clock::now() < spinEnd) {
if (ready()) {
return true;
}
CpuRelax();
}
for (;;) {
// Announce, FENCE, then re-test. The fence is the mechanism -
// see the file header - so setting the flag itself is relaxed.
parked.store(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
if (ready()) {
parked.store(0, std::memory_order_relaxed);
return true;
}
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
parked.store(0, std::memory_order_relaxed);
return ready();
}
std::uint32_t chunkMs = kWaitForever;
if (timeoutMs != kWaitForever) {
const auto remaining =
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count();
chunkMs = remaining <= 0 ? 0 : static_cast<std::uint32_t>(remaining);
}
Park(chunkMs);
// Clearing is relaxed on purpose: a notifier that reads a
// stale 1 only rings a bell nobody is waiting on, which the
// doorbell remembers and the next Park consumes. The dangerous
// direction - a notifier reading 0 while the waiter is really
// parked - is the one the fence above rules out.
parked.store(0, std::memory_order_relaxed);
if (ready()) {
return true;
}
if (Dead()) {
// Nothing can ring this bell again and parking on it no
// longer blocks, so looping here would spin at full clock
// for as long as the caller is willing to wait - which,
// with kWaitForever, is forever.
return false;
}
if (timeoutMs != kWaitForever && std::chrono::steady_clock::now() >= deadline) {
return false;
}
}
}
protected:
Doorbell() = default;
};
// Rings `bell` only when the peer said it is parked.
//
// PRECONDITION: whatever the waiter's condition reads - the ring head, a
// sequence watermark, a queue push - is ALREADY published when this is
// called. The fence only orders what precedes it, so ringing before
// publishing reopens the window this closes. The fence pairs with the one
// in Doorbell::Wait; see the file header for why the flag's own memory
// order is not what makes this sound.
inline void NotifyIfParked(Doorbell& bell, std::atomic<std::uint32_t>& parked) {
std::atomic_thread_fence(std::memory_order_seq_cst);
if (parked.load(std::memory_order_relaxed) != 0) {
bell.Notify();
}
}
// `inproc`: one process, two threads.
class CondVarDoorbell final : public Doorbell {
public:
CondVarDoorbell();
~CondVarDoorbell() override;
void Notify() override;
bool Park(std::uint32_t timeoutMs) override;
void Reset() override;
bool Dead() const override { return m_dead.load(std::memory_order_acquire); }
// Hangs the bell up for good: every parked waiter returns false now and
// every later Park returns false at once. The inproc twin of the socket
// peer closing its end (SocketDoorbell latches m_dead on EOF), and what
// InProcessChannel::Close rings instead of Notify. A Notify is consumed
// by ONE Park; Doorbell::Wait then re-tests its condition, finds
// nothing published, finds the bell alive, and with kWaitForever parks
// again - so a Shutdown that only rang could never join a server thread
// sitting in the design's own steady state (spun, set consumerParked,
// blocked). Irreversible by design, like the socket's.
void Kill();
private:
struct Impl;
Impl* m_impl;
std::atomic<bool> m_dead{false};
};
#if !defined(_WIN32)
// `spawn`: one byte on a socket (one direction of a socketpair, or the aux
// socket). POSIX only; the Windows path will use an overlapped named pipe
// and is not part of this skeleton.
class SocketDoorbell final : public Doorbell {
public:
// `fd` must be one end of an AF_UNIX socket pair, not a pipe: Notify
// uses send() with MSG_DONTWAIT|MSG_NOSIGNAL and Park uses
// poll()+recv(), which a pipe end refuses with ENOTSOCK. Prefer
// SOCK_STREAM for the spawn transport - measured on Linux, a closed
// peer makes a stream end report POLLIN|POLLHUP with recv()==0, which
// is how death is detected, while a SOCK_DGRAM end reports no
// readiness at all and a waiter with no deadline would simply hang.
// When `ownsFd` the descriptor is closed with this object. `code` is
// the byte written by Notify.
SocketDoorbell(int fd, std::uint8_t code, bool ownsFd);
~SocketDoorbell() override;
void Notify() override;
bool Park(std::uint32_t timeoutMs) override;
void Reset() override;
bool Dead() const override { return m_dead; }
int Fd() const { return m_fd; }
private:
// Consumes every queued wakeup byte and returns how many. Latches
// m_dead on EOF: recv returning 0 on a stream socket is the peer's
// hangup, not a wakeup, and the descriptor stays poll-ready forever
// afterwards.
std::uint64_t Drain();
int m_fd;
std::uint8_t m_code;
bool m_ownsFd;
bool m_dead = false;
};
#endif
} // namespace MobileGL::MG_Remote::Transport
-323
View File
@@ -1,323 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/FdPassing.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "FdPassing.h"
#include <MG_Util/Debug/Log.h>
#include <chrono>
#include <cstring>
#if !defined(_WIN32)
#include <cerrno>
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#endif
// MSG_NOSIGNAL is Linux (and Android). macOS and the BSDs spell the same protection as the
// SO_NOSIGPIPE socket option, set once per socket at creation (CreateSocketPair below, and
// SocketDoorbell's constructor). With neither, a write to a hung-up peer raises SIGPIPE and
// kills the process instead of returning EPIPE.
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
#define MSG_NOSIGNAL 0
#endif
namespace MobileGL::MG_Remote::Transport::FdPassing {
#if defined(_WIN32)
bool Supported() { return false; }
MobileGLResult CreateSocketPair(int[2]) { return MOBILEGL_ERR_UNSUPPORTED; }
MobileGLResult SendFd(int, int, MobileGLByteSpan) { return MOBILEGL_ERR_UNSUPPORTED; }
MobileGLResult ReceiveFd(int, int*, MobileGLMutableByteSpan, std::uint64_t*, std::uint32_t) {
return MOBILEGL_ERR_UNSUPPORTED;
}
#else
namespace {
// Every datagram starts with this, so the sideband length is explicit
// and a stray datagram is recognisable.
struct SidebandHeader {
std::uint32_t magic;
std::uint32_t sidebandSize;
};
constexpr std::uint32_t kSidebandMagic = 0x4446474Du; // 'MGFD' on the wire
int WaitReadable(int socket, std::uint32_t timeoutMs) {
const auto start = std::chrono::steady_clock::now();
for (;;) {
int pollTimeout = -1;
if (timeoutMs != 0xFFFFFFFFu) {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
}
struct pollfd pfd{};
pfd.fd = socket;
pfd.events = POLLIN;
const int ready = ::poll(&pfd, 1, pollTimeout);
if (ready < 0 && errno == EINTR) {
continue;
}
return ready;
}
}
} // namespace
bool Supported() { return true; }
MobileGLResult CreateSocketPair(int outFds[2]) {
if (outFds == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
int fds[2] = {-1, -1};
int type = SOCK_DGRAM;
#if defined(SOCK_CLOEXEC)
type |= SOCK_CLOEXEC;
#endif
if (::socketpair(AF_UNIX, type, 0, fds) != 0) {
MGLOG_E("MG_Remote fd passing: socketpair failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
#if defined(SO_NOSIGPIPE)
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one.
for (int fd : fds) {
const int one = 1;
(void)::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
}
#endif
outFds[0] = fds[0];
outFds[1] = fds[1];
return MOBILEGL_OK;
}
MobileGLResult SendFd(int socket, int fd, MobileGLByteSpan sideband) {
if (socket < 0 || fd < 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (sideband.size > kMaxSidebandBytes || (sideband.size != 0 && sideband.data == nullptr)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::uint8_t payload[sizeof(SidebandHeader) + kMaxSidebandBytes];
SidebandHeader header{};
header.magic = kSidebandMagic;
header.sidebandSize = static_cast<std::uint32_t>(sideband.size);
std::memcpy(payload, &header, sizeof(header));
if (sideband.size != 0) {
std::memcpy(payload + sizeof(header), sideband.data,
static_cast<std::size_t>(sideband.size));
}
const std::size_t payloadSize = sizeof(header) + static_cast<std::size_t>(sideband.size);
struct iovec iov{};
iov.iov_base = payload;
iov.iov_len = payloadSize;
// CMSG_SPACE, not sizeof: the control buffer has to hold the aligned
// cmsghdr as well as the descriptor.
union {
struct cmsghdr align;
char bytes[CMSG_SPACE(sizeof(int))];
} control{};
std::memset(&control, 0, sizeof(control));
struct msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control.bytes;
msg.msg_controllen = sizeof(control.bytes);
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
std::memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
for (;;) {
const ssize_t sent = ::sendmsg(socket, &msg, MSG_NOSIGNAL);
if (sent >= 0) {
if (static_cast<std::size_t>(sent) != payloadSize) {
// A datagram socket sends all or nothing.
MGLOG_E("MG_Remote fd passing: short datagram (%zd of %zu bytes)", sent,
payloadSize);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
return MOBILEGL_OK;
}
if (errno == EINTR) {
continue;
}
if (errno == EPIPE || errno == ECONNRESET) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MGLOG_E("MG_Remote fd passing: sendmsg failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
}
MobileGLResult ReceiveFd(int socket, int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) {
if (socket < 0 || outFd == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outFd = -1;
if (outSidebandSize != nullptr) {
*outSidebandSize = 0;
}
// Checked before the recvmsg: a datagram cannot be partially consumed,
// so a too-small destination must never cost us the descriptor.
if (sideband.size < kMaxSidebandBytes) {
if (outSidebandSize != nullptr) {
*outSidebandSize = kMaxSidebandBytes;
}
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (sideband.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
const int ready = WaitReadable(socket, timeoutMs);
if (ready < 0) {
MGLOG_E("MG_Remote fd passing: poll failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (ready == 0) {
return MOBILEGL_ERR_TIMEOUT;
}
std::uint8_t payload[sizeof(SidebandHeader) + kMaxSidebandBytes];
struct iovec iov{};
iov.iov_base = payload;
iov.iov_len = sizeof(payload);
union {
struct cmsghdr align;
char bytes[CMSG_SPACE(sizeof(int) * 4)];
} control{};
std::memset(&control, 0, sizeof(control));
struct msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control.bytes;
msg.msg_controllen = sizeof(control.bytes);
ssize_t got = 0;
for (;;) {
int flags = 0;
#if defined(MSG_CMSG_CLOEXEC)
flags |= MSG_CMSG_CLOEXEC;
#endif
got = ::recvmsg(socket, &msg, flags);
if (got >= 0) {
break;
}
if (errno == EINTR) {
continue;
}
if (errno == ECONNRESET) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MGLOG_E("MG_Remote fd passing: recvmsg failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (got == 0) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
// Collect every descriptor first, so an unexpected extra one is closed
// rather than leaked, whatever else is wrong with the message.
int received[4];
int receivedCount = 0;
for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); cmsg != nullptr;
cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) {
continue;
}
const std::size_t bytes = cmsg->cmsg_len - CMSG_LEN(0);
const int count = static_cast<int>(bytes / sizeof(int));
for (int i = 0; i < count && receivedCount < 4; ++i) {
int fd = -1;
std::memcpy(&fd, CMSG_DATA(cmsg) + i * sizeof(int), sizeof(fd));
received[receivedCount++] = fd;
}
}
#if !defined(MSG_CMSG_CLOEXEC)
// No atomic close-on-exec on receive here (macOS, the BSDs): set it by hand on every
// descriptor that arrived, before anything else can fork. The window between the
// recvmsg and this loop is the platform's, not ours; leaving the flag off altogether
// would hand every shared segment to every child the process ever spawns.
for (int i = 0; i < receivedCount; ++i) {
if (received[i] >= 0) {
(void)::fcntl(received[i], F_SETFD, FD_CLOEXEC);
}
}
#endif
const auto closeAll = [&](int keepIndex) {
for (int i = 0; i < receivedCount; ++i) {
if (i != keepIndex && received[i] >= 0) {
::close(received[i]);
}
}
};
if ((msg.msg_flags & MSG_CTRUNC) != 0) {
// The kernel dropped ancillary data: whatever arrived is not a
// complete offer, and silently continuing would hand the caller a
// half-transferred segment.
MGLOG_E("MG_Remote fd passing: ancillary data truncated; the descriptor did not "
"arrive intact");
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (receivedCount != 1) {
MGLOG_E("MG_Remote fd passing: expected exactly one descriptor, got %d", receivedCount);
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (static_cast<std::size_t>(got) < sizeof(SidebandHeader)) {
MGLOG_E("MG_Remote fd passing: %zd byte datagram is shorter than the header", got);
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
SidebandHeader header{};
std::memcpy(&header, payload, sizeof(header));
if (header.magic != kSidebandMagic ||
header.sidebandSize > kMaxSidebandBytes ||
sizeof(SidebandHeader) + header.sidebandSize != static_cast<std::size_t>(got)) {
MGLOG_E("MG_Remote fd passing: bad sideband header (magic=0x%08X size=%u datagram=%zd)",
header.magic, header.sidebandSize, got);
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (header.sidebandSize != 0) {
std::memcpy(sideband.data, payload + sizeof(SidebandHeader), header.sidebandSize);
}
if (outSidebandSize != nullptr) {
*outSidebandSize = header.sidebandSize;
}
*outFd = received[0];
closeAll(0);
return MOBILEGL_OK;
}
#endif // _WIN32
} // namespace MobileGL::MG_Remote::Transport::FdPassing
-67
View File
@@ -1,67 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/FdPassing.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SCM_RIGHTS descriptor passing over an AF_UNIX socket pair. POSIX only.
//
// This is the FIRST transport commit, deliberately (inherited design, plan
// section 8.1, "SCM_RIGHTS must be implemented in the first transport
// commit"). The earlier branch pushed it to a later phase and hardcoded
// `out->fd = -1` in its offer poll, so on the only platform that matters its
// data plane could never move a byte: every segment announcement resolved to
// "no descriptor". A transport whose shm cannot cross the process boundary is
// not a transport.
//
// Channel shape: a dedicated AF_UNIX SOCK_DGRAM socketpair, NOT the control
// byte stream. Two reasons:
// - SOCK_DGRAM preserves message boundaries on every POSIX (SOCK_SEQPACKET
// does not exist on macOS), so one sendmsg is exactly one recvmsg and the
// ancillary data can never be split away from its payload;
// - ancillary data attached to a byte stream binds to whichever ordinary
// byte happens to be at the front of the reader's buffer, which is
// unmanageable once frames are being reassembled.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <cstdint>
namespace MobileGL::MG_Remote::Transport::FdPassing {
// Upper bound for the bytes that travel with a descriptor (a SegmentRef
// sized announcement, not payload).
inline constexpr std::uint64_t kMaxSidebandBytes = 256;
// False on platforms without SCM_RIGHTS (Windows).
bool Supported();
// Creates the aux socket pair. Both descriptors are CLOEXEC and owned by
// the caller. outFds[0] is conventionally the client end, [1] the server's
// (the one that is inherited or passed to the spawned process).
MobileGLResult CreateSocketPair(int outFds[2]);
// Sends `fd` with `sideband` attached. The caller keeps ownership of `fd`
// (the peer gets its own descriptor for the same open file description).
// sideband.size must be <= kMaxSidebandBytes.
MobileGLResult SendFd(int socket, int fd, MobileGLByteSpan sideband);
// Receives one descriptor and its sideband bytes.
//
// `sideband` must be at least kMaxSidebandBytes: a datagram cannot be
// partially consumed, so the capacity is checked BEFORE anything is read.
// A short buffer returns MOBILEGL_ERR_BUFFER_TOO_SMALL with
// *outSidebandSize = kMaxSidebandBytes and consumes nothing, so no
// descriptor is ever dropped on the floor.
//
// On success *outFd owns a descriptor this process must close.
// MOBILEGL_ERR_TIMEOUT when nothing arrived (timeoutMs 0 = poll),
// MOBILEGL_ERR_TRANSPORT_CLOSED on peer close.
MobileGLResult ReceiveFd(int socket, int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs);
} // namespace MobileGL::MG_Remote::Transport::FdPassing
-208
View File
@@ -1,208 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/Framing.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Control-channel wire framing: [u32 magic 'MGLF'][u32 payloadLength][payload].
// Length excludes the 8-byte header and is capped at 64 MiB.
//
// Two defects of the earlier branch's codec are fixed here, and both are the
// reason this file is not a copy of it:
//
// 1. Its Feed() unconditionally returned OK and its header peek merely
// returned false on a bad magic or an oversized length. A corrupt or
// desynchronized stream therefore turned into a silent, permanent hang -
// the reader kept waiting for a message that could never be parsed, with
// no error anywhere. Here a violation latches a failed state, is logged at
// ERROR, and every later call returns MOBILEGL_ERR_PROTOCOL_MISMATCH.
//
// 2. Its receive path failed the call and consumed the message when the
// caller's buffer was too small, wedging the stream. Here
// MOBILEGL_ERR_BUFFER_TOO_SMALL reports the required size and KEEPS the
// message queued.
//
// The reader is a plain byte-stream reassembler: it never assumes a read()
// returned a whole frame.
#pragma once
#include "../Protocol/mg_protocol_base.h"
// NOT <MG_Util/Debug/Log.h>: that header pulls the GL frontend's umbrella into
// every translation unit that reassembles a frame. See WireLog.h.
#include "WireLog.h"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
namespace MobileGL::MG_Remote::Transport {
// 'MGLF', little-endian on the wire (both ends are the same machine).
inline constexpr std::uint32_t kFrameMagic = 0x464C474Du;
inline constexpr std::uint64_t kFrameHeaderSize = 8;
inline constexpr std::uint64_t kMaxFramePayloadSize = 64ull * 1024 * 1024;
// Compaction threshold: consumed bytes are dropped from the front once
// enough of them accumulate, so a long-lived reader neither memmoves per
// message nor grows without bound.
inline constexpr std::uint64_t kFrameReaderCompactThreshold = 64ull * 1024;
// Appends one framed message to `out`.
inline MobileGLResult AppendFrame(std::vector<std::uint8_t>& out, const void* payload,
std::uint64_t size) {
if (size > kMaxFramePayloadSize) {
WireLogError("MG_Remote framing: refusing to send a %llu byte payload (cap %llu); "
"bulk bytes belong in shm",
static_cast<unsigned long long>(size),
static_cast<unsigned long long>(kMaxFramePayloadSize));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (size != 0 && payload == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::uint8_t header[kFrameHeaderSize];
const std::uint32_t magic = kFrameMagic;
const std::uint32_t length = static_cast<std::uint32_t>(size);
std::memcpy(header + 0, &magic, sizeof(magic));
std::memcpy(header + 4, &length, sizeof(length));
out.insert(out.end(), header, header + kFrameHeaderSize);
const auto* bytes = static_cast<const std::uint8_t*>(payload);
out.insert(out.end(), bytes, bytes + size);
return MOBILEGL_OK;
}
// Incremental frame extractor over a raw byte stream.
class FrameReader {
public:
// Feeds raw stream bytes. Validates the frame header the moment enough
// bytes for one exist - a bad magic or an oversized length is reported
// here, not swallowed.
MobileGLResult Feed(const void* data, std::uint64_t size) {
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (size != 0) {
if (data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
const auto* bytes = static_cast<const std::uint8_t*>(data);
m_buffer.insert(m_buffer.end(), bytes, bytes + size);
}
return ParseHeader();
}
bool Failed() const { return m_failed; }
bool HasMessage() const {
return !m_failed && m_haveHeader && Available() >= kFrameHeaderSize + m_pendingSize;
}
// Size of the next complete message, or 0 when none is complete yet.
std::uint64_t PendingMessageSize() const { return HasMessage() ? m_pendingSize : 0; }
std::uint64_t BufferedBytes() const { return Available(); }
// Copies the next complete message out.
// MOBILEGL_OK - copied, *outSize set, message consumed
// MOBILEGL_ERR_BUFFER_TOO_SMALL - *outSize = required size, message KEPT
// MOBILEGL_ERR_TIMEOUT - no complete message buffered
// MOBILEGL_ERR_PROTOCOL_MISMATCH- the stream is latched failed
MobileGLResult TakeMessage(MobileGLMutableByteSpan buffer, std::uint64_t* outSize) {
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (!HasMessage()) {
return MOBILEGL_ERR_TIMEOUT;
}
if (outSize != nullptr) {
*outSize = m_pendingSize;
}
if (buffer.size < m_pendingSize) {
// The message stays queued; the caller retries with a big
// enough buffer.
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (m_pendingSize != 0) {
if (buffer.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::memcpy(buffer.data, m_buffer.data() + m_readPos + kFrameHeaderSize,
static_cast<std::size_t>(m_pendingSize));
}
Consume();
return MOBILEGL_OK;
}
// Convenience overload that sizes the destination itself.
MobileGLResult TakeMessage(std::vector<std::uint8_t>& out) {
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (!HasMessage()) {
return MOBILEGL_ERR_TIMEOUT;
}
const auto* first = m_buffer.data() + m_readPos + kFrameHeaderSize;
out.assign(first, first + m_pendingSize);
Consume();
return MOBILEGL_OK;
}
private:
std::uint64_t Available() const { return m_buffer.size() - m_readPos; }
MobileGLResult ParseHeader() {
if (m_haveHeader || Available() < kFrameHeaderSize) {
return MOBILEGL_OK;
}
std::uint32_t magic = 0;
std::uint32_t length = 0;
std::memcpy(&magic, m_buffer.data() + m_readPos, sizeof(magic));
std::memcpy(&length, m_buffer.data() + m_readPos + 4, sizeof(length));
if (magic != kFrameMagic) {
m_failed = true;
WireLogError("MG_Remote framing: bad frame magic 0x%08X (expected 0x%08X); the "
"control stream is desynchronized and this transport is now dead",
magic, kFrameMagic);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (length > kMaxFramePayloadSize) {
m_failed = true;
WireLogError("MG_Remote framing: frame length %u exceeds the %llu byte cap; "
"refusing to allocate on a peer-supplied length",
length, static_cast<unsigned long long>(kMaxFramePayloadSize));
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
m_pendingSize = length;
m_haveHeader = true;
return MOBILEGL_OK;
}
void Consume() {
m_readPos += kFrameHeaderSize + m_pendingSize;
m_pendingSize = 0;
m_haveHeader = false;
if (m_readPos == m_buffer.size()) {
m_buffer.clear();
m_readPos = 0;
} else if (m_readPos >= kFrameReaderCompactThreshold) {
m_buffer.erase(m_buffer.begin(),
m_buffer.begin() + static_cast<std::ptrdiff_t>(m_readPos));
m_readPos = 0;
}
// Header of the next message may already be buffered.
(void)ParseHeader();
}
std::vector<std::uint8_t> m_buffer;
std::uint64_t m_readPos = 0;
std::uint64_t m_pendingSize = 0;
bool m_haveHeader = false;
bool m_failed = false;
};
} // namespace MobileGL::MG_Remote::Transport
-130
View File
@@ -1,130 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/ITransport.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The control-plane transport interface.
//
// It is deliberately dumb: complete messages in, complete messages out, plus
// the one thing shared memory cannot do without help - handing a file
// descriptor to the peer. No session routing, no seq accounting, no
// serialization; those live above, in the protocol layer.
//
// Everything on the hot path bypasses this interface entirely: records go into
// the SEG_CMD ring (Ring.h) and the peer is woken through a Doorbell
// (Doorbell.h). ITransport carries the handshake, surface ops, resync, aux
// requests and fatals - the rare, variable-length, must-evolve traffic that
// plan section 7.1 assigns to FlatBuffers tables.
//
// This header stays dependency-light on purpose (mg_protocol_base.h plus the
// standard library): it is included by both roles and by the eventual
// server-side binary, and nothing about a byte pipe needs the GL frontend's
// umbrella header.
//
// Threading: one instance is not internally synchronized for send; callers
// serialize sends. ReceiveFrame/ReceiveFd may be called from one dedicated
// reader thread concurrently with sends from another.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
// Which end of the connection this instance is.
enum class TransportRole : std::uint32_t {
Server = 1, // accepts the client connection
Client = 2, // connects to the server endpoint
InProcess = 3, // same-process hand-off (CI / inproc delivery mode)
};
class ITransport {
public:
virtual ~ITransport() = default;
ITransport(const ITransport&) = delete;
ITransport& operator=(const ITransport&) = delete;
// ---- control plane -------------------------------------------------
// Sends one complete message. `bytes` is borrowed: the implementation
// either copies it or completes the underlying write before returning.
// A payload larger than Framing::kMaxFramePayloadSize is rejected with
// MOBILEGL_ERR_INVALID_ARGUMENT - bulk bytes belong in shm, never here.
virtual MobileGLResult SendFrame(MobileGLByteSpan bytes) = 0;
// Receives the next complete message.
//
// MOBILEGL_OK - copied into `buffer`, *outSize is
// the message size, message consumed.
// MOBILEGL_ERR_BUFFER_TOO_SMALL - `buffer` is too small. *outSize is
// the size required and THE MESSAGE
// STAYS QUEUED: call again with a
// buffer of at least that size and it
// is still there.
// MOBILEGL_ERR_TIMEOUT - nothing arrived within timeoutMs
// (0 = non-blocking poll).
// MOBILEGL_ERR_TRANSPORT_CLOSED - peer gone, nothing left buffered.
// MOBILEGL_ERR_PROTOCOL_MISMATCH- framing violated; the transport is
// latched failed and never recovers.
//
// The buffer-too-small half of that contract is the whole point of
// having one: the earlier branch's transport failed the call AND
// dropped the message, which wedges the stream permanently the first
// time a message is bigger than the reader's guess.
virtual MobileGLResult ReceiveFrame(MobileGLMutableByteSpan buffer, std::uint64_t* outSize,
std::uint32_t timeoutMs) = 0;
// Size of the next pending message, or 0 when none is buffered. Lets a
// caller size its buffer without a failed receive first.
virtual std::uint64_t PeekFrameSize() = 0;
// ---- descriptor passing --------------------------------------------
// Hands `fd` to the peer. POSIX: SCM_RIGHTS over the aux socket (see
// FdPassing.h). Windows: not applicable, returns
// MOBILEGL_ERR_UNSUPPORTED - the section name travels inside SegmentRef
// instead. The caller keeps ownership of `fd` and closes it itself.
//
// This is a first-class member of the interface, not a later phase: the
// earlier branch deferred it and hardcoded `out->fd = -1` in its offer
// poll, so its data plane could not move a single byte on the only
// platform that matters.
virtual MobileGLResult ShareFd(int fd, MobileGLByteSpan sideband) = 0;
// Receives one fd previously shared by the peer. On success *outFd owns
// a descriptor this process must close. `sideband` receives the bytes
// that travelled with it (may be empty) and must be at least
// FdPassing::kMaxSidebandBytes: an fd offer is one datagram and cannot
// be half-consumed, so the capacity is checked BEFORE anything is read
// and a short buffer returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the
// required size, having consumed nothing and dropped no descriptor.
virtual MobileGLResult ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) = 0;
// ---- lifecycle ------------------------------------------------------
// Idempotent. Tears down the WHOLE connection, not just this end:
// both directions are half-closed, so after either endpoint calls it
// neither side can send any more (SendFrame returns
// MOBILEGL_ERR_TRANSPORT_CLOSED) and every waiter on either side is
// unblocked. That is what closing a socket does, and the spawn
// transport behaves the same way, so a one-sided contract here would
// be a promise only the in-process implementation could keep.
//
// Messages already queued stay readable until drained: a peer that
// shuts down right after sending does not lose its last message.
virtual void Shutdown() = 0;
virtual TransportRole Role() const = 0;
protected:
ITransport() = default;
};
} // namespace MobileGL::MG_Remote::Transport
@@ -1,293 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/InProcessTransport.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "InProcessTransport.h"
#include "FdPassing.h"
#include "Framing.h"
#include <MG_Util/Debug/Log.h>
#include <cerrno>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <mutex>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
namespace MobileGL::MG_Remote::Transport {
namespace {
struct FdOffer {
int fd = -1;
std::vector<std::uint8_t> sideband;
};
} // namespace
// One direction of the channel: everything queued FOR one endpoint.
class InProcessChannel {
public:
struct Direction {
std::mutex mutex;
// One variable per predicate. A single cv signalled with
// notify_one would let a SendFrame's wakeup land on a thread
// blocked in ReceiveFd, which re-tests its own predicate and goes
// straight back to sleep - leaving a queued message undelivered
// until some unrelated later event. ITransport narrows the
// contract to one dedicated reader thread, but a comment is not a
// reason to ship a primitive that breaks the moment someone
// splits the reader.
std::condition_variable cv; // messages
std::condition_variable fdCv; // fdOffers
std::deque<std::vector<std::uint8_t>> messages;
std::deque<FdOffer> fdOffers;
bool closed = false;
};
~InProcessChannel() {
for (Direction& dir : m_directions) {
for (FdOffer& offer : dir.fdOffers) {
#if !defined(_WIN32)
if (offer.fd >= 0) {
::close(offer.fd);
}
#endif
}
dir.fdOffers.clear();
}
}
Direction& Inbox(int endpoint) { return m_directions[endpoint]; }
Direction& Outbox(int endpoint) { return m_directions[1 - endpoint]; }
CondVarDoorbell& Bell(int endpoint) { return m_bells[endpoint]; }
void Close() {
for (Direction& dir : m_directions) {
{
std::lock_guard<std::mutex> lock(dir.mutex);
dir.closed = true;
}
dir.cv.notify_all();
dir.fdCv.notify_all();
}
// Anything parked on a ring doorbell has to come back too, or a
// shutdown mid-frame hangs the peer forever. Kill, not Notify: a
// ring is consumed by one Park, after which Doorbell::Wait re-tests
// a condition nothing published and - the bell still reporting
// alive - parks again, with no deadline forever. Only Dead() ends
// that loop.
for (CondVarDoorbell& bell : m_bells) {
bell.Kill();
}
}
private:
Direction m_directions[2];
CondVarDoorbell m_bells[2];
};
InProcessTransport::InProcessTransport(std::shared_ptr<InProcessChannel> channel, int endpoint)
: m_channel(std::move(channel)), m_endpoint(endpoint) {}
InProcessTransport::~InProcessTransport() = default;
void InProcessTransport::CreatePair(std::unique_ptr<InProcessTransport>& outClient,
std::unique_ptr<InProcessTransport>& outServer) {
auto channel = std::make_shared<InProcessChannel>();
outClient.reset(new InProcessTransport(channel, 0));
outServer.reset(new InProcessTransport(channel, 1));
}
MobileGLResult InProcessTransport::SendFrame(MobileGLByteSpan bytes) {
if (bytes.size != 0 && bytes.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Same cap as the byte-stream transports, so nothing legal here becomes
// illegal the day the delivery mode changes to `spawn`.
if (bytes.size > kMaxFramePayloadSize) {
MGLOG_E("MG_Remote inproc: refusing a %llu byte message (cap %llu)",
static_cast<unsigned long long>(bytes.size),
static_cast<unsigned long long>(kMaxFramePayloadSize));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
InProcessChannel::Direction& dir = m_channel->Outbox(m_endpoint);
{
std::lock_guard<std::mutex> lock(dir.mutex);
if (dir.closed) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
const auto* first = static_cast<const std::uint8_t*>(bytes.data);
dir.messages.emplace_back(first, first + bytes.size);
}
dir.cv.notify_one();
return MOBILEGL_OK;
}
MobileGLResult InProcessTransport::ReceiveFrame(MobileGLMutableByteSpan buffer,
std::uint64_t* outSize,
std::uint32_t timeoutMs) {
if (outSize != nullptr) {
*outSize = 0;
}
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
std::unique_lock<std::mutex> lock(dir.mutex);
if (dir.messages.empty() && !dir.closed && timeoutMs != 0) {
const auto ready = [&dir] { return !dir.messages.empty() || dir.closed; };
if (timeoutMs == kWaitForever) {
dir.cv.wait(lock, ready);
} else {
dir.cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready);
}
}
if (dir.messages.empty()) {
// Queued messages outlive the peer's Shutdown; only an empty inbox
// is a closed one.
return dir.closed ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
}
const std::vector<std::uint8_t>& front = dir.messages.front();
const std::uint64_t size = front.size();
if (outSize != nullptr) {
*outSize = size;
}
if (buffer.size < size) {
// Contract: the message STAYS QUEUED. The earlier branch's
// transport failed the call and popped the message anyway, which
// wedges the stream permanently the first time a reader guesses the
// size wrong.
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (size != 0) {
if (buffer.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::memcpy(buffer.data, front.data(), static_cast<std::size_t>(size));
}
dir.messages.pop_front();
return MOBILEGL_OK;
}
std::uint64_t InProcessTransport::PeekFrameSize() {
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
std::lock_guard<std::mutex> lock(dir.mutex);
return dir.messages.empty() ? 0 : dir.messages.front().size();
}
MobileGLResult InProcessTransport::ShareFd(int fd, MobileGLByteSpan sideband) {
#if defined(_WIN32)
(void)fd;
(void)sideband;
return MOBILEGL_ERR_UNSUPPORTED;
#else
if (fd < 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (sideband.size > FdPassing::kMaxSidebandBytes ||
(sideband.size != 0 && sideband.data == nullptr)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Same ownership rule as SCM_RIGHTS: the peer gets its own descriptor
// for the same open file description and the caller keeps its own.
const int duplicate = ::dup(fd);
if (duplicate < 0) {
MGLOG_E("MG_Remote inproc: dup failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
FdOffer offer;
offer.fd = duplicate;
if (sideband.size != 0) {
const auto* first = static_cast<const std::uint8_t*>(sideband.data);
offer.sideband.assign(first, first + sideband.size);
}
InProcessChannel::Direction& dir = m_channel->Outbox(m_endpoint);
{
std::lock_guard<std::mutex> lock(dir.mutex);
if (dir.closed) {
::close(duplicate);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
dir.fdOffers.push_back(std::move(offer));
}
dir.fdCv.notify_one();
return MOBILEGL_OK;
#endif
}
MobileGLResult InProcessTransport::ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize,
std::uint32_t timeoutMs) {
if (outFd == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outFd = -1;
if (outSidebandSize != nullptr) {
*outSidebandSize = 0;
}
#if defined(_WIN32)
(void)sideband;
(void)timeoutMs;
return MOBILEGL_ERR_UNSUPPORTED;
#else
// Symmetric with FdPassing::ReceiveFd so callers behave identically in
// both delivery modes.
if (sideband.size < FdPassing::kMaxSidebandBytes) {
if (outSidebandSize != nullptr) {
*outSidebandSize = FdPassing::kMaxSidebandBytes;
}
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (sideband.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
std::unique_lock<std::mutex> lock(dir.mutex);
if (dir.fdOffers.empty() && !dir.closed && timeoutMs != 0) {
const auto ready = [&dir] { return !dir.fdOffers.empty() || dir.closed; };
if (timeoutMs == kWaitForever) {
dir.fdCv.wait(lock, ready);
} else {
dir.fdCv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready);
}
}
if (dir.fdOffers.empty()) {
return dir.closed ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
}
FdOffer offer = std::move(dir.fdOffers.front());
dir.fdOffers.pop_front();
if (!offer.sideband.empty()) {
std::memcpy(sideband.data, offer.sideband.data(), offer.sideband.size());
}
if (outSidebandSize != nullptr) {
*outSidebandSize = offer.sideband.size();
}
*outFd = offer.fd;
return MOBILEGL_OK;
#endif
}
// Whole-connection teardown, as ITransport::Shutdown documents: both
// directions are half-closed and both ring doorbells are KILLED, because a
// peer parked on a ring doorbell mid-frame would otherwise never come back
// (a mere ring is consumed once and the waiter parks again).
void InProcessTransport::Shutdown() { m_channel->Close(); }
Doorbell& InProcessTransport::PeerDoorbell() { return m_channel->Bell(1 - m_endpoint); }
Doorbell& InProcessTransport::SelfDoorbell() { return m_channel->Bell(m_endpoint); }
} // namespace MobileGL::MG_Remote::Transport
@@ -1,77 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/InProcessTransport.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The `inproc` transport: two in-memory message queues and a pair of condvar
// doorbells, one connected endpoint at each end.
//
// It is not a test double. `inproc` is a delivery mode of its own - the server
// side is the monolith's own render thread, which is the single largest CPU
// lever this project has, and it is also the CI form of the split build. What
// it does NOT exercise is serialization of the byte stream, so the framing
// codec is covered separately by FramingTest.
//
// It is built by MOBILEGL_BUILD_DISAGGREGATED, the one option this skeleton
// adds, and selected at RUNTIME (plan appendix B: MOBILEGL_TRANSPORT =
// monolith / inproc / spawn / ...). The plan also reserves a separate
// MOBILEGL_BUILD_DISAGGREGATED_INPROC option for the role-isolation shim that
// a single-process CI build will need; that option does not exist yet, and
// nothing here depends on it.
//
// Messages are queued whole, so no framing bytes are involved; the size cap is
// still enforced so that a payload which would be illegal on a socket is
// illegal here too and does not pass CI only to fail after the switch to
// `spawn`.
//
// Descriptor passing is a plain dup(): both ends are the same process, so
// there is nothing to transfer, but the API stays identical so callers can be
// written once.
#pragma once
#include "Doorbell.h"
#include "ITransport.h"
#include <memory>
namespace MobileGL::MG_Remote::Transport {
class InProcessChannel;
class InProcessTransport final : public ITransport {
public:
~InProcessTransport() override;
// Creates one connected pair. Endpoint 0 is the client, endpoint 1 the
// server; both share one channel and either may be destroyed first.
static void CreatePair(std::unique_ptr<InProcessTransport>& outClient,
std::unique_ptr<InProcessTransport>& outServer);
MobileGLResult SendFrame(MobileGLByteSpan bytes) override;
MobileGLResult ReceiveFrame(MobileGLMutableByteSpan buffer, std::uint64_t* outSize,
std::uint32_t timeoutMs) override;
std::uint64_t PeekFrameSize() override;
MobileGLResult ShareFd(int fd, MobileGLByteSpan sideband) override;
MobileGLResult ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) override;
void Shutdown() override;
TransportRole Role() const override { return TransportRole::InProcess; }
// The wake channel for the SEG_CMD/SEG_STAGE rings living beside this
// transport: ring the peer's bell after publishing a watermark (only
// when its park flag is set - see NotifyIfParked), park on your own.
Doorbell& PeerDoorbell();
Doorbell& SelfDoorbell();
private:
InProcessTransport(std::shared_ptr<InProcessChannel> channel, int endpoint);
std::shared_ptr<InProcessChannel> m_channel;
int m_endpoint = 0;
};
} // namespace MobileGL::MG_Remote::Transport
-306
View File
@@ -1,306 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/Ring.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "Ring.h"
#include <MG_Util/Debug/Log.h>
#include <cstring>
namespace MobileGL::MG_Remote::Transport {
namespace {
constexpr std::uint64_t Align8(std::uint64_t value) {
return (value + (kRingRecordAlignment - 1)) & ~(kRingRecordAlignment - 1);
}
bool IsPowerOfTwo(std::uint64_t value) { return value != 0 && (value & (value - 1)) == 0; }
std::atomic<std::uint64_t>& Head(RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdHead : c.stageHead;
}
const std::atomic<std::uint64_t>& Head(const RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdHead : c.stageHead;
}
std::atomic<std::uint64_t>& AppliedTail(RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdAppliedTail : c.stageAppliedTail;
}
const std::atomic<std::uint64_t>& AppliedTail(const RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdAppliedTail : c.stageAppliedTail;
}
std::atomic<std::uint64_t>& RetiredTail(RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdRetiredTail : c.stageRetiredTail;
}
const std::atomic<std::uint64_t>& RetiredTail(const RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdRetiredTail : c.stageRetiredTail;
}
} // namespace
void InitRingControl(RingControl& control) {
std::memset(static_cast<void*>(&control), 0, sizeof(RingControl));
// 0 means "uninitialized" for both generations, so a peer that reads a
// zero page can tell it from a legal generation.
control.serverEpoch.store(1, std::memory_order_relaxed);
control.ringGeneration.store(1, std::memory_order_relaxed);
}
bool RingCursorsValid(const RingControl& control, RingCursorSet cursors,
std::uint64_t capacityBytes) {
const std::uint64_t head = Head(control, cursors).load(std::memory_order_acquire);
const std::uint64_t applied = AppliedTail(control, cursors).load(std::memory_order_acquire);
const std::uint64_t retired = RetiredTail(control, cursors).load(std::memory_order_acquire);
if (applied > head || retired > applied) {
return false;
}
return head - retired <= capacityBytes;
}
MobileGLResult HardDrainRing(RingControl& control, RingCursorSet cursors) {
const std::uint64_t head = Head(control, cursors).load(std::memory_order_acquire);
const std::uint64_t applied = AppliedTail(control, cursors).load(std::memory_order_acquire);
const std::uint64_t retired = RetiredTail(control, cursors).load(std::memory_order_acquire);
if (head != applied || applied != retired) {
MGLOG_E("MG_Remote ring: hard drain refused, ring is not quiesced "
"(head=%llu applied=%llu retired=%llu)",
static_cast<unsigned long long>(head),
static_cast<unsigned long long>(applied),
static_cast<unsigned long long>(retired));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Cursors stay monotonic across the drain - only the generation moves,
// so any offset either side cached is now recognisably stale.
control.ringGeneration.fetch_add(1, std::memory_order_acq_rel);
return MOBILEGL_OK;
}
// -----------------------------------------------------------------------
// Producer
// -----------------------------------------------------------------------
RingProducer::RingProducer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors)
: m_control(control), m_base(static_cast<std::uint8_t*>(base)), m_capacity(capacityBytes),
m_mask(capacityBytes - 1), m_cursors(cursors) {
if (control == nullptr || base == nullptr || !IsPowerOfTwo(capacityBytes) ||
capacityBytes < kMinRingCapacity || capacityBytes > kMaxRingCapacity) {
MGLOG_E("MG_Remote ring: producer rejected, capacity %llu must be a power of two "
"between %llu and %llu bytes over a non-null mapping (a record may be at most "
"half the ring, and the record header's size field is 32-bit, so a bigger ring "
"would truncate it)",
static_cast<unsigned long long>(capacityBytes),
static_cast<unsigned long long>(kMinRingCapacity),
static_cast<unsigned long long>(kMaxRingCapacity));
m_control = nullptr;
m_base = nullptr;
m_capacity = 0;
m_mask = 0;
return;
}
m_localHead = Head(*control, cursors).load(std::memory_order_acquire);
}
std::uint64_t RingProducer::TailForReclaim() const {
// The conservative watermark: a slot borrowed into the GPU timeline is
// only free after retiredTail passes it. A consumer that never borrows
// publishes retired together with applied, so this costs nothing there.
return RetiredTail(*m_control, m_cursors).load(std::memory_order_acquire);
}
std::uint64_t RingProducer::FreeBytes() const {
if (m_control == nullptr) {
return 0;
}
const std::uint64_t inFlight = m_localHead - TailForReclaim();
return inFlight >= m_capacity ? 0 : m_capacity - inFlight;
}
void* RingProducer::Reserve(std::uint16_t kind, std::uint16_t flags,
std::uint64_t payloadBytes) {
if (m_control == nullptr) {
return nullptr;
}
const std::uint64_t total = Align8(sizeof(RingRecordHeader) + payloadBytes);
if (total > MaxRecordBytes()) {
// A single record larger than HALF the ring is a caller bug: the
// record catalogue has to chunk oversized payloads (large subdata
// becomes several records) rather than emit one giant record.
//
// Half, not the whole ring, because a record has to be placeable at
// EVERY head offset of an empty ring. Straddling the wrap boundary
// costs a pad of spaceToEnd bytes on top of the record, and with
// spaceToEnd < total that is at most 2*total-8, which stays within
// the capacity exactly up to capacity/2. Above it the record is
// placeable at some offsets and not at others: at head offset 16 of
// an empty 256-byte ring a 248-byte record needs 240+248 bytes while
// FreeBytes() reports 256, so a producer that waits for FreeBytes()
// >= total stalls forever, and nothing is ever logged. Refusing here
// makes that impossible - a nullptr with FreeBytes() >= total can no
// longer mean "wait".
MGLOG_E("MG_Remote ring: record kind %u of %llu bytes exceeds half of a %llu byte ring; "
"the emitter must chunk it",
static_cast<unsigned>(kind), static_cast<unsigned long long>(total),
static_cast<unsigned long long>(m_capacity));
return nullptr;
}
const std::uint64_t offset = m_localHead & m_mask;
const std::uint64_t spaceToEnd = m_capacity - offset;
// Every record is a multiple of 8, so the distance to the wrap boundary
// is too, and a pad header always fits.
const bool needsPad = spaceToEnd < total;
const std::uint64_t needed = needsPad ? spaceToEnd + total : total;
if (FreeBytes() < needed) {
MGLOG_D("MG_Remote ring: full, %llu bytes free, %llu needed",
static_cast<unsigned long long>(FreeBytes()),
static_cast<unsigned long long>(needed));
return nullptr;
}
if (needsPad) {
RingRecordHeader pad{};
pad.kind = kRingPadRecordKind;
pad.flags = kRecPad;
pad.size = static_cast<std::uint32_t>(spaceToEnd);
std::memcpy(SlotAt(m_localHead), &pad, sizeof(pad));
m_localHead += spaceToEnd;
}
RingRecordHeader header{};
header.kind = kind;
header.flags = static_cast<std::uint16_t>(flags & ~static_cast<std::uint16_t>(kRecPad));
header.size = static_cast<std::uint32_t>(total);
std::uint8_t* slot = SlotAt(m_localHead);
std::memcpy(slot, &header, sizeof(header));
m_localHead += total;
return slot + sizeof(RingRecordHeader);
}
void RingProducer::Publish() {
if (m_control == nullptr) {
return;
}
// Release: everything written into the slots happens-before the peer's
// acquire load of the head.
Head(*m_control, m_cursors).store(m_localHead, std::memory_order_release);
}
// -----------------------------------------------------------------------
// Consumer
// -----------------------------------------------------------------------
RingConsumer::RingConsumer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors)
: m_control(control), m_base(static_cast<const std::uint8_t*>(base)),
m_capacity(capacityBytes), m_mask(capacityBytes - 1), m_cursors(cursors) {
if (control == nullptr || base == nullptr || !IsPowerOfTwo(capacityBytes) ||
capacityBytes < kMinRingCapacity || capacityBytes > kMaxRingCapacity) {
MGLOG_E("MG_Remote ring: consumer rejected, capacity %llu must be a power of two "
"between %llu and %llu bytes over a non-null mapping (a record may be at most "
"half the ring, and the record header's size field is 32-bit, so a bigger ring "
"would truncate it)",
static_cast<unsigned long long>(capacityBytes),
static_cast<unsigned long long>(kMinRingCapacity),
static_cast<unsigned long long>(kMaxRingCapacity));
m_control = nullptr;
m_base = nullptr;
m_capacity = 0;
m_mask = 0;
return;
}
m_localTail = AppliedTail(*control, cursors).load(std::memory_order_acquire);
}
bool RingConsumer::Pop(RingRecordView& out, bool* outCorrupt) {
if (outCorrupt != nullptr) {
*outCorrupt = false;
}
if (m_control == nullptr) {
return false;
}
const std::uint64_t head = Head(*m_control, m_cursors).load(std::memory_order_acquire);
while (m_localTail != head) {
const std::uint64_t available = head - m_localTail;
if (available < sizeof(RingRecordHeader) || available > m_capacity) {
MGLOG_E("MG_Remote ring: %llu bytes between tail and head is impossible for a %llu "
"byte ring",
static_cast<unsigned long long>(available),
static_cast<unsigned long long>(m_capacity));
if (outCorrupt != nullptr) {
*outCorrupt = true;
}
return false;
}
const std::uint64_t offset = m_localTail & m_mask;
RingRecordHeader header{};
std::memcpy(&header, m_base + offset, sizeof(header));
// SEG_CMD is written by the peer process: compile-time asserts on
// record sizes cannot see runtime corruption, so every dispatch is
// preceded by these bounds checks and a violation is fatal, never a
// retry (plan section 6.3, runtime bounds discipline).
const std::uint64_t size = header.size;
if (size < sizeof(RingRecordHeader) || (size % kRingRecordAlignment) != 0 ||
size > available || offset + size > m_capacity) {
MGLOG_E("MG_Remote ring: corrupt record header at cursor %llu "
"(kind=%u flags=0x%04X size=%u available=%llu)",
static_cast<unsigned long long>(m_localTail),
static_cast<unsigned>(header.kind), static_cast<unsigned>(header.flags),
header.size, static_cast<unsigned long long>(available));
if (outCorrupt != nullptr) {
*outCorrupt = true;
}
return false;
}
if ((header.flags & kRecPad) != 0) {
m_localTail += size;
continue;
}
out.kind = header.kind;
out.flags = header.flags;
out.payload = m_base + offset + sizeof(RingRecordHeader);
// Includes the alignment tail; the record catalogue knows the real
// payload length.
out.payloadSize = size - sizeof(RingRecordHeader);
out.cursor = m_localTail;
m_localTail += size;
return true;
}
return false;
}
void RingConsumer::PublishApplied() {
if (m_control == nullptr) {
return;
}
AppliedTail(*m_control, m_cursors).store(m_localTail, std::memory_order_release);
}
void RingConsumer::PublishRetired() {
if (m_control == nullptr) {
return;
}
// retiredTail must never overtake appliedTail, so publish both.
AppliedTail(*m_control, m_cursors).store(m_localTail, std::memory_order_release);
RetiredTail(*m_control, m_cursors).store(m_localTail, std::memory_order_release);
}
void RingConsumer::PublishRetiredUpTo(std::uint64_t cursor) {
if (m_control == nullptr) {
return;
}
const std::uint64_t applied = AppliedTail(*m_control, m_cursors).load(std::memory_order_acquire);
const std::uint64_t clamped = cursor > applied ? applied : cursor;
const std::uint64_t current = RetiredTail(*m_control, m_cursors).load(std::memory_order_relaxed);
if (clamped > current) {
RetiredTail(*m_control, m_cursors).store(clamped, std::memory_order_release);
}
}
} // namespace MobileGL::MG_Remote::Transport
-256
View File
@@ -1,256 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/Ring.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SEG_CMD / SEG_STAGE ring control and the SPSC producer/consumer over it.
//
// RingControl is the shared page at the head of SEG_CMD, laid out exactly as
// the inherited transport design (plan section 8.1, referring the earlier
// plan's section 6.2) specifies:
//
// - TWO independent cursor triples, one for SEG_CMD and one for SEG_STAGE.
// The stage ring needs its own because "SEG_STAGE has less than a quarter
// left" is a publish trigger and that occupancy cannot be derived from the
// command ring's cursors, and because a stage slot retires on a different
// event than a command record does.
// - THREE separate sequence watermarks. Conflating them is the classic bug:
// appliedSeq releases *AppliedTail, submittedSeq releases staging,
// retiredSeq / completedFrameSerial release *RetiredTail and adopted
// stores.
// - TWO tails per ring, not one. Once the server borrows a ring slot into
// the GPU timeline instead of copying it out again, that slot can only be
// recycled after completedFrameSerial; a single tail would silently
// degrade to conservative reclaim the day borrowing lands.
// - Both park flags, because the doorbell is bidirectional: without the
// server->client direction every client wait degenerates into a
// cross-process spin on one shared cache line (a whole 16.6ms frame of a
// big core, on a phone, competing with the GPU and the game's JVM).
//
// Cursors are monotonically increasing byte counts; the ring is indexed with a
// power-of-two mask. They are never reset, so a torn read can never look like
// a valid earlier position. ringGeneration is bumped after a hard drain to
// invalidate every cached offset.
//
// Record framing inside the ring is the 8-byte header below, which is the
// layout the plan's RecHeader already fixes ({u16 kind, u16 flags, u32 size},
// size including the header and a multiple of 8). The record CATALOGUE
// (Records.def / PipeCalls.def) is a separate deliverable; the ring itself
// only needs kind/flags/size, so it can carry the real records the day they
// land without changing shape.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
// The shared control page. One 4 KiB page so it can be mapped alone, with
// each contended group on its own cache line.
struct alignas(4096) RingControl {
// ---- SEG_CMD cursors ------------------------------------------------
alignas(64) std::atomic<std::uint64_t> cmdHead; // producer: bytes written
alignas(64) std::atomic<std::uint64_t> cmdAppliedTail; // consumer: bytes decoded/copied out
std::atomic<std::uint64_t> cmdRetiredTail; // consumer: borrowed slots released
// ---- SEG_STAGE cursors ----------------------------------------------
alignas(64) std::atomic<std::uint64_t> stageHead;
alignas(64) std::atomic<std::uint64_t> stageAppliedTail;
std::atomic<std::uint64_t> stageRetiredTail;
// ---- sequence / frame watermarks -------------------------------------
alignas(64) std::atomic<std::uint64_t> appliedSeq; // records applied
std::atomic<std::uint64_t> submittedSeq; // handed to the driver
std::atomic<std::uint64_t> retiredSeq; // GPU finished
std::atomic<std::uint64_t> completedFrameSerial;
std::atomic<std::uint64_t> presentAckSerial;
// ---- doorbell / generation -------------------------------------------
alignas(64) std::atomic<std::uint32_t> serverEpoch; // ++ on context loss / server restart
std::atomic<std::uint32_t> ringGeneration; // ++ after a hard drain
std::atomic<std::uint32_t> consumerParked; // server asleep, producer must ring
std::atomic<std::uint32_t> producerParked; // client asleep, server must ring
std::atomic<std::uint32_t> eventRingFull; // SEG_EVENT full, server stopped applying
std::atomic<std::uint32_t> eventDropped; // dropped lossy events
};
static_assert(sizeof(RingControl) == 4096, "RingControl must be exactly one page");
static_assert(alignof(RingControl) == 4096, "RingControl must be page aligned");
static_assert(std::atomic<std::uint64_t>::is_always_lock_free,
"the ring cursors are shared across processes: they must be lock-free");
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
"the doorbell flags are shared across processes: they must be lock-free");
// Per-record header. Prefix-identical to the plan's RecHeader so the
// generated record catalogue drops straight in.
struct RingRecordHeader {
std::uint16_t kind;
std::uint16_t flags;
std::uint32_t size; // header + payload + alignment padding, multiple of 8
};
static_assert(sizeof(RingRecordHeader) == 8, "RecHeader is 8 bytes on the wire");
enum RingRecordFlags : std::uint16_t {
kRecNone = 0,
kRecNeedsAck = 1u << 0,
kRecHasBlob = 1u << 1,
kRecPad = 1u << 2, // filler to the wrap boundary, no payload meaning
kRecBorrowSlot = 1u << 3, // slot is borrowed into the GPU timeline; retires late
kRecVarTail = 1u << 4,
};
// Reserved kind for the wrap filler. The catalogue starts at 1.
inline constexpr std::uint16_t kRingPadRecordKind = 0;
inline constexpr std::uint64_t kRingRecordAlignment = 8;
// Largest ring the 8-byte header can describe. Both a record's size and a
// wrap filler's size are bounded only by the capacity and are stored in
// RingRecordHeader::size, which is 32 bits by wire contract: a ring of
// 4 GiB or more would silently truncate them, and the consumer would then
// bounds-check the truncated value against the real one. SEG_CMD is 8 MiB
// and SEG_STAGE 32 MiB today, so this is unreachable - it is the same
// class of construction-time guard as the power-of-two check beside it.
inline constexpr std::uint64_t kMaxRingCapacity = 0xFFFFFFFFull;
// Smallest ring: two record headers. A record may be at most HALF the ring
// (see RingProducer::Reserve), so a ring of one header could carry nothing
// at all - not even the smallest record, a bare header.
inline constexpr std::uint64_t kMinRingCapacity = 2 * sizeof(RingRecordHeader);
// Which cursor triple a producer/consumer pair drives.
enum class RingCursorSet : std::uint32_t {
Cmd = 0,
Stage = 1,
};
// Zeroes every cursor and starts serverEpoch / ringGeneration at 1, so that
// a zero read is always "uninitialized", never a legal generation.
void InitRingControl(RingControl& control);
// head >= appliedTail >= retiredTail, and the ring never holds more than
// its capacity. False means the shared page is corrupt (or a peer is
// misbehaving), which is a Fatal{ProtocolCorruption}, never a retry.
bool RingCursorsValid(const RingControl& control, RingCursorSet cursors,
std::uint64_t capacityBytes);
// Bumps ringGeneration, invalidating every offset either side has cached.
// Both sides must be quiesced and the ring fully drained
// (head == appliedTail == retiredTail); otherwise this returns
// MOBILEGL_ERR_INVALID_ARGUMENT and changes nothing.
MobileGLResult HardDrainRing(RingControl& control, RingCursorSet cursors);
// A record as seen by the consumer.
struct RingRecordView {
std::uint16_t kind = 0;
std::uint16_t flags = 0;
const void* payload = nullptr;
std::uint64_t payloadSize = 0;
std::uint64_t cursor = 0; // producer cursor at the START of this record
};
// Single producer. Not thread-safe: one writer thread, by construction.
class RingProducer {
public:
RingProducer() = default;
// `base` is the ring's byte area (NOT the control page) and
// `capacityBytes` must be a power of two between kMinRingCapacity and
// kMaxRingCapacity. Anything else leaves Valid() false.
RingProducer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors);
bool Valid() const { return m_control != nullptr; }
// Bytes still writable before the consumer has to catch up.
std::uint64_t FreeBytes() const;
// Reserves room for one record and returns a pointer to its payload,
// or nullptr when the ring is full. The payload is uninitialized;
// alignment padding at its tail is NOT zeroed. Emits a pad record
// automatically when the record would straddle the wrap boundary, so
// every record is contiguous.
//
// A record whose total (header + payload, rounded up to 8) exceeds
// MaxRecordBytes() == Capacity()/2 is refused outright, with an error
// log and however empty the ring is: chunking it is the emitter's job
// (plan section 8.2, the G3 chunking rule). Half is exact, not
// conservative - it is the largest record EVERY head offset can place,
// because a wrap pad costs at most total-8 bytes on top of the record
// and 2*total-8 <= capacity-8 holds exactly up to capacity/2. Above it
// a record is placeable at some offsets and not at others, and a
// producer waiting for FreeBytes() >= total stalls forever on an empty
// ring. So: nullptr with FreeBytes() >= total never means "wait"; it
// can only mean "too big, chunk".
void* Reserve(std::uint16_t kind, std::uint16_t flags, std::uint64_t payloadBytes);
// The largest header+payload total Reserve accepts: Capacity()/2. This
// is the number the emitter chunks against.
std::uint64_t MaxRecordBytes() const { return m_capacity / 2; }
// Makes every reserved record visible to the consumer (release store on
// the head cursor). Cheap: publishing per record is fine, batching 8-16
// only amortizes the doorbell store.
void Publish();
// Producer-local cursor including records not yet published.
std::uint64_t LocalHead() const { return m_localHead; }
std::uint64_t Capacity() const { return m_capacity; }
private:
std::uint64_t TailForReclaim() const;
std::uint8_t* SlotAt(std::uint64_t cursor) const {
return m_base + static_cast<std::size_t>(cursor & m_mask);
}
RingControl* m_control = nullptr;
std::uint8_t* m_base = nullptr;
std::uint64_t m_capacity = 0;
std::uint64_t m_mask = 0;
std::uint64_t m_localHead = 0;
RingCursorSet m_cursors = RingCursorSet::Cmd;
};
// Single consumer. Not thread-safe: one reader thread, by construction.
class RingConsumer {
public:
RingConsumer() = default;
RingConsumer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors);
bool Valid() const { return m_control != nullptr; }
// Pops the next record, skipping wrap fillers. Returns false when the
// ring is empty at this moment. A record whose header is impossible
// (size not 8-aligned, smaller than a header, or larger than what the
// producer has published) is refused: *outCorrupt is set, which the
// caller must escalate to Fatal{ProtocolCorruption} rather than retry.
bool Pop(RingRecordView& out, bool* outCorrupt = nullptr);
// Publishes the applied cursor, releasing those bytes to the producer.
void PublishApplied();
// Publishes the retired cursor. Records without kRecBorrowSlot retire
// as soon as they are applied; borrowed slots retire on
// completedFrameSerial, which is why this is a separate call.
void PublishRetired();
void PublishRetiredUpTo(std::uint64_t cursor);
std::uint64_t LocalTail() const { return m_localTail; }
std::uint64_t Capacity() const { return m_capacity; }
private:
RingControl* m_control = nullptr;
const std::uint8_t* m_base = nullptr;
std::uint64_t m_capacity = 0;
std::uint64_t m_mask = 0;
std::uint64_t m_localTail = 0;
RingCursorSet m_cursors = RingCursorSet::Cmd;
};
} // namespace MobileGL::MG_Remote::Transport
@@ -1,49 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegment.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Platform-independent half of ShmSegment. The create/map/close bodies live in
// ShmSegmentPosix.cpp and ShmSegmentWin32.cpp.
#include "ShmSegment.h"
#include <cstring>
#include <utility>
namespace MobileGL::MG_Remote::Transport {
ShmSegment::~ShmSegment() { Close(); }
ShmSegment::ShmSegment(ShmSegment&& other) noexcept { Steal(std::move(other)); }
ShmSegment& ShmSegment::operator=(ShmSegment&& other) noexcept {
if (this != &other) {
Close();
Steal(std::move(other));
}
return *this;
}
void ShmSegment::Steal(ShmSegment&& other) noexcept {
std::memcpy(m_name, other.m_name, sizeof(m_name));
m_mapping = other.m_mapping;
m_nativeHandle = other.m_nativeHandle;
m_size = other.m_size;
m_fd = other.m_fd;
m_readOnly = other.m_readOnly;
std::memset(other.m_name, 0, sizeof(other.m_name));
other.m_mapping = nullptr;
other.m_nativeHandle = nullptr;
other.m_size = 0;
other.m_fd = -1;
other.m_readOnly = false;
}
bool ShmSegment::Valid() const { return m_size != 0 && (m_fd >= 0 || m_nativeHandle != nullptr); }
} // namespace MobileGL::MG_Remote::Transport
-87
View File
@@ -1,87 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegment.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// One shared-memory segment: SEG_CMD, SEG_STAGE, SEG_REPLY, SEG_EVENT, a
// per-object SEG_SHADOW or a SEG_ADOPT store (inherited segment layout, plan
// section 8.1).
//
// Creation matrix (earlier plan section 6.1):
// - Android: ASharedMemory_create (API 26; libc's memfd_create wrapper
// only appears at API 30, which is above our floor)
// - desktop Linux: syscall(SYS_memfd_create, ...) directly, for the same
// reason - the glibc wrapper is recent and this file has to
// build against old sysroots
// - other POSIX: shm_open + immediate shm_unlink, the fd keeps it alive
// - Windows: CreateFileMappingW in the Local\ namespace
//
// Transfer is NOT done here. On POSIX the fd travels by SCM_RIGHTS
// (FdPassing.h / ITransport::ShareFd) and the name is only a debugging label;
// on Windows the section name travels inside the SegmentRef table.
//
// The Windows implementation is compile-guarded and untested at the time it
// was written: no Windows machine is a correctness gate for this project.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <cstddef>
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
inline constexpr std::size_t kShmNameMax = 128;
class ShmSegment {
public:
ShmSegment() = default;
~ShmSegment();
ShmSegment(const ShmSegment&) = delete;
ShmSegment& operator=(const ShmSegment&) = delete;
ShmSegment(ShmSegment&& other) noexcept;
ShmSegment& operator=(ShmSegment&& other) noexcept;
// Creates a segment of `size` bytes owned by this process. `nameHint`
// is a short debug label (Windows: part of the section name peers
// resolve). The segment is NOT mapped yet.
static MobileGLResult Create(const char* nameHint, std::uint64_t size, ShmSegment& out);
// POSIX only: adopts a descriptor received over SCM_RIGHTS. Takes
// ownership of `fd` on success; on failure the caller still owns it.
static MobileGLResult Adopt(int fd, std::uint64_t size, ShmSegment& out);
// Windows only: opens a section the peer published by name.
static MobileGLResult OpenNamed(const char* name, std::uint64_t size, ShmSegment& out);
// Maps the whole segment. Read-only mappings are what the peer gets for
// a segment it does not own (SEG_CMD/SEG_STAGE on the server side).
MobileGLResult Map(bool readOnly);
void Unmap();
void Close(); // unmaps and releases the descriptor/handle
bool Valid() const;
void* Data() const { return m_mapping; }
std::uint64_t Size() const { return m_size; }
bool MappedReadOnly() const { return m_readOnly; }
const char* Name() const { return m_name; }
// POSIX: the descriptor to hand to ShareFd. -1 on Windows.
int Fd() const { return m_fd; }
private:
void Steal(ShmSegment&& other) noexcept;
char m_name[kShmNameMax] = {};
void* m_mapping = nullptr;
void* m_nativeHandle = nullptr; // Windows HANDLE; unused on POSIX
std::uint64_t m_size = 0;
int m_fd = -1;
bool m_readOnly = false;
};
} // namespace MobileGL::MG_Remote::Transport
@@ -1,191 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "ShmSegment.h"
#if !defined(_WIN32)
#include <MG_Util/Debug/Log.h>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#if defined(__ANDROID__)
#include <android/sharedmem.h>
#elif defined(__linux__)
#include <sys/syscall.h>
#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 0x0001U
#endif
#endif
namespace MobileGL::MG_Remote::Transport {
namespace {
void CopyName(char (&dst)[kShmNameMax], const char* src) {
if (src == nullptr) {
dst[0] = '\0';
return;
}
std::snprintf(dst, kShmNameMax, "%s", src);
}
#if !defined(__ANDROID__)
// Unique per process; only used by the shm_open fallback, whose name
// must not collide with a concurrent creator's.
std::atomic<std::uint32_t> g_shmCounter{0};
#endif
} // namespace
MobileGLResult ShmSegment::Create(const char* nameHint, std::uint64_t size, ShmSegment& out) {
if (size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
char label[kShmNameMax];
std::snprintf(label, sizeof(label), "mgl-%s", nameHint != nullptr ? nameHint : "seg");
int fd = -1;
#if defined(__ANDROID__)
// API 26. libc's memfd_create wrapper is API 30, above MobileGL's floor.
fd = ASharedMemory_create(label, static_cast<size_t>(size));
if (fd < 0) {
MGLOG_W("MG_Remote shm: ASharedMemory_create(%s, %llu) failed (errno=%d)", label,
static_cast<unsigned long long>(size), errno);
}
#elif defined(__linux__)
// Raw syscall, not the glibc wrapper: the wrapper is too recent to rely
// on across the sysroots this builds against.
fd = static_cast<int>(::syscall(SYS_memfd_create, label, MFD_CLOEXEC));
if (fd >= 0 && ::ftruncate(fd, static_cast<off_t>(size)) != 0) {
MGLOG_E("MG_Remote shm: ftruncate(%llu) failed (errno=%d)",
static_cast<unsigned long long>(size), errno);
::close(fd);
fd = -1;
}
#endif
#if !defined(__ANDROID__)
if (fd < 0) {
// Fallback: shm_open + immediate unlink. The name disappears at
// once; the descriptor is what keeps the object alive and what
// travels by SCM_RIGHTS.
char shmName[kShmNameMax];
std::snprintf(shmName, sizeof(shmName), "/mgl-%d-%u-%s", static_cast<int>(::getpid()),
g_shmCounter.fetch_add(1, std::memory_order_relaxed),
nameHint != nullptr ? nameHint : "seg");
fd = ::shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd < 0) {
MGLOG_E("MG_Remote shm: shm_open(%s) failed (errno=%d)", shmName, errno);
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
::shm_unlink(shmName);
if (::ftruncate(fd, static_cast<off_t>(size)) != 0) {
MGLOG_E("MG_Remote shm: ftruncate(%llu) failed (errno=%d)",
static_cast<unsigned long long>(size), errno);
::close(fd);
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
CopyName(out.m_name, shmName);
} else {
CopyName(out.m_name, label);
}
#else
if (fd < 0) {
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
CopyName(out.m_name, label);
#endif
out.m_fd = fd;
out.m_size = size;
out.m_nativeHandle = nullptr;
out.m_mapping = nullptr;
out.m_readOnly = false;
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::Adopt(int fd, std::uint64_t size, ShmSegment& out) {
if (fd < 0 || size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// The peer's declared size is not trusted: a segment smaller than what
// the announcement claims would turn every later offset into an
// out-of-bounds map.
struct stat st{};
if (::fstat(fd, &st) == 0 && st.st_size > 0 &&
static_cast<std::uint64_t>(st.st_size) < size) {
MGLOG_E("MG_Remote shm: peer announced %llu bytes but the descriptor is %lld",
static_cast<unsigned long long>(size), static_cast<long long>(st.st_size));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
out.m_fd = fd; // ownership transferred
out.m_size = size;
out.m_nativeHandle = nullptr;
out.m_mapping = nullptr;
out.m_readOnly = false;
CopyName(out.m_name, "adopted");
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::OpenNamed(const char*, std::uint64_t, ShmSegment&) {
// POSIX shares descriptors, not names.
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ShmSegment::Map(bool readOnly) {
if (m_fd < 0 || m_size == 0) {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
if (m_mapping != nullptr) {
if (m_readOnly == readOnly) {
return MOBILEGL_OK;
}
Unmap();
}
const int prot = readOnly ? PROT_READ : (PROT_READ | PROT_WRITE);
void* addr = ::mmap(nullptr, static_cast<size_t>(m_size), prot, MAP_SHARED, m_fd, 0);
if (addr == MAP_FAILED) {
MGLOG_E("MG_Remote shm: mmap of %llu bytes failed (errno=%d)",
static_cast<unsigned long long>(m_size), errno);
return MOBILEGL_ERR_OUT_OF_MEMORY;
}
m_mapping = addr;
m_readOnly = readOnly;
return MOBILEGL_OK;
}
void ShmSegment::Unmap() {
if (m_mapping != nullptr) {
::munmap(m_mapping, static_cast<size_t>(m_size));
m_mapping = nullptr;
}
}
void ShmSegment::Close() {
Unmap();
if (m_fd >= 0) {
::close(m_fd);
m_fd = -1;
}
m_size = 0;
m_readOnly = false;
m_name[0] = '\0';
}
} // namespace MobileGL::MG_Remote::Transport
#endif // !_WIN32
@@ -1,162 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Windows half of ShmSegment: a named file-mapping section in the Local\
// namespace, which the peer opens by the name carried in SegmentRef.
//
// UNTESTED. This project's Windows machine is not a correctness gate (its
// Vulkan lacks vkCreateHeadlessSurfaceEXT and accounts for most of its
// baseline integration failures), and the whole disaggregated build is gated
// behind MOBILEGL_BUILD_DISAGGREGATED, which is OFF by default. It is written
// now so the abstraction is shaped by two real platforms rather than one.
#include "ShmSegment.h"
#if defined(_WIN32)
#include <MG_Util/Debug/Log.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
namespace MobileGL::MG_Remote::Transport {
namespace {
std::atomic<std::uint32_t> g_sectionCounter{0};
bool ToWide(const char* utf8, wchar_t* out, int outChars) {
if (utf8 == nullptr || out == nullptr || outChars <= 0) {
return false;
}
const int written = ::MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out, outChars);
return written > 0;
}
} // namespace
MobileGLResult ShmSegment::Create(const char* nameHint, std::uint64_t size, ShmSegment& out) {
if (size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
char name[kShmNameMax];
std::snprintf(name, sizeof(name), "Local\\mgl-%lu-%u-%s",
static_cast<unsigned long>(::GetCurrentProcessId()),
g_sectionCounter.fetch_add(1, std::memory_order_relaxed),
nameHint != nullptr ? nameHint : "seg");
wchar_t wide[kShmNameMax];
if (!ToWide(name, wide, static_cast<int>(kShmNameMax))) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
HANDLE section = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
static_cast<DWORD>(size >> 32),
static_cast<DWORD>(size & 0xFFFFFFFFull), wide);
if (section == nullptr) {
MGLOG_E("MG_Remote shm: CreateFileMappingW(%s, %llu) failed (GetLastError=%lu)", name,
static_cast<unsigned long long>(size),
static_cast<unsigned long>(::GetLastError()));
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
if (::GetLastError() == ERROR_ALREADY_EXISTS) {
::CloseHandle(section);
MGLOG_E("MG_Remote shm: section name %s already exists", name);
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
std::snprintf(out.m_name, kShmNameMax, "%s", name);
out.m_nativeHandle = section;
out.m_size = size;
out.m_fd = -1;
out.m_mapping = nullptr;
out.m_readOnly = false;
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::Adopt(int, std::uint64_t, ShmSegment&) {
// No SCM_RIGHTS here: Windows peers resolve the section by name.
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ShmSegment::OpenNamed(const char* name, std::uint64_t size, ShmSegment& out) {
if (name == nullptr || name[0] == '\0' || size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
wchar_t wide[kShmNameMax];
if (!ToWide(name, wide, static_cast<int>(kShmNameMax))) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
HANDLE section = ::OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wide);
if (section == nullptr) {
MGLOG_E("MG_Remote shm: OpenFileMappingW(%s) failed (GetLastError=%lu)", name,
static_cast<unsigned long>(::GetLastError()));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::snprintf(out.m_name, kShmNameMax, "%s", name);
out.m_nativeHandle = section;
out.m_size = size;
out.m_fd = -1;
out.m_mapping = nullptr;
out.m_readOnly = false;
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::Map(bool readOnly) {
if (m_nativeHandle == nullptr || m_size == 0) {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
if (m_mapping != nullptr) {
if (m_readOnly == readOnly) {
return MOBILEGL_OK;
}
Unmap();
}
void* view = ::MapViewOfFile(static_cast<HANDLE>(m_nativeHandle),
readOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, 0, 0,
static_cast<SIZE_T>(m_size));
if (view == nullptr) {
MGLOG_E("MG_Remote shm: MapViewOfFile of %llu bytes failed (GetLastError=%lu)",
static_cast<unsigned long long>(m_size),
static_cast<unsigned long>(::GetLastError()));
return MOBILEGL_ERR_OUT_OF_MEMORY;
}
m_mapping = view;
m_readOnly = readOnly;
return MOBILEGL_OK;
}
void ShmSegment::Unmap() {
if (m_mapping != nullptr) {
::UnmapViewOfFile(m_mapping);
m_mapping = nullptr;
}
}
void ShmSegment::Close() {
Unmap();
if (m_nativeHandle != nullptr) {
::CloseHandle(static_cast<HANDLE>(m_nativeHandle));
m_nativeHandle = nullptr;
}
m_size = 0;
m_readOnly = false;
m_name[0] = '\0';
}
} // namespace MobileGL::MG_Remote::Transport
#endif // _WIN32
-33
View File
@@ -1,33 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/WireLog.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WireLog.h"
#include <MG_Util/Debug/Log.h>
#include <cstdarg>
#include <cstdio>
namespace MobileGL::MG_Remote::Transport {
void WireLogError(const char* format, ...) {
// One stack line, no allocation: this runs on paths that have just
// decided the connection is unusable.
char line[512];
va_list args;
va_start(args, format);
const int written = std::vsnprintf(line, sizeof(line), format, args);
va_end(args);
if (written < 0) {
MGLOG_E("MG_Remote wire: unformattable diagnostic (format=%s)", format);
return;
}
MGLOG_E("%s", line);
}
} // namespace MobileGL::MG_Remote::Transport
-38
View File
@@ -1,38 +0,0 @@
// MobileGL - MobileGL/MG_Remote/Transport/WireLog.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// A one-function logging shim for the wire layer's header-only code.
//
// MG_Util/Debug/Log.h includes <Includes.h>, the GL frontend's umbrella
// header - 661 headers, measured with `clang++ -H`. That is fine inside a
// .cpp, and Ring.cpp / Doorbell.cpp / the transports all do it. It is not fine
// in a header of this layer: ITransport.h states the rule ("nothing about a
// byte pipe needs the GL frontend's umbrella header") because these headers
// are included by both roles and by the eventual server-side binary, and
// because the disaggregated build's include-graph purity gate (plan section
// 10.3, gate A) asserts on `-H` output rather than on symbols. Framing.h was
// the one header under Transport/ that broke the rule; it now calls this
// instead, and the umbrella stays inside WireLog.cpp.
//
// ERROR only, deliberately. Everything routed here is a latched protocol
// violation, never per-frame noise; non-critical wire lines use MGLOG_D from a
// .cpp, where the INFO build compiles them out entirely.
#pragma once
namespace MobileGL::MG_Remote::Transport {
// Formats one line and emits it at ERROR level (MGLOG_E). printf-style,
// with the format checked against the arguments at compile time.
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 1, 2)))
#endif
void
WireLogError(const char* format, ...);
} // namespace MobileGL::MG_Remote::Transport
-32
View File
@@ -14,8 +14,6 @@
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <Config.h>
#include <atomic>
namespace MobileGL::MG_State {
void Init() {
MGLOG_D("Initializing MobileGL State...");
@@ -1261,33 +1259,6 @@ namespace MobileGL::MG_State {
return m_renderbufferState.ValidateRenderbufferObject(index);
}
Uint64 GLContext::AllocateTransformFeedbackLifetimeId() {
// Starts at 1 so a zero-initialised backend slot can never carry a live object's id.
static std::atomic<Uint64> nextId{1};
return nextId.fetch_add(1, std::memory_order_relaxed);
}
GLContext::GLContext() {
// The default transform feedback object (name 0) exists from the start of the context
// (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it.
// Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain
// const read instead of an operator[] insert on the draw path.
m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId;
}
Bool GLContext::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
if (lifetimeId == 0) return false;
for (const auto& [name, object] : m_transformFeedbackObjects) {
if (object.lifetimeId != lifetimeId) continue;
// The bound object's span state is live in the context; its saved copy is only
// written when a bind swaps it out.
return name == m_boundTransformFeedback ? m_transformFeedbackActive : object.active;
}
// No object carries this identity any more: it was deleted, and a deleted object can
// never resume.
return false;
}
void GLContext::SaveBoundTransformFeedbackState() {
auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
@@ -1324,9 +1295,6 @@ namespace MobileGL::MG_State {
m_transformFeedbackGeneration = object.generation;
m_transformFeedbackCapturedVertices = object.capturedVertices;
m_transformFeedbackInputPrimitives = object.inputPrimitives;
// Every route that changes which object is bound - BindTransformFeedbackObject and the
// revert a delete of the bound object performs - comes through here.
m_boundTransformFeedbackLifetimeId = object.lifetimeId;
}
void GLContext::GenTransformFeedbackNames(Uint number, Vector<Uint>& ids) {
+1 -33
View File
@@ -60,7 +60,7 @@ namespace MobileGL {
class GLContext {
public:
GLContext();
GLContext() = default;
// Error
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
@@ -427,27 +427,6 @@ namespace MobileGL {
void BindTransformFeedbackObject(Uint index);
void MarkTransformFeedbackObjectForDeletion(Uint index);
Uint GetBoundTransformFeedbackName() const { return m_boundTransformFeedback; }
// The bound object's never-reused identity, for a backend that keys a per-object
// resource on it. The NAME is not an identity: glGenTransformFeedbacks recycles a
// deleted one (LIFO), so a memo keyed on the name hands a brand-new object the dead
// one's slot. Cached rather than looked up on demand: the backend asks twice per
// captured draw, and an operator[] on m_transformFeedbackObjects would be an
// INSERT on the draw path - ska::flat_hash_map invalidates every reference into
// itself when it rehashes. The cache is refreshed by
// RestoreBoundTransformFeedbackState, which every bind (and the revert a delete
// performs) goes through, and seeded for the default object by the constructor.
// Never returns 0 - the counter starts at 1 so a zero-initialised memo slot cannot
// be mistaken for a live object.
Uint64 GetBoundTransformFeedbackLifetimeId() const { return m_boundTransformFeedbackLifetimeId; }
// Whether the object carrying this identity still has an OPEN capture span - one
// that glBeginTransformFeedback started and glEndTransformFeedback has not closed,
// paused or not. A backend that hands out a bounded set of per-object slots must
// never take one of these over: a paused span's counters are precisely what its
// resume reads, and GL only lets other objects capture WHILE it is paused, so the
// paused object is also the one that looks idle. An identity no live object
// carries any more (its object was deleted) answers false, which is what makes
// such a slot reclaimable.
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
// Vertices the object captured in its last completed span; the vertex count
// glDrawTransformFeedback replays.
Uint64 GetTransformFeedbackRecordedVertices(Uint index) const;
@@ -535,9 +514,6 @@ namespace MobileGL {
GLuint m_conditionalRenderQuery = 0;
GLenum m_conditionalRenderMode = GL_NONE;
// Process-wide, never-reused. See GetBoundTransformFeedbackLifetimeId(); same
// contract as BufferObject::AllocateLifetimeId().
static Uint64 AllocateTransformFeedbackLifetimeId();
// Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState {
struct SavedBufferBinding {
@@ -556,10 +532,6 @@ namespace MobileGL {
Uint64 recordedVertices = 0;
Bool hasCompletedSpan = false;
Bool everBound = false;
// Assigned by the default member initialiser, so every way an object comes into
// being - operator[] materialisation, `= {}` in Gen/Create - gets a fresh one,
// and a recycled NAME never brings the dead object's id back with it.
Uint64 lifetimeId = AllocateTransformFeedbackLifetimeId();
};
void SaveBoundTransformFeedbackState();
void RestoreBoundTransformFeedbackState();
@@ -568,10 +540,6 @@ namespace MobileGL {
UnorderedMap<Uint, TransformFeedbackObjectState> m_transformFeedbackObjects;
IndexGenerator<Uint> m_transformFeedbackNames;
Uint m_boundTransformFeedback = 0;
// Mirror of m_transformFeedbackObjects[m_boundTransformFeedback].lifetimeId, so
// the per-draw read is a load rather than a hash lookup that could insert.
// Seeded by the constructor and rewritten by RestoreBoundTransformFeedbackState.
Uint64 m_boundTransformFeedbackLifetimeId = 0;
// Map membership is object EXISTENCE, which is not the same as the answer
// glIsProgramPipeline gives: any command that needs somewhere to put state
// materializes a reserved name, so the object can exist well before it is
@@ -9,21 +9,9 @@
#include "RenderbufferObject.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <atomic>
namespace MobileGL {
namespace MG_State {
namespace GLState {
namespace {
// Starts at 1 so a zero-initialized cache slot can never carry a live
// renderbuffer's id.
std::atomic<Uint64> g_nextRenderbufferLifetimeId{1};
}
Uint64 RenderbufferObject::AllocateLifetimeId() {
return g_nextRenderbufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
}
RenderbufferObject::RenderbufferObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
Uint RenderbufferObject::GetExternalIndex() const {
@@ -42,20 +42,9 @@ namespace MobileGL {
Int GetDepthSize() const;
Int GetStencilSize() const;
Int GetSamples() const;
// Globally-unique, never-reused id for THIS object's lifetime - same contract
// and same motivation as BufferObject::GetLifetimeId(),
// ProgramObject::GetLifetimeId() and VertexArrayObject::GetLifetimeId(). A
// backend that folds a renderbuffer's IDENTITY into a cache key must use this,
// never the GL name (LIFO-recycled by glGenRenderbuffers) and never the heap
// address (recycled by the allocator): both let a deleted-and-recreated
// renderbuffer answer to a dead one's cache entry.
Uint64 GetLifetimeId() const { return m_lifetimeId; }
private:
static Uint64 AllocateLifetimeId();
Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = AllocateLifetimeId();
TextureInternalFormat m_internalFormat = TextureInternalFormat::RGBA;
Int m_width = 0;
Int m_height = 0;
@@ -37,11 +37,6 @@ namespace {
std::size_t ioBlockDraws = 0;
// Behavior knobs, configured per test before running the probe.
GLint maxVertexSsboBlocks = 4;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE per axis, answered through glGetIntegeri_v.
// Above the GL minimums and distinct per axis, so a loader that left an initialiser in
// place or copied one axis into another is caught.
GLint maxComputeWorkGroupCount[3] = {70001, 70002, 70003};
GLint maxComputeWorkGroupSize[3] = {1500, 1501, 100};
GLint glesMajorVersion = 3;
GLint glesMinorVersion = 1;
GLint maxVertexImageUniforms = 2;
@@ -465,20 +460,8 @@ namespace {
if (data == nullptr) return;
for (int i = 0; i < 4; ++i) data[i] = GL_TRUE;
};
funcs.glGetIntegeri_v = [](GLenum pname, GLuint index, GLint* data) {
if (data == nullptr) return;
*data = 0;
if (index >= 3) return;
switch (pname) {
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
*data = g_fake.maxComputeWorkGroupCount[index];
break;
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
*data = g_fake.maxComputeWorkGroupSize[index];
break;
default:
break;
}
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) {
if (data != nullptr) *data = 0;
};
funcs.glGetProgramInfoLog = [](GLuint, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
if (infoLog != nullptr && bufSize > 0) infoLog[0] = '\0';
@@ -1568,23 +1551,3 @@ TEST(LocatedIoBlockProbe, ReportsTheDefectOnlyWhenTheUnlocatedControlCarriesTheP
EXPECT_FALSE(ProbeLocatedIoBlocksLosePayload(crippled).detected);
EXPECT_EQ(g_fake.ioBlockDraws, 0u) << "an entry-point-gated probe must not draw at all";
}
// The six per-axis compute limits are the backend-owned answers that cross the MGPipe boundary
// inside MGPCaps (DynamicBackendParameters::MaxComputeWorkGroupCount/Size), so the loader has
// to take EACH axis from glGetIntegeri_v rather than leave an initialiser - or one axis's
// answer - in the other slots. The integration side (AdvertisedLimitsScenario) pins the copy
// against the live getter on both backends; this pins the driver-to-caps step on its own.
TEST(ComputeWorkGroupCapabilities, TakesEveryAxisFromTheIndexedQuery) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
for (int axis = 0; axis < 3; ++axis) {
EXPECT_EQ(caps.MaxComputeWorkGroupCount[axis], g_fake.maxComputeWorkGroupCount[axis]) << "axis " << axis;
EXPECT_EQ(caps.MaxComputeWorkGroupSize[axis], g_fake.maxComputeWorkGroupSize[axis]) << "axis " << axis;
}
// The initialisers are the GL 4.3 minimums and every fake answer is above them, so a
// value equal to its initialiser here would mean the query never ran.
EXPECT_GT(caps.MaxComputeWorkGroupCount[0], 65535);
EXPECT_GT(caps.MaxComputeWorkGroupSize[2], 64);
}
-8
View File
@@ -85,9 +85,6 @@ add_subdirectory(VertexArray)
add_subdirectory(Program)
add_subdirectory(Query)
add_subdirectory(Pipeline)
# The MGPipe catalogue arithmetic: no GL context and no driver, just the .def, the seven
# generated files and the payload layouts.
add_subdirectory(Pipe)
add_subdirectory(ShaderTranspiler)
add_subdirectory(Util)
add_subdirectory(SelfTest)
@@ -97,8 +94,3 @@ add_subdirectory(Backend/DirectGLES)
if (ENABLE_INTEGRATION_TESTS)
add_subdirectory(Backend/DirectVulkan)
endif()
# The wire layer only exists in the disaggregated configuration, so its suite
# is only registered there. Nothing under MG_Remote is compiled otherwise.
if (MOBILEGL_BUILD_DISAGGREGATED)
add_subdirectory(Wire)
endif()
-28
View File
@@ -1,28 +0,0 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
PipeCatalogueTest
PipeCatalogueTest.cpp
)
target_include_directories(PipeCatalogueTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/MobileGL/MG_Pipe
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
PipeCatalogueTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
-289
View File
@@ -1,289 +0,0 @@
// MobileGL - MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The arithmetic of the MGPipe catalogue (plan B section 4.4, appendix A). Everything here
// is cheap on purpose: it is the test that fails when PipeCalls.def and the seven generated
// files stop agreeing, and it must not need a GL context to say so.
#include <gtest/gtest.h>
#include <cstring>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// Counting expansions of the catalogue. The Class parameter is a real enumerator, so a
// per-class count is a constant expression too.
#define MGP_COUNT_ONE(Name, Payload, Class, Flags) +1
#define MGP_COUNT_CLASS(Name, Payload, Class, Flags) +((Class) == countedClass ? 1 : 0)
constexpr SizeT kExpandedCallCount = 0 MGP_CALL_LIST(MGP_COUNT_ONE);
template <MGPipeCallClass countedClass>
constexpr SizeT ClassCount() {
return 0 MGP_CALL_LIST(MGP_COUNT_CLASS);
}
// Every payload named in the catalogue must be a memcpy-able POD, and so must every
// payload the verify comparator knows about.
#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags) \
static_assert(std::is_trivially_copyable_v<Payload>, #Name "'s payload " #Payload " is not trivially copyable");
MGP_CALL_LIST(MGP_ASSERT_CALL_PAYLOAD_POD)
#define MGP_ASSERT_VERIFY_PAYLOAD_POD(Payload) \
static_assert(std::is_trivially_copyable_v<Payload>, #Payload " is not trivially copyable");
MGP_VERIFY_PAYLOAD_LIST(MGP_ASSERT_VERIFY_PAYLOAD_POD)
} // namespace
// The handle is the whole object model. Eight bytes, a register pair, no padding.
TEST(PipeCatalogue, HandleIsEightBytes) {
static_assert(sizeof(MGPipeHandle) == 8);
static_assert(alignof(MGPipeHandle) == 4);
static_assert(std::is_trivially_copyable_v<MGPipeHandle>);
EXPECT_EQ(sizeof(MGPipeHandle), 8u);
// The two reserved handles, and the composite band that the program-pipeline resolver
// allocates out of.
EXPECT_TRUE(MGPipeHandleIsNull(kMGPipeNullHandle));
EXPECT_FALSE(MGPipeHandleIsNull(kMGPipeDefaultFramebuffer));
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(kMGPipeFirstAllocatableSlot));
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(kMGPipeShaderCsoCompositeSlotBase));
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(kMGPipeShaderCsoSlotLimit));
}
// The catalogue, the number documented in its header, and the two generated tables are one
// fact stated three times. This is the test that notices when they stop being.
TEST(PipeCatalogue, EntryCountMatchesTheDocumentedCount) {
static_assert(kExpandedCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT);
static_assert(kExpandedCallCount == kMGPipeCallCount);
EXPECT_EQ(kExpandedCallCount, static_cast<SizeT>(MGP_CALL_LIST_DOCUMENTED_COUNT));
EXPECT_EQ(kMGPipeCallCount, kExpandedCallCount);
}
TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
static_assert(ClassCount<kScreen>() == kMGPipeScreenCallCount);
static_assert(ClassCount<kScreen>() + ClassCount<kCtxCso>() + ClassCount<kCtxState>() +
ClassCount<kCtxObject>() + ClassCount<kCtxVerb>() + ClassCount<kCtxQuery>() ==
kMGPipeCallCount);
// The tables ARE their function pointers: a struct that is bigger than its call count
// has grown a member no generator knows about.
static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()));
static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()));
EXPECT_EQ(kMGPipeScreenCallCount, ClassCount<kScreen>());
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
// The per-class counts PipeCalls.def documents in its header.
EXPECT_EQ(ClassCount<kScreen>(), 11u);
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 17u);
EXPECT_EQ(ClassCount<kCtxObject>(), 9u);
EXPECT_EQ(ClassCount<kCtxVerb>(), 13u);
}
// An uninstalled pipe is every entry null - which is exactly what "this subsystem has not
// been migrated, keep pulling" means (plan B section 4.1).
TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
EXPECT_EQ(screen[i], nullptr) << "screen entry " << i;
}
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
EXPECT_EQ(context[i], nullptr) << "context entry " << i;
}
}
// The retirement ratchet of the migration carrier (section 6.3): the constant and the
// struct must agree, and the constant only ever goes down.
TEST(PipeCatalogue, ResidualBlockSizeIsPinned) {
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE);
EXPECT_EQ(sizeof(ResidualValueBlock), static_cast<SizeT>(MGL_RESIDUAL_BLOCK_SIZE));
// It carries the whole of both value structs today; that is what the later stages eat.
EXPECT_GE(sizeof(ResidualValueBlock), sizeof(RenderStateParameters) + sizeof(PixelStoreParameters));
}
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), kMGPipeCallCount + 1);
EXPECT_EQ(sizeof(MGPWireRecHeader), 8u);
// Every record is a multiple of the stream's 8-byte granularity, which is half of the
// applier's precondition.
EXPECT_EQ(sizeof(MGPWireRec_DrawVbo) % 8, 0u);
EXPECT_EQ(sizeof(MGPWireRec_BindRenderState) % 8, 0u);
EXPECT_EQ(sizeof(MGPWireRec_SetResidualValueState) % 8, 0u);
}
// Records are append-only. The three carriers added after the first cut - for the live
// GLFunctionsTable entries GetGpuTimestampNs, QueryCounterTimestamp and WaitSync - sit at
// the END of the list, after SetSwapInterval, so no opcode the first cut assigned has moved.
TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetSwapInterval), 68);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::QueryTimestamp), 69);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::QueryCounter), 70);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::FenceWaitServer), 71);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 72);
}
// A well-formed record passes the applier's bounds gate. P0 has no applier, so "accepted"
// is reported as "not applied" rather than "fatal".
TEST(PipeCatalogue, ApplierAcceptsAWellFormedRecord) {
MGPWireRec_Present record{};
record.Header.Op = static_cast<Uint16>(MGPWireOp::Present);
record.Header.Size = sizeof(record);
record.Payload.FrameSerial = 42;
EXPECT_FALSE(MGPipeApplyWireRecord(MGPWireOp::Present, &record, sizeof(record), sizeof(record)));
}
// G4 reports the FIRST differing field by name, and compares field by field so that
// padding cannot produce a difference that does not exist.
TEST(PipeCatalogue, VerifyComparatorNamesTheDifferingField) {
MGPDrawInfo a{};
MGPDrawInfo b{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.InstanceCount = 7;
EXPECT_FALSE(MGPipeVerify(a, b, &field));
EXPECT_STREQ(field, "InstanceCount");
// Padding bytes are not fields: writing to them cannot make two payloads differ.
MGPBindRenderState c{};
MGPBindRenderState d{};
c.Cso = MGPipeHandle{3, 1};
d.Cso = MGPipeHandle{3, 1};
field = nullptr;
EXPECT_TRUE(MGPipeVerify(c, d, &field));
// Nested payloads recurse, and arrays compare element-wise.
MGPFramebufferState left{};
MGPFramebufferState right{};
right.Color[3].Level = 2;
EXPECT_FALSE(MGPipeVerify(left, right, &field));
EXPECT_STREQ(field, "Color");
}
// G6's join over the backend read inventory. P0 allows unmapped rows; from P5 the gate is
// zero, so the numbers are asserted here to make a regression visible the day it happens.
TEST(PipeCatalogue, CoverageAccountsForEveryInventoryRow) {
EXPECT_EQ(kMGPipeInventoryReadPoints, 477u);
EXPECT_EQ(kMGPipeInventoryUnmapped, 0u);
EXPECT_EQ(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +
kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +
kMGPipeInventoryUnmapped,
kMGPipeInventoryReadPoints);
EXPECT_GT(kMGPipeCoverageEntryCount, 0u);
}
// G5's field ids come from the same accessor list as the coverage table, and every field
// starts un-filled: reading one before its verb fills it is the poison's whole job.
TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
EXPECT_EQ(kMGPipeInputFieldCount, 61u);
MGPipeFilledState state{};
state.CurrentVerbSerial = 1;
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
state.FilledGen[static_cast<SizeT>(MGPipeInputField::GetRenderStateParameters)] = 1;
EXPECT_TRUE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
// The next verb makes the same value stale, which a written-once bitmap could not see.
state.CurrentVerbSerial = 2;
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
}
// G7 pins the member list the pipeline/dynamic split is derived from.
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
EXPECT_STREQ(kMGPipePipelineStateMembers[0], "CullFaceEnabled");
EXPECT_STREQ(kMGPipePipelineStateMembers[kMGPipePipelineStateMemberCount - 1], "ColorMasks");
}
// The reverse channel is exactly ten callbacks (section 7.1).
TEST(PipeCatalogue, ReverseChannelHasTenCallbacks) {
EXPECT_EQ(kMGPipeCallbackCount, 10u);
EXPECT_EQ(sizeof(MGPipeCallbacks), kMGPipeCallbackCount * sizeof(void (*)()));
}
// The one shape that changes with the transport. In a monolith it resolves to the pointer
// it was given; with no transport installed a segment-backed span resolves to nothing
// rather than to garbage.
TEST(PipeCatalogue, HostSpanResolvesTheMonolithPointer) {
static_assert(sizeof(MGHostSpan) == 32);
const Uint8 bytes[8] = {0, 1, 2, 3, 4, 5, 6, 7};
MGHostSpan span{};
span.Ptr = bytes;
span.Size = sizeof(bytes);
span.Offset = 2;
EXPECT_EQ(MGPipeHostBytes(span), bytes + 2);
MGHostSpan staged{};
staged.Seg = 4;
staged.Size = 16;
EXPECT_EQ(gMGPipeSegmentResolver, nullptr);
EXPECT_EQ(MGPipeHostBytes(staged), nullptr);
}
// D-B8: a bound buffer range carries no inline host span. The named-UBO bytes are an
// optional second var-tail announced by HostSpanCount, so the SSBO, atomic-counter and XFB
// ranges - the majority - pay nothing for a payload whose shape is not frozen yet.
TEST(PipeCatalogue, BufferRangeCarriesNoInlineHostSpan) {
static_assert(sizeof(MGPBufferRange) == 24);
static_assert(sizeof(MGPShaderBuffers) == 32);
EXPECT_LT(sizeof(MGPBufferRange), sizeof(MGHostSpan));
// The call still declares the span it may carry, so the transport lays the tail out.
Uint32 flags = 0;
#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags) \
if (std::strcmp(#Name, "SetShaderBuffers") == 0) flags = static_cast<Uint32>(Flags);
MGP_CALL_LIST(MGP_FLAGS_OF_SET_SHADER_BUFFERS)
#undef MGP_FLAGS_OF_SET_SHADER_BUFFERS
EXPECT_EQ(flags & (kVarTail | kHostSpan), static_cast<Uint32>(kVarTail | kHostSpan));
// And the comparator sees the count that announces the tail.
MGPShaderBuffers a{};
MGPShaderBuffers b{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.HostSpanCount = 4;
EXPECT_FALSE(MGPipeVerify(a, b, &field));
EXPECT_STREQ(field, "HostSpanCount");
}
// The buffer half of resource_subdata has no level and no box of its own: [offset, size)
// rides in UnionBox.X / UnionBox.W, and only through the two helpers, which also say where
// one record stops and the emitter has to split.
TEST(PipeCatalogue, SubDataBufferRangeRidesInTheUnionBox) {
MGPSubData record{};
record.Level = 3;
record.RegionCount = 2;
ASSERT_TRUE(MGPipeSetSubDataBufferRange(record, 4096, 65536));
EXPECT_EQ(record.UnionBox.X, 4096);
EXPECT_EQ(record.UnionBox.W, 65536u);
EXPECT_EQ(record.UnionBox.Y, 0);
EXPECT_EQ(record.UnionBox.Z, 0);
EXPECT_EQ(record.UnionBox.H, 1u);
EXPECT_EQ(record.UnionBox.D, 1u);
EXPECT_EQ(record.Level, 0);
EXPECT_EQ(record.RegionCount, 0u);
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 4096u);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 65536u);
// The largest range one record expresses...
ASSERT_TRUE(MGPipeSetSubDataBufferRange(record, 0x7FFFFFFFull, 0xFFFFFFFFull));
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
// ...and beyond it the emitter splits: refused, record untouched.
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0x80000000ull, 1));
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0, 0x100000000ull));
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
}
+1
View File
@@ -18,6 +18,7 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
add_executable(
DriverBugProbesTest
DriverBugProbesTest.cpp
PersistentBufferOrderingProbeTest.cpp
)
target_include_directories(DriverBugProbesTest PRIVATE
@@ -0,0 +1,326 @@
// MobileGL - MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <MG_Util/SelfTest/PersistentBufferOrderingProbe.h>
#include <map>
#include <set>
using namespace MobileGL;
using namespace MobileGL::MG_Util::SelfTest;
namespace {
// Deferred vertex fetch, not canned ReadPixels answers: a broken mapped destination
// reads its current bytes at Finish instead of the bytes at DrawArrays. ReadPixels also
// drains these jobs, so inserting an early readback into the probe hides the bug here too.
struct FakeDriver {
struct Buffer {
Bool mapped = false;
Bool arena = false;
Bool copied = false;
Int channel = 0;
Vector<Uint8> staging;
};
struct Draw { GLuint fbo, buffer; Int channel; Bool late; };
std::map<GLuint, Buffer> buffers;
std::map<GLuint, GLuint> vaoBuffers;
std::map<GLuint, Int> colors;
Vector<Draw> draws;
std::set<GLuint> live;
std::map<GLenum, GLint> state = {
{GL_CURRENT_PROGRAM, 1}, {GL_VERTEX_ARRAY_BINDING, 2}, {GL_ARRAY_BUFFER, 3},
{GL_COPY_READ_BUFFER, 4}, {GL_COPY_WRITE_BUFFER, 5},
{GL_DRAW_FRAMEBUFFER_BINDING, 6}, {GL_READ_FRAMEBUFFER_BINDING, 7},
{GL_TEXTURE_BINDING_2D, 8}, {GL_PIXEL_PACK_BUFFER, 9},
{GL_PACK_ALIGNMENT, 8}, {GL_PACK_ROW_LENGTH, 31},
{GL_PACK_SKIP_PIXELS, 4}, {GL_PACK_SKIP_ROWS, 5}};
std::map<GLenum, GLboolean> enabled = {{GL_BLEND, GL_TRUE}, {GL_SCISSOR_TEST, GL_TRUE},
{GL_SAMPLE_MASK, GL_TRUE}, {GL_RASTERIZER_DISCARD, GL_TRUE}};
std::array<GLint, 4> viewport = {3, 4, 5, 6};
std::array<GLfloat, 4> clear = {.25f, .5f, .75f, 0};
std::array<GLboolean, 4> mask = {GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE};
GLuint next = 100;
GLenum error = GL_NO_ERROR;
Bool extension = true, corruptSubData = false, corruptCopy = false;
Bool corruptUnmapped = false, corruptSerialized = false;
Bool failMap = false, failAllocation = false, failFramebuffer = false;
Int failReadbackAt = 0, readbacks = 0;
Uint arenaAllocations = 0;
Uint8 pointerSentinel = 0;
GLuint Create() { live.insert(next); return next++; }
void Generate(GLsizei count, GLuint* ids) { for (Int i = 0; i < count; ++i) ids[i] = Create(); }
void Delete(GLsizei count, const GLuint* ids) {
for (Int i = 0; i < count; ++i) {
live.erase(ids[i]);
buffers.erase(ids[i]);
}
}
GLint Get(GLenum name) const {
const auto found = state.find(name);
return found == state.end() ? 0 : found->second;
}
static Int Channel(const void* data) {
GLfloat color[3];
std::memcpy(color, static_cast<const Uint8*>(data) + 2 * sizeof(GLfloat), sizeof(color));
return color[0] > .5f ? 0 : color[1] > .5f ? 1 : 2;
}
void Finish() {
for (const auto& draw : draws) {
const auto& buffer = buffers.at(draw.buffer);
colors[draw.fbo] = draw.late ? buffer.channel : draw.channel;
if (buffer.mapped && corruptSerialized) colors[draw.fbo] = (draw.channel + 1) % 3;
}
draws.clear();
}
} driver;
MG_External::GLESFunctionsTable Table() {
MG_External::GLESFunctionsTable gl{};
gl.glGetIntegerv = [](GLenum name, GLint* out) {
if (name == GL_MAJOR_VERSION) *out = 3;
else if (name == GL_MINOR_VERSION) *out = 2;
else if (name == GL_NUM_EXTENSIONS) *out = driver.extension ? 1 : 0;
else if (name == GL_VIEWPORT) std::copy(driver.viewport.begin(), driver.viewport.end(), out);
else if (name == GL_ARRAY_BUFFER_BINDING) *out = driver.Get(GL_ARRAY_BUFFER);
else if (name == GL_PIXEL_PACK_BUFFER_BINDING) *out = driver.Get(GL_PIXEL_PACK_BUFFER);
else *out = driver.Get(name);
};
gl.glGetBooleanv = [](GLenum, GLboolean* out) { std::copy(driver.mask.begin(), driver.mask.end(), out); };
gl.glGetFloatv = [](GLenum, GLfloat* out) { std::copy(driver.clear.begin(), driver.clear.end(), out); };
gl.glGetStringi = [](GLenum, GLuint) { return reinterpret_cast<const GLubyte*>("GL_EXT_buffer_storage"); };
gl.glGetError = []() { return std::exchange(driver.error, GL_NO_ERROR); };
gl.glIsEnabled = [](GLenum name) -> GLboolean { return driver.enabled[name]; };
gl.glEnable = [](GLenum name) { driver.enabled[name] = GL_TRUE; };
gl.glDisable = [](GLenum name) { driver.enabled[name] = GL_FALSE; };
gl.glCreateShader = [](GLenum) { return driver.Create(); };
gl.glShaderSource = [](GLuint, GLsizei, const GLchar* const*, const GLint*) {};
gl.glCompileShader = [](GLuint) {};
gl.glGetShaderiv = [](GLuint, GLenum, GLint* out) { *out = GL_TRUE; };
gl.glGetShaderInfoLog = [](GLuint, GLsizei, GLsizei*, GLchar* out) { *out = 0; };
gl.glDeleteShader = [](GLuint id) { driver.Delete(1, &id); };
gl.glCreateProgram = []() { return driver.Create(); };
gl.glAttachShader = [](GLuint, GLuint) {};
gl.glLinkProgram = [](GLuint) {};
gl.glGetProgramiv = [](GLuint, GLenum, GLint* out) { *out = GL_TRUE; };
gl.glGetProgramInfoLog = gl.glGetShaderInfoLog;
gl.glDeleteProgram = gl.glDeleteShader;
gl.glUseProgram = [](GLuint id) { driver.state[GL_CURRENT_PROGRAM] = id; };
gl.glGenBuffers = [](GLsizei count, GLuint* ids) { driver.Generate(count, ids); };
gl.glBindBuffer = [](GLenum target, GLuint id) { driver.state[target] = id; };
gl.glBufferStorageEXT = [](GLenum target, GLsizeiptr size, const void*, GLbitfield) {
auto& buffer = driver.buffers[driver.Get(target)];
buffer.arena = size >= 16 * 1024 * 1024;
if (buffer.arena) ++driver.arenaAllocations;
if (driver.failAllocation) driver.error = GL_OUT_OF_MEMORY;
if (!buffer.arena) buffer.staging.resize(size);
};
gl.glBufferData = [](GLenum target, GLsizeiptr size, const void*, GLenum) {
driver.buffers[driver.Get(target)].staging.resize(size);
};
gl.glMapBufferRange = [](GLenum target, GLintptr offset, GLsizeiptr, GLbitfield) -> void* {
if (driver.failMap) return nullptr;
auto& buffer = driver.buffers[driver.Get(target)];
buffer.mapped = true;
return buffer.arena ? &driver.pointerSentinel : buffer.staging.data() + offset;
};
gl.glUnmapBuffer = [](GLenum) -> GLboolean { return GL_TRUE; }; // Preserve allocation history.
gl.glBufferSubData = [](GLenum target, GLintptr offset, GLsizeiptr size, const void* data) {
auto& buffer = driver.buffers[driver.Get(target)];
if (buffer.arena) {
buffer.channel = FakeDriver::Channel(data);
buffer.copied = false;
} else std::memcpy(buffer.staging.data() + offset, data, size);
};
gl.glCopyBufferSubData = [](GLenum read, GLenum write, GLintptr offset, GLintptr, GLsizeiptr) {
auto& source = driver.buffers[driver.Get(read)];
auto& dest = driver.buffers[driver.Get(write)];
dest.channel = FakeDriver::Channel(source.staging.data() + offset);
dest.copied = true;
};
gl.glDeleteBuffers = [](GLsizei count, const GLuint* ids) { driver.Delete(count, ids); };
gl.glGenVertexArrays = gl.glGenBuffers;
gl.glBindVertexArray = [](GLuint id) { driver.state[GL_VERTEX_ARRAY_BINDING] = id; };
gl.glVertexAttribPointer = [](GLuint, GLint, GLenum, GLboolean, GLsizei, const void*) {
driver.vaoBuffers[driver.Get(GL_VERTEX_ARRAY_BINDING)] = driver.Get(GL_ARRAY_BUFFER);
};
gl.glEnableVertexAttribArray = [](GLuint) {};
gl.glDeleteVertexArrays = gl.glDeleteBuffers;
gl.glGenTextures = gl.glGenBuffers;
gl.glBindTexture = [](GLenum, GLuint id) { driver.state[GL_TEXTURE_BINDING_2D] = id; };
gl.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
gl.glDeleteTextures = gl.glDeleteBuffers;
gl.glGenFramebuffers = gl.glGenBuffers;
gl.glBindFramebuffer = [](GLenum target, GLuint id) {
if (target != GL_READ_FRAMEBUFFER) driver.state[GL_DRAW_FRAMEBUFFER_BINDING] = id;
if (target != GL_DRAW_FRAMEBUFFER) driver.state[GL_READ_FRAMEBUFFER_BINDING] = id;
};
gl.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
gl.glCheckFramebufferStatus = [](GLenum) -> GLenum {
return driver.failFramebuffer ? GL_FRAMEBUFFER_UNSUPPORTED : GL_FRAMEBUFFER_COMPLETE;
};
gl.glDeleteFramebuffers = gl.glDeleteBuffers;
gl.glViewport = [](GLint x, GLint y, GLsizei w, GLsizei h) { driver.viewport = {x, y, w, h}; };
gl.glColorMask = [](GLboolean r, GLboolean g, GLboolean b, GLboolean a) { driver.mask = {r, g, b, a}; };
gl.glClearColor = [](GLfloat r, GLfloat g, GLfloat b, GLfloat a) { driver.clear = {r, g, b, a}; };
gl.glClear = [](GLbitfield) {};
gl.glDrawArrays = [](GLenum, GLint, GLsizei) {
const GLuint id = driver.vaoBuffers.at(driver.Get(GL_VERTEX_ARRAY_BINDING));
const auto& buffer = driver.buffers.at(id);
const Bool late = buffer.mapped ? (buffer.copied ? driver.corruptCopy : driver.corruptSubData)
: driver.corruptUnmapped;
driver.draws.push_back({GLuint(driver.Get(GL_DRAW_FRAMEBUFFER_BINDING)), id, buffer.channel, late});
};
gl.glFinish = []() { driver.Finish(); };
gl.glMemoryBarrier = [](GLbitfield) {};
gl.glPixelStorei = [](GLenum name, GLint value) { driver.state[name] = value; };
gl.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum, GLenum, void* data) {
driver.Finish(); // Models the implicit wait that must NOT occur between subject draws.
if (++driver.readbacks == driver.failReadbackAt) {
driver.error = GL_INVALID_OPERATION;
return;
}
EXPECT_EQ(driver.Get(GL_PIXEL_PACK_BUFFER), 0);
EXPECT_EQ(driver.Get(GL_PACK_ROW_LENGTH), 0);
const Int channel = driver.colors.at(driver.Get(GL_READ_FRAMEBUFFER_BINDING));
auto* pixels = static_cast<Uint8*>(data);
for (Int i = 0; i < width * height; ++i)
for (Int c = 0; c < 4; ++c) pixels[4 * i + c] = c == channel || c == 3 ? 255 : 0;
};
return gl;
}
class PersistentBufferOrderingProbeTest : public ::testing::Test {
protected:
void SetUp() override { driver = FakeDriver{}; }
void TearDown() override { EXPECT_TRUE(driver.live.empty()); EXPECT_TRUE(driver.draws.empty()); }
};
}
TEST_F(PersistentBufferOrderingProbeTest, RequiresExtensionAndCompleteDispatchBeforeAllocating) {
auto gl = Table();
driver.extension = false;
EXPECT_FALSE(ProbePersistentBufferUpdateOrdering(gl).supported);
driver.extension = true;
gl.glCopyBufferSubData = nullptr;
EXPECT_FALSE(ProbePersistentBufferUpdateOrdering(gl).supported);
EXPECT_EQ(driver.arenaAllocations, 0u);
}
TEST_F(PersistentBufferOrderingProbeTest, OrderedDriverPassesAllUploadsAndRestoresCallerState) {
const auto saved = driver;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
ASSERT_TRUE(measurement.supported);
for (const auto& row : measurement.uploads) {
EXPECT_TRUE(row.unmapped.Passed());
EXPECT_TRUE(row.mapped.Passed());
EXPECT_EQ(row.mapped.frames, 240u); // Three fresh attempts before a negative result.
EXPECT_EQ(row.finishBoth.status, BufferOrderingProbeStatus::NotRun);
}
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
EXPECT_EQ(driver.state, saved.state);
EXPECT_EQ(driver.viewport, saved.viewport);
EXPECT_EQ(driver.clear, saved.clear);
EXPECT_EQ(driver.mask, saved.mask);
for (const auto& [cap, value] : driver.enabled) {
const auto found = saved.enabled.find(cap);
EXPECT_EQ(value, found == saved.enabled.end() ? GL_FALSE : found->second);
}
}
TEST_F(PersistentBufferOrderingProbeTest, DeferredMappedSubDataFetchIsDetectedWithPassingControls) {
driver.corruptSubData = true;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
EXPECT_TRUE(measurement.uploads[0].Detected());
EXPECT_TRUE(measurement.uploads[0].finishBefore.Passed());
EXPECT_GT(measurement.uploads[0].mapThenUnmap.badFrames, 0u);
EXPECT_GT(measurement.uploads[0].barrierBefore.badFrames, 0u);
EXPECT_FALSE(measurement.uploads[1].Detected());
EXPECT_FALSE(measurement.uploads[2].Detected());
const auto finding = DescribePersistentBufferOrderingBug(measurement);
ASSERT_TRUE(finding);
EXPECT_EQ(finding->verdict, DriverBugVerdict::Unfixable);
EXPECT_NE(finding->detail.find("SubData:"), String::npos);
EXPECT_NE(finding->detail.find("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1"), String::npos);
}
TEST_F(PersistentBufferOrderingProbeTest, DeferredCopyFetchIsDetectedWithBothStagingSources) {
driver.corruptCopy = true;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
EXPECT_FALSE(measurement.uploads[0].Detected());
EXPECT_TRUE(measurement.uploads[1].Detected());
EXPECT_TRUE(measurement.uploads[2].Detected());
}
TEST_F(PersistentBufferOrderingProbeTest, PostCollectorIncludesTheMeasuredFinding) {
driver.corruptSubData = true;
const auto findings = CollectGlesKnownDriverBugs(Table());
const auto found = std::find_if(findings.begin(), findings.end(), [](const auto& finding) {
return finding.name == "Persistent-mapped vertex buffers lose upload/draw ordering";
});
ASSERT_NE(found, findings.end());
EXPECT_NE(found->detail.find("never-mapped 0/80"), String::npos);
EXPECT_EQ(found->verdict, DriverBugVerdict::Unfixable);
}
TEST_F(PersistentBufferOrderingProbeTest, CorruptNeverMappedControlCannotAccusePersistentMapping) {
driver.corruptSubData = driver.corruptCopy = driver.corruptUnmapped = true;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
for (const auto& row : measurement.uploads) {
EXPECT_GT(row.unmapped.badFrames, 0u);
EXPECT_EQ(row.mapped.status, BufferOrderingProbeStatus::NotRun);
}
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
}
TEST_F(PersistentBufferOrderingProbeTest, CorruptSerializedControlCannotConfirmOrderingDefect) {
driver.corruptSubData = driver.corruptCopy = driver.corruptSerialized = true;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
for (const auto& row : measurement.uploads) EXPECT_GT(row.finishBoth.badFrames, 0u);
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
}
TEST_F(PersistentBufferOrderingProbeTest, FailedMappingIsInconclusiveAndReleasesResources) {
driver.failMap = true;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
EXPECT_EQ(measurement.uploads[0].mapped.status, BufferOrderingProbeStatus::Failed);
EXPECT_EQ(measurement.uploads[1].unmapped.status, BufferOrderingProbeStatus::Failed);
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
}
TEST_F(PersistentBufferOrderingProbeTest, AllocationFailureIsInconclusiveAndRestoresBindings) {
driver.failAllocation = true;
const auto saved = driver.state;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
for (const auto& row : measurement.uploads) {
EXPECT_EQ(row.unmapped.status, BufferOrderingProbeStatus::Failed);
EXPECT_EQ(row.unmapped.error, GLenum(GL_OUT_OF_MEMORY));
}
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
EXPECT_EQ(driver.state, saved);
}
TEST_F(PersistentBufferOrderingProbeTest, ReadbackErrorAfterAMismatchDoesNotProduceAFinding) {
driver.corruptSubData = true;
driver.failReadbackAt = 82; // Eighty clean control readbacks, then one corrupt subject FBO.
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
EXPECT_GT(measurement.uploads[0].mapped.badFrames, 0u);
EXPECT_EQ(measurement.uploads[0].mapped.status, BufferOrderingProbeStatus::Failed);
EXPECT_EQ(measurement.uploads[0].mapped.error, GLenum(GL_INVALID_OPERATION));
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
}
TEST_F(PersistentBufferOrderingProbeTest, IncompleteFramebufferIsInconclusiveAndRestoresBindings) {
driver.failFramebuffer = true;
const auto saved = driver.state;
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
EXPECT_EQ(driver.arenaAllocations, 0u);
EXPECT_EQ(driver.state, saved);
}
-25
View File
@@ -45,31 +45,6 @@ endif()
include(GoogleTest)
gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
TransformFeedbackLifetimeIdTest
TransformFeedbackLifetimeIdTest.cpp
)
target_include_directories(TransformFeedbackLifetimeIdTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TransformFeedbackLifetimeIdTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(TransformFeedbackLifetimeIdTest PRIVATE /Zc:preprocessor)
endif()
gtest_discover_tests(TransformFeedbackLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
RenderStateTest
RenderStateTest.cpp
@@ -32,7 +32,6 @@
#include "Includes.h"
#include <MG_State/GLState/BufferState/BufferObject.h>
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
using namespace MobileGL;
@@ -139,20 +138,3 @@ TEST(ObjectLifetimeIdTest, LiveVertexArrayObjectsHaveDistinctLifetimeIds) {
TEST(ObjectLifetimeIdTest, LiveBufferObjectsHaveDistinctLifetimeIds) {
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::BufferObject>("BufferObject");
}
// The renderbuffer had no lifetime id at all until plan B §11 P0 gave it one: it is
// the one FBO attachment source whose identity a backend twin registry can only have
// keyed on the heap address or the GL name, both of which recycle.
TEST(ObjectLifetimeIdTest, RenderbufferObjectAtARecycledAddressCarriesAFreshLifetimeId) {
using MG_State::GLState::RenderbufferObject;
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<RenderbufferObject>("RenderbufferObject");
if (reuseCount == 0) {
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
"construct/destroy rounds, so the recycled-address case was never exercised";
}
RecordProperty("address_reuses_observed", reuseCount);
}
TEST(ObjectLifetimeIdTest, LiveRenderbufferObjectsHaveDistinctLifetimeIds) {
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::RenderbufferObject>("RenderbufferObject");
}
@@ -1,143 +0,0 @@
// MobileGL - MobileGL/MG_Test/State/TransformFeedbackLifetimeIdTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// D21 (plan B v2 §4.7.3): DirectVulkan hands every transform feedback object one of sixteen
// counter-buffer groups, and the group carries that span's resume offset. The map was keyed on
// the GL NAME, which glGenTransformFeedbacks recycles the moment the object is deleted, so an
// object created on a recycled name was served the DEAD object's group together with its
// m_xfbCountersValid / m_xfbLastSeenGeneration entries.
//
// The transform feedback object is a plain struct inside a map rather than a heap object, so the
// reuse to defend against is the NAME's, not an address's - which is why these cases live here
// and not in ObjectLifetimeIdTest.cpp with the heap-allocated object types. Keeping them in
// their own translation unit also keeps the D21 commit textually independent of the rest of the
// branch, which plan B §10.4-5 asks for so it can be cherry-picked to dev on its own.
//
// The second case pins the OTHER half of the backend contract: a bounded slot table has to be
// able to tell an object whose span is still open (and may yet resume) from one whose span is
// closed or whose object is gone.
#include <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <MG_State/GLState/Core.h>
using namespace MobileGL;
namespace {
MG_State::GLState::GLContext& FreshContext() {
MobileGL::Initialize();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
return *MG_State::pGLContext;
}
} // namespace
TEST(TransformFeedbackLifetimeIdTest, AnObjectAtARecycledNameCarriesAFreshLifetimeId) {
auto& context = FreshContext();
// Before anything is bound. The default object exists from the start of the context, and the
// identity has to exist with it: a backend reading 0 here would match every FREE slot in its
// table without ever claiming one, which is the same bug this id was added to remove.
EXPECT_NE(context.GetBoundTransformFeedbackLifetimeId(), 0u)
<< "the default transform feedback object has no identity until something binds it";
Vector<Uint> names;
context.GenTransformFeedbackNames(1, names);
ASSERT_EQ(names.size(), 1u);
const Uint name = names[0];
ASSERT_NE(name, 0u);
context.BindTransformFeedbackObject(name);
const Uint64 firstId = context.GetBoundTransformFeedbackLifetimeId();
EXPECT_NE(firstId, 0u) << "a live transform feedback object answered to id 0, which is the value a "
"zero-initialised backend slot already carries";
// The default object is a different object and must not share the id.
context.BindTransformFeedbackObject(0);
EXPECT_NE(context.GetBoundTransformFeedbackLifetimeId(), firstId)
<< "the default transform feedback object shares an identity with a generated one";
// Deleting while bound reverts to the default object (GL 4.6 core 13.2.1), which is the
// shape the backend sees; delete from there anyway so the test does not depend on it.
context.BindTransformFeedbackObject(name);
context.MarkTransformFeedbackObjectForDeletion(name);
Vector<Uint> reborn;
context.GenTransformFeedbackNames(1, reborn);
ASSERT_EQ(reborn.size(), 1u);
if (reborn[0] != name) {
GTEST_SKIP() << "inconclusive, not proven: the name generator did not hand the deleted name back, so "
"the recycled-name case was never exercised";
}
context.BindTransformFeedbackObject(reborn[0]);
EXPECT_NE(context.GetBoundTransformFeedbackLifetimeId(), firstId)
<< "a transform feedback object created on a recycled name reports the DEAD object's lifetime id - "
"DirectVulkan would hand it the dead span's counter slot, and with it that span's resume state";
}
// The predicate DirectVulkan's slot table asks before it takes a group over. The case that
// matters is the middle one: object A is PAUSED and another object is bound and capturing, so A
// looks completely idle to a least-recently-used rule while being exactly the object whose
// counters must survive.
TEST(TransformFeedbackLifetimeIdTest, APausedSpanStaysOpenWhileAnotherObjectCaptures) {
auto& context = FreshContext();
Vector<Uint> names;
context.GenTransformFeedbackNames(2, names);
ASSERT_EQ(names.size(), 2u);
const Uint nameA = names[0];
const Uint nameB = names[1];
context.BindTransformFeedbackObject(nameA);
const Uint64 idA = context.GetBoundTransformFeedbackLifetimeId();
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idA)) << "an object that never began a span reads as open";
context.BeginTransformFeedback(GL_POINTS, nullptr);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA));
// Pausing is what makes interleaving legal (ARB_transform_feedback2); the span is still open.
context.SetTransformFeedbackPaused(true);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA));
// Now the shape the slot table sees: B is bound and capturing, A is paused and untouched.
context.BindTransformFeedbackObject(nameB);
const Uint64 idB = context.GetBoundTransformFeedbackLifetimeId();
EXPECT_NE(idB, idA);
context.BeginTransformFeedback(GL_POINTS, nullptr);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA))
<< "a paused span stopped reading as open the moment another object was bound - a backend "
"reclaiming slots by 'is this owner still going' would take A's counters away";
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idB));
context.EndTransformFeedback();
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idB)) << "a closed span still reads as open";
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA));
// A closes its own span; its slot becomes reclaimable.
context.BindTransformFeedbackObject(nameA);
context.EndTransformFeedback();
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idA));
// A deleted object can never resume, so its identity must not hold a slot either.
context.BindTransformFeedbackObject(nameB);
context.BeginTransformFeedback(GL_POINTS, nullptr);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idB));
context.MarkTransformFeedbackObjectForDeletion(nameB);
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idB))
<< "the identity of a deleted transform feedback object still claims an open span, so its counter "
"group would be pinned for the life of the context";
// Identities the context never issued, and the free-slot sentinel, are not open spans.
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(0));
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(~0ull));
}
-19
View File
@@ -35,25 +35,6 @@ target_link_libraries(
${LINK_LIBRARIES}
)
# The MGPipe boundary counters: the enable latch, the byte/call/gate arithmetic, the
# payload histogram's bucketing and the two report formats. No GL context, no driver.
add_executable(
PipeStatsTest
PipeStatsTest.cpp
)
target_include_directories(PipeStatsTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
PipeStatsTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeStatsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
-287
View File
@@ -1,287 +0,0 @@
// MobileGL - MobileGL/MG_Test/Util/PipeStatsTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The MGPipe boundary counters (plan B section 11 P0, corollary in section 2.3.1).
// No GL context and no driver: the module is arithmetic over a fixed set of counters,
// which is exactly what has to be pinned before anyone reads a number off a device.
#include <gtest/gtest.h>
#include <Config.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdio>
#include <fstream>
#include <sstream>
namespace {
namespace PS = MobileGL::MG_Util::PipeStats;
using MobileGL::String;
using MobileGL::Uint32;
using MobileGL::Uint64;
class PipeStatsTest : public ::testing::Test {
protected:
void SetUp() override {
PS::ResetForTesting();
PS::SetEnabledForTesting(true);
}
void TearDown() override {
PS::SetEnabledForTesting(false);
PS::ResetForTesting();
}
};
// The off latch is the whole cost argument: every counting site in the two backends is
// written as `if (Enabled()) ...`, so a false latch has to mean "nothing is counted".
TEST_F(PipeStatsTest, EnabledLatchIsTheOnlyGate) {
PS::SetEnabledForTesting(false);
EXPECT_FALSE(PS::Enabled());
PS::SetEnabledForTesting(true);
EXPECT_TRUE(PS::Enabled());
}
TEST_F(PipeStatsTest, ByteClassesAccumulateIndependently) {
PS::AddBytes(PS::ByteClass::StageBuffer, 100);
PS::AddBytes(PS::ByteClass::StageBuffer, 40);
PS::AddBytes(PS::ByteClass::StageTexture, 7);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageBuffer), 140u);
EXPECT_EQ(PS::FrameBytes(PS::ByteClass::StageBuffer), 140u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageTexture), 7u);
// Every other class untouched, the residual-value-block placeholder included.
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboGlobal), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboNamed), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageIndirectCmd), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::ResidualValueBlock), 0u);
}
// The frame accumulator is what feeds TracyPlot; the run total is what feeds the JSON
// dump. A present must clear the first and keep the second.
TEST_F(PipeStatsTest, PresentClearsTheFrameButKeepsTheTotal) {
PS::AddBytes(PS::ByteClass::StageTexture, 512);
PS::AddCalls(PS::CallClass::Draws, 3);
PS::CountGate(PS::Gate::EsprytRenderState, /*hit=*/true);
PS::OnPresent();
EXPECT_EQ(PS::FrameBytes(PS::ByteClass::StageTexture), 0u);
EXPECT_EQ(PS::FrameCalls(PS::CallClass::Draws), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageTexture), 512u);
EXPECT_EQ(PS::TotalCalls(PS::CallClass::Draws), 3u);
EXPECT_EQ(PS::TotalGateHits(PS::Gate::EsprytRenderState), 1u);
EXPECT_EQ(PS::FrameCount(), 1u);
}
TEST_F(PipeStatsTest, InitLatchesTheSummaryPeriodFromTheConfigAndNeverKeepsZero) {
// The device retrace harness never reaches the teardown dump, so the summary
// cadence is the only way a short fixture yields numbers at all: it must follow
// MOBILEGL_PIPE_STATS_PERIOD, and a zero must fall back rather than divide.
const Uint32 saved = MobileGL::MG_Config::Features.PipeStatsPeriod;
MobileGL::MG_Config::Features.PipeStatsPeriod = 7;
PS::Init();
EXPECT_EQ(PS::SummaryFramePeriod(), 7u);
MobileGL::MG_Config::Features.PipeStatsPeriod = 0;
PS::Init();
EXPECT_EQ(PS::SummaryFramePeriod(), PS::kDefaultSummaryFramePeriod);
MobileGL::MG_Config::Features.PipeStatsPeriod = saved;
PS::Init();
}
TEST_F(PipeStatsTest, GateHitsAndMissesAreSeparateCounters) {
for (Uint32 i = 0; i < 5; ++i) {
PS::CountGate(PS::Gate::MagmaPipelineMemo, /*hit=*/true);
}
PS::CountGate(PS::Gate::MagmaPipelineMemo, /*hit=*/false);
PS::CountGate(PS::Gate::MagmaDrawFastPath, /*hit=*/false);
EXPECT_EQ(PS::TotalGateHits(PS::Gate::MagmaPipelineMemo), 5u);
EXPECT_EQ(PS::TotalGateMisses(PS::Gate::MagmaPipelineMemo), 1u);
EXPECT_EQ(PS::TotalGateHits(PS::Gate::MagmaDrawFastPath), 0u);
EXPECT_EQ(PS::TotalGateMisses(PS::Gate::MagmaDrawFastPath), 1u);
}
// Bucket 0 is "no payload"; bucket n>0 is [2^(n-1), 2^n). The placeholder histogram is
// the SEG_CMD sizing input (section 4.5.7), so its bucketing is pinned now rather than
// when a generator first calls it.
TEST_F(PipeStatsTest, PayloadHistogramBucketsByPowerOfTwo) {
PS::RecordDrawPayloadBytes(0);
PS::RecordDrawPayloadBytes(1); // [1, 2) -> bucket 1
PS::RecordDrawPayloadBytes(2); // [2, 4) -> bucket 2
PS::RecordDrawPayloadBytes(3); // [2, 4) -> bucket 2
PS::RecordDrawPayloadBytes(48); // [32, 64) -> bucket 6
PS::RecordDrawPayloadBytes(64); // [64, 128)-> bucket 7
EXPECT_EQ(PS::TotalPayloadBucket(0), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(1), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(2), 2u);
EXPECT_EQ(PS::TotalPayloadBucket(6), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(7), 1u);
}
// A record far larger than the last bucket must land in the last bucket, not past the
// end of the array.
TEST_F(PipeStatsTest, PayloadHistogramSaturatesInsteadOfOverflowing) {
PS::RecordDrawPayloadBytes(~Uint64{0});
EXPECT_EQ(PS::TotalPayloadBucket(PS::kPayloadHistogramBuckets - 1), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(PS::kPayloadHistogramBuckets), 0u);
}
// The summary line's shape is what an operator greps and what the smoke check in this
// package matches, so it is pinned here rather than left to the log reader's memory.
TEST_F(PipeStatsTest, SummaryLineCarriesEveryClassAndGate) {
PS::AddCalls(PS::CallClass::Draws, 4);
PS::AddCalls(PS::CallClass::AccessorCalls, 50);
PS::AddBytes(PS::ByteClass::StageBuffer, 4096);
PS::OnPresent();
const String line = PS::FormatWindowLine();
EXPECT_NE(line.find("MGPipe stats:"), String::npos) << line;
EXPECT_NE(line.find("draws=4"), String::npos) << line;
// 50 accessor calls over 4 draws, two decimals, no <iomanip>.
EXPECT_NE(line.find("acc/draw=12.50"), String::npos) << line;
EXPECT_NE(line.find("buf=4096.00"), String::npos) << line;
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
EXPECT_NE(line.find("="), String::npos);
}
EXPECT_NE(line.find("gates["), String::npos) << line;
EXPECT_NE(line.find("tex[emit="), String::npos) << line;
}
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
// and load-bearing (bytes/f sizes SEG_STAGE), and integer division silently rounds a
// whole unit off each of them. 26 draws over 14 frames is 1.86, not 1.
TEST_F(PipeStatsTest, PerFrameFieldsKeepTwoDecimals) {
PS::AddCalls(PS::CallClass::Draws, 26);
PS::AddBytes(PS::ByteClass::StageBuffer, 1360);
for (Uint32 i = 0; i < 14; ++i) {
PS::OnPresent();
}
const String line = PS::FormatWindowLine();
EXPECT_NE(line.find("draws/f=1.86"), String::npos) << line;
EXPECT_NE(line.find("buf=97.14"), String::npos) << line;
}
// Successive summaries report WINDOWS, not run totals: a run total over a workload that
// changes shape (load, then steady state) averages away the very number section 2.3.1
// wants. Advancing the window is an explicit call, not a side effect of formatting.
TEST_F(PipeStatsTest, SummaryLinesReportDisjointWindows) {
PS::AddCalls(PS::CallClass::Draws, 10);
PS::OnPresent();
const String first = PS::FormatWindowLine();
EXPECT_NE(first.find("draws=10"), String::npos) << first;
PS::AdvanceSummaryWindow();
PS::AddCalls(PS::CallClass::Draws, 3);
PS::OnPresent();
const String second = PS::FormatWindowLine();
EXPECT_NE(second.find("draws=3"), String::npos) << second;
EXPECT_NE(second.find("frames=2"), String::npos) << second;
}
// FormatWindowLine is pure. It used to rewrite the window bases as a side effect of
// formatting, so any second reader - a probe, a test, a second reporting channel -
// silently zeroed the next window.
TEST_F(PipeStatsTest, FormattingTwiceDoesNotConsumeTheWindow) {
PS::AddCalls(PS::CallClass::Draws, 7);
PS::OnPresent();
const String first = PS::FormatWindowLine();
const String second = PS::FormatWindowLine();
EXPECT_EQ(first, second) << first << "\n" << second;
EXPECT_NE(second.find("draws=7"), String::npos) << second;
// ...and advancing explicitly does close it.
PS::AdvanceSummaryWindow();
const String third = PS::FormatWindowLine();
EXPECT_NE(third.find("draws=0"), String::npos) << third;
}
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroDraws) {
PS::AddCalls(PS::CallClass::AccessorCalls, 12);
PS::OnPresent();
const String line = PS::FormatWindowLine();
// No draw in the window means there is no per-draw number - and "0.00" beside a
// non-zero acc= would read as one.
EXPECT_NE(line.find("acc/draw=n/a"), String::npos) << line;
EXPECT_NE(line.find("acc=12"), String::npos) << line;
}
// A window with no Present in it has no per-frame reading at all. This used to divide by
// a faked 1 and print the window TOTALS under a "/f" label: a scenario slice that draws
// 47 times and never presents reported 1,404,550 staged bytes as a per-frame figure,
// which is a 47x overstatement of the SEG_STAGE sizing input this package exists to
// produce.
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroFrames) {
PS::AddCalls(PS::CallClass::Draws, 47);
PS::AddBytes(PS::ByteClass::StageBuffer, 1404550);
const String line = PS::FormatWindowLine();
EXPECT_EQ(PS::FrameCount(), 0u);
EXPECT_NE(line.find("window=0"), String::npos) << line;
EXPECT_NE(line.find("draws/f=n/a"), String::npos) << line;
// The bracket is relabelled rather than divided: totals, and marked as totals.
EXPECT_EQ(line.find("bytes/f["), String::npos) << line;
EXPECT_NE(line.find("bytes[buf=1404550"), String::npos) << line;
}
TEST_F(PipeStatsTest, JsonDumpNamesEveryCounter) {
PS::AddBytes(PS::ByteClass::StageUboNamed, 256);
PS::CountGate(PS::Gate::MagmaDynamicTail, /*hit=*/false);
PS::RecordDrawPayloadBytes(9);
PS::OnPresent();
const String json = PS::FormatJson();
for (Uint32 i = 0; i < static_cast<Uint32>(PS::ByteClass::Count); ++i) {
const String name = PS::NameOf(static_cast<PS::ByteClass>(i));
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
}
for (Uint32 i = 0; i < static_cast<Uint32>(PS::CallClass::Count); ++i) {
const String name = PS::NameOf(static_cast<PS::CallClass>(i));
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
}
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
const String name = PS::NameOf(static_cast<PS::Gate>(i));
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
}
EXPECT_NE(json.find("\"stage-ubo-named\": 256"), String::npos) << json;
EXPECT_NE(json.find("\"frames\": 1"), String::npos) << json;
EXPECT_NE(json.find("cmd-bytes-per-draw-histogram"), String::npos) << json;
}
// The counter names are the TracyPlot series names and the JSON keys; a rename is a
// breaking change for every recorded baseline, so the whole set is pinned.
TEST_F(PipeStatsTest, CounterNamesAreStable) {
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageBuffer), "stage-buffer");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageTexture), "stage-texture");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboGlobal), "stage-ubo-global");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboNamed), "stage-ubo-named");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageVertexClient), "stage-vertex-client");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndexClient), "stage-index-client");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndirectCmd), "stage-indirect-cmd");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::PersistentMapPush), "persistent-map-push");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::ResidualValueBlock), "residual-value-block");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytUnitBindingsEpoch), "espryt-unit-bindings-epoch");
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaDrawFastPath), "magma-draw-fastpath");
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaPipelineMemo), "magma-pipeline-memo");
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaDynamicTail), "magma-dynamic-tail");
}
// A summary is emitted every kSummaryFramePeriod presents. The period is a constant the
// smoke check depends on, so a change to it has to break a test.
TEST_F(PipeStatsTest, SummaryPeriodIsOneHundredAndTwentyFrames) {
EXPECT_EQ(PS::SummaryFramePeriod(), 120u);
for (Uint64 i = 0; i < PS::SummaryFramePeriod(); ++i) {
PS::OnPresent();
}
EXPECT_EQ(PS::FrameCount(), PS::SummaryFramePeriod());
}
} // namespace
-36
View File
@@ -1,36 +0,0 @@
cmake_minimum_required(VERSION 3.14)
# The MG_Remote wire layer: framing, the SPSC ring, the in-process transport,
# SCM_RIGHTS descriptor passing and the generated control-plane schema. Only
# reachable with MOBILEGL_BUILD_DISAGGREGATED=ON (see MG_Test/CMakeLists.txt).
set(MOBILEGL_WIRE_TESTS
FramingTest
RingTest
InProcessTransportTest
ProtocolSmokeTest
)
if (NOT WIN32)
# SCM_RIGHTS and fork(): POSIX only.
list(APPEND MOBILEGL_WIRE_TESTS FdPassingTest)
endif()
include(GoogleTest)
foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
add_executable(${test} ${test}.cpp)
target_include_directories(${test} PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/flatbuffers/include
)
target_link_libraries(${test} PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
gtest_discover_tests(${test} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
endforeach ()
-305
View File
@@ -1,305 +0,0 @@
// MobileGL - MobileGL/MG_Test/Wire/FdPassingTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SCM_RIGHTS across a real process boundary: a forked child creates a shared
// segment, fills it, and hands the descriptor over the aux socket; the parent
// adopts it, maps it read-only and compares every byte.
//
// This is the test the earlier branch never had. Its transport hardcoded
// `out->fd = -1` in the offer poll, so its data plane could not move a byte
// between processes - and nothing in its suite noticed, because everything ran
// in one process.
#include <MG_Remote/Transport/Doorbell.h>
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/ShmSegment.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <string>
#include <sys/socket.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>
#include <vector>
using namespace MobileGL::MG_Remote::Transport;
namespace {
constexpr std::uint64_t kSegmentSize = 64 * 1024;
std::uint8_t ByteAt(std::uint64_t index) {
return static_cast<std::uint8_t>((index * 31u + 7u) & 0xFFu);
}
// Child-side exit codes, so a failure says where it happened.
enum ChildStatus : int {
kChildOk = 0,
kChildCreateFailed = 2,
kChildMapFailed = 3,
kChildSendFailed = 4,
};
} // namespace
TEST(FdPassingTest, IsSupportedOnThisPlatform) { EXPECT_TRUE(FdPassing::Supported()); }
TEST(FdPassingTest, ChildSharesASegmentThatTheParentMapsAndVerifies) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
const std::string sideband = "SegmentRef{id=7,kind=Stage}";
const pid_t pid = ::fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
// Child. No gtest assertions here: a failed expectation in a forked
// child would report into a copy of the parent's test state.
::close(sockets[0]);
int status = kChildOk;
ShmSegment segment;
if (ShmSegment::Create("fdpass", kSegmentSize, segment) != MOBILEGL_OK) {
status = kChildCreateFailed;
} else if (segment.Map(false) != MOBILEGL_OK) {
status = kChildMapFailed;
} else {
auto* bytes = static_cast<std::uint8_t*>(segment.Data());
for (std::uint64_t i = 0; i < kSegmentSize; ++i) {
bytes[i] = ByteAt(i);
}
const MobileGLByteSpan span{sideband.data(), sideband.size()};
if (FdPassing::SendFd(sockets[1], segment.Fd(), span) != MOBILEGL_OK) {
status = kChildSendFailed;
}
}
::close(sockets[1]);
::_exit(status);
}
// Parent.
::close(sockets[1]);
// A destination smaller than kMaxSidebandBytes is refused BEFORE the
// datagram is consumed, so the descriptor is not lost by a caller that
// guessed the size wrong.
std::vector<std::uint8_t> small(8);
int fd = -1;
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, smallSpan, &required, 5000),
MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, FdPassing::kMaxSidebandBytes);
EXPECT_EQ(fd, -1);
std::vector<std::uint8_t> sidebandBuffer(FdPassing::kMaxSidebandBytes);
std::uint64_t sidebandSize = 0;
MobileGLMutableByteSpan sidebandSpan{sidebandBuffer.data(), sidebandBuffer.size()};
ASSERT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, sidebandSpan, &sidebandSize, 5000),
MOBILEGL_OK);
ASSERT_GE(fd, 0);
EXPECT_EQ(std::string(reinterpret_cast<const char*>(sidebandBuffer.data()),
static_cast<std::size_t>(sidebandSize)),
sideband);
ShmSegment adopted;
ASSERT_EQ(ShmSegment::Adopt(fd, kSegmentSize, adopted), MOBILEGL_OK);
EXPECT_TRUE(adopted.Valid());
ASSERT_EQ(adopted.Map(true), MOBILEGL_OK);
EXPECT_TRUE(adopted.MappedReadOnly());
const auto* bytes = static_cast<const std::uint8_t*>(adopted.Data());
ASSERT_NE(bytes, nullptr);
std::uint64_t mismatches = 0;
for (std::uint64_t i = 0; i < kSegmentSize; ++i) {
if (bytes[i] != ByteAt(i)) {
++mismatches;
}
}
EXPECT_EQ(mismatches, 0u);
int childStatus = 0;
ASSERT_EQ(::waitpid(pid, &childStatus, 0), pid);
ASSERT_TRUE(WIFEXITED(childStatus));
EXPECT_EQ(WEXITSTATUS(childStatus), kChildOk);
::close(sockets[0]);
}
TEST(FdPassingTest, ReceiveTimesOutWithNoOffer) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
std::vector<std::uint8_t> sidebandBuffer(FdPassing::kMaxSidebandBytes);
int fd = -1;
std::uint64_t sidebandSize = 0;
MobileGLMutableByteSpan span{sidebandBuffer.data(), sidebandBuffer.size()};
EXPECT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, span, &sidebandSize, 20), MOBILEGL_ERR_TIMEOUT);
EXPECT_EQ(fd, -1);
::close(sockets[0]);
::close(sockets[1]);
}
TEST(FdPassingTest, RejectsBadArguments) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
const std::vector<std::uint8_t> tooBig(FdPassing::kMaxSidebandBytes + 1, 0);
const MobileGLByteSpan oversized{tooBig.data(), tooBig.size()};
EXPECT_EQ(FdPassing::SendFd(sockets[1], sockets[0], oversized), MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_EQ(FdPassing::SendFd(sockets[1], -1, MobileGLByteSpan{nullptr, 0}),
MOBILEGL_ERR_INVALID_ARGUMENT);
::close(sockets[0]);
::close(sockets[1]);
}
TEST(FdPassingTest, SegmentWithoutASidebandStillCarriesItsDescriptor) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
ShmSegment segment;
ASSERT_EQ(ShmSegment::Create("nosideband", 4096, segment), MOBILEGL_OK);
ASSERT_EQ(segment.Map(false), MOBILEGL_OK);
static_cast<std::uint8_t*>(segment.Data())[0] = 0xA5;
ASSERT_EQ(FdPassing::SendFd(sockets[1], segment.Fd(), MobileGLByteSpan{nullptr, 0}),
MOBILEGL_OK);
std::vector<std::uint8_t> sidebandBuffer(FdPassing::kMaxSidebandBytes);
int fd = -1;
std::uint64_t sidebandSize = 123;
MobileGLMutableByteSpan span{sidebandBuffer.data(), sidebandBuffer.size()};
ASSERT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, span, &sidebandSize, 5000), MOBILEGL_OK);
EXPECT_EQ(sidebandSize, 0u);
ASSERT_GE(fd, 0);
ShmSegment adopted;
ASSERT_EQ(ShmSegment::Adopt(fd, 4096, adopted), MOBILEGL_OK);
ASSERT_EQ(adopted.Map(true), MOBILEGL_OK);
EXPECT_EQ(static_cast<const std::uint8_t*>(adopted.Data())[0], 0xA5);
::close(sockets[0]);
::close(sockets[1]);
}
// The `spawn` doorbell rides the same kind of socket as the fd channel, so it
// is covered here rather than beside the in-process one.
TEST(FdPassingTest, SocketDoorbellWakesAParkedWaiter) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
// One end each: the waiter reads its own end, the notifier writes the
// other, exactly as the two processes will.
SocketDoorbell waiterBell(sockets[0], kDoorbellWatermarkAdvanced, /*ownsFd=*/false);
SocketDoorbell notifierBell(sockets[1], kDoorbellWatermarkAdvanced, /*ownsFd=*/false);
std::atomic<std::uint32_t> parked{0};
std::atomic<bool> ready{false};
std::atomic<bool> woke{false};
std::thread waiter([&] {
woke.store(waiterBell.Wait(
parked, [&] { return ready.load(std::memory_order_acquire); }, kDefaultSpinUs, 5000));
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ready.store(true, std::memory_order_release);
NotifyIfParked(notifierBell, parked);
waiter.join();
EXPECT_TRUE(woke.load());
EXPECT_EQ(parked.load(), 0u);
::close(sockets[0]);
::close(sockets[1]);
}
TEST(FdPassingTest, SocketDoorbellTimesOutAndRemembersAnEarlyWakeup) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
SocketDoorbell waiterBell(sockets[0], kDoorbellRingAdvanced, /*ownsFd=*/false);
SocketDoorbell notifierBell(sockets[1], kDoorbellRingAdvanced, /*ownsFd=*/false);
// Nothing rings: the park has to end on its deadline, not hang.
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(waiterBell.Park(30));
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
20);
// A wakeup that arrives before anyone parks is not lost - it is sitting in
// the socket buffer, so the next Park returns at once.
notifierBell.Notify();
EXPECT_TRUE(waiterBell.Park(1000));
// ...and it was consumed, so the one after that times out again.
EXPECT_FALSE(waiterBell.Park(10));
::close(sockets[0]);
::close(sockets[1]);
}
// A doorbell whose peer has hung up must report that, not keep saying "ready".
// Park used to treat any `poll` return > 0 as a wakeup without ever looking at
// revents, and a closed peer leaves a stream socket permanently poll-ready
// with nothing to read - so Doorbell::Wait re-parked in a tight loop at full
// clock, unbounded when the caller passed kWaitForever. That is the pathology
// the bidirectional doorbell exists to prevent, arrived at from the other
// side.
TEST(FdPassingTest, SocketDoorbellStopsParkingWhenThePeerHangsUp) {
// A SOCK_STREAM pair, not FdPassing::CreateSocketPair's datagram pair:
// measured on Linux, a closed peer makes a stream end report
// POLLIN|POLLHUP with recv()==0, while a datagram end reports no readiness
// at all. The stream shape is what the spawn transport will use, and it is
// the shape that used to spin.
int sockets[2] = {-1, -1};
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), 0);
SocketDoorbell waiterBell(sockets[0], kDoorbellRingAdvanced, /*ownsFd=*/true);
ASSERT_EQ(::close(sockets[1]), 0);
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(waiterBell.Park(kWaitForever));
EXPECT_TRUE(waiterBell.Dead());
// Latched: no second syscall storm either.
EXPECT_FALSE(waiterBell.Park(kWaitForever));
// ...and a Wait with no deadline at all gives up instead of re-parking.
std::atomic<std::uint32_t> parked{0};
EXPECT_FALSE(waiterBell.Wait(
parked, [] { return false; }, /*spinUs=*/0, kWaitForever));
EXPECT_EQ(parked.load(), 0u);
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
1000);
}
TEST(FdPassingTest, SocketDoorbellStillDeliversTheLastRingBeforeAHangup) {
int sockets[2] = {-1, -1};
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), 0);
SocketDoorbell waiterBell(sockets[0], kDoorbellRingAdvanced, /*ownsFd=*/true);
SocketDoorbell notifierBell(sockets[1], kDoorbellRingAdvanced, /*ownsFd=*/false);
// Ring, then die. Detecting the hangup must not swallow the wakeup that
// was already queued - the peer's last publish is the one a waiter is
// most likely to be blocked on.
notifierBell.Notify();
ASSERT_EQ(::close(sockets[1]), 0);
EXPECT_TRUE(waiterBell.Park(1000));
EXPECT_TRUE(waiterBell.Dead());
EXPECT_FALSE(waiterBell.Park(kWaitForever));
}
-197
View File
@@ -1,197 +0,0 @@
// MobileGL - MobileGL/MG_Test/Wire/FramingTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The control-channel frame codec, and specifically the two contracts the
// earlier branch's codec got wrong: a bad header must be REPORTED (it used to
// turn into a silent permanent hang) and a too-small destination buffer must
// KEEP the message (it used to fail the call and drop it, wedging the stream).
#include <MG_Remote/Transport/Framing.h>
#include <gtest/gtest.h>
#include <cstring>
#include <numeric>
#include <vector>
using namespace MobileGL::MG_Remote::Transport;
namespace {
std::vector<std::uint8_t> Pattern(std::size_t size, std::uint8_t seed) {
std::vector<std::uint8_t> out(size);
for (std::size_t i = 0; i < size; ++i) {
out[i] = static_cast<std::uint8_t>(seed + i * 7u);
}
return out;
}
} // namespace
TEST(FramingTest, RoundTripsTwoMessagesFedOneByteAtATime) {
const std::vector<std::uint8_t> first = Pattern(37, 0x11);
const std::vector<std::uint8_t> second = Pattern(120, 0x83);
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, first.data(), first.size()), MOBILEGL_OK);
ASSERT_EQ(AppendFrame(stream, second.data(), second.size()), MOBILEGL_OK);
EXPECT_EQ(stream.size(), 2 * kFrameHeaderSize + first.size() + second.size());
// A stream transport hands over arbitrary fragments; one byte at a time is
// the worst case and must work.
FrameReader reader;
std::vector<std::vector<std::uint8_t>> received;
for (std::uint8_t byte : stream) {
ASSERT_EQ(reader.Feed(&byte, 1), MOBILEGL_OK);
while (reader.HasMessage()) {
std::vector<std::uint8_t> message;
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
received.push_back(std::move(message));
}
}
ASSERT_EQ(received.size(), 2u);
EXPECT_EQ(received[0], first);
EXPECT_EQ(received[1], second);
EXPECT_FALSE(reader.Failed());
EXPECT_EQ(reader.BufferedBytes(), 0u);
}
TEST(FramingTest, MagicIsOnTheWireAsMGLF) {
std::vector<std::uint8_t> stream;
const std::uint8_t payload = 0xAB;
ASSERT_EQ(AppendFrame(stream, &payload, 1), MOBILEGL_OK);
ASSERT_GE(stream.size(), 4u);
EXPECT_EQ(stream[0], 'M');
EXPECT_EQ(stream[1], 'G');
EXPECT_EQ(stream[2], 'L');
EXPECT_EQ(stream[3], 'F');
}
TEST(FramingTest, EmptyPayloadRoundTrips) {
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, nullptr, 0), MOBILEGL_OK);
FrameReader reader;
ASSERT_EQ(reader.Feed(stream.data(), stream.size()), MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
EXPECT_EQ(reader.PendingMessageSize(), 0u);
std::vector<std::uint8_t> message{0xFF};
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
EXPECT_TRUE(message.empty());
}
TEST(FramingTest, BadMagicIsReportedAndLatchesTheReaderDead) {
std::uint8_t header[8] = {};
const std::uint32_t wrongMagic = 0xDEADBEEF;
const std::uint32_t length = 4;
std::memcpy(header + 0, &wrongMagic, sizeof(wrongMagic));
std::memcpy(header + 4, &length, sizeof(length));
FrameReader reader;
// The failure surfaces at Feed time, not as a message that never arrives.
EXPECT_EQ(reader.Feed(header, sizeof(header)), MOBILEGL_ERR_PROTOCOL_MISMATCH);
EXPECT_TRUE(reader.Failed());
EXPECT_FALSE(reader.HasMessage());
// And it stays dead: a desynchronized stream is never re-synchronized by
// feeding it more bytes.
const std::uint8_t more[4] = {1, 2, 3, 4};
EXPECT_EQ(reader.Feed(more, sizeof(more)), MOBILEGL_ERR_PROTOCOL_MISMATCH);
std::vector<std::uint8_t> message;
EXPECT_EQ(reader.TakeMessage(message), MOBILEGL_ERR_PROTOCOL_MISMATCH);
}
TEST(FramingTest, OversizedLengthIsRejectedBeforeAnyAllocation) {
std::uint8_t header[8] = {};
const std::uint32_t magic = kFrameMagic;
const std::uint32_t length = static_cast<std::uint32_t>(kMaxFramePayloadSize) + 1;
std::memcpy(header + 0, &magic, sizeof(magic));
std::memcpy(header + 4, &length, sizeof(length));
FrameReader reader;
EXPECT_EQ(reader.Feed(header, sizeof(header)), MOBILEGL_ERR_PROTOCOL_MISMATCH);
EXPECT_TRUE(reader.Failed());
}
TEST(FramingTest, SendRefusesAPayloadOverTheCap) {
std::vector<std::uint8_t> stream;
const std::uint8_t dummy = 0;
// The size check happens before the payload is touched, so no 64MiB
// allocation is needed to cover it.
EXPECT_EQ(AppendFrame(stream, &dummy, kMaxFramePayloadSize + 1), MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_TRUE(stream.empty());
}
TEST(FramingTest, BufferTooSmallReportsTheSizeAndKeepsTheMessage) {
const std::vector<std::uint8_t> payload = Pattern(200, 0x5A);
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, payload.data(), payload.size()), MOBILEGL_OK);
FrameReader reader;
ASSERT_EQ(reader.Feed(stream.data(), stream.size()), MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
std::vector<std::uint8_t> small(8);
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(reader.TakeMessage(smallSpan, &required), MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, payload.size());
// Still there. This is the whole point: the old transport dropped it here
// and the stream never recovered.
ASSERT_TRUE(reader.HasMessage());
std::vector<std::uint8_t> big(required);
std::uint64_t got = 0;
MobileGLMutableByteSpan bigSpan{big.data(), big.size()};
ASSERT_EQ(reader.TakeMessage(bigSpan, &got), MOBILEGL_OK);
EXPECT_EQ(got, payload.size());
EXPECT_EQ(big, payload);
EXPECT_FALSE(reader.HasMessage());
}
TEST(FramingTest, TakeWithNoCompleteMessageDoesNotBlockOrCorrupt) {
const std::vector<std::uint8_t> payload = Pattern(64, 0x22);
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, payload.data(), payload.size()), MOBILEGL_OK);
FrameReader reader;
// Header plus half the payload.
ASSERT_EQ(reader.Feed(stream.data(), kFrameHeaderSize + 32), MOBILEGL_OK);
EXPECT_FALSE(reader.HasMessage());
EXPECT_EQ(reader.PendingMessageSize(), 0u);
std::vector<std::uint8_t> message;
EXPECT_EQ(reader.TakeMessage(message), MOBILEGL_ERR_TIMEOUT);
ASSERT_EQ(reader.Feed(stream.data() + kFrameHeaderSize + 32,
stream.size() - kFrameHeaderSize - 32),
MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
EXPECT_EQ(message, payload);
}
TEST(FramingTest, ManyMessagesCompactTheBufferInsteadOfGrowing) {
// Drives the reader past its compaction threshold so the "consumed bytes
// are reclaimed" path is actually taken.
const std::vector<std::uint8_t> payload = Pattern(1024, 0x07);
FrameReader reader;
for (int i = 0; i < 300; ++i) {
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, payload.data(), payload.size()), MOBILEGL_OK);
ASSERT_EQ(reader.Feed(stream.data(), stream.size()), MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
std::vector<std::uint8_t> message;
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
ASSERT_EQ(message, payload);
}
EXPECT_EQ(reader.BufferedBytes(), 0u);
}
@@ -1,398 +0,0 @@
// MobileGL - MobileGL/MG_Test/Wire/InProcessTransportTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The `inproc` transport: message queues in both directions, the
// buffer-too-small contract, shutdown semantics, descriptor hand-off, and the
// condvar doorbells the rings park on.
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/InProcessTransport.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <cstring>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
using namespace MobileGL::MG_Remote::Transport;
namespace {
MobileGLByteSpan Span(const std::string& text) {
return MobileGLByteSpan{text.data(), text.size()};
}
std::string Receive(ITransport& transport, std::uint32_t timeoutMs = 1000) {
std::vector<std::uint8_t> buffer(4096);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
const MobileGLResult result = transport.ReceiveFrame(span, &size, timeoutMs);
if (result != MOBILEGL_OK) {
return std::string("<result=") + std::to_string(static_cast<int>(result)) + ">";
}
return std::string(reinterpret_cast<const char*>(buffer.data()),
static_cast<std::size_t>(size));
}
} // namespace
TEST(InProcessTransportTest, CarriesFramesInBothDirections) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
ASSERT_TRUE(client && server);
EXPECT_EQ(client->Role(), TransportRole::InProcess);
const std::string hello = "Hello{abiMajor=1}";
const std::string welcome = "Welcome{serverPid=42}";
ASSERT_EQ(client->SendFrame(Span(hello)), MOBILEGL_OK);
EXPECT_EQ(server->PeekFrameSize(), hello.size());
// A message goes to the PEER's inbox, never back to the sender.
EXPECT_EQ(client->PeekFrameSize(), 0u);
EXPECT_EQ(Receive(*server), hello);
ASSERT_EQ(server->SendFrame(Span(welcome)), MOBILEGL_OK);
EXPECT_EQ(Receive(*client), welcome);
}
TEST(InProcessTransportTest, PreservesOrder) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
for (int i = 0; i < 64; ++i) {
const std::string message = "msg-" + std::to_string(i);
ASSERT_EQ(client->SendFrame(Span(message)), MOBILEGL_OK);
}
for (int i = 0; i < 64; ++i) {
EXPECT_EQ(Receive(*server), "msg-" + std::to_string(i));
}
}
TEST(InProcessTransportTest, BufferTooSmallKeepsTheMessage) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
const std::string message(300, 'x');
ASSERT_EQ(client->SendFrame(Span(message)), MOBILEGL_OK);
std::vector<std::uint8_t> small(16);
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(server->ReceiveFrame(smallSpan, &required, 0), MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, message.size());
// Still queued - the caller just retries with the size it was told.
EXPECT_EQ(server->PeekFrameSize(), message.size());
EXPECT_EQ(Receive(*server), message);
}
TEST(InProcessTransportTest, PollAndTimeoutDoNotBlockForever) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::vector<std::uint8_t> buffer(64);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
EXPECT_EQ(server->ReceiveFrame(span, &size, 0), MOBILEGL_ERR_TIMEOUT);
const auto start = std::chrono::steady_clock::now();
EXPECT_EQ(server->ReceiveFrame(span, &size, 30), MOBILEGL_ERR_TIMEOUT);
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
20);
}
TEST(InProcessTransportTest, ShutdownDrainsBeforeItCloses) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
const std::string last = "Fatal{code=DeviceLost}";
ASSERT_EQ(client->SendFrame(Span(last)), MOBILEGL_OK);
client->Shutdown();
// A peer that shuts down right after sending must not lose its last
// message - that is usually the one that says why it is going away.
EXPECT_EQ(Receive(*server), last);
std::vector<std::uint8_t> buffer(64);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
EXPECT_EQ(server->ReceiveFrame(span, &size, 100), MOBILEGL_ERR_TRANSPORT_CLOSED);
EXPECT_EQ(server->SendFrame(Span(last)), MOBILEGL_ERR_TRANSPORT_CLOSED);
}
TEST(InProcessTransportTest, BlockedReceiverWakesOnSendAndOnShutdown) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::atomic<bool> got{false};
std::thread reader([&] {
got.store(Receive(*server, kWaitForever) == "wake");
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ASSERT_EQ(client->SendFrame(Span(std::string("wake"))), MOBILEGL_OK);
reader.join();
EXPECT_TRUE(got.load());
std::thread closer([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
client->Shutdown();
});
std::vector<std::uint8_t> buffer(64);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
EXPECT_EQ(server->ReceiveFrame(span, &size, kWaitForever), MOBILEGL_ERR_TRANSPORT_CLOSED);
closer.join();
}
TEST(InProcessTransportTest, RefusesAPayloadOverTheFrameCap) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
// Not allocated: the cap is checked before the bytes are touched. Keeping
// the same limit as the socket transports means nothing passes CI here and
// then fails after the switch to `spawn`.
const std::uint8_t dummy = 0;
MobileGLByteSpan huge{&dummy, 64ull * 1024 * 1024 + 1};
EXPECT_EQ(client->SendFrame(huge), MOBILEGL_ERR_INVALID_ARGUMENT);
}
#if !defined(_WIN32)
TEST(InProcessTransportTest, HandsOverADescriptorAndItsSideband) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
int pipeFds[2] = {-1, -1};
ASSERT_EQ(::pipe(pipeFds), 0);
const std::string sideband = "SegmentRef{id=1,kind=Cmd}";
ASSERT_EQ(client->ShareFd(pipeFds[0], Span(sideband)), MOBILEGL_OK);
// Symmetric with the SCM_RIGHTS path: a short sideband buffer is refused
// before anything is consumed, so the descriptor is never dropped.
std::vector<std::uint8_t> small(8);
int fd = -1;
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(server->ReceiveFd(&fd, smallSpan, &required, 0), MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, FdPassing::kMaxSidebandBytes);
EXPECT_EQ(fd, -1);
std::vector<std::uint8_t> big(FdPassing::kMaxSidebandBytes);
std::uint64_t sidebandSize = 0;
MobileGLMutableByteSpan bigSpan{big.data(), big.size()};
ASSERT_EQ(server->ReceiveFd(&fd, bigSpan, &sidebandSize, 100), MOBILEGL_OK);
ASSERT_GE(fd, 0);
EXPECT_EQ(std::string(reinterpret_cast<const char*>(big.data()),
static_cast<std::size_t>(sidebandSize)),
sideband);
// Same open file description, independent descriptor.
const char payload[] = "bytes";
ASSERT_EQ(::write(pipeFds[1], payload, sizeof(payload)), static_cast<ssize_t>(sizeof(payload)));
char readBack[sizeof(payload)] = {};
ASSERT_EQ(::read(fd, readBack, sizeof(readBack)), static_cast<ssize_t>(sizeof(payload)));
EXPECT_STREQ(readBack, payload);
::close(fd);
::close(pipeFds[0]);
::close(pipeFds[1]);
}
TEST(InProcessTransportTest, AFrameWakeupIsNotEatenByAWaiterOnDescriptors) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
// Two readers on the SAME endpoint, blocked on two different predicates.
// With one condition_variable per direction and notify_one, the SendFrame
// below could be delivered to the descriptor waiter, which re-tests its
// own predicate and goes back to sleep - and the message then sits
// undelivered until some unrelated later event. ITransport narrows the
// contract to one dedicated reader thread, but that is a comment, and the
// first caller that splits its reader should not have to discover this.
std::atomic<bool> fdWaiterStarted{false};
std::thread fdWaiter([&] {
std::vector<std::uint8_t> sideband(FdPassing::kMaxSidebandBytes);
MobileGLMutableByteSpan span{sideband.data(), sideband.size()};
int fd = -1;
std::uint64_t size = 0;
fdWaiterStarted.store(true);
// Never offered a descriptor: this one ends on the Shutdown below.
EXPECT_EQ(client->ReceiveFd(&fd, span, &size, kWaitForever),
MOBILEGL_ERR_TRANSPORT_CLOSED);
EXPECT_EQ(fd, -1);
});
while (!fdWaiterStarted.load()) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::atomic<bool> frameWaiterStarted{false};
std::string got;
std::thread frameWaiter([&] {
frameWaiterStarted.store(true);
got = Receive(*client, 4000);
});
while (!frameWaiterStarted.load()) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::string message = "wake the right waiter";
const auto start = std::chrono::steady_clock::now();
ASSERT_EQ(server->SendFrame(Span(message)), MOBILEGL_OK);
frameWaiter.join();
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
EXPECT_EQ(got, message);
// Not "eventually, when the receive timed out and re-checked".
EXPECT_LT(elapsedMs, 2000);
client->Shutdown();
fdWaiter.join();
}
#endif
TEST(InProcessTransportTest, DoorbellWakesAParkedWaiter) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
// producerParked / consumerParked live in RingControl; here a standalone
// flag stands in for one.
std::atomic<std::uint32_t> parked{0};
std::atomic<bool> ready{false};
std::atomic<bool> woke{false};
std::thread waiter([&] {
woke.store(client->SelfDoorbell().Wait(
parked, [&] { return ready.load(std::memory_order_acquire); }, kDefaultSpinUs, 5000));
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ready.store(true, std::memory_order_release);
// The peer only rings when the waiter says it parked, which is what makes
// the common (spin-only) case free.
NotifyIfParked(server->PeerDoorbell(), parked);
waiter.join();
EXPECT_TRUE(woke.load());
EXPECT_EQ(parked.load(), 0u);
}
TEST(InProcessTransportTest, DoorbellReturnsImmediatelyWhenAlreadyReady) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::atomic<std::uint32_t> parked{0};
// No notification is sent at all: a condition that is already true must
// never park, or the lost-wakeup window would be reachable.
EXPECT_TRUE(client->SelfDoorbell().Wait(
parked, [] { return true; }, kDefaultSpinUs, 0));
EXPECT_EQ(parked.load(), 0u);
}
TEST(InProcessTransportTest, DoorbellTimesOutWhenNothingHappens) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::atomic<std::uint32_t> parked{0};
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client->SelfDoorbell().Wait(
parked, [] { return false; }, kDefaultSpinUs, 30));
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
20);
EXPECT_EQ(parked.load(), 0u);
}
// The design's own steady state: the consumer spun, set consumerParked and blocked
// with NO deadline. Shutdown has to bring that thread back, and a single Notify
// cannot - Doorbell::Wait consumes it, re-tests a condition that is still false,
// and with kWaitForever parks again. Only a bell that reports Dead() ends the
// loop, which is what InProcessChannel::Close rings now.
//
// A regression here is a HANG, so the join is bounded: the waiter owns its state
// through a shared_ptr and is detached on timeout, and the test fails red after
// five seconds instead of wedging the CI job.
TEST(InProcessTransportTest, ShutdownUnparksAWaiterWithNoDeadline) {
struct Shared {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
std::atomic<std::uint32_t> parked{0};
std::atomic<bool> returned{false};
std::atomic<bool> woke{true};
};
auto shared = std::make_shared<Shared>();
InProcessTransport::CreatePair(shared->client, shared->server);
std::thread waiter([shared] {
shared->woke.store(shared->server->SelfDoorbell().Wait(
shared->parked, [] { return false; }, kDefaultSpinUs, kWaitForever));
shared->returned.store(true, std::memory_order_release);
});
// Past the spin and announced as parked; a little longer and it is inside
// Park. (A Kill that lands before the Park is handled too - Park returns at
// once on a dead bell - but the case under test is the parked one.)
while (shared->parked.load() == 0) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ASSERT_FALSE(shared->returned.load());
shared->client->Shutdown();
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!shared->returned.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (!shared->returned.load(std::memory_order_acquire)) {
waiter.detach();
FAIL() << "Shutdown did not unpark a waiter with no deadline within 5 s: the inproc doorbell "
"has no death state, so the waiter consumed the ring and parked again";
}
waiter.join();
// No wakeup was consumed - the bell died - and the park flag is clear.
EXPECT_FALSE(shared->woke.load());
EXPECT_TRUE(shared->server->SelfDoorbell().Dead());
EXPECT_TRUE(shared->client->SelfDoorbell().Dead());
EXPECT_EQ(shared->parked.load(), 0u);
// Sticky: a wait with no deadline that ARRIVES after the Shutdown returns at
// once rather than parking, so a late thread cannot hang either.
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(shared->server->SelfDoorbell().Wait(
shared->parked, [] { return false; }, 0, kWaitForever));
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start)
.count(),
1000);
EXPECT_EQ(shared->parked.load(), 0u);
}
-149
View File
@@ -1,149 +0,0 @@
// MobileGL - MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The committed control-plane schema: encode/decode a handshake through the
// generated header, and pin the union tag values, which are wire numbers that
// may only ever be appended to.
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Transport/Framing.h>
#include <MG_Remote/Transport/InProcessTransport.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
using namespace MobileGL::Wire;
namespace Transport = MobileGL::MG_Remote::Transport;
namespace {
std::vector<std::uint8_t> BuildHello() {
::flatbuffers::FlatBufferBuilder builder(1024);
const std::vector<std::uint8_t> config{1, 2, 3, 4};
auto hello = CreateHelloDirect(builder, MOBILEGL_PROTOCOL_ABI_MAJOR,
MOBILEGL_PROTOCOL_ABI_MINOR, "mobilegl-test-build",
/*backendType=*/2, /*pid=*/4242, &config);
auto envelope = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union());
FinishCtrlEnvelopeBuffer(builder, envelope);
const std::uint8_t* begin = builder.GetBufferPointer();
return std::vector<std::uint8_t>(begin, begin + builder.GetSize());
}
} // namespace
TEST(ProtocolSmokeTest, HelloRoundTrips) {
const std::vector<std::uint8_t> buffer = BuildHello();
// Every message from the peer is verified before a single field is read:
// the control plane is parsed from another process's memory.
::flatbuffers::Verifier verifier(buffer.data(), buffer.size());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
ASSERT_TRUE(CtrlEnvelopeBufferHasIdentifier(buffer.data()));
const CtrlEnvelope* envelope = GetCtrlEnvelope(buffer.data());
ASSERT_NE(envelope, nullptr);
ASSERT_EQ(envelope->msg_type(), CtrlMsg::Hello);
const Hello* hello = envelope->msg_as_Hello();
ASSERT_NE(hello, nullptr);
EXPECT_EQ(hello->abiMajor(), static_cast<std::uint32_t>(MOBILEGL_PROTOCOL_ABI_MAJOR));
EXPECT_EQ(hello->abiMinor(), static_cast<std::uint32_t>(MOBILEGL_PROTOCOL_ABI_MINOR));
ASSERT_NE(hello->buildFingerprint(), nullptr);
EXPECT_EQ(hello->buildFingerprint()->str(), "mobilegl-test-build");
EXPECT_EQ(hello->backendType(), 2u);
EXPECT_EQ(hello->pid(), 4242u);
ASSERT_NE(hello->configBlob(), nullptr);
ASSERT_EQ(hello->configBlob()->size(), 4u);
EXPECT_EQ(hello->configBlob()->Get(3), 4u);
// A message of the wrong kind reads back as null rather than as garbage.
EXPECT_EQ(envelope->msg_as_Welcome(), nullptr);
}
TEST(ProtocolSmokeTest, WelcomeCarriesTheFourSegmentAnnouncements) {
::flatbuffers::FlatBufferBuilder builder(1024);
auto cmd = CreateSegmentRefDirect(builder, 1, SegmentKind::Cmd, 8ull * 1024 * 1024, "cmd");
auto stage = CreateSegmentRefDirect(builder, 2, SegmentKind::Stage, 32ull * 1024 * 1024, "stage");
auto reply = CreateSegmentRefDirect(builder, 3, SegmentKind::Reply, 8ull * 1024 * 1024, "reply");
auto event = CreateSegmentRefDirect(builder, 4, SegmentKind::Event, 256ull * 1024, "event");
auto welcome = CreateWelcome(builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR,
/*serverPid=*/99, cmd, stage, reply, event);
auto envelope = CreateCtrlEnvelope(builder, CtrlMsg::Welcome, welcome.Union());
FinishCtrlEnvelopeBuffer(builder, envelope);
::flatbuffers::Verifier verifier(builder.GetBufferPointer(), builder.GetSize());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
const Welcome* parsed = GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_Welcome();
ASSERT_NE(parsed, nullptr);
EXPECT_EQ(parsed->serverPid(), 99u);
ASSERT_NE(parsed->cmdRing(), nullptr);
EXPECT_EQ(parsed->cmdRing()->kind(), SegmentKind::Cmd);
EXPECT_EQ(parsed->cmdRing()->sizeBytes(), 8ull * 1024 * 1024);
ASSERT_NE(parsed->stageRing(), nullptr);
EXPECT_EQ(parsed->stageRing()->sizeBytes(), 32ull * 1024 * 1024);
ASSERT_NE(parsed->eventRing(), nullptr);
EXPECT_EQ(parsed->eventRing()->sizeBytes(), 256ull * 1024);
}
TEST(ProtocolSmokeTest, UnionTagsAreFrozenWireValues) {
// Appending to CtrlMsg is a compatible change; reordering it is not. If
// this test has to be edited, the schema change was a wire break.
EXPECT_EQ(static_cast<int>(CtrlMsg::NONE), 0);
EXPECT_EQ(static_cast<int>(CtrlMsg::Hello), 1);
EXPECT_EQ(static_cast<int>(CtrlMsg::Welcome), 2);
EXPECT_EQ(static_cast<int>(CtrlMsg::CapsSnapshot), 3);
EXPECT_EQ(static_cast<int>(CtrlMsg::SurfaceOp), 4);
EXPECT_EQ(static_cast<int>(CtrlMsg::SurfaceReply), 5);
EXPECT_EQ(static_cast<int>(CtrlMsg::ResyncRequest), 6);
EXPECT_EQ(static_cast<int>(CtrlMsg::ResyncDone), 7);
EXPECT_EQ(static_cast<int>(CtrlMsg::AuxRequest), 8);
EXPECT_EQ(static_cast<int>(CtrlMsg::Fatal), 9);
EXPECT_EQ(static_cast<int>(CtrlMsg::LogLine), 10);
EXPECT_EQ(static_cast<int>(SegmentKind::Cmd), 1);
EXPECT_EQ(static_cast<int>(SegmentKind::Adopt), 6);
EXPECT_EQ(static_cast<int>(LogLevel::Error), 3);
EXPECT_EQ(static_cast<int>(FatalCode::ProtocolCorruption), 1);
}
TEST(ProtocolSmokeTest, TruncatedMessageFailsVerificationInsteadOfReadingGarbage) {
std::vector<std::uint8_t> buffer = BuildHello();
ASSERT_GT(buffer.size(), 8u);
buffer.resize(buffer.size() / 2);
::flatbuffers::Verifier verifier(buffer.data(), buffer.size());
EXPECT_FALSE(VerifyCtrlEnvelopeBuffer(verifier));
}
TEST(ProtocolSmokeTest, TravelsAcrossTheTransportUnchanged) {
std::unique_ptr<Transport::InProcessTransport> client;
std::unique_ptr<Transport::InProcessTransport> server;
Transport::InProcessTransport::CreatePair(client, server);
const std::vector<std::uint8_t> sent = BuildHello();
ASSERT_EQ(client->SendFrame(MobileGLByteSpan{sent.data(), sent.size()}), MOBILEGL_OK);
const std::uint64_t pending = server->PeekFrameSize();
ASSERT_EQ(pending, sent.size());
std::vector<std::uint8_t> received(pending);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{received.data(), received.size()};
ASSERT_EQ(server->ReceiveFrame(span, &size, 1000), MOBILEGL_OK);
ASSERT_EQ(size, sent.size());
::flatbuffers::Verifier verifier(received.data(), received.size());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
const Hello* hello = GetCtrlEnvelope(received.data())->msg_as_Hello();
ASSERT_NE(hello, nullptr);
EXPECT_EQ(hello->pid(), 4242u);
}
-519
View File
@@ -1,519 +0,0 @@
// MobileGL - MobileGL/MG_Test/Wire/RingTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The SEG_CMD/SEG_STAGE SPSC ring: layout of the shared control page, cursor
// invariants, wrap-around, backpressure, the generation bump after a hard
// drain, and a real two-thread producer/consumer run.
#include <MG_Remote/Transport/Doorbell.h>
#include <MG_Remote/Transport/Ring.h>
#include <gtest/gtest.h>
#include <atomic>
#include <cstring>
#include <thread>
#include <vector>
using namespace MobileGL::MG_Remote::Transport;
namespace {
// A ring plus its control page, sized like a small SEG_CMD.
class RingFixture {
public:
explicit RingFixture(std::uint64_t capacity, RingCursorSet cursors = RingCursorSet::Cmd)
: m_bytes(static_cast<std::size_t>(capacity)), m_capacity(capacity) {
InitRingControl(m_control);
m_producer = RingProducer(&m_control, m_bytes.data(), capacity, cursors);
m_consumer = RingConsumer(&m_control, m_bytes.data(), capacity, cursors);
m_cursors = cursors;
}
RingControl& Control() { return m_control; }
RingProducer& Producer() { return m_producer; }
RingConsumer& Consumer() { return m_consumer; }
std::uint64_t Capacity() const { return m_capacity; }
bool Invariants() const { return RingCursorsValid(m_control, m_cursors, m_capacity); }
// Writes one record whose payload is `size` bytes of a recognisable
// pattern seeded by `seed`.
bool WriteRecord(std::uint16_t kind, std::uint64_t size, std::uint8_t seed) {
void* payload = m_producer.Reserve(kind, kRecNone, size);
if (payload == nullptr) {
return false;
}
auto* bytes = static_cast<std::uint8_t*>(payload);
for (std::uint64_t i = 0; i < size; ++i) {
bytes[i] = static_cast<std::uint8_t>(seed + i);
}
m_producer.Publish();
return true;
}
static bool CheckPattern(const RingRecordView& view, std::uint64_t size, std::uint8_t seed) {
const auto* bytes = static_cast<const std::uint8_t*>(view.payload);
for (std::uint64_t i = 0; i < size; ++i) {
if (bytes[i] != static_cast<std::uint8_t>(seed + i)) {
return false;
}
}
return true;
}
private:
alignas(4096) RingControl m_control{};
std::vector<std::uint8_t> m_bytes;
RingProducer m_producer;
RingConsumer m_consumer;
std::uint64_t m_capacity;
RingCursorSet m_cursors = RingCursorSet::Cmd;
};
} // namespace
TEST(RingTest, ControlPageLayoutIsTheSharedContract) {
// The page is mapped by two processes; its size and alignment are wire
// contract, not an implementation detail.
EXPECT_EQ(sizeof(RingControl), 4096u);
EXPECT_EQ(alignof(RingControl), 4096u);
EXPECT_EQ(sizeof(RingRecordHeader), 8u);
alignas(4096) RingControl control{};
InitRingControl(control);
// Zero is reserved for "uninitialized" on both generations.
EXPECT_EQ(control.serverEpoch.load(), 1u);
EXPECT_EQ(control.ringGeneration.load(), 1u);
EXPECT_EQ(control.cmdHead.load(), 0u);
EXPECT_EQ(control.stageHead.load(), 0u);
EXPECT_EQ(control.consumerParked.load(), 0u);
EXPECT_EQ(control.producerParked.load(), 0u);
EXPECT_EQ(control.eventRingFull.load(), 0u);
EXPECT_EQ(control.eventDropped.load(), 0u);
// Each contended group on its own cache line.
const auto offset = [&control](const void* member) {
return reinterpret_cast<const std::uint8_t*>(member) -
reinterpret_cast<const std::uint8_t*>(&control);
};
EXPECT_EQ(offset(&control.cmdHead) % 64, 0);
EXPECT_EQ(offset(&control.cmdAppliedTail) % 64, 0);
EXPECT_EQ(offset(&control.stageHead) % 64, 0);
EXPECT_EQ(offset(&control.stageAppliedTail) % 64, 0);
EXPECT_EQ(offset(&control.appliedSeq) % 64, 0);
EXPECT_EQ(offset(&control.serverEpoch) % 64, 0);
// cmdHead and cmdAppliedTail are written by different processes: they must
// not share a line.
EXPECT_NE(offset(&control.cmdHead) / 64, offset(&control.cmdAppliedTail) / 64);
}
TEST(RingTest, RejectsANonPowerOfTwoCapacity) {
alignas(4096) RingControl control{};
InitRingControl(control);
std::vector<std::uint8_t> bytes(1000);
RingProducer producer(&control, bytes.data(), 1000, RingCursorSet::Cmd);
EXPECT_FALSE(producer.Valid());
EXPECT_EQ(producer.Reserve(1, kRecNone, 8), nullptr);
}
TEST(RingTest, RejectsACapacityTheRecordHeaderCannotDescribe) {
alignas(4096) RingControl control{};
InitRingControl(control);
// 4 GiB is a legal power of two, but RingRecordHeader::size is 32 bits and
// both a record's size and a wrap filler's size are bounded only by the
// capacity: they would be truncated on the way in and then bounds-checked
// in their truncated form on the way out. Nothing is mapped here - the
// constructor rejects before it ever touches the base pointer.
std::uint8_t dummy = 0;
constexpr std::uint64_t kFourGiB = 4ull * 1024 * 1024 * 1024;
EXPECT_GT(kFourGiB, kMaxRingCapacity);
RingProducer producer(&control, &dummy, kFourGiB, RingCursorSet::Cmd);
EXPECT_FALSE(producer.Valid());
RingConsumer consumer(&control, &dummy, kFourGiB, RingCursorSet::Cmd);
EXPECT_FALSE(consumer.Valid());
// The largest ring the header CAN describe stays accepted.
RingProducer biggest(&control, &dummy, 1ull << 31, RingCursorSet::Cmd);
EXPECT_TRUE(biggest.Valid());
}
TEST(RingTest, RoundTripsRecordsInOrder) {
RingFixture ring(4096);
ASSERT_TRUE(ring.WriteRecord(1, 16, 0x10));
ASSERT_TRUE(ring.WriteRecord(2, 24, 0x20));
EXPECT_TRUE(ring.Invariants());
RingRecordView view{};
bool corrupt = false;
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt));
EXPECT_FALSE(corrupt);
EXPECT_EQ(view.kind, 1u);
EXPECT_EQ(view.payloadSize, 16u);
EXPECT_TRUE(RingFixture::CheckPattern(view, 16, 0x10));
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt));
EXPECT_EQ(view.kind, 2u);
EXPECT_EQ(view.payloadSize, 24u);
EXPECT_TRUE(RingFixture::CheckPattern(view, 24, 0x20));
EXPECT_FALSE(ring.Consumer().Pop(view, &corrupt));
ring.Consumer().PublishRetired();
EXPECT_TRUE(ring.Invariants());
EXPECT_EQ(ring.Control().cmdAppliedTail.load(), ring.Control().cmdHead.load());
EXPECT_EQ(ring.Control().cmdRetiredTail.load(), ring.Control().cmdHead.load());
}
TEST(RingTest, PayloadIsPaddedToTheRecordAlignment) {
RingFixture ring(4096);
ASSERT_TRUE(ring.WriteRecord(7, 3, 0x77));
RingRecordView view{};
ASSERT_TRUE(ring.Consumer().Pop(view));
// 8 (header) + 3 rounded up to 16 -> 8 bytes of payload space.
EXPECT_EQ(view.payloadSize, 8u);
EXPECT_TRUE(RingFixture::CheckPattern(view, 3, 0x77));
}
TEST(RingTest, WrapsWithoutSplittingARecord) {
// Small ring, records that do not divide it evenly, so the wrap boundary
// lands mid-record and the pad path is exercised many times.
RingFixture ring(256);
std::uint8_t seed = 0;
for (int i = 0; i < 200; ++i) {
const std::uint64_t size = 24 + (i % 5) * 8;
ASSERT_TRUE(ring.WriteRecord(static_cast<std::uint16_t>(1 + (i % 3)), size, seed))
<< "record " << i;
RingRecordView view{};
bool corrupt = false;
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt)) << "record " << i;
ASSERT_FALSE(corrupt);
EXPECT_EQ(view.kind, static_cast<std::uint16_t>(1 + (i % 3)));
// Contiguity: the payload never straddles the end of the mapping.
EXPECT_TRUE(RingFixture::CheckPattern(view, size, seed)) << "record " << i;
ring.Consumer().PublishRetired();
ASSERT_TRUE(ring.Invariants());
seed = static_cast<std::uint8_t>(seed + 13);
}
// Cursors are monotonic byte counts, so they are far past the capacity.
EXPECT_GT(ring.Control().cmdHead.load(), ring.Capacity());
}
TEST(RingTest, FullRingRefusesAndRecoversWhenTheConsumerRetires) {
RingFixture ring(256);
int written = 0;
while (ring.WriteRecord(1, 24, static_cast<std::uint8_t>(written))) {
++written;
ASSERT_LT(written, 100);
}
EXPECT_GT(written, 0);
// Backpressure, not corruption.
EXPECT_TRUE(ring.Invariants());
EXPECT_LT(ring.Producer().FreeBytes(), 32u);
RingRecordView view{};
ASSERT_TRUE(ring.Consumer().Pop(view));
// Applied alone does not free a slot that may still be borrowed by the GPU
// timeline: reclaim follows the retired cursor.
ring.Consumer().PublishApplied();
EXPECT_EQ(ring.Producer().FreeBytes(), 0u);
ring.Consumer().PublishRetired();
EXPECT_GT(ring.Producer().FreeBytes(), 0u);
EXPECT_TRUE(ring.WriteRecord(1, 24, 0xEE));
}
TEST(RingTest, RecordLargerThanTheRingIsRefused) {
RingFixture ring(256);
EXPECT_EQ(ring.Producer().Reserve(1, kRecNone, 4096), nullptr);
EXPECT_TRUE(ring.Invariants());
}
TEST(RingTest, RecordLargerThanHalfTheRingIsRefused) {
// 256-byte ring: the bound is 128 bytes of header + payload.
RingFixture ring(256);
EXPECT_EQ(ring.Producer().MaxRecordBytes(), 128u);
// 8 + 240 = 248: fits the whole ring, does not fit half of it.
EXPECT_EQ(ring.Producer().Reserve(1, kRecNone, 240), nullptr);
// 8 + 128 = 136: one step over the bound, refused the same way...
EXPECT_EQ(ring.Producer().Reserve(1, kRecNone, 128), nullptr);
// ...and 8 + 120 = 128, exactly the bound, is accepted.
EXPECT_NE(ring.Producer().Reserve(1, kRecNone, 120), nullptr);
EXPECT_TRUE(ring.Invariants());
}
// The scenario that motivated the bound, as the negative control. Whether a record
// can be placed must not depend on where the head happens to be. With "total <=
// capacity" as the only rule, a 248-byte record is accepted at head offset 0 of an
// empty 256-byte ring and refused forever at head offset 16 of the same empty
// ring - it would need a 240-byte wrap pad plus itself, 488 bytes - while
// FreeBytes() reports 256 the whole time, so a producer waiting for FreeBytes()
// >= 248 spins on nullptr with nothing logged. Both answers have to be the same
// refusal, and it has to be the loud one.
TEST(RingTest, RecordPlaceabilityDoesNotDependOnTheHeadOffset) {
RingFixture atOffsetZero(256);
void* atZero = atOffsetZero.Producer().Reserve(1, kRecNone, 240);
RingFixture atOffsetSixteen(256);
ASSERT_TRUE(atOffsetSixteen.WriteRecord(1, 8, 0x01)); // 8 + 8 = 16 bytes
RingRecordView view{};
ASSERT_TRUE(atOffsetSixteen.Consumer().Pop(view));
atOffsetSixteen.Consumer().PublishRetired();
ASSERT_EQ(atOffsetSixteen.Producer().LocalHead(), 16u);
ASSERT_EQ(atOffsetSixteen.Producer().FreeBytes(), 256u);
void* atSixteen = atOffsetSixteen.Producer().Reserve(1, kRecNone, 240);
EXPECT_EQ(atSixteen, nullptr);
EXPECT_EQ(atZero, nullptr)
<< "a 248-byte record was accepted at head offset 0 but is unplaceable at head offset 16 of "
"the same empty ring: the emitter cannot tell a refusal it must chunk from a full ring it "
"must wait on";
EXPECT_TRUE(atOffsetZero.Invariants());
EXPECT_TRUE(atOffsetSixteen.Invariants());
}
// The positive half of the same argument: a record of exactly half the capacity is
// placeable at EVERY head offset of an empty ring, because the wrap pad in front of
// it costs at most total-8 bytes. Walk the head to each 8-byte offset with bare
// header records and reserve the maximal record there.
TEST(RingTest, HalfCapacityRecordFitsAtEveryHeadOffset) {
RingFixture ring(256);
const std::uint64_t mask = ring.Capacity() - 1;
const std::uint64_t maximal = ring.Producer().MaxRecordBytes() - sizeof(RingRecordHeader); // 120
for (std::uint64_t target = 0; target < ring.Capacity(); target += 8) {
// A bare header never straddles the boundary, so no pad appears on the way.
while ((ring.Producer().LocalHead() & mask) != target) {
ASSERT_TRUE(ring.WriteRecord(1, 0, 0));
RingRecordView filler{};
ASSERT_TRUE(ring.Consumer().Pop(filler));
ring.Consumer().PublishRetired();
}
ASSERT_EQ(ring.Producer().FreeBytes(), ring.Capacity()) << "head offset " << target;
void* payload = ring.Producer().Reserve(2, kRecNone, maximal);
ASSERT_NE(payload, nullptr) << "head offset " << target;
ring.Producer().Publish();
RingRecordView view{};
bool corrupt = false;
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt)) << "head offset " << target;
ASSERT_FALSE(corrupt);
EXPECT_EQ(view.kind, 2u);
EXPECT_EQ(view.payloadSize, maximal);
ring.Consumer().PublishRetired();
ASSERT_TRUE(ring.Invariants()) << "head offset " << target;
}
}
TEST(RingTest, RejectsARingTooSmallForTheSmallestRecord) {
alignas(4096) RingControl control{};
InitRingControl(control);
std::uint8_t bytes[16] = {};
// One header's worth of ring can carry nothing once a record may be at most
// half the ring; two headers' worth carries a bare header.
RingProducer tooSmall(&control, bytes, sizeof(RingRecordHeader), RingCursorSet::Cmd);
EXPECT_FALSE(tooSmall.Valid());
RingProducer smallest(&control, bytes, kMinRingCapacity, RingCursorSet::Cmd);
ASSERT_TRUE(smallest.Valid());
EXPECT_EQ(smallest.MaxRecordBytes(), sizeof(RingRecordHeader));
EXPECT_NE(smallest.Reserve(1, kRecNone, 0), nullptr);
}
TEST(RingTest, HardDrainBumpsTheGenerationOnlyWhenQuiesced) {
RingFixture ring(256);
ASSERT_TRUE(ring.WriteRecord(1, 32, 0x01));
const std::uint32_t before = ring.Control().ringGeneration.load();
// Records still in flight: the drain is refused and nothing changes.
EXPECT_EQ(HardDrainRing(ring.Control(), RingCursorSet::Cmd), MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_EQ(ring.Control().ringGeneration.load(), before);
RingRecordView view{};
ASSERT_TRUE(ring.Consumer().Pop(view));
ring.Consumer().PublishRetired();
EXPECT_EQ(HardDrainRing(ring.Control(), RingCursorSet::Cmd), MOBILEGL_OK);
EXPECT_EQ(ring.Control().ringGeneration.load(), before + 1);
// Cursors stay monotonic across the drain - only the generation moves.
EXPECT_EQ(ring.Control().cmdHead.load(), ring.Control().cmdAppliedTail.load());
EXPECT_GT(ring.Control().cmdHead.load(), 0u);
}
TEST(RingTest, CorruptHeaderIsRefusedRatherThanDispatched) {
// SEG_CMD is written by the peer process, so a compile-time size assert on
// the record catalogue proves nothing about what is actually in the
// mapping. Hand-build a ring whose first header is impossible (a size that
// is not a multiple of 8) and check the consumer refuses it instead of
// dispatching into undefined behaviour.
alignas(4096) RingControl control{};
InitRingControl(control);
std::vector<std::uint8_t> bytes(256, 0);
RingRecordHeader bad{};
bad.kind = 5;
bad.flags = kRecNone;
bad.size = 13; // not 8-aligned
std::memcpy(bytes.data(), &bad, sizeof(bad));
control.cmdHead.store(64, std::memory_order_release);
RingConsumer consumer(&control, bytes.data(), bytes.size(), RingCursorSet::Cmd);
RingRecordView view{};
bool corrupt = false;
EXPECT_FALSE(consumer.Pop(view, &corrupt));
EXPECT_TRUE(corrupt);
// A record claiming more bytes than the producer has published is the same
// class of violation and is refused the same way.
bad.size = 128;
std::memcpy(bytes.data(), &bad, sizeof(bad));
RingConsumer second(&control, bytes.data(), bytes.size(), RingCursorSet::Cmd);
corrupt = false;
EXPECT_FALSE(second.Pop(view, &corrupt));
EXPECT_TRUE(corrupt);
}
TEST(RingTest, SpscProducerConsumerThreadsAgreeOnEveryRecord) {
constexpr int kRecords = 20000;
RingFixture ring(4096);
std::atomic<bool> failed{false};
std::atomic<int> consumed{0};
std::thread consumer([&] {
int next = 0;
while (next < kRecords) {
RingRecordView view{};
bool corrupt = false;
if (!ring.Consumer().Pop(view, &corrupt)) {
if (corrupt) {
failed.store(true);
return;
}
std::this_thread::yield();
continue;
}
const std::uint32_t expectedKind = static_cast<std::uint16_t>(1 + (next % 7));
if (view.kind != expectedKind || view.payloadSize < sizeof(std::uint32_t)) {
failed.store(true);
return;
}
std::uint32_t value = 0;
std::memcpy(&value, view.payload, sizeof(value));
if (value != static_cast<std::uint32_t>(next)) {
failed.store(true);
return;
}
++next;
consumed.store(next, std::memory_order_relaxed);
// Retire as we go; a consumer that never retires would deadlock the
// producer, which is exactly the contract being pinned.
ring.Consumer().PublishRetired();
}
});
for (int i = 0; i < kRecords; ++i) {
const std::uint64_t payloadSize = sizeof(std::uint32_t) + (i % 4) * 8;
void* payload = nullptr;
while ((payload = ring.Producer().Reserve(static_cast<std::uint16_t>(1 + (i % 7)),
kRecNone, payloadSize)) == nullptr) {
if (failed.load()) {
break;
}
std::this_thread::yield();
}
if (payload == nullptr) {
break;
}
const std::uint32_t value = static_cast<std::uint32_t>(i);
std::memcpy(payload, &value, sizeof(value));
ring.Producer().Publish();
}
consumer.join();
EXPECT_FALSE(failed.load());
EXPECT_EQ(consumed.load(), kRecords);
EXPECT_TRUE(ring.Invariants());
EXPECT_EQ(ring.Control().cmdRetiredTail.load(), ring.Control().cmdHead.load());
}
// The publish/park protocol end to end, in both directions: publish the
// watermark, THEN NotifyIfParked; park with Doorbell::Wait. A lost wakeup on
// either side shows up as a Wait that times out with work available rather
// than as a hang, so the failure is a red test and not a stuck CI job.
//
// This cannot prove the seq_cst fence pairing (no test can - x86 needs the
// store buffer to hold the release store across the flag read, and it usually
// does not), but it does exercise the exact call order the fences assume, so a
// future edit that rings the bell BEFORE publishing has somewhere to fail.
TEST(RingTest, DoorbellHandoffWakesBothSidesOnEveryPublish) {
// 4 byte payloads: every record is exactly 16 bytes and 4096 is a multiple
// of that, so no wrap filler ever appears and "head != tail" is exactly
// "a record is waiting".
RingFixture ring(4096);
CondVarDoorbell consumerBell;
CondVarDoorbell producerBell;
std::atomic<bool> ok{true};
constexpr int kRecords = 2000;
constexpr std::uint64_t kRecordBytes = 16;
std::thread consumerThread([&] {
int seen = 0;
while (seen < kRecords) {
const bool woke = consumerBell.Wait(
ring.Control().consumerParked,
[&] {
return ring.Control().cmdHead.load(std::memory_order_acquire) !=
ring.Consumer().LocalTail();
},
kDefaultSpinUs, 5000);
if (!woke) {
ok.store(false); // a wakeup was lost, or the producer stalled
return;
}
RingRecordView view{};
bool corrupt = false;
while (ring.Consumer().Pop(view, &corrupt)) {
std::uint32_t value = 0;
std::memcpy(&value, view.payload, sizeof(value));
if (value != static_cast<std::uint32_t>(seen)) {
ok.store(false);
return;
}
++seen;
}
if (corrupt) {
ok.store(false);
return;
}
ring.Consumer().PublishRetired();
NotifyIfParked(producerBell, ring.Control().producerParked);
}
});
for (int i = 0; i < kRecords && ok.load(); ++i) {
void* payload = nullptr;
while ((payload = ring.Producer().Reserve(1, kRecNone, sizeof(std::uint32_t))) == nullptr) {
if (!ok.load()) {
break;
}
if (!producerBell.Wait(
ring.Control().producerParked,
[&] { return ring.Producer().FreeBytes() >= kRecordBytes; }, kDefaultSpinUs,
5000)) {
ok.store(false);
break;
}
}
if (payload == nullptr) {
break;
}
const std::uint32_t value = static_cast<std::uint32_t>(i);
std::memcpy(payload, &value, sizeof(value));
// Publish first, ring second. The other order reopens the lost-wakeup
// window no matter how strong the flag's memory order is.
ring.Producer().Publish();
NotifyIfParked(consumerBell, ring.Control().consumerParked);
}
consumerThread.join();
EXPECT_TRUE(ok.load());
EXPECT_TRUE(ring.Invariants());
}
@@ -1145,8 +1145,6 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxFragmentShaderStorageBlocks = 4;
GLint maxComputeUniformBlocks = 12;
GLint maxComputeWorkGroupInvocations = 128;
GLint maxComputeWorkGroupCount[3] = {65535, 65535, 65535};
GLint maxComputeWorkGroupSize[3] = {1024, 1024, 64};
GLint maxShaderStorageBufferBindings = 8;
GLint maxTextureBufferSize = 65536;
GLint maxUniformBufferBindings = 24;
@@ -1278,17 +1276,6 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, &maxCombinedShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &maxComputeUniformBlocks);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &maxComputeWorkGroupInvocations);
// The per-axis pair beside it, through the indexed query. ES 3.1 core like the
// invocations limit, so it sits inside the same bracketed run: a 3.0 context rejects
// it, the drain below swallows the error and the locals keep the GL 4.3 minimums.
// These are the six backend-owned indexed answers that cross the MGPipe boundary in
// MGPCaps (DynamicBackendParameters::MaxComputeWorkGroupCount/Size).
if (glesFuncs.glGetIntegeri_v) {
for (GLuint axis = 0; axis < 3; ++axis) {
glesFuncs.glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, axis, &maxComputeWorkGroupCount[axis]);
glesFuncs.glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &maxComputeWorkGroupSize[axis]);
}
}
glesFuncs.glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorageBufferBindings);
// GL_MAX_TEXTURE_BUFFER_SIZE is deliberately NOT batched here: like
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT below, the pname only exists once buffer textures do,
@@ -1597,10 +1584,6 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
for (SizeT axis = 0; axis < 3; ++axis) {
caps.MaxComputeWorkGroupCount[axis] = maxComputeWorkGroupCount[axis];
caps.MaxComputeWorkGroupSize[axis] = maxComputeWorkGroupSize[axis];
}
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
caps.MaxTextureBufferSize = maxTextureBufferSize;
// Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves
@@ -1698,10 +1681,6 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_COUNT: %d %d %d", caps.MaxComputeWorkGroupCount[0],
caps.MaxComputeWorkGroupCount[1], caps.MaxComputeWorkGroupCount[2]);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_SIZE: %d %d %d", caps.MaxComputeWorkGroupSize[0],
caps.MaxComputeWorkGroupSize[1], caps.MaxComputeWorkGroupSize[2]);
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
// Three distinct states, and the suffix must not conflate them: a driver answer, a floor
// kept because there are no buffer textures to ask about, and a floor kept because the
@@ -1309,11 +1309,6 @@ namespace MobileGL {
Int MaxFragmentShaderStorageBlocks = 4;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE per axis, as the driver answers
// glGetIntegeri_v. Raw: the frontend floors them at the GL minimums itself. The
// initialisers are those minimums, for a context that rejects the query.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
@@ -196,10 +196,6 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
caps.MaxComputeUniformBlocks = SaturateToInt(p.limits.maxPerStageDescriptorUniformBuffers);
caps.MaxComputeWorkGroupInvocations = SaturateToInt(p.limits.maxComputeWorkGroupInvocations);
for (SizeT axis = 0; axis < 3; ++axis) {
caps.MaxComputeWorkGroupCount[axis] = SaturateToInt(p.limits.maxComputeWorkGroupCount[axis]);
caps.MaxComputeWorkGroupSize[axis] = SaturateToInt(p.limits.maxComputeWorkGroupSize[axis]);
}
caps.MaxShaderStorageBufferBindings = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
caps.MaxTextureBufferSize = SaturateToInt(p.limits.maxTexelBufferElements);
caps.TextureBufferOffsetAlignment =
@@ -337,10 +333,6 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
caps.MaxComputeUniformBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorUniformBuffers);
caps.MaxComputeWorkGroupInvocations = SaturateToInt(properties.limits.maxComputeWorkGroupInvocations);
for (SizeT axis = 0; axis < 3; ++axis) {
caps.MaxComputeWorkGroupCount[axis] = SaturateToInt(properties.limits.maxComputeWorkGroupCount[axis]);
caps.MaxComputeWorkGroupSize[axis] = SaturateToInt(properties.limits.maxComputeWorkGroupSize[axis]);
}
caps.MaxShaderStorageBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
caps.MaxTextureBufferSize = SaturateToInt(properties.limits.maxTexelBufferElements);
caps.TextureBufferOffsetAlignment =
@@ -56,11 +56,6 @@ namespace MobileGL {
Int MaxCombinedShaderStorageBlocks = 32;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount / maxComputeWorkGroupSize per
// axis, saturated to Int like every other limit here. Raw: the frontend floors them
// at the GL minimums itself.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
-488
View File
@@ -1,488 +0,0 @@
// MobileGL - MobileGL/MG_Util/Metrics/PipeStats.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "PipeStats.h"
#include <Config.h>
#include <fstream>
// ---------------------------------------------------------------------------------------
// SITE INVENTORY - what these counters DO and DO NOT cover.
//
// This list is the contract. A byte class that reads 0 while a real copy runs uncounted is
// worse than a missing counter, because the zero is then read as an answer, so every path
// that moves bytes and is NOT wired is named here by file and function.
//
// Byte classes
// stage-buffer ESPRYT (DirectGLES Managers.cpp): RespecifyStorageNow's
// glBufferData, FlushPendingRangesNow's three shapes (map-write,
// glBufferSubData, upload-ring stage), and the pool-recycle reseed
// in SyncBufferObject.
// MAGMA (DirectVulkan VkBufferManager.cpp): every host->device copy
// of a buffer object's contents - SwapStorageAndUploadAll, the
// StagedRangeCopy staging fill, the in-place uploads in OnRespecify /
// OnSubData / OnFlushMappedRange, the AcquirePersistentMap seed, the
// AcquireResidentSlice initial upload and the AcquireStreamedSlice
// arena fill.
// NOT covered: bytes an app writes THROUGH a persistent map. Those
// never pass through either backend (D4/D-B4) - see
// persistent-map-push.
// stage-texture ESPRYT (Managers.cpp texture upload): the bytes of whichever of
// the three upload shapes ran (rect list / union box / whole level).
// MAGMA (VkTextureManager.cpp): the packed staging slice of an
// upload batch item set.
// NOT covered: Espryt's compressed-texture path, and both backends'
// readback (device->host) paths, which are a different direction and
// want their own class when the reverse channel of section 7 exists.
// stage-ubo-global ESPRYT (DirectGLES.cpp): the default-uniform-block image, both the
// UBO-ring memcpy and the glBufferSubData fallback.
// MAGMA (UniformManager::ResolveDynamicUboDescriptor): the same
// image, counted after the per-frame slice memo, so a frame that
// re-uses the slice correctly contributes nothing.
// stage-ubo-named DirectVulkan UniformManager::ResolveUniformBufferPayload - the
// bytes Magma repacks into its own UBO ring, counted AFTER the
// zero-copy direct-bind decision (a direct bind repacks nothing).
// Espryt contributes nothing by construction (D-B8).
// stage-vertex-client ESPRYT: BackendVertexArrayObject::SyncClientSideAttributesFor-
// DrawArrays (both the Float64-narrowing and the verbatim shapes)
// and the VBO-backed Float64->Float32 narrowing scratch upload.
// MAGMA: VkBufferManager::UploadTransient(BufferKind::Vertex), which
// is the single chokepoint for the converted-vertex-stream and
// client-array staging.
// stage-index-client ESPRYT: the primitive-restart substitution buffer, and MultiDraw's
// rewritten (rebased) index stream.
// MAGMA: VkBufferManager::UploadTransient(BufferKind::Index).
// stage-indirect-cmd ESPRYT MultiDraw.cpp: the DrawElementsIndirectCommand array staged
// for the indirect tiers, and the compute tier's per-draw info
// array. Kept out of stage-index-client because these are draw
// PARAMETERS - the population that becomes MGPipe command-record
// payload, not resource bytes.
// NOT covered: Magma builds no such array (it issues one vkCmdDraw*
// per sub-draw), so this class is Espryt-only by construction.
// persistent-map-push Not wired in P0: today a persistent map is a permanent address
// space donation (D4/D-B4) that survives the whole monolith track,
// so there is no push to count until the IPC track breaks it.
// residual-value-block Placeholder, always 0 until P2 (plan section 6.3).
//
// Call classes
// draws DirectGLES PrepareForDraw and DirectVulkan SetupDraw's entry. A
// dispatch is not a draw and is not counted.
// accessor-calls STATIC TALLIES at the instrumented entry points, NOT a wrapper
// around all 293 pGLContext-> sites. Each instrumented function adds
// the number of GLContext accessor calls that its OWN body executed
// on the path taken, and each tally sits AFTER the last early return
// that would skip those reads. Covered: PrepareForDraw's own reads,
// SyncRenderState, CaptureDrawTextureSyncKeys/CurrentUnitBindings-
// Epoch, SyncNeccessaryTextures' walk, TrySetupDrawFastPath,
// GetOrCreatePipeline and ApplyDynamicDrawStateTail. NOT covered:
// the reads inside the callees those functions invoke (buffer/VAO/
// FBO/program sync, the pipeline payload builder's ~40 reads on a
// memo miss), and every non-draw entry point. The number is
// therefore a LOWER BOUND on the per-draw accessor count, and it is
// the bound over exactly the six gates section 2.3.1 tabulates.
// texture-* Per (target, level) emission, both backends.
//
// Gates: the six of section 2.3.1, each counted exactly once per probe.
//
// READING acc/draw. The accessor tally covers the instrumented functions wherever they
// run, and three of them (SyncRenderState, the texture-key capture, SyncNeccessaryTextures)
// are also reached from NON-draw call sites - Clear, readbacks, the DSA by-name entry
// points - which the `draws` counter deliberately does not count. So acc/draw is the
// per-draw steady-state number section 2.3.1 asks for only in a DRAW-DOMINATED window; in a
// window dominated by clears and readbacks it is inflated by exactly those non-draw
// probes, and the gate hit/miss pairs are the honest reading there.
// ---------------------------------------------------------------------------------------
namespace MobileGL::MG_Util::PipeStats {
Bool g_pipeStatsEnabled = false;
namespace {
constexpr Uint32 kByteClassCount = static_cast<Uint32>(ByteClass::Count);
constexpr Uint32 kCallClassCount = static_cast<Uint32>(CallClass::Count);
constexpr Uint32 kGateCount = static_cast<Uint32>(Gate::Count);
using Counter = std::atomic<Uint64>;
Counter g_frameBytes[kByteClassCount];
Counter g_totalBytes[kByteClassCount];
Counter g_frameCalls[kCallClassCount];
Counter g_totalCalls[kCallClassCount];
Counter g_frameGateHit[kGateCount];
Counter g_totalGateHit[kGateCount];
Counter g_frameGateMiss[kGateCount];
Counter g_totalGateMiss[kGateCount];
Counter g_totalPayloadBuckets[kPayloadHistogramBuckets];
Counter g_frameCount{0};
// Window bases: the run totals as of the previous summary line. Only ever touched
// from OnPresent()/Shutdown() (the present thread), so plain integers.
Uint64 g_windowBaseBytes[kByteClassCount] = {};
Uint64 g_windowBaseCalls[kCallClassCount] = {};
Uint64 g_windowBaseGateHit[kGateCount] = {};
Uint64 g_windowBaseGateMiss[kGateCount] = {};
Uint64 g_windowBaseFrames = 0;
Bool g_shutdownDone = false;
// Frames per summary line, latched by Init() from MOBILEGL_PIPE_STATS_PERIOD.
Uint64 g_summaryPeriod = kDefaultSummaryFramePeriod;
inline void Bump(Counter& counter, Uint64 amount) {
counter.fetch_add(amount, std::memory_order_relaxed);
}
inline Uint64 Read(const Counter& counter) { return counter.load(std::memory_order_relaxed); }
// Bucket 0 is "0 bytes", bucket n>0 holds [2^(n-1), 2^n). Saturates at the last
// bucket so a pathological record cannot index out of the array.
Uint32 PayloadBucketOf(Uint64 bytes) {
if (bytes == 0) {
return 0;
}
Uint32 bucket = 1;
while (bucket + 1 < kPayloadHistogramBuckets && bytes >= (Uint64{1} << bucket)) {
++bucket;
}
return bucket;
}
// Two decimals without <iomanip>. Every per-frame and per-draw field in the summary
// goes through this: the numbers are small (a per-draw accessor count in the 10-25
// band, a per-frame byte count that sizes SEG_STAGE), so truncating integer division
// loses up to a whole unit on exactly the figures the package exists to produce.
// A zero denominator is "n/a" rather than a division by a faked 1.
String FormatFixed2(Uint64 numerator, Uint64 denominator) {
if (denominator == 0) {
return "n/a";
}
const Uint64 hundredths = (numerator * 100 + denominator / 2) / denominator;
return std::to_string(hundredths / 100) + "." + (hundredths % 100 < 10 ? "0" : "") +
std::to_string(hundredths % 100);
}
const char* const kByteClassNames[kByteClassCount] = {
"stage-buffer", "stage-texture", "stage-ubo-global",
"stage-ubo-named", "stage-vertex-client", "stage-index-client",
"stage-indirect-cmd", "persistent-map-push", "residual-value-block",
};
const char* const kCallClassNames[kCallClassCount] = {
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
"tex-upload-jobs",
};
const char* const kGateNames[kGateCount] = {
"espryt-render-state", "espryt-texture-sync-list", "espryt-unit-bindings-epoch",
"magma-draw-fastpath", "magma-pipeline-memo", "magma-dynamic-tail",
};
// Tracy needs a stable string literal per series, and a gate is TWO series: plotting
// only the misses (which is what the first cut did) hides the denominator, and a
// gate's whole point is the ratio.
const char* const kGateHitPlotNames[kGateCount] = {
"espryt-render-state-hit", "espryt-texture-sync-list-hit", "espryt-unit-bindings-epoch-hit",
"magma-draw-fastpath-hit", "magma-pipeline-memo-hit", "magma-dynamic-tail-hit",
};
const char* const kGateMissPlotNames[kGateCount] = {
"espryt-render-state-miss", "espryt-texture-sync-list-miss", "espryt-unit-bindings-epoch-miss",
"magma-draw-fastpath-miss", "magma-pipeline-memo-miss", "magma-dynamic-tail-miss",
};
// Short forms, so the per-120-frame line stays one terminal line wide.
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon", "vtxc",
"idxc", "icmd", "pmap", "resid"};
const char* const kGateShort[kGateCount] = {"ers", "etl", "eub", "mfp", "mpm", "mdt"};
void ResetCounters() {
for (Uint32 i = 0; i < kByteClassCount; ++i) {
g_frameBytes[i].store(0, std::memory_order_relaxed);
g_totalBytes[i].store(0, std::memory_order_relaxed);
g_windowBaseBytes[i] = 0;
}
for (Uint32 i = 0; i < kCallClassCount; ++i) {
g_frameCalls[i].store(0, std::memory_order_relaxed);
g_totalCalls[i].store(0, std::memory_order_relaxed);
g_windowBaseCalls[i] = 0;
}
for (Uint32 i = 0; i < kGateCount; ++i) {
g_frameGateHit[i].store(0, std::memory_order_relaxed);
g_totalGateHit[i].store(0, std::memory_order_relaxed);
g_frameGateMiss[i].store(0, std::memory_order_relaxed);
g_totalGateMiss[i].store(0, std::memory_order_relaxed);
g_windowBaseGateHit[i] = 0;
g_windowBaseGateMiss[i] = 0;
}
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
g_totalPayloadBuckets[i].store(0, std::memory_order_relaxed);
}
g_frameCount.store(0, std::memory_order_relaxed);
g_windowBaseFrames = 0;
}
void EmitSummaryLine() {
const String line = FormatWindowLine();
// MGLOG_I on purpose, against the project's usual "MGLOG_D for anything
// non-critical" rule: the line has to survive an INFO build (that is the only
// build a device ever runs), it is emitted at most once per 120 frames, and it
// exists at all only when the operator set MOBILEGL_PIPE_STATS=1. It is an
// opt-in measurement channel, not per-frame noise.
MGLOG_I("%s", line.c_str());
AdvanceSummaryWindow();
}
void WriteJsonDump() {
const String& path = MG_Config::Features.PipeStatsFile;
if (path.empty()) {
return;
}
std::ofstream out(path, std::ios::out | std::ios::trunc);
if (!out) {
MGLOG_W("PipeStats: could not open MOBILEGL_PIPE_STATS_FILE='%s' for writing", path.c_str());
return;
}
out << FormatJson();
out.flush();
if (!out) {
MGLOG_W("PipeStats: failed writing MOBILEGL_PIPE_STATS_FILE='%s'", path.c_str());
return;
}
MGLOG_I("MGPipe stats: wrote JSON dump to %s", path.c_str());
}
} // namespace
void Init() {
ResetCounters();
g_shutdownDone = false;
g_pipeStatsEnabled = MG_Config::Features.PipeStats;
g_summaryPeriod = MG_Config::Features.PipeStatsPeriod == 0
? kDefaultSummaryFramePeriod
: static_cast<Uint64>(MG_Config::Features.PipeStatsPeriod);
if (g_pipeStatsEnabled) {
MGLOG_I("MGPipe stats: counters ON (MOBILEGL_PIPE_STATS), summary every %llu frames%s%s",
static_cast<unsigned long long>(g_summaryPeriod),
MG_Config::Features.PipeStatsFile.empty() ? "" : ", JSON dump to ",
MG_Config::Features.PipeStatsFile.c_str());
}
}
Uint64 SummaryFramePeriod() { return g_summaryPeriod; }
void Shutdown() {
if (!g_pipeStatsEnabled || g_shutdownDone) {
return;
}
g_shutdownDone = true;
EmitSummaryLine();
WriteJsonDump();
}
void AddBytes(ByteClass byteClass, Uint64 bytes) {
const Uint32 index = static_cast<Uint32>(byteClass);
Bump(g_frameBytes[index], bytes);
Bump(g_totalBytes[index], bytes);
}
void AddCalls(CallClass callClass, Uint64 count) {
const Uint32 index = static_cast<Uint32>(callClass);
Bump(g_frameCalls[index], count);
Bump(g_totalCalls[index], count);
}
void CountGate(Gate gate, Bool hit) {
const Uint32 index = static_cast<Uint32>(gate);
if (hit) {
Bump(g_frameGateHit[index], 1);
Bump(g_totalGateHit[index], 1);
} else {
Bump(g_frameGateMiss[index], 1);
Bump(g_totalGateMiss[index], 1);
}
}
void RecordDrawPayloadBytes(Uint64 bytes) { Bump(g_totalPayloadBuckets[PayloadBucketOf(bytes)], 1); }
void OnPresent() {
// Every frame accumulator is EXCHANGED for zero, and the exchanged value is what gets
// plotted. A read followed by a store(0) would lose any Bump that lands in between -
// buffer and texture staging reach these counters from more than one thread - from
// the plot AND from every frame; an exchange hands every add to exactly one frame.
// Without Tracy the value is taken and dropped: the clear is still the point.
//
// One plot per counter, the frame's value. Tracy keeps the series by name, and the
// names are the static literals above, which is what TracyPlot requires. A gate is
// two series - hits and misses - because the ratio is the deliverable and a miss
// count alone cannot be read.
//
// The payload histogram is deliberately NOT plotted: it is a run-total distribution
// over draws (section 4.5.7), not a per-frame scalar, and Tracy has no histogram
// series. It reaches the operator through the JSON dump.
const auto take = [](Counter& counter) { return counter.exchange(0, std::memory_order_relaxed); };
for (Uint32 i = 0; i < kByteClassCount; ++i) {
const Uint64 value = take(g_frameBytes[i]);
(void)value;
#ifdef TRACY_ENABLE
TracyPlot(kByteClassNames[i], static_cast<Int64>(value));
#endif
}
for (Uint32 i = 0; i < kCallClassCount; ++i) {
const Uint64 value = take(g_frameCalls[i]);
(void)value;
#ifdef TRACY_ENABLE
TracyPlot(kCallClassNames[i], static_cast<Int64>(value));
#endif
}
for (Uint32 i = 0; i < kGateCount; ++i) {
const Uint64 hits = take(g_frameGateHit[i]);
const Uint64 misses = take(g_frameGateMiss[i]);
(void)hits;
(void)misses;
#ifdef TRACY_ENABLE
TracyPlot(kGateHitPlotNames[i], static_cast<Int64>(hits));
TracyPlot(kGateMissPlotNames[i], static_cast<Int64>(misses));
#endif
}
const Uint64 frames = g_frameCount.fetch_add(1, std::memory_order_relaxed) + 1;
if (frames % g_summaryPeriod == 0) {
EmitSummaryLine();
}
}
Uint64 FrameBytes(ByteClass byteClass) { return Read(g_frameBytes[static_cast<Uint32>(byteClass)]); }
Uint64 TotalBytes(ByteClass byteClass) { return Read(g_totalBytes[static_cast<Uint32>(byteClass)]); }
Uint64 FrameCalls(CallClass callClass) { return Read(g_frameCalls[static_cast<Uint32>(callClass)]); }
Uint64 TotalCalls(CallClass callClass) { return Read(g_totalCalls[static_cast<Uint32>(callClass)]); }
Uint64 TotalGateHits(Gate gate) { return Read(g_totalGateHit[static_cast<Uint32>(gate)]); }
Uint64 TotalGateMisses(Gate gate) { return Read(g_totalGateMiss[static_cast<Uint32>(gate)]); }
Uint64 TotalPayloadBucket(Uint32 bucket) {
return bucket < kPayloadHistogramBuckets ? Read(g_totalPayloadBuckets[bucket]) : 0;
}
Uint64 FrameCount() { return Read(g_frameCount); }
const char* NameOf(ByteClass byteClass) { return kByteClassNames[static_cast<Uint32>(byteClass)]; }
const char* NameOf(CallClass callClass) { return kCallClassNames[static_cast<Uint32>(callClass)]; }
const char* NameOf(Gate gate) { return kGateNames[static_cast<Uint32>(gate)]; }
String FormatWindowLine() {
// Window values: everything since the previous summary. A run total over a workload
// whose shape changes (load, then steady state) hides exactly the number P2 wants.
const Uint64 frames = Read(g_frameCount);
const Uint64 windowFrames = frames - g_windowBaseFrames;
// A window with no Present in it (teardown before the first frame, or a slice whose
// whole workload runs off-screen) has NO per-frame reading. Printing the window
// totals under a "/f" label there is how a 47x overstatement of the SEG_STAGE sizing
// input got printed as a per-frame figure; the label changes instead.
const Bool perFrame = windowFrames != 0;
Uint64 bytes[kByteClassCount];
for (Uint32 i = 0; i < kByteClassCount; ++i) {
bytes[i] = Read(g_totalBytes[i]) - g_windowBaseBytes[i];
}
Uint64 calls[kCallClassCount];
for (Uint32 i = 0; i < kCallClassCount; ++i) {
calls[i] = Read(g_totalCalls[i]) - g_windowBaseCalls[i];
}
Uint64 gateHit[kGateCount];
Uint64 gateMiss[kGateCount];
for (Uint32 i = 0; i < kGateCount; ++i) {
gateHit[i] = Read(g_totalGateHit[i]) - g_windowBaseGateHit[i];
gateMiss[i] = Read(g_totalGateMiss[i]) - g_windowBaseGateMiss[i];
}
const Uint64 draws = calls[static_cast<Uint32>(CallClass::Draws)];
const Uint64 accessorCalls = calls[static_cast<Uint32>(CallClass::AccessorCalls)];
String line = "MGPipe stats:";
line += " frames=" + std::to_string(frames);
line += " window=" + std::to_string(windowFrames);
line += " draws=" + std::to_string(draws);
line += " draws/f=" + FormatFixed2(draws, windowFrames);
line += " acc=" + std::to_string(accessorCalls);
// Same rule as the per-frame fields: a window with no draw in it has no per-draw
// number, and "0.00" next to a non-zero acc= is the same lie in a smaller font.
line += " acc/draw=" + FormatFixed2(accessorCalls, draws);
// "bytes/f[...]" only when there IS a frame to divide by; otherwise the bracket is
// labelled "bytes[...]" and carries the window totals verbatim.
line += perFrame ? " bytes/f[" : " bytes[";
for (Uint32 i = 0; i < kByteClassCount; ++i) {
if (i != 0) {
line += " ";
}
line += kByteClassShort[i];
line += "=";
line += perFrame ? FormatFixed2(bytes[i], windowFrames) : std::to_string(bytes[i]);
}
line += "] tex[emit=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadEmissions)]);
line += " box=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadBoxEmissions)]);
line += " rect=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadRectEmissions)]);
line += " jobs=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadJobs)]);
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
if (i != 0) {
line += " ";
}
line += kGateShort[i];
line += "=";
line += std::to_string(gateHit[i]);
line += "/";
line += std::to_string(gateMiss[i]);
}
line += "]";
return line;
}
void AdvanceSummaryWindow() {
for (Uint32 i = 0; i < kByteClassCount; ++i) {
g_windowBaseBytes[i] = Read(g_totalBytes[i]);
}
for (Uint32 i = 0; i < kCallClassCount; ++i) {
g_windowBaseCalls[i] = Read(g_totalCalls[i]);
}
for (Uint32 i = 0; i < kGateCount; ++i) {
g_windowBaseGateHit[i] = Read(g_totalGateHit[i]);
g_windowBaseGateMiss[i] = Read(g_totalGateMiss[i]);
}
g_windowBaseFrames = Read(g_frameCount);
}
String FormatJson() {
String json = "{\n";
json += " \"frames\": " + std::to_string(Read(g_frameCount)) + ",\n";
json += " \"bytes\": {\n";
for (Uint32 i = 0; i < kByteClassCount; ++i) {
json += " \"";
json += kByteClassNames[i];
json += "\": " + std::to_string(Read(g_totalBytes[i]));
json += (i + 1 == kByteClassCount) ? "\n" : ",\n";
}
json += " },\n \"calls\": {\n";
for (Uint32 i = 0; i < kCallClassCount; ++i) {
json += " \"";
json += kCallClassNames[i];
json += "\": " + std::to_string(Read(g_totalCalls[i]));
json += (i + 1 == kCallClassCount) ? "\n" : ",\n";
}
json += " },\n \"gates\": {\n";
for (Uint32 i = 0; i < kGateCount; ++i) {
json += " \"";
json += kGateNames[i];
json += "\": {\"hit\": " + std::to_string(Read(g_totalGateHit[i])) +
", \"miss\": " + std::to_string(Read(g_totalGateMiss[i])) + "}";
json += (i + 1 == kGateCount) ? "\n" : ",\n";
}
json += " },\n \"cmd-bytes-per-draw-histogram\": [";
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
if (i != 0) {
json += ", ";
}
json += std::to_string(Read(g_totalPayloadBuckets[i]));
}
json += "]\n}\n";
return json;
}
void SetEnabledForTesting(Bool enabled) { g_pipeStatsEnabled = enabled; }
void ResetForTesting() { ResetCounters(); }
} // namespace MobileGL::MG_Util::PipeStats
-192
View File
@@ -1,192 +0,0 @@
// MobileGL - MobileGL/MG_Util/Metrics/PipeStats.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
// MGPipe boundary counters (plan B section 11 "P0 - hygiene, measurement, gates and
// skeleton", and the corollary in section 2.3.1).
//
// WHAT THIS IS FOR. The disaggregation plan has to size two things it cannot size by
// reading the tree: how many BYTES cross the frontend/backend boundary per frame (that
// sizes SEG_STAGE and the command segment), and how many accessor CALLS and memo-gate
// probes the backends actually execute per draw (that decides whether pushing state is
// cheaper than pulling it at all). Section 2.3.1 makes the second one the load-bearing
// number: the static call-site counts everyone quoted - Espryt 124 / Magma 169 - are NOT
// the dynamic per-draw cost, because every one of those paths is memo-gated, and the real
// steady state is believed to be 10-25 accessor calls per backend per draw. Without a
// dynamic counter the P2 verdict stays a guess.
//
// COST WHEN OFF. g_pipeStatsEnabled is a plain global Bool latched once at Init() from
// MG_Config::Features.PipeStats (MOBILEGL_PIPE_STATS). Every counting site in the two
// backends is written as
//
// if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::Add...(...);
//
// so with the feature off a site costs one load of a hot global plus one never-taken,
// perfectly-predicted branch, and none of the counter state is touched. The counters
// themselves are relaxed atomics rather than plain integers because texture and buffer
// staging can be reached from more than one thread; relaxed adds cost nothing extra on the
// off path, which never reaches them. The off-path cost is not a guess: see the paired
// A/B in the branch's evidence.
//
// WHAT IS COUNTED AND WHAT IS NOT: see the site inventory in PipeStats.cpp. That inventory
// is the contract - it names every path that is NOT wired, because a byte class that reads
// zero while a real copy runs uncounted is worse than a missing counter.
namespace MobileGL::MG_Util::PipeStats {
// Byte classes. Every one of these names a population of bytes that would have to be
// MOVED across the boundary once the backend no longer shares an address space with
// the frontend, which is why they are grouped this way rather than by call site.
enum class ByteClass : Uint32 {
// Buffer object contents flushed to the driver: glBufferData / glBufferSubData /
// map-write ranges / the persistent upload ring (Espryt), and every host->device
// copy of a buffer object's contents (Magma).
StageBuffer = 0,
// Texel bytes handed to glTexSubImage & friends / packed into the Vulkan upload
// staging slice, whichever upload shape was chosen.
StageTexture,
// The default-uniform-block ("global UBO") image, uploaded at most once per program
// per frame.
StageUboGlobal,
// Named uniform-block bytes that a backend has to repack itself, i.e. Magma's UBO
// ring. Espryt binds the frontend buffer straight to the driver and contributes
// nothing here - which is exactly the asymmetry D-B8 is about.
StageUboNamed,
// Client-memory vertex arrays uploaded into a scratch VBO / transient arena slice on
// the draw path.
StageVertexClient,
// Client-memory / rewritten index data staged on the draw path.
StageIndexClient,
// Draw-parameter bytes a backend synthesises and stages for the draw itself: the
// indirect-command array and the compute path's per-draw info array. These are the
// bytes that become MGPipe command-record payload once the boundary is explicit,
// which is why they are not folded into the index class.
StageIndirectCmd,
// Bytes pushed because a persistently mapped range was published to the backend.
PersistentMapPush,
// PLACEHOLDER (plan section 6.3): the residual value block does not exist yet. The
// class is minted now so the counter names never churn; it stays at 0 until P2.
ResidualValueBlock,
Count
};
// Call classes: the dynamic per-draw cost section 2.3.1 says P2 cannot be decided
// without.
enum class CallClass : Uint32 {
// Draws that reached an instrumented backend draw-preparation entry point. The
// denominator for every "per draw" number below.
Draws = 0,
// GLContext accessor calls actually EXECUTED on the instrumented paths. Counted in
// static tallies at the ~10 hot entry points, not by wrapping all 293 call sites -
// see the inventory in PipeStats.cpp for exactly what is and is not in this number.
AccessorCalls,
// Texture upload emissions: one per (upload target, level) that actually shipped
// texels. The eventual resource_subdata record count.
TextureUploadEmissions,
// Emissions that took the union-box shape (one driver upload job).
TextureUploadBoxEmissions,
// Emissions that took the refined rect-list shape (N driver upload jobs). The
// box/rect split is the thing SSIM cannot see and the +6 ms/frame Mali cliff came
// from, so it is counted separately from the byte total.
TextureUploadRectEmissions,
// Driver upload jobs issued by those emissions: 1 per box emission, N per rect-list
// emission.
TextureUploadJobs,
Count
};
// Memo gates. Each is a place where a backend decides "nothing moved, skip the work".
// Hit == the gate short-circuited; Miss == it fell through and did the work. The six
// are exactly the ones section 2.3.1 tabulates.
enum class Gate : Uint32 {
// DirectGLES.cpp SyncRenderState: the render-state-version early-out.
EsprytRenderState = 0,
// DirectGLES.cpp SyncNeccessaryTextures: the six-value sync-list key compare.
EsprytTextureSyncList,
// DirectGLES.cpp CurrentUnitBindingsEpoch: the (context, max unit, bind generation)
// shutter over the unit walk.
EsprytUnitBindingsEpoch,
// VulkanRenderer.cpp TrySetupDrawFastPath: the whole snapshot fast path.
MagmaDrawFastPath,
// VulkanRenderer.cpp GetOrCreatePipeline: the pipeline memo.
MagmaPipelineMemo,
// VulkanRenderer.cpp ApplyDynamicDrawStateTail: the version+extent tail gate.
MagmaDynamicTail,
Count
};
// Per-draw command payload size histogram (plan section 4.5.7: SEG_CMD has to be sized
// off the DISTRIBUTION, not off a per-frame total). PLACEHOLDER in P0: MGPipe emits no
// records yet, so nothing in the backends calls RecordDrawPayloadBytes. The bucketing
// and the reporting are implemented and unit-tested so that the first generator to
// emit records only has to add the one call.
inline constexpr Uint32 kPayloadHistogramBuckets = 24;
// Frames between two summary lines when MOBILEGL_PIPE_STATS=1.
inline constexpr Uint64 kDefaultSummaryFramePeriod = 120;
// The period Init() latched from MOBILEGL_PIPE_STATS_PERIOD (kDefaultSummaryFramePeriod
// when unset); never 0.
Uint64 SummaryFramePeriod();
// The latch. Read directly by Enabled() so the off path is a global load and a
// predicted branch - do not turn this into a function call.
extern Bool g_pipeStatsEnabled;
inline Bool Enabled() { return g_pipeStatsEnabled; }
// Latches g_pipeStatsEnabled from MG_Config::Features.PipeStats and clears every
// counter. Called from MobileGL::Initialize() right after the config load.
void Init();
// Final summary line plus, if MOBILEGL_PIPE_STATS_FILE names a path, the JSON dump.
// Called from MobileGL's teardown. Idempotent.
void Shutdown();
void AddBytes(ByteClass byteClass, Uint64 bytes);
void AddCalls(CallClass callClass, Uint64 count);
void CountGate(Gate gate, Bool hit);
void RecordDrawPayloadBytes(Uint64 bytes);
// Frame boundary: publishes the frame's values to Tracy (when TRACY_ENABLE), folds them
// into the run totals, clears the frame accumulators, and every kSummaryFramePeriod
// frames emits the summary line. Called from each backend's Present().
void OnPresent();
// --- introspection, for the unit test and the JSON dump -------------------------
Uint64 FrameBytes(ByteClass byteClass);
Uint64 TotalBytes(ByteClass byteClass);
Uint64 FrameCalls(CallClass callClass);
Uint64 TotalCalls(CallClass callClass);
Uint64 TotalGateHits(Gate gate);
Uint64 TotalGateMisses(Gate gate);
Uint64 TotalPayloadBucket(Uint32 bucket);
Uint64 FrameCount();
const char* NameOf(ByteClass byteClass);
const char* NameOf(CallClass callClass);
const char* NameOf(Gate gate);
// The compact fixed-format one-liner MGLOG_I prints, covering the CURRENT window (see
// AdvanceSummaryWindow). PURE: calling it twice returns the same text and changes no
// counter, so a probe, a test or a second reporting channel can format the window
// without stealing it from the log.
String FormatWindowLine();
// Closes the current window: the run totals as of now become the base the next
// FormatWindowLine() subtracts. Emitting the line and advancing the window are separate
// on purpose - the pair used to be one function whose name promised a formatter.
void AdvanceSummaryWindow();
// The teardown dump. Run totals only: a per-frame JSON stream is a different tool.
String FormatJson();
// Test hooks, used by no shipping path. Init() clears the counters through an internal
// ResetCounters() rather than by calling ResetForTesting().
void SetEnabledForTesting(Bool enabled);
void ResetForTesting();
} // namespace MobileGL::MG_Util::PipeStats
@@ -7,6 +7,7 @@
// End of Source File Header
#include "DriverBugProbes.h"
#include "PersistentBufferOrderingProbe.h"
#include <Config.h>
#include <MG_Util/Debug/Log.h>
@@ -2445,6 +2446,10 @@ namespace MobileGL::MG_Util::SelfTest {
DriverBugVerdict::Unfixable, detail};
}
Optional<DriverBugFinding> ProbePersistentBufferOrderingBug(const GLESFunctionsTable& gl) {
return DescribePersistentBufferOrderingBug(ProbePersistentBufferUpdateOrdering(gl));
}
// The table. One row per known driver bug; see the header for how to add a sibling.
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
@@ -2457,6 +2462,7 @@ namespace MobileGL::MG_Util::SelfTest {
&ProbeLayeredBlitDestinationBug,
&ProbeLocatedIoBlockPayloadBug,
&ProbeCopyImagePacked16FieldOrderBug,
&ProbePersistentBufferOrderingBug,
};
} // namespace
@@ -0,0 +1,413 @@
// MobileGL - MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "PersistentBufferOrderingProbe.h"
#include <MG_Util/Debug/Log.h>
#include <array>
#include <cmath>
#include <cstring>
#include <new>
namespace MobileGL::MG_Util::SelfTest {
namespace {
using MG_External::GLESFunctionsTable;
constexpr GLbitfield kPersistent = 0x0040;
constexpr GLbitfield kCoherent = 0x0080;
constexpr GLbitfield kDynamicStorage = 0x0100;
constexpr GLbitfield kMapFlags = GL_MAP_WRITE_BIT | kPersistent | kCoherent;
constexpr GLsizeiptr kArenaSize = 128 * 1024 * 1024;
constexpr GLintptr kOffset = 96 * 1024 * 1024 + 28;
constexpr GLsizei kSide = 128;
constexpr GLsizei kSlots = 8;
constexpr Int kBatches = 10;
constexpr Int kDraws = 32;
constexpr GLsizei kQuads = 64 * 32;
constexpr Int kAttempts = 3;
constexpr std::array<const char*, 3> kUploadNames = {"SubData", "Copy/persistent staging",
"Copy/SubData staging"};
enum class Shape { Unmapped, Mapped, FinishBefore, FinishBoth, MapThenUnmap, BarrierBefore };
struct Vertex { GLfloat x, y, r, g, b; };
constexpr GLsizeiptr kPayloadSize = kQuads * 6 * sizeof(Vertex);
static_assert(kOffset + kPayloadSize <= kArenaSize);
void DrainErrors(const GLESFunctionsTable& gl) {
for (Int i = 0; i < 32 && gl.glGetError() != GL_NO_ERROR; ++i) {}
}
Bool CanProbe(const GLESFunctionsTable& gl) {
if (!(gl.glGetIntegerv && gl.glGetBooleanv && gl.glGetFloatv && gl.glGetError &&
gl.glGetStringi && gl.glIsEnabled && gl.glEnable && gl.glDisable &&
gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
gl.glGetShaderInfoLog && gl.glDeleteShader && gl.glCreateProgram && gl.glAttachShader &&
gl.glLinkProgram && gl.glGetProgramiv && gl.glGetProgramInfoLog && gl.glDeleteProgram &&
gl.glUseProgram && gl.glGenBuffers && gl.glBindBuffer && gl.glBufferStorageEXT &&
gl.glMapBufferRange && gl.glUnmapBuffer && gl.glBufferData && gl.glBufferSubData &&
gl.glCopyBufferSubData && gl.glDeleteBuffers && gl.glGenVertexArrays &&
gl.glBindVertexArray && gl.glVertexAttribPointer && gl.glEnableVertexAttribArray &&
gl.glDeleteVertexArrays && gl.glGenTextures && gl.glBindTexture && gl.glTexStorage2D &&
gl.glDeleteTextures && gl.glGenFramebuffers && gl.glBindFramebuffer &&
gl.glFramebufferTexture2D && gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers &&
gl.glViewport && gl.glColorMask && gl.glClearColor && gl.glClear && gl.glDrawArrays &&
gl.glFinish && gl.glMemoryBarrier && gl.glPixelStorei && gl.glReadPixels)) return false;
DrainErrors(gl);
GLint major = 0, minor = 0, count = 0;
gl.glGetIntegerv(GL_MAJOR_VERSION, &major);
gl.glGetIntegerv(GL_MINOR_VERSION, &minor);
gl.glGetIntegerv(GL_NUM_EXTENSIONS, &count);
if (gl.glGetError() != GL_NO_ERROR || major < 3 || (major == 3 && minor < 1)) return false;
for (GLint i = 0; i < count; ++i) {
const auto* extension = gl.glGetStringi(GL_EXTENSIONS, i);
if (extension && std::strcmp(reinterpret_cast<const char*>(extension),
"GL_EXT_buffer_storage") == 0) return true;
}
return false;
}
// This probe touches no images/SSBO bindings. Keep its scope independent from the
// other POST probes, including pack state and the caller's currently active texture unit.
struct StateScope {
const GLESFunctionsTable& gl;
GLint program = 0, vao = 0, array = 0, copyRead = 0, copyWrite = 0;
GLint drawFbo = 0, readFbo = 0, texture = 0, packBuffer = 0;
GLint viewport[4]{};
GLfloat clear[4]{};
GLboolean colorMask[4]{};
static constexpr std::array<GLenum, 10> enables = {
GL_BLEND, GL_DEPTH_TEST, GL_STENCIL_TEST, GL_CULL_FACE, GL_SCISSOR_TEST,
GL_RASTERIZER_DISCARD, GL_DITHER, GL_SAMPLE_ALPHA_TO_COVERAGE,
GL_SAMPLE_COVERAGE, GL_SAMPLE_MASK};
static constexpr std::array<GLenum, 4> packNames = {
GL_PACK_ALIGNMENT, GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS};
std::array<GLboolean, enables.size()> enabled{};
std::array<GLint, packNames.size()> pack{};
explicit StateScope(const GLESFunctionsTable& api) : gl(api) {
gl.glGetIntegerv(GL_CURRENT_PROGRAM, &program);
gl.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &vao);
gl.glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &array);
gl.glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &copyRead);
gl.glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &copyWrite);
gl.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo);
gl.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFbo);
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture);
gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer);
gl.glGetIntegerv(GL_VIEWPORT, viewport);
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, clear);
gl.glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
for (SizeT i = 0; i < enables.size(); ++i) enabled[i] = gl.glIsEnabled(enables[i]);
for (SizeT i = 0; i < packNames.size(); ++i) gl.glGetIntegerv(packNames[i], &pack[i]);
}
void Prepare() {
for (auto cap : enables) gl.glDisable(cap);
gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
for (auto name : packNames) gl.glPixelStorei(name, name == GL_PACK_ALIGNMENT ? 1 : 0);
gl.glViewport(0, 0, kSide, kSide);
gl.glClearColor(0, 0, 0, 1);
}
~StateScope() {
gl.glUseProgram(program);
gl.glBindVertexArray(vao);
gl.glBindBuffer(GL_ARRAY_BUFFER, array);
gl.glBindBuffer(GL_COPY_READ_BUFFER, copyRead);
gl.glBindBuffer(GL_COPY_WRITE_BUFFER, copyWrite);
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, packBuffer);
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, drawFbo);
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo);
gl.glBindTexture(GL_TEXTURE_2D, texture);
gl.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
gl.glClearColor(clear[0], clear[1], clear[2], clear[3]);
gl.glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]);
for (SizeT i = 0; i < enables.size(); ++i) {
if (enabled[i]) gl.glEnable(enables[i]); else gl.glDisable(enables[i]);
}
for (SizeT i = 0; i < packNames.size(); ++i) gl.glPixelStorei(packNames[i], pack[i]);
}
};
struct Resources {
const GLESFunctionsTable& gl;
GLuint program = 0, vao = 0;
std::array<GLuint, kSlots> fbos{}, textures{};
explicit Resources(const GLESFunctionsTable& api) : gl(api) {}
~Resources() {
gl.glDeleteFramebuffers(kSlots, fbos.data());
gl.glDeleteTextures(kSlots, textures.data());
gl.glDeleteVertexArrays(1, &vao);
if (program) gl.glDeleteProgram(program);
}
GLuint Compile(GLenum type, const char* source) {
GLuint shader = gl.glCreateShader(type);
if (!shader) return 0;
gl.glShaderSource(shader, 1, &source, nullptr);
gl.glCompileShader(shader);
GLint compiled = 0;
gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
char log[512]{};
gl.glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
MGLOG_I("[driver-bug] persistent buffer ordering: shader failed: %s", log);
gl.glDeleteShader(shader);
return 0;
}
return shader;
}
Bool Setup() {
const GLuint vs = Compile(GL_VERTEX_SHADER,
"#version 310 es\nlayout(location=0) in vec2 pos; layout(location=1) in vec3 color;\n"
"out highp vec3 vColor; void main(){gl_Position=vec4(pos,0,1);vColor=color;}\n");
const GLuint fs = Compile(GL_FRAGMENT_SHADER,
"#version 310 es\nprecision highp float; in highp vec3 vColor;\n"
"layout(location=0) out vec4 outColor; void main(){outColor=vec4(vColor,1);}\n");
if (vs && fs) {
program = gl.glCreateProgram();
if (program) {
gl.glAttachShader(program, vs);
gl.glAttachShader(program, fs);
gl.glLinkProgram(program);
}
}
if (vs) gl.glDeleteShader(vs);
if (fs) gl.glDeleteShader(fs);
if (!program) return false;
GLint linked = 0;
gl.glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) {
char log[512]{};
gl.glGetProgramInfoLog(program, sizeof(log), nullptr, log);
MGLOG_I("[driver-bug] persistent buffer ordering: link failed: %s", log);
return false;
}
gl.glUseProgram(program);
gl.glGenVertexArrays(1, &vao);
gl.glBindVertexArray(vao);
gl.glGenFramebuffers(kSlots, fbos.data());
gl.glGenTextures(kSlots, textures.data());
for (Int i = 0; i < kSlots; ++i) {
if (!vao || !fbos[i] || !textures[i]) return false;
gl.glBindTexture(GL_TEXTURE_2D, textures[i]);
gl.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSide, kSide);
gl.glBindFramebuffer(GL_FRAMEBUFFER, fbos[i]);
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
textures[i], 0);
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
}
return gl.glGetError() == GL_NO_ERROR;
}
};
struct Buffers {
const GLESFunctionsTable& gl;
GLuint arena = 0, staging = 0;
explicit Buffers(const GLESFunctionsTable& api) : gl(api) {}
~Buffers() {
// All normal batches finish before cleanup; also retire a partially queued
// batch on an error path before destroying a mapped staging source.
gl.glFinish();
gl.glDeleteBuffers(1, &arena);
gl.glDeleteBuffers(1, &staging);
}
};
void FillVertices(Vector<Vertex>& vertices, Int channel) {
constexpr std::array<Vertex, 6> quad = {{{-1,-1,0,0,0}, {1,-1,0,0,0}, {1,1,0,0,0},
{-1,-1,0,0,0}, {1,1,0,0,0}, {-1,1,0,0,0}}};
for (SizeT k = 0; k < vertices.size(); ++k) {
auto& v = vertices[k];
v = quad[k % 6];
const SizeT q = k / 6;
v.x = v.x / 64.f - 1.f + (2 * (q % 64) + 1) / 64.f;
v.y = v.y / 32.f - 1.f + (2 * (q / 64) + 1) / 32.f;
v.r = channel == 0 ? 1.f : 0.f;
v.g = channel == 1 ? 1.f : 0.f;
v.b = channel == 2 ? 1.f : 0.f;
}
}
BufferOrderingSample Run(const GLESFunctionsTable& gl, const Resources& resources,
const Vector<Uint8>& seed, Int upload, Shape shape) {
BufferOrderingSample sample;
sample.status = BufferOrderingProbeStatus::Failed;
Buffers buffers(gl);
Vector<Vertex> payload(kQuads * 6);
Vector<Uint8> pixels(kSide * kSide * 4);
DrainErrors(gl);
do {
gl.glGenBuffers(1, &buffers.arena);
if (!buffers.arena) break;
gl.glBindBuffer(GL_ARRAY_BUFFER, buffers.arena);
gl.glBufferStorageEXT(GL_ARRAY_BUFFER, kArenaSize, seed.data(), kMapFlags | kDynamicStorage);
sample.error = gl.glGetError();
if (sample.error != GL_NO_ERROR) break;
if (shape != Shape::Unmapped) {
// Deliberately never dereference the destination pointer. All destination
// writes below are ordered GL commands, with no client mapping accesses.
if (!gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, kArenaSize, kMapFlags)) break;
if (shape == Shape::MapThenUnmap && !gl.glUnmapBuffer(GL_ARRAY_BUFFER)) break;
}
gl.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<const void*>(kOffset));
gl.glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<const void*>(kOffset + 2 * sizeof(GLfloat)));
gl.glEnableVertexAttribArray(0);
gl.glEnableVertexAttribArray(1);
void* sourceMap = nullptr;
if (upload != 0) {
gl.glGenBuffers(1, &buffers.staging);
if (!buffers.staging) break;
gl.glBindBuffer(GL_COPY_READ_BUFFER, buffers.staging);
if (upload == 1) {
gl.glBufferStorageEXT(GL_COPY_READ_BUFFER, kSlots * kPayloadSize, nullptr, kMapFlags);
sourceMap = gl.glMapBufferRange(GL_COPY_READ_BUFFER, 0, kSlots * kPayloadSize, kMapFlags);
if (!sourceMap) break;
} else {
gl.glBufferData(GL_COPY_READ_BUFFER, kSlots * kPayloadSize, nullptr, GL_STREAM_DRAW);
}
gl.glBindBuffer(GL_COPY_WRITE_BUFFER, buffers.arena);
}
sample.error = gl.glGetError();
if (sample.error != GL_NO_ERROR) break;
for (Int batch = 0; batch < kBatches; ++batch) {
for (Int slot = 0; slot < kSlots; ++slot) {
FillVertices(payload, (batch * kSlots + slot) % 3);
if (shape == Shape::FinishBefore || shape == Shape::FinishBoth) gl.glFinish();
if (shape == Shape::BarrierBefore) gl.glMemoryBarrier(GL_ALL_BARRIER_BITS);
if (upload == 0) {
gl.glBufferSubData(GL_ARRAY_BUFFER, kOffset, kPayloadSize, payload.data());
} else {
// No slot is reused until the entire batch has finished on the GPU.
if (upload == 1) {
std::memcpy(static_cast<Uint8*>(sourceMap) + slot * kPayloadSize,
payload.data(), kPayloadSize);
} else {
gl.glBufferSubData(GL_COPY_READ_BUFFER, slot * kPayloadSize,
kPayloadSize, payload.data());
}
gl.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
slot * kPayloadSize, kOffset, kPayloadSize);
}
if (shape == Shape::FinishBoth) gl.glFinish();
gl.glBindFramebuffer(GL_FRAMEBUFFER, resources.fbos[slot]);
gl.glClear(GL_COLOR_BUFFER_BIT);
for (Int draw = 0; draw < kDraws; ++draw) gl.glDrawArrays(GL_TRIANGLES, 0, kQuads * 6);
}
// No readback/Finish between subject update/draw pairs. Early readback
// would hide precisely the old-reader/new-writer overlap being tested.
gl.glFinish();
sample.error = gl.glGetError();
if (sample.error != GL_NO_ERROR) break;
for (Int slot = 0; slot < kSlots; ++slot) {
gl.glBindFramebuffer(GL_FRAMEBUFFER, resources.fbos[slot]);
gl.glReadPixels(0, 0, kSide, kSide, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
sample.error = gl.glGetError();
if (sample.error != GL_NO_ERROR) break;
const Int channel = (batch * kSlots + slot) % 3;
Uint bad = 0;
for (Int pixel = 0; pixel < kSide * kSide; ++pixel) {
for (Int c = 0; c < 3; ++c) {
const Int expected = c == channel ? 255 : 0;
if (std::abs(Int(pixels[pixel * 4 + c]) - expected) > 8) ++bad;
}
}
++sample.frames;
if (bad != 0) ++sample.badFrames;
sample.badComponents += bad;
}
if (sample.error != GL_NO_ERROR) break;
}
if (sample.error == GL_NO_ERROR && sample.frames == kBatches * kSlots)
sample.status = BufferOrderingProbeStatus::Complete;
} while (false);
if (sample.error == GL_NO_ERROR) sample.error = gl.glGetError();
return sample;
}
String Describe(const BufferOrderingSample& sample) {
if (sample.status == BufferOrderingProbeStatus::NotRun) return "not run";
if (sample.status == BufferOrderingProbeStatus::Failed)
return format("inconclusive (GL error 0x{:x}, {} readbacks)", sample.error, sample.frames);
return format("{}/{} bad FBOs ({} components)", sample.badFrames, sample.frames, sample.badComponents);
}
String DescribeUpload(const BufferOrderingUploadMeasurement& row, Int upload) {
return format("{}: mapped {}, never-mapped {}, Finish-before {}, Finish-both {}, "
"map-then-unmap {}, barrier-before {}", kUploadNames[upload], Describe(row.mapped),
Describe(row.unmapped), Describe(row.finishBefore), Describe(row.finishBoth),
Describe(row.mapThenUnmap), Describe(row.barrierBefore));
}
} // namespace
PersistentBufferOrderingMeasurement ProbePersistentBufferUpdateOrdering(const GLESFunctionsTable& gl) try {
PersistentBufferOrderingMeasurement measurement;
if (!CanProbe(gl)) return measurement;
measurement.supported = true;
StateScope state(gl);
state.Prepare();
Resources resources(gl);
if (gl.glGetError() != GL_NO_ERROR || !resources.Setup()) {
for (auto& row : measurement.uploads) row.unmapped.status = BufferOrderingProbeStatus::Failed;
MGLOG_I("[driver-bug] persistent buffer ordering: setup failed; inconclusive");
return measurement;
}
Vector<Uint8> seed(kArenaSize, 0);
for (Int upload = 0; upload < Int(measurement.uploads.size()); ++upload) {
auto& row = measurement.uploads[upload];
row.unmapped = Run(gl, resources, seed, upload, Shape::Unmapped);
if (row.unmapped.Passed()) {
// A single allocation can miss on Mali. Stop once a mismatch is measured,
// otherwise retry with fresh storage rather than treating one pass as proof.
for (Int attempt = 0; attempt < kAttempts; ++attempt) {
const auto sample = Run(gl, resources, seed, upload, Shape::Mapped);
row.mapped.status = sample.status;
row.mapped.error = sample.error;
row.mapped.frames += sample.frames;
row.mapped.badFrames += sample.badFrames;
row.mapped.badComponents += sample.badComponents;
if (sample.status != BufferOrderingProbeStatus::Complete || sample.badFrames) break;
}
if (row.mapped.status == BufferOrderingProbeStatus::Complete && row.mapped.badFrames) {
row.finishBoth = Run(gl, resources, seed, upload, Shape::FinishBoth);
row.finishBefore = Run(gl, resources, seed, upload, Shape::FinishBefore);
row.mapThenUnmap = Run(gl, resources, seed, upload, Shape::MapThenUnmap);
row.barrierBefore = Run(gl, resources, seed, upload, Shape::BarrierBefore);
}
}
MGLOG_I("[driver-bug] persistent buffer ordering: %s; %s", DescribeUpload(row, upload).c_str(),
row.Detected() ? "detected" : "not detected or inconclusive");
}
return measurement;
} catch (const std::bad_alloc&) {
// The CPU initializer is arena-sized too. An allocation failure must not discard
// the rest of the POST report or turn a partially sampled case into a finding.
MGLOG_I("[driver-bug] persistent buffer ordering: host allocation failed; inconclusive");
PersistentBufferOrderingMeasurement measurement;
measurement.supported = true;
for (auto& row : measurement.uploads) row.unmapped.status = BufferOrderingProbeStatus::Failed;
return measurement;
}
Optional<DriverBugFinding> DescribePersistentBufferOrderingBug(
const PersistentBufferOrderingMeasurement& measurement) {
String detail;
for (Int upload = 0; upload < Int(measurement.uploads.size()); ++upload) {
if (!measurement.uploads[upload].Detected()) continue;
if (!detail.empty()) detail += "; ";
detail += DescribeUpload(measurement.uploads[upload], upload);
}
if (detail.empty()) return std::nullopt;
detail += ". A 128 MiB immutable vertex destination was mapped WRITE|PERSISTENT|COHERENT, "
"but never accessed through its client pointer. Queued uploads/draws corrupt vertex data; "
"identical never-mapped and Finish-before-and-after controls pass. "
"This POST does not enable a workaround. MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1 "
"avoids automatic arena adoption; explicit application mappings remain separate. "
"FBO counts describe this bounded stress probe, not application flicker frequency.";
return DriverBugFinding{"Persistent-mapped vertex buffers lose upload/draw ordering",
DriverBugVerdict::Unfixable, Move(detail)};
}
} // namespace MobileGL::MG_Util::SelfTest
@@ -0,0 +1,66 @@
// MobileGL - MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "DriverBugProbes.h"
#include <array>
namespace MobileGL::MG_Util::SelfTest {
enum class BufferOrderingProbeStatus : Uint8 { NotRun, Complete, Failed };
struct BufferOrderingSample {
BufferOrderingProbeStatus status = BufferOrderingProbeStatus::NotRun;
Uint frames = 0; // Independent FBO readbacks, not draw calls or application frames.
Uint badFrames = 0;
Uint badComponents = 0;
GLenum error = GL_NO_ERROR;
Bool Passed() const { return status == BufferOrderingProbeStatus::Complete && badFrames == 0; }
};
struct BufferOrderingUploadMeasurement {
BufferOrderingSample unmapped;
BufferOrderingSample mapped;
BufferOrderingSample finishBefore;
BufferOrderingSample finishBoth;
BufferOrderingSample mapThenUnmap;
BufferOrderingSample barrierBefore;
Bool Detected() const {
return unmapped.Passed() && finishBoth.Passed() &&
mapped.status == BufferOrderingProbeStatus::Complete && mapped.badFrames != 0;
}
};
struct PersistentBufferOrderingMeasurement {
Bool supported = false;
// SubData; CopyBufferSubData from coherent persistent staging; CopyBufferSubData
// from ordinary SubData staging. Each has its OWN otherwise-identical controls.
std::array<BufferOrderingUploadMeasurement, 3> uploads;
};
// POST-only: native GLES calls, no MobileGL buffers, renderer-name rules or config changes.
// The Mali r54p1 finding: updating an immutable vertex arena that has been persistently
// mapped can corrupt queued draws even when the application never accesses that mapping.
// Queue eight update/draw pairs into separate FBOs BEFORE any Finish/readback, then check
// every pixel of both old and new draws. Staging slots never overlap while in flight.
//
// Each upload runs a never-mapped control with identical storage flags. Try up to three
// fresh mapped allocations to catch intermittent failures. On corruption, measure explicit
// waits, map-then-unmap and a barrier as diagnostics. Only a passing never-mapped AND
// Finish-before-and-after control permits a finding. Setup/GL failures are inconclusive.
// Explicit allocations are one 128 MiB arena, its initializer, and small staging/FBOs;
// allocations, batches and draws are bounded. Every touched GL state is restored.
PersistentBufferOrderingMeasurement ProbePersistentBufferUpdateOrdering(
const MG_External::GLESFunctionsTable& gl);
// Used by the POST collector. A report never labels an inconclusive sample as a bug.
Optional<DriverBugFinding> DescribePersistentBufferOrderingBug(
const PersistentBufferOrderingMeasurement& measurement);
} // namespace MobileGL::MG_Util::SelfTest
@@ -234,10 +234,6 @@ target_link_libraries(glretrace_common PUBLIC retrace_common glhelpers glproc)
add_library(trace_replay_runner SHARED
trace_replay_core.cpp
trace_replay_jni.cpp
# P0 spike A. Android-only, and deliberately its own TU: trace_replay_core.cpp is
# shared verbatim with the desktop mobilegl_trace_replay runner
# (tools/trace_replay/CMakeLists.txt), which cannot see <android/log.h>.
spawn_spike.cpp
"${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay/apitrace_fbo_dump.cpp")
target_compile_features(trace_replay_runner PRIVATE cxx_std_17)
@@ -253,5 +249,4 @@ target_link_libraries(trace_replay_runner
retrace_common
image
android
log
dl)
@@ -1,227 +0,0 @@
// P0 spike A - the Android half of the delivery chain (PLAN-B.md §8.1, inheriting
// PLAN.md §11.1-§11.6). See spawn_spike.hpp for what the spike is asking.
//
// Android-only on purpose: this TU is listed only by
// android-plugin/app/src/trace/cpp/CMakeLists.txt. Its sibling trace_replay_core.cpp is
// shared with the DESKTOP mobilegl_trace_replay runner, which has no <android/log.h>,
// so nothing Android-specific may live there.
#include "spawn_spike.hpp"
#if !defined(__ANDROID__)
#error "spawn_spike.cpp is Android-only; do not add it to the desktop trace replay build"
#endif
#include <android/log.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
// execve needs the environment the parent already has: a server process started from
// the app must inherit it, and handing it an empty one would change what is being tested.
extern "C" char** environ;
namespace mobilegl_trace {
namespace {
constexpr const char* kSpikeLogTag = "MobileGLTraceRunner";
std::string ReadWholeFile(const std::string& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
return {};
}
std::ostringstream contents;
contents << input.rdbuf();
std::string text = contents.str();
while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == '\0')) {
text.pop_back();
}
return text;
}
// The domain this process is in. `u:r:untrusted_app:s0:...` is the whole point of the
// spike: an exec that works from an `adb run-as` shell says nothing about whether the
// app itself is allowed to do it, because that shell is a different SELinux domain.
std::string ReadSelfSelinuxContext() {
const std::string context = ReadWholeFile("/proc/self/attr/current");
return context.empty() ? "<unreadable>" : context;
}
// Starts the child with its stdout and stderr redirected into `outputPath`, reports the
// child pid through `childPid` and, when the exec itself was refused, the child's errno
// through `execErrno`. Returns 0, or the errno of a failure that happened before the
// child existed at all.
//
// fork/execve, not posix_spawn: bionic only declares posix_spawn from API 28 while
// MobileGL ships at minSdk 26 (the root CMakeLists.txt pins MOBILEGL_ANDROID_API_LEVEL
// to 26 and refuses to configure lower), so posix_spawn is not available to the shipping
// build and this is the shape the production spawn path has to take. Nothing happens
// between fork and execve except open/dup2/execve/write/_exit, all async-signal-safe,
// because the parent is a multi-threaded JVM process.
int SpawnSpikeChild(const std::string& serverPath,
const std::string& markerPath,
const std::string& outputPath,
pid_t* childPid,
int* execErrno) {
*execErrno = 0;
char* argv[] = {const_cast<char*>(serverPath.c_str()),
const_cast<char*>(markerPath.c_str()), nullptr};
// The errno of a refused exec is the answer this spike is here to bring back, and it
// is raised in a process that cannot return anything: by the time the parent sees a
// wait status the reason has been flattened into an exit code. So the child writes
// the raw errno into a close-on-exec pipe. A successful exec closes the write end for
// free and the parent reads EOF; a refused one leaves the four bytes behind. EACCES
// (SELinux, or a noexec mount) and ENOEXEC (a mangled or non-PIE file) are entirely
// different verdicts for the design and this is the only thing that separates them.
int report[2] = {-1, -1};
if (pipe2(report, O_CLOEXEC) != 0) {
return errno;
}
const pid_t forked = fork();
if (forked < 0) {
const int forkErrno = errno;
close(report[0]);
close(report[1]);
return forkErrno;
}
if (forked == 0) {
close(report[0]);
// Without this the child's output is unobservable: an Android app process has
// stdout on /dev/null, so a printed line would vanish and the spike could not
// tell "ran and printed" apart from "never ran".
const int outputFd = open(outputPath.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0664);
if (outputFd >= 0) {
dup2(outputFd, STDOUT_FILENO);
dup2(outputFd, STDERR_FILENO);
if (outputFd != STDOUT_FILENO && outputFd != STDERR_FILENO) {
close(outputFd);
}
}
execve(serverPath.c_str(), argv, environ);
const int failure = errno;
// Only reached when the exec was refused - the one outcome this spike is about.
const ssize_t written = write(report[1], &failure, sizeof(failure));
static_cast<void>(written);
// 127 is the shell's convention for "could not exec" and is distinguishable from
// every status the stub itself can return.
_exit(127);
}
close(report[1]);
int failure = 0;
ssize_t got = 0;
// Blocks until the child either execs (the write end closes, read returns 0) or
// reports why it could not.
while ((got = read(report[0], &failure, sizeof(failure))) < 0 && errno == EINTR) {
}
close(report[0]);
if (got == static_cast<ssize_t>(sizeof(failure))) {
*execErrno = failure;
}
*childPid = forked;
return 0;
}
} // namespace
SpawnSpikeResult RunSpawnSpike(const SpawnSpikeRequest& request) {
SpawnSpikeResult result;
result.parentSelinuxContext = ReadSelfSelinuxContext();
if (request.serverPath.empty() || request.markerPath.empty()) {
result.message = "spike-spawn: serverPath and markerPath are both required";
return result;
}
// A stale marker from a previous run would otherwise be read back as this run's
// proof. Remove it first, so "the marker exists" can only mean the child wrote it.
unlink(request.markerPath.c_str());
const std::string childOutputPath = request.markerPath + ".stdout";
unlink(childOutputPath.c_str());
struct stat serverStat {};
if (stat(request.serverPath.c_str(), &serverStat) != 0) {
result.spawnErrno = errno;
result.message = "spike-spawn: " + request.serverPath + " does not exist: " +
std::strerror(errno);
__android_log_print(ANDROID_LOG_ERROR, kSpikeLogTag, "%s", result.message.c_str());
return result;
}
pid_t childPid = -1;
int execErrno = 0;
const int spawnStatus = SpawnSpikeChild(request.serverPath, request.markerPath,
childOutputPath, &childPid, &execErrno);
if (spawnStatus != 0) {
result.spawnErrno = spawnStatus;
result.message = "spike-spawn: could not start " + request.serverPath +
": spawnErrno=" + std::to_string(spawnStatus) + " (" +
std::strerror(spawnStatus) + ")";
__android_log_print(ANDROID_LOG_ERROR, kSpikeLogTag, "%s (parentSelinux=%s)",
result.message.c_str(), result.parentSelinuxContext.c_str());
return result;
}
result.spawned = true;
result.childPid = static_cast<int>(childPid);
result.execErrno = execErrno;
int waitStatus = 0;
while (waitpid(childPid, &waitStatus, 0) < 0) {
if (errno != EINTR) {
result.message = "spike-spawn: waitpid failed: " + std::string(std::strerror(errno));
__android_log_print(ANDROID_LOG_ERROR, kSpikeLogTag, "%s", result.message.c_str());
return result;
}
}
result.waitStatus = waitStatus;
if (WIFEXITED(waitStatus)) {
result.exitCode = WEXITSTATUS(waitStatus);
}
if (WIFSIGNALED(waitStatus)) {
result.termSignal = WTERMSIG(waitStatus);
}
result.markerContent = ReadWholeFile(request.markerPath);
result.childOutput = ReadWholeFile(childOutputPath);
result.succeeded =
result.execErrno == 0 && result.exitCode == 0 && !result.markerContent.empty();
std::ostringstream message;
message << "spike-spawn: " << (result.succeeded ? "OK" : "FAILED")
<< " server=" << request.serverPath
<< " pid=" << result.childPid
<< " exit=" << result.exitCode
<< " signal=" << result.termSignal
// Always printed, including on the success path, so a reader never has to
// guess whether the field was collected or merely absent.
<< " execErrno=" << result.execErrno
<< " (" << (result.execErrno == 0 ? "exec succeeded"
: std::strerror(result.execErrno)) << ")"
<< " parentSelinux=" << result.parentSelinuxContext
<< " marker=[" << result.markerContent << "]"
<< " childStdout=[" << result.childOutput << "]";
result.message = message.str();
__android_log_print(result.succeeded ? ANDROID_LOG_INFO : ANDROID_LOG_ERROR, kSpikeLogTag,
"%s", result.message.c_str());
// The Activity is normally gone as soon as the run finishes, so the verdict also goes
// to a file next to the marker; that is what a device lane copies out.
std::ofstream report(request.markerPath + ".report", std::ios::trunc);
if (report) {
report << result.message << "\n";
}
return result;
}
} // namespace mobilegl_trace
@@ -1,60 +0,0 @@
#pragma once
// ---------------------------------------------------------------------------
// P0 spike A: exec a second packaged native executable from this process.
//
// Answers one question and nothing else: can an ordinary Android application
// process (untrusted_app, NOT an `adb run-as` shell, which runs in a different
// SELinux domain and would prove nothing) exec a binary that was shipped inside
// its own APK as lib/<abi>/lib*.so? The disaggregated design needs a server
// process on Android and this is its only supported delivery route (PLAN-B.md
// §8.1, inheriting PLAN.md §11.1-§11.6).
//
// This lives beside trace_replay_core.hpp rather than inside it because
// trace_replay_core.cpp is ALSO compiled by the desktop mobilegl_trace_replay
// runner (tools/trace_replay/CMakeLists.txt names it directly), where <android/log.h>
// does not exist. The spike is Android-only, so it gets an Android-only TU;
// spawn_spike.cpp is listed only by the trace APK's CMakeLists.
//
// Nothing in the replay path calls this; it runs only when the trace Activity is
// launched with the `mobilegl_spike_spawn` intent extra.
// ---------------------------------------------------------------------------
#include <string>
namespace mobilegl_trace {
struct SpawnSpikeRequest {
// Absolute path of the executable, normally
// getApplicationInfo().nativeLibraryDir + "/libMobileGLServer.so".
std::string serverPath;
// Marker file the child is asked to write, passed to it as argv[1]. The child's
// stdout and stderr are captured next to it, with ".stdout" appended.
std::string markerPath;
};
struct SpawnSpikeResult {
bool spawned = false;
// Exec'd, waited for, exited 0, and the marker file came back non-empty.
bool succeeded = false;
// errno of the pre-fork or fork failure - the parent could not even try.
int spawnErrno = 0;
// errno of a REFUSED execve, carried out of the child over a close-on-exec pipe.
// This is the one datum the spike exists to produce: EACCES (SELinux or the mount's
// noexec) and ENOEXEC (the packager mangled the file) are different verdicts, and
// the exit status alone cannot tell them apart.
int execErrno = 0;
int childPid = -1;
int waitStatus = -1;
int exitCode = -1;
int termSignal = -1;
// /proc/self/attr/current of THIS process - the domain the exec was attempted from.
std::string parentSelinuxContext;
std::string markerContent;
std::string childOutput;
std::string message;
};
SpawnSpikeResult RunSpawnSpike(const SpawnSpikeRequest& request);
} // namespace mobilegl_trace
@@ -1,74 +0,0 @@
#pragma once
// The generic environment passthrough of the retrace lane, split out of
// trace_replay_jni.cpp and trace_replay_core.cpp so a host-side test can pin it.
//
// One intent extra (`--es mobilegl_env "K=V;K=V"`) carries every MOBILEGL_* knob that has
// no dedicated flag, which is what PLAN-B.md §11 P0 needs when it adds MOBILEGL_PIPE_*.
// That makes this hand-rolled two-level parse the single point where the whole batch can
// be silently misread, and the only lane that exercises it end to end runs on a device -
// hence tools/trace_replay/trace_env_overrides_test.cpp, which every desktop configure
// that builds the replay runner runs at build time.
#include <cstddef>
#include <string>
#include <vector>
namespace mobilegl_trace {
// Splits `A;B;C` into its entries, dropping empty ones. Also used for the texture and
// FBO dump lists, whose entries carry their own ',' and ':' separators. A value that
// itself contains ';' therefore cannot be expressed - that is the format's limit, not a
// bug to work around here.
inline std::vector<std::string> SplitSemicolonList(const std::string& value) {
std::vector<std::string> values;
std::size_t begin = 0;
while (begin < value.size()) {
const std::size_t end = value.find(';', begin);
const std::string entry = value.substr(begin, end - begin);
if (!entry.empty()) {
values.push_back(entry);
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
return values;
}
enum class EnvOverrideAction {
// Nothing to do: the entry is empty, or names an empty key.
Ignore,
// setenv(key, value, 1). `K=` is a Set of the empty string, deliberately distinct
// from Unset: a knob read with getenv() != nullptr treats them differently.
Set,
// unsetenv(key). An entry with no '=' means this, and it is the only way for a
// caller to clear a variable the per-knob marshalling above it already set.
Unset,
};
// Classifies one `KEY=VALUE` / `KEY` entry. The first '=' separates; later ones belong to
// the value, so `KEY=a=b` sets KEY to `a=b`.
inline EnvOverrideAction ParseEnvOverride(const std::string& entry,
std::string* key,
std::string* value) {
key->clear();
value->clear();
const std::size_t separator = entry.find('=');
if (separator == std::string::npos) {
if (entry.empty()) {
return EnvOverrideAction::Ignore;
}
*key = entry;
return EnvOverrideAction::Unset;
}
if (separator == 0) {
return EnvOverrideAction::Ignore;
}
*key = entry.substr(0, separator);
*value = entry.substr(separator + 1);
return EnvOverrideAction::Set;
}
} // namespace mobilegl_trace
@@ -4,7 +4,6 @@
#include "apitrace_exit.hpp"
#include "png.h"
#include "trace_benchmark.hpp"
#include "trace_env_overrides.hpp"
#include <algorithm>
#include <cerrno>
@@ -131,32 +130,6 @@ std::string JsonEscape(const std::string& value) {
return out.str();
}
// Generic environment passthrough. One intent extra carries `K=V;K=V`, so a new
// MOBILEGL_* knob costs nothing in the five files between the CI script and this
// setenv - the per-knob plumbing above is what this replaces going forward
// (PLAN-B.md §11 P0, which adds a batch of MOBILEGL_PIPE_* switches).
//
// Applied last, immediately before the library is loaded: it is the escape hatch, so it
// has to be able to override the fields marshalled above, and MobileGL's ConfigLoader
// reads the environment during dlopen. The decision of what each entry means lives in
// trace_env_overrides.hpp so a host-side test can pin it; this is only the setenv.
void ApplyEnvOverrides(const std::vector<std::string>& entries) {
for (const std::string& entry : entries) {
std::string key;
std::string value;
switch (ParseEnvOverride(entry, &key, &value)) {
case EnvOverrideAction::Set:
setenv(key.c_str(), value.c_str(), 1);
break;
case EnvOverrideAction::Unset:
unsetenv(key.c_str());
break;
case EnvOverrideAction::Ignore:
break;
}
}
}
bool LoadMobileGL(const Request& request, std::string& error) {
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
@@ -234,8 +207,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
setenv("MOBILEGL_TRACE_DUMP_TEXTURE_2D", dumpPoints.c_str(), 1);
}
ApplyEnvOverrides(request.envOverrides);
void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL);
if (handle == nullptr) {
const char* dlError = dlerror();
@@ -63,11 +63,6 @@ struct Request {
bool deriveNumSubgroups = false;
bool iterationRPFixBarrier = false;
int holdMs = 0;
// Generic environment passthrough, each entry `KEY=VALUE` (an entry with no '='
// unsets KEY). Applied last, right before libMobileGL.so is loaded, so a knob that
// has no dedicated field above can still be forwarded from the CI script without
// touching this struct again.
std::vector<std::string> envOverrides;
};
struct Result {
@@ -1,8 +1,5 @@
#include "trace_replay_core.hpp"
#include "spawn_spike.hpp"
#include "trace_env_overrides.hpp"
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <jni.h>
@@ -29,7 +26,22 @@ std::string ToString(JNIEnv* env, jstring value) {
return out;
}
using mobilegl_trace::SplitSemicolonList;
std::vector<std::string> SplitSemicolonList(const std::string& value) {
std::vector<std::string> values;
std::size_t begin = 0;
while (begin < value.size()) {
const std::size_t end = value.find(';', begin);
const std::string entry = value.substr(begin, end - begin);
if (!entry.empty()) {
values.push_back(entry);
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
return values;
}
jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) {
jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult");
@@ -117,8 +129,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean benchmarkMode,
jint benchmarkTailFrames,
jboolean benchmarkFinish,
jstring benchmarkResultPath,
jstring envOverrides) {
jstring benchmarkResultPath) {
mobilegl_trace::Request request;
request.tracePath = ToString(env, tracePath);
request.goldenPath = ToString(env, goldenPath);
@@ -155,7 +166,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
: mobilegl_trace::kDefaultBenchmarkTailFrames;
request.benchmarkFinish = benchmarkFinish == JNI_TRUE;
request.benchmarkResultPath = ToString(env, benchmarkResultPath);
request.envOverrides = SplitSemicolonList(ToString(env, envOverrides));
ScopedTraceReplayState replayState;
mobilegl_trace_set_requested_size(request.width, request.height);
@@ -186,20 +196,3 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
}
return MakeResult(env, result);
}
// P0 spike A: exec the packaged MobileGLServer stub from this app process and report what
// happened. Deliberately a separate entry point rather than another parameter on the
// replay call - it shares nothing with a replay, and the trace lane must be able to run
// it without a trace, a golden or a surface.
extern "C" JNIEXPORT jstring JNICALL
Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunSpawnSpike(JNIEnv* env,
jclass,
jstring serverPath,
jstring markerPath) {
mobilegl_trace::SpawnSpikeRequest request;
request.serverPath = ToString(env, serverPath);
request.markerPath = ToString(env, markerPath);
const mobilegl_trace::SpawnSpikeResult result = mobilegl_trace::RunSpawnSpike(request);
return env->NewStringUTF(result.message.c_str());
}
@@ -54,14 +54,6 @@ public final class TraceReplayActivity extends Activity {
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
));
// P0 spike A: when asked, exec the packaged server stub out of nativeLibraryDir
// instead of replaying anything. This mode needs no trace and no render surface.
String spikeLibrary = spawnSpikeLibrary(intent);
if (spikeLibrary != null) {
runSpawnSpike(spikeLibrary);
return;
}
SurfaceHolder holder = surfaceView.getHolder();
if (request.width > 0 && request.height > 0) {
holder.setFixedSize(request.width, request.height);
@@ -131,8 +123,7 @@ public final class TraceReplayActivity extends Activity {
request.benchmark,
request.benchmarkTailFrames,
request.benchmarkFinish,
request.benchmarkResultPath,
request.envOverrides
request.benchmarkResultPath
);
Log.i(TAG, result.toString());
TraceReplayResult finalResult = result;
@@ -172,57 +163,9 @@ public final class TraceReplayActivity extends Activity {
boolean benchmark,
int benchmarkTailFrames,
boolean benchmarkFinish,
String benchmarkResultPath,
String envOverrides
String benchmarkResultPath
);
// ---------------------------------------------------------------------------
// P0 spike A: prove an APK can ship a second native executable and exec it.
//
// The exec has to happen here, in the application's own process: an `adb shell
// run-as` invocation runs in a different SELinux domain, so it can succeed while
// the real app is denied. The child reports the domain it ended up in, and the
// parent reports the domain it spawned from, so the log line stands on its own.
// ---------------------------------------------------------------------------
private static final String EXTRA_SPAWN_SPIKE = "mobilegl_spike_spawn";
private static final String DEFAULT_SPAWN_SPIKE_LIBRARY = "libMobileGLServer.so";
private static String spawnSpikeLibrary(Intent intent) {
if (!intent.hasExtra(EXTRA_SPAWN_SPIKE)) {
return null;
}
// Accepts --ez (boolean, arrives as a null string) and --es with either a truthy
// marker or the library file name to exec.
String value = intent.getStringExtra(EXTRA_SPAWN_SPIKE);
if (value == null || value.isEmpty() || "1".equals(value) || "true".equals(value)) {
return DEFAULT_SPAWN_SPIKE_LIBRARY;
}
return value;
}
private void runSpawnSpike(String libraryName) {
// The surface callbacks fire regardless; this keeps them from starting a replay
// underneath the spike.
started = true;
File outputDir = new File(request.outputDir);
String serverPath = new File(getApplicationInfo().nativeLibraryDir, libraryName)
.getAbsolutePath();
String markerPath = new File(outputDir, "spike-spawn.txt").getAbsolutePath();
statusView.setText("Running spawn spike\n" + serverPath);
new Thread(() -> {
outputDir.mkdirs();
String message = nativeRunSpawnSpike(serverPath, markerPath);
Log.i(TAG, message);
runOnUiThread(() -> {
statusView.setText(message);
finish();
});
}, "MobileGLSpawnSpike").start();
}
private static native String nativeRunSpawnSpike(String serverPath, String markerPath);
private static final class TraceReplayRequest {
final String tracePath;
final String goldenPath;
@@ -256,9 +199,6 @@ public final class TraceReplayActivity extends Activity {
final int benchmarkTailFrames;
final boolean benchmarkFinish;
final String benchmarkResultPath;
// Generic environment passthrough, `K=V;K=V`. A future MOBILEGL_* knob needs no
// new intent extra, no new JNI parameter and no new field beside this one.
final String envOverrides;
private TraceReplayRequest(
String tracePath,
@@ -289,8 +229,7 @@ public final class TraceReplayActivity extends Activity {
boolean benchmark,
int benchmarkTailFrames,
boolean benchmarkFinish,
String benchmarkResultPath,
String envOverrides
String benchmarkResultPath
) {
this.tracePath = tracePath;
this.goldenPath = goldenPath;
@@ -321,7 +260,6 @@ public final class TraceReplayActivity extends Activity {
this.benchmarkTailFrames = benchmarkTailFrames;
this.benchmarkFinish = benchmarkFinish;
this.benchmarkResultPath = benchmarkResultPath;
this.envOverrides = envOverrides;
}
static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) {
@@ -358,8 +296,7 @@ public final class TraceReplayActivity extends Activity {
intent.getBooleanExtra("benchmark", false),
intent.getIntExtra("benchmark_tail_frames", 200),
intent.getBooleanExtra("benchmark_finish", true),
benchmarkResultPath,
readString(intent, "mobilegl_env", "")
benchmarkResultPath
);
}
-14
View File
@@ -32,7 +32,6 @@ Usage:
[--avoid-angle-llvmpipe-explicit-lod-bias] \
[--coherent-as-flush] \
[--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \
[--env "K=V;K=V"] \
[--benchmark] \
[--benchmark-tail-frames N] \
[--benchmark-finish 0|1] \
@@ -61,11 +60,6 @@ copies benchmark.json (per-frame times plus mean/median/p95) out of the app, and
"passed" only means the replay reached the end of the trace without an error.
Pass --reuse-fixture to skip re-extracting and re-pushing the trace, for repeat
runs of a case whose fixture is already in /data/local/tmp.
Pass --env "K=V;K=V" (or set MOBILEGL_TRACE_ENV) to hand arbitrary environment
variables to the replay process. They are applied last, immediately before
libMobileGL.so is loaded, so they override every flag above; an entry with no "="
unsets the variable instead. This is the generic passthrough: a MOBILEGL_* knob
that has no flag of its own needs no plumbing to be forwarded.
EOF
}
@@ -125,7 +119,6 @@ avoid_angle_llvmpipe_sampler_mipmap_min_filter=0
avoid_angle_llvmpipe_explicit_lod_bias=0
coherent_as_flush=0
texture_2d_dumps=""
env_overrides="${MOBILEGL_TRACE_ENV:-}"
benchmark=0
benchmark_tail_frames=200
benchmark_finish=1
@@ -171,7 +164,6 @@ while [ "$#" -gt 0 ]; do
;;
--coherent-as-flush) coherent_as_flush=1; shift 1 ;;
--dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;;
--env) env_overrides="$(next_arg "$@")"; shift 2 ;;
--benchmark) benchmark=1; shift 1 ;;
--benchmark-tail-frames) benchmark_tail_frames="$(next_arg "$@")"; shift 2 ;;
--benchmark-finish) benchmark_finish="$(next_arg "$@")"; shift 2 ;;
@@ -408,12 +400,6 @@ run_retrace() {
if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi
if [ -n "${env_overrides}" ]; then
# adb joins the argv with spaces and hands the result to the device shell, so a value
# holding the ';' that separates entries would otherwise be read there as a command
# separator. The single quotes make it one token again.
set -- "$@" --es mobilegl_env "'${env_overrides}'"
fi
if [ "${benchmark}" -eq 1 ]; then
set -- "$@" --ez benchmark true
set -- "$@" --ei benchmark_tail_frames "${benchmark_tail_frames}"
-17
View File
@@ -5,16 +5,6 @@ def mobileGlLogActiveLevel = {
}
def standalonePluginBuild = rootProject.name == 'MobileGLPlugin'
// P0 spike A opt-in. OFF by default: the spike binary is dead weight in every trace APK
// that is not running the spike, and a flavour that silently carries an extra executable
// is exactly the kind of thing nobody notices until it ships. Turn it on for the spike
// build only, with either
// ./gradlew :app:assembleTraceDebug -Pmobilegl.buildServerSpike=ON
// or MOBILEGL_BUILD_SERVER_SPIKE=ON in the environment.
def mobileGlBuildServerSpike = {
(rootProject.findProperty('mobilegl.buildServerSpike') ?: System.getenv('MOBILEGL_BUILD_SERVER_SPIKE') ?: 'OFF') as String
}
android {
namespace 'top.mobilegl.mobilegl'
compileSdk 34
@@ -63,13 +53,6 @@ android {
externalNativeBuild {
cmake {
arguments '-DMOBILEGL_TRACE_ANGLE_VARIANTS=ON'
// P0 spike A: the second native executable that proves an APK
// can ship one and exec it from nativeLibraryDir. Off unless
// asked for, and only offered here - the shipping plugin
// flavour cannot turn it on at all, so no released artifact can
// grow a binary nothing loads. See mobileGlBuildServerSpike
// above for the two ways to enable it.
arguments "-DMOBILEGL_BUILD_SERVER_SPIKE=${mobileGlBuildServerSpike()}"
}
}
}

Some files were not shown because too many files have changed in this diff Show More