mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
2
Commits
6672778b80
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eae98581f | ||
|
|
d7655247f7 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
-1
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
+1
-108
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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");
|
||||
@@ -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[];
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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";
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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 ()
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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, ©Read);
|
||||
gl.glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, ©Write);
|
||||
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());
|
||||
}
|
||||
|
||||
+4
-67
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user