mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
32
Commits
dev
...
6672778b80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6672778b80 | ||
|
|
6e0e3df372 | ||
|
|
bee22f9d26 | ||
|
|
8952b14024 | ||
|
|
458ccde176 | ||
|
|
901d48a678 | ||
|
|
e8ee7b1a88 | ||
|
|
1154f9a00d | ||
|
|
7ef7c7e543 | ||
|
|
6c7ad0a1bf | ||
|
|
38d4c2372c | ||
|
|
8a239177ac | ||
|
|
87ee17c68c | ||
|
|
aa005720d0 | ||
|
|
c1a7ffac94 | ||
|
|
bdd4bed431 | ||
|
|
10315e71f3 | ||
|
|
bfa087d0f7 | ||
|
|
a1e22c26ab | ||
|
|
bd2b4158e0 | ||
|
|
9c7339b214 | ||
|
|
50815a232e | ||
|
|
d380a01f32 | ||
|
|
7566a0b002 | ||
|
|
42e0f47ebb | ||
|
|
9bbf71990c | ||
|
|
2f8d0f0d51 | ||
|
|
3363258908 | ||
|
|
9c773182bb | ||
|
|
8349babe90 | ||
|
|
1794ac94b1 | ||
|
|
8b31de2f8d |
@@ -295,6 +295,33 @@ 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
|
||||
@@ -778,3 +805,57 @@ 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,3 +34,6 @@
|
||||
[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
|
||||
|
||||
+1
Submodule 3rdparty/flatbuffers added at 7e163021e5
+108
@@ -14,6 +14,14 @@ 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")
|
||||
|
||||
@@ -238,6 +246,8 @@ 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
|
||||
@@ -417,6 +427,49 @@ 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
|
||||
@@ -467,11 +520,19 @@ 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}
|
||||
@@ -482,6 +543,13 @@ 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}
|
||||
)
|
||||
@@ -698,3 +766,43 @@ 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,6 +316,46 @@ 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,6 +159,38 @@ 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");
|
||||
@@ -207,6 +239,21 @@ 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,6 +17,7 @@
|
||||
#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>
|
||||
@@ -42,6 +43,11 @@ 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,
|
||||
@@ -102,6 +108,10 @@ 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,9 +192,23 @@ 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);
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
// 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.
|
||||
// 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
|
||||
@@ -364,6 +378,19 @@ 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,8 +1255,6 @@ 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;
|
||||
@@ -1417,6 +1415,13 @@ 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,6 +28,7 @@
|
||||
#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>
|
||||
@@ -1412,10 +1413,21 @@ 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);
|
||||
@@ -1505,6 +1517,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1534,6 +1550,11 @@ 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
|
||||
@@ -1546,6 +1567,13 @@ 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) {
|
||||
@@ -2006,8 +2034,22 @@ 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();
|
||||
|
||||
@@ -2924,6 +2966,14 @@ 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,
|
||||
@@ -3370,6 +3420,10 @@ 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;
|
||||
@@ -3389,6 +3443,11 @@ 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);
|
||||
}
|
||||
@@ -4306,6 +4365,12 @@ 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;
|
||||
}
|
||||
@@ -7252,138 +7317,22 @@ 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;
|
||||
|
||||
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;
|
||||
if (g_GLESFuncs.glGetIntegeri_v) {
|
||||
g_GLESFuncs.glGetIntegeri_v(target, index, data);
|
||||
} else {
|
||||
*data = 0;
|
||||
}
|
||||
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
|
||||
@@ -10638,6 +10587,12 @@ 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,8 +92,6 @@ 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,6 +15,7 @@
|
||||
#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>
|
||||
@@ -779,6 +780,12 @@ 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;
|
||||
@@ -886,6 +893,13 @@ 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
|
||||
@@ -953,6 +967,10 @@ 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)) {
|
||||
@@ -1520,6 +1538,13 @@ 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();
|
||||
@@ -2545,6 +2570,10 @@ 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,
|
||||
@@ -2575,6 +2604,10 @@ 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;
|
||||
@@ -2669,6 +2702,12 @@ 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;
|
||||
@@ -4391,6 +4430,42 @@ 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,6 +9,7 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -156,7 +157,10 @@ 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.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
// 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) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
@@ -169,6 +173,9 @@ 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;
|
||||
}
|
||||
@@ -183,7 +190,8 @@ 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, SizeT& outOffset) {
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
@@ -207,6 +215,9 @@ 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;
|
||||
@@ -417,7 +428,8 @@ 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(), commandBase)) {
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -532,7 +544,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -737,10 +750,16 @@ 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())) {
|
||||
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)) {
|
||||
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,8 +740,6 @@ 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;
|
||||
@@ -939,6 +937,15 @@ 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,6 +13,7 @@
|
||||
#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>
|
||||
@@ -77,7 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 blockBindingVersion = 0;
|
||||
Vector<StorageBlockResource> storageBlocks;
|
||||
Vector<BufferVariableResource> bufferVariables;
|
||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||
};
|
||||
|
||||
struct DrawElementsIndirectCommand {
|
||||
@@ -208,16 +208,6 @@ 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) {
|
||||
@@ -682,130 +672,40 @@ 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;
|
||||
@@ -1430,5 +1330,12 @@ 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,8 +95,6 @@ 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,6 +20,7 @@
|
||||
#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>
|
||||
@@ -2076,6 +2077,16 @@ 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;
|
||||
}
|
||||
|
||||
@@ -2283,6 +2294,13 @@ 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,6 +10,8 @@
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
|
||||
@@ -229,8 +231,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
|
||||
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
|
||||
(void)kind;
|
||||
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, 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;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::InitializeTransientArenas() {
|
||||
@@ -339,6 +371,9 @@ 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;
|
||||
}
|
||||
@@ -353,6 +388,11 @@ 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;
|
||||
@@ -422,6 +462,8 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +489,9 @@ 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;
|
||||
}
|
||||
@@ -484,6 +529,9 @@ 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;
|
||||
}
|
||||
@@ -554,6 +602,13 @@ 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;
|
||||
@@ -602,6 +657,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -681,6 +740,9 @@ 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,6 +13,7 @@
|
||||
#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>
|
||||
@@ -3150,6 +3151,32 @@ 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,6 +27,7 @@
|
||||
#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"
|
||||
@@ -3266,8 +3267,9 @@ void main() {
|
||||
}
|
||||
m_vertexInputStateFactory.reset();
|
||||
m_xfbCounterBuffer.Destroy();
|
||||
m_xfbCounterSlotByObject.clear();
|
||||
m_xfbNextCounterSlot = 0;
|
||||
m_xfbCounterSlotOwner.fill(0);
|
||||
m_xfbCounterSlotLastUse.fill(0);
|
||||
m_xfbCounterSlotUseSerial = 0;
|
||||
m_xfbCountersValid.fill(false);
|
||||
m_xfbLastSeenGeneration.fill(0);
|
||||
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
|
||||
@@ -4999,9 +5001,19 @@ 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
|
||||
@@ -5242,6 +5254,24 @@ 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,
|
||||
@@ -5890,8 +5920,18 @@ 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
|
||||
@@ -6365,6 +6405,15 @@ 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;
|
||||
}
|
||||
|
||||
@@ -6390,9 +6439,27 @@ 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)) {
|
||||
@@ -11133,16 +11200,66 @@ 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() {
|
||||
const Uint name = MG_State::pGLContext->GetBoundTransformFeedbackName();
|
||||
const auto it = m_xfbCounterSlotByObject.find(name);
|
||||
if (it != m_xfbCounterSlotByObject.end()) {
|
||||
return it->second;
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
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;
|
||||
return slot;
|
||||
}
|
||||
|
||||
|
||||
@@ -675,8 +675,18 @@ 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;
|
||||
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||
Uint32 m_xfbNextCounterSlot = 0;
|
||||
// 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;
|
||||
// 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,6 +51,7 @@ endif()
|
||||
add_executable(MobileGLIntegrationTest
|
||||
Main.cpp
|
||||
Harness/HeadlessGL.cpp
|
||||
Harness/BackendCapsPeek.cpp
|
||||
Scenarios/OrientationScenario.cpp
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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,9 +26,11 @@
|
||||
// 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"
|
||||
|
||||
@@ -378,5 +380,250 @@ 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
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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
|
||||
@@ -0,0 +1,806 @@
|
||||
// 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
|
||||
@@ -0,0 +1,177 @@
|
||||
// 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).
|
||||
@@ -0,0 +1,238 @@
|
||||
// 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
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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");
|
||||
@@ -0,0 +1,308 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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[];
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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");
|
||||
@@ -0,0 +1,302 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
// 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;
|
||||
@@ -0,0 +1,931 @@
|
||||
// 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
@@ -0,0 +1,120 @@
|
||||
// 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
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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";
|
||||
@@ -0,0 +1,259 @@
|
||||
// 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
|
||||
@@ -0,0 +1,268 @@
|
||||
// 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
|
||||
@@ -0,0 +1,323 @@
|
||||
// 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
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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
|
||||
@@ -0,0 +1,208 @@
|
||||
// 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
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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
|
||||
@@ -0,0 +1,293 @@
|
||||
// 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
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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
|
||||
@@ -0,0 +1,306 @@
|
||||
// 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
|
||||
@@ -0,0 +1,256 @@
|
||||
// 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
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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
|
||||
@@ -0,0 +1,191 @@
|
||||
// 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
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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,6 +14,8 @@
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <Config.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_State {
|
||||
void Init() {
|
||||
MGLOG_D("Initializing MobileGL State...");
|
||||
@@ -1259,6 +1261,33 @@ 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) {
|
||||
@@ -1295,6 +1324,9 @@ 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() = default;
|
||||
GLContext();
|
||||
|
||||
// Error
|
||||
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
|
||||
@@ -427,6 +427,27 @@ 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;
|
||||
@@ -514,6 +535,9 @@ 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 {
|
||||
@@ -532,6 +556,10 @@ 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();
|
||||
@@ -540,6 +568,10 @@ 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,9 +9,21 @@
|
||||
#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,9 +42,20 @@ 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,6 +37,11 @@ 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;
|
||||
@@ -460,8 +465,20 @@ namespace {
|
||||
if (data == nullptr) return;
|
||||
for (int i = 0; i < 4; ++i) data[i] = GL_TRUE;
|
||||
};
|
||||
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) {
|
||||
if (data != nullptr) *data = 0;
|
||||
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.glGetProgramInfoLog = [](GLuint, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
|
||||
if (infoLog != nullptr && bufSize > 0) infoLog[0] = '\0';
|
||||
@@ -1551,3 +1568,23 @@ 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,6 +85,9 @@ 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)
|
||||
@@ -94,3 +97,8 @@ 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()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -45,6 +45,31 @@ 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,6 +32,7 @@
|
||||
#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;
|
||||
@@ -138,3 +139,20 @@ 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");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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,6 +35,25 @@ 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)
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
// 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
|
||||
@@ -0,0 +1,36 @@
|
||||
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 ()
|
||||
@@ -0,0 +1,305 @@
|
||||
// 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));
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
// 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,6 +1145,8 @@ 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;
|
||||
@@ -1276,6 +1278,17 @@ 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,
|
||||
@@ -1584,6 +1597,10 @@ 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
|
||||
@@ -1681,6 +1698,10 @@ 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,6 +1309,11 @@ 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,6 +196,10 @@ 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 =
|
||||
@@ -333,6 +337,10 @@ 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,6 +56,11 @@ 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.
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
// 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
|
||||
@@ -0,0 +1,192 @@
|
||||
// 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
|
||||
@@ -234,6 +234,10 @@ 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)
|
||||
@@ -249,4 +253,5 @@ target_link_libraries(trace_replay_runner
|
||||
retrace_common
|
||||
image
|
||||
android
|
||||
log
|
||||
dl)
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// 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
|
||||
@@ -0,0 +1,60 @@
|
||||
#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
|
||||
@@ -0,0 +1,74 @@
|
||||
#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,6 +4,7 @@
|
||||
#include "apitrace_exit.hpp"
|
||||
#include "png.h"
|
||||
#include "trace_benchmark.hpp"
|
||||
#include "trace_env_overrides.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
@@ -130,6 +131,32 @@ 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);
|
||||
@@ -207,6 +234,8 @@ 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,6 +63,11 @@ 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,5 +1,8 @@
|
||||
#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>
|
||||
@@ -26,22 +29,7 @@ std::string ToString(JNIEnv* env, jstring value) {
|
||||
return out;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
using mobilegl_trace::SplitSemicolonList;
|
||||
|
||||
jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) {
|
||||
jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult");
|
||||
@@ -129,7 +117,8 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
||||
jboolean benchmarkMode,
|
||||
jint benchmarkTailFrames,
|
||||
jboolean benchmarkFinish,
|
||||
jstring benchmarkResultPath) {
|
||||
jstring benchmarkResultPath,
|
||||
jstring envOverrides) {
|
||||
mobilegl_trace::Request request;
|
||||
request.tracePath = ToString(env, tracePath);
|
||||
request.goldenPath = ToString(env, goldenPath);
|
||||
@@ -166,6 +155,7 @@ 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);
|
||||
@@ -196,3 +186,20 @@ 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());
|
||||
}
|
||||
|
||||
+67
-4
@@ -54,6 +54,14 @@ 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);
|
||||
@@ -123,7 +131,8 @@ public final class TraceReplayActivity extends Activity {
|
||||
request.benchmark,
|
||||
request.benchmarkTailFrames,
|
||||
request.benchmarkFinish,
|
||||
request.benchmarkResultPath
|
||||
request.benchmarkResultPath,
|
||||
request.envOverrides
|
||||
);
|
||||
Log.i(TAG, result.toString());
|
||||
TraceReplayResult finalResult = result;
|
||||
@@ -163,9 +172,57 @@ public final class TraceReplayActivity extends Activity {
|
||||
boolean benchmark,
|
||||
int benchmarkTailFrames,
|
||||
boolean benchmarkFinish,
|
||||
String benchmarkResultPath
|
||||
String benchmarkResultPath,
|
||||
String envOverrides
|
||||
);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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;
|
||||
@@ -199,6 +256,9 @@ 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,
|
||||
@@ -229,7 +289,8 @@ public final class TraceReplayActivity extends Activity {
|
||||
boolean benchmark,
|
||||
int benchmarkTailFrames,
|
||||
boolean benchmarkFinish,
|
||||
String benchmarkResultPath
|
||||
String benchmarkResultPath,
|
||||
String envOverrides
|
||||
) {
|
||||
this.tracePath = tracePath;
|
||||
this.goldenPath = goldenPath;
|
||||
@@ -260,6 +321,7 @@ 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) {
|
||||
@@ -296,7 +358,8 @@ public final class TraceReplayActivity extends Activity {
|
||||
intent.getBooleanExtra("benchmark", false),
|
||||
intent.getIntExtra("benchmark_tail_frames", 200),
|
||||
intent.getBooleanExtra("benchmark_finish", true),
|
||||
benchmarkResultPath
|
||||
benchmarkResultPath,
|
||||
readString(intent, "mobilegl_env", "")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ 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] \
|
||||
@@ -60,6 +61,11 @@ 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
|
||||
}
|
||||
|
||||
@@ -119,6 +125,7 @@ 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
|
||||
@@ -164,6 +171,7 @@ 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 ;;
|
||||
@@ -400,6 +408,12 @@ 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,6 +5,16 @@ 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
|
||||
@@ -53,6 +63,13 @@ 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()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
# MGPipe 设计与架构
|
||||
|
||||
> 本文描述**已决定**的设计。每条决定附一行理由;数字凡有实测的取实测(见 `MEASUREMENTS.md`)。落地状态以 `feat/disaggregated@458ccde1` 为准:标注"P0 已落地"的是树里的代码,其余是后续阶段要实现的形状(阶段号见 `ROADMAP.md`)。
|
||||
|
||||
## 1. 边界
|
||||
|
||||
### 1.1 一句话
|
||||
|
||||
`MG_Backend` 已经是一台贴着目标 API 的状态机(Espryt 有逐字节的渲染状态镜像、6 个 twin registry、三条 persistent ring;Magma 有 `SetupDrawSnapshot`、pipeline memo、5 个 `Vk*Manager`)。它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明:前端在每条 verb 之前把变化**推**过去,后端不再拉 `MG_State::pGLContext`。server 进程因此只装 `MG_Backend` + MGPipe 对象表,不链接 `MG_State`、`MG_Impl`、glslang。
|
||||
|
||||
接口不是从 gallium 自顶向下设计的,而是从两个后端自己维护的关键结构反推出来的:`SetupDrawSnapshot` 的字段并集 → `set_*` 组;`DrawTextureSyncKeys` → `set_sampler_views`+`create_sampler_view`+`set_texture_params`;`ResolvedDrawBuffers`/`ResolvedVertexBindings` → vertex elements 三件;`g_syncedRenderStateParameters` → render-state CSO;`UnpackStagingBlock` → `MGPSubData` 的 region 形状;`BufferBackendOps`(7 个 hook,注释自称 `pipe_context` 类比)→ `resource_*` 全族。gallium 是目的地(词汇可读、可迁移),不是推导前提;与 gallium 的十条偏离见 §3.5。
|
||||
|
||||
### 1.2 两张函数指针表
|
||||
|
||||
`MGPipeScreen`(share-group 作用域:caps、resource、persistent map、fence)与 `MGPipeContext`(其余全部:query 命名空间、CSO、`set_*`、对象操作、verb),由 `PipeCalls.def` 经 G1 生成(`MG_Pipe/generated/PipeTables.inc`)。**P0 已落地。**
|
||||
|
||||
- 函数指针 struct 而非虚基类:边界今天就是函数指针 struct(`gBackendFunctionsTable`);**null 项已经表示"未实现,前端回退"**,正好就是"这个子系统还没迁移,继续拉取";`MG_Test` 已用替换整张表的方式 mock 后端。
|
||||
- 两张表从第一天分开:事后拆分意味着给记录重新编号。v1 只有一个 screen、一个 context、一条 flow(`pGLContext` 是进程全局,share group 全库无人读取)。
|
||||
- EGL 生命周期 8 项与 caps 面留在 `pActiveBackendObject` 的虚函数上(罕见路径)。
|
||||
|
||||
### 1.3 三种形态,一份后端
|
||||
|
||||
| 形态 | 表里装的是什么 | 用途 |
|
||||
|---|---|---|
|
||||
| `monolith`(默认) | backend 自己的函数;`MGPipeCallbacks` 是对 `MG_State` 的直调;`MGHostSpan.Ptr` 指向 client shadow(零新增拷贝) | 出货 |
|
||||
| `inproc` | 发射器 → 同进程第二个线程上的 applier | CI 形态;同时就是 monolith 的**渲染线程**(把 `PrepareForDraw` 与驱动调用搬离 GL 线程,是本项目手上最大的单一 CPU 杠杆) |
|
||||
| `spawn` | 发射器 → SPSC shm ring → 另一个进程的 applier → 同一批 backend 函数 | 两进程出货形态 |
|
||||
|
||||
唯一 hook 点是 `MG_Backend::Init()`(`MG_Backend/Init.cpp`)里一个 `#if MOBILEGL_BUILD_DISAGGREGATED` 分支:`MG_Config::Transport != Monolith` 时装 `MG_Remote::BackendObject_Remote`,否则走今天的 `switch`。下游 `MG_Impl` 的边界调用点零 `#ifdef`。(分支在 P5 落地;P0 的 `Init.cpp` 尚未含它。)
|
||||
|
||||
## 2. 对象模型
|
||||
|
||||
### 2.1 句柄 = `{slot, gen}`(P0 已落地,`MG_Pipe/MGPipeHandles.h`)
|
||||
|
||||
- 8 字节 POD,按值走寄存器对;**client 铸造,server 永不返回句柄** → 整份目录零创建 round trip(对 gallium 的偏离 D1)。
|
||||
- slot 稠密、**按 kind 分配**(free list + 高水位),server 对象表是数组而非哈希表。与 `IndexGenerator` 无关——后者的 LIFO 名字复用正是句柄要关掉的问题。
|
||||
- `gen` 只在 slot 复用时 ++,不在 respecify 时 ++;同一 slot 复用 2³² 次才回绕(1000 fps 逐帧复用约 50 天),debug 分配器断言回绕。
|
||||
- kind:`Buffer, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context`。
|
||||
- 保留句柄:`{0,0}` = null;`{0,1}` of `Framebuffer` = 默认帧缓冲(退役 Espryt 四处 `pDefaultFramebufferInfo->defaultFBO` 身份比较);`ShaderCso` slot 空间的高 1/16 保留给 program pipeline 合成体(`MobileGL/MG_Pipe/MGPipeHandles.h:88-90`)。
|
||||
- GL name 只以 `GlNameForDiag` 出现在 `MGPResourceDesc` 里,永不做身份、永不进 memo 键或 content hash;`GetLifetimeId()` 留在 client 作 tracker 自己的身份,client 维护 `lifetimeId → slot`。
|
||||
|
||||
### 2.2 两种世代,严格分开
|
||||
|
||||
| | 拥有者 | 回答 | 过线 |
|
||||
|---|---|---|---|
|
||||
| `MGPipeHandle::Gen` | client | "还是同一个 GL 对象吗?" | 是 |
|
||||
| `MGGen`(`g_bufferMutationEpoch`、`m_textureImageEpoch`、`m_cacheStructureEpoch` 等 12 个后端纪元) | server | "我自己是否重铸了驱动对象?" | **永不**;server→client 只以纹理拉取请求出现(§8.4) |
|
||||
|
||||
规范:任何 MGPipe 调用不得要求 client 提供或知晓 `MGGen`;反过来,client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明——过线时要么加宽、要么与 `{slot, gen}` 同行。
|
||||
|
||||
### 2.3 CSO 与可变对象
|
||||
|
||||
| 类别 | 形态 | 对应后端已有缓存 |
|
||||
|---|---|---|
|
||||
| `VertexElementsCso` | create/bind/delete | `VertexInputStateFactory::m_cache` |
|
||||
| `SamplerCso` | create/delete + `bind_sampler_states` | `VkSamplerManager::m_samplers`、`BackendSamplerObject` |
|
||||
| `SamplerViewCso` | create/delete + `set_sampler_views` | `TextureResource::{perMipViews,…}`、`SyncTextureViewToBackend` |
|
||||
| `ShaderCso` | create/bind/delete + server 侧惰性特化 | `ProgramFactory::m_cache`、`BackendProgramObjectImpl` |
|
||||
| `RenderStateCso` | create/bind/delete,身份 = pipeline 子集 | Espryt 值镜像;Magma `ComputePipelineStateHash` |
|
||||
| Buffer / Texture / Renderbuffer | create / respecify / subdata / destroy | 各自 twin |
|
||||
| Framebuffer / Xfb | per-context 身份 + `set_*` payload | `BackendFramebufferObject`、`m_xfbCounterSlotByObject` |
|
||||
|
||||
CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map<xxHash, MGPipeHandle>`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。
|
||||
|
||||
## 3. 调用目录(P0 已落地)
|
||||
|
||||
### 3.1 单一真相源
|
||||
|
||||
`MobileGL/MG_Pipe/PipeCalls.def`:一行一个调用 `X(Name, PayloadStruct, Class, Flags)`。**线上 opcode 就是行在文件里的位置**(1-based),所以目录必须是唯一记录的集合,新调用只能**追加**到文件末尾、退役的调用保留槽位。`MGP_CALL_LIST_DOCUMENTED_COUNT = 71`(`MobileGL/MG_Pipe/PipeCalls.def:69`)由 `MG_Test/Pipe/PipeCatalogueTest.cpp` 钉住。
|
||||
|
||||
七个生成器(`scripts/gen_pipe.py`,产物提交进树,CI `pipe-gates` 重生成并 `git diff --exit-code`):
|
||||
|
||||
| | 产物 | 内容 |
|
||||
|---|---|---|
|
||||
| G1 | `PipeTables.inc` | 两张函数指针表 |
|
||||
| G2 | `PipeThunks.inc` | monolith 直调 thunk `MGP_<Name>()`,`MG_Impl` 的约 93 个 `gBackendFunctionsTable.GL.*` 站点逐名改到它上面 |
|
||||
| G3 | `PipeWire.inc` | wire 记录 + 每种一条尺寸 `static_assert` + applier 分发前的运行期边界检查 → `Fatal{ProtocolCorruption}` |
|
||||
| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` 的逐字段比对器(字段表来自 `PipeFields.def`;浮点按位比较,NaN patch level 不会误报) |
|
||||
| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id(61 个)与逐 verb 世代 poison |
|
||||
| G6 | `PipeCoverage.inc` | 477 行后端读点清单 → MGPipe 调用的映射(`Coverage.def` 手工维护一半):299 → 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED** |
|
||||
| G7 | `PipeSpanTable.inc` | render-state pipeline 子集的成员名表(24 个,取自 `ComputePipelineStateHash` 今天哈希的字段,`scripts/gen_pipe.py:67-92`);带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2 |
|
||||
|
||||
### 3.2 分组与计数
|
||||
|
||||
| Class | 条 | 内容 |
|
||||
|---|---|---|
|
||||
| `kScreen` | 11 | `GetCaps`(R)、`ResourceCreate/Respecify/Destroy`、`MapPersistent`(R,O)/`UnmapPersistent`(O)、`FenceCreate/Status(R)/Wait(R)/Destroy`、追加的 `FenceWaitServer`(`glWaitSync`,GPU 侧等待) |
|
||||
| `kCtxQuery` | 8 | `QueryCreate/Begin/End/Available(R)/Result(R)/Destroy`、追加的 `QueryTimestamp`(R)(`glGetInteger64v(GL_TIMESTAMP)`)与 `QueryCounter`(`glQueryCounter`) |
|
||||
| `kCtxCso` | 13 | create/delete × {render state, vertex elements, sampler, sampler view, shader} + bind × {render state, vertex elements, shader};sampler 与 sampler view 的绑定是下一组的批量调用 |
|
||||
| `kCtxState` | 17 | `SetDynamicState`(B)、`SetFramebufferState`、`SetVertexBuffers`(V)、`SetIndexBuffer`、`SetIndirectBuffers`、`SetSamplerViews`(V)、`BindSamplerStates`(V)、`SetShaderImages`(V)、`SetShaderBuffers`(V,H)、`SetStreamOutputTargets`(V)、`SetGlobalConstants`(B)、`SetVertexAttribDefaults`(V)、`SetPixelPackState`、`SetPatchState`、`SetDrawProgram`、`SetDispatchProgram`、迁移期临时的 `SetResidualValueState`(B) |
|
||||
| `kCtxObject` | 9 | 按资源寻址:`SetTextureParams`、`ResourceSubData`(B,V)、`BufferSubDataResident`(B,O)、`ResourceSubDataComplete`、`ResourceFlushRange`、`ResourceReadback`(R)、`ResourceCopyRegion`、`GenerateMipmap`、`GetTextureImage`(R) |
|
||||
| `kCtxVerb` | 13 | 按上下文寻址:`Blit`、`Clear`、`ReadPixels`(R)、`DrawVbo`(H,V)、`LaunchGrid`、`MemoryBarrier`、`Begin/End/Pause/ResumeStreamOutput`、`Flush`、`Present`、`SetSwapInterval`(O) |
|
||||
|
||||
Flags:`kNeedsAck`(调用方等 server 确认;目录里目前无条目携带,见 §8.3)、`kHasBlob`(B)、`kVarTail`(V)、`kHostSpan`(H)、`kReplySlot`(R,答进 `MGPReplySlot`,永不阻塞)、`kOptional`(O,后端表里可为 null:Magma 故意不注册 `BufferSubDataResident` 与 `SetSwapInterval`)。
|
||||
|
||||
- 今天 20 个 draw 入口塌成 `DrawVbo` 一条,`MGPDrawRange[]` 就是 `MultiDraw*` 族今天的形状;`Clear` 一条判别式合并 `glClear` + 4 个 `glClearBuffer*` + 4 个 `glClearNamedFramebuffer*`。
|
||||
- `SetSamplerViews` / `BindSamplerStates` **没有 stage 维度**:MobileGL 的纹理单元空间是合并的(`TextureState::m_textureUnits` 是 192 个单元的一个数组,每 stage 32 只是广告数字),同一单元可被两个 stage 采样;stage 只在目标 API 需要时由 server 从反射归档推导。
|
||||
- `SetTextureParams` 按资源寻址、与 sampler view 分开(D10):只作 FBO attachment / image 单元 / `glCopyImageSubData` 端点的纹理没有 sampler view,但 Espryt 对 attachment 也同步纹理参数,且 `RequireImageBindableStorage` 需要在前端参数版本不动时强制重同步。
|
||||
- `SetIndexBuffer` 独立于 VAO 配置版本(D5):索引 slot 重绑不移动 VAO config version。
|
||||
- `SetGlobalConstants` 只覆盖默认 uniform block(D6):`globalUboScratch` 是 link phase B 的 CPU 数组,没有 GL name、没有 `BufferObject`。
|
||||
|
||||
**显式不移植**:`GetIntegeri_v`/`GetInteger64i_v`/`GetProgramiv`(后两项 P0 已从 `GLFunctionsTable` 删除,`50815a23`;唯一属于后端的带下标答案 `GL_MAX_COMPUTE_WORK_GROUP_COUNT/SIZE` 进 `MGPCaps`,`e8ee7b1a`;`GL_COMPUTE_WORK_GROUP_SIZE` 是前端反射查询)、`ShaderStorageBlockBinding`(折进反射归档)、`set_pixel_unpack_state`(不存在:前端已在 `glTexImage` 时解析压缩格式、强制默认 unpack)、压缩格式概念、`pipe_transfer`。
|
||||
|
||||
### 3.3 能力位(`MGPCapBit`)
|
||||
|
||||
`kCapViewportArray`、`kCapFloat64VertexAttrib`、`kCapResidentSubData`、`kCapCpuXfbPrimitiveAccounting`、`kCapTimerQuery`、`kCapOcclusionQuery`、`kCapXfbPrimitivesQuery`、`kCapNeedsHostIndexBytes`(server 做 restart 重写 / multi-draw 展平,split 下开启索引宿主镜像,§10.3)、`kCapNeedsHostUboBytes`(server 把具名 UBO 打进自己的 ring,需要 `SetShaderBuffers` 的 host payload)。`CallMask` 取代"槽位是否为 null"这个隐式能力探测。
|
||||
|
||||
不存在 `kCapPrimitiveRestart` / `kCapMultiDraw*` 一类"归属开关"(D-B7):`ResolveTierForBatch` 逐 batch 用 `programReadsDrawID`(转译后 ESSL 的性质,只存在于 server)选档,两个后端都做 restart 重写,所以这类归属不可用 cap 表达。规则一句话:**multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说需要时提供索引字节。**
|
||||
|
||||
一个待转显式能力位的现有陷阱:`GL_Drawing.cpp` 把 `EndTransformFeedback` 槽位的非空当作"后端按 GL 顶点序捕获"来跳过 `FixupGsStripCaptureOrder`。MGPipe 下改为显式 `kCapDriverOrderedXfbCapture` 一类的位(P8/P9)。
|
||||
|
||||
`MGPCaps` = `DynamicBackendParameters`(整块包含,~90 个标量含六个 compute 限制)+ `CallMask` + 两个 blob(format 能力表、renderer 字符串),握手后一次快照,取代 40 个 `pActiveBackendObject->` 站点与 89 个 caps 读点。
|
||||
|
||||
## 4. 记录与 payload 约定(P0 已落地,`MG_Pipe/MGPipeTypes.h`)
|
||||
|
||||
- 每个 payload 是平坦 POD、显式 padding、`static_assert` 平凡可复制与**精确尺寸**;**永不含指针**。
|
||||
- `MGPBlobRef{Offset, Size, Seg}`(24 B)指向 blob 区:monolith 下 `Seg == kMGHostSpanSegNone`、Offset 是调用方 staging arena 内地址;split 下 Seg 命名传输段。
|
||||
- `MGHostSpan`(32 B,`MG_Pipe/MGPipeHostSpan.h`)是整份接口里**唯一形状随传输而变**的东西:monolith 下 `Ptr` 指向 shadow 或应用内存;split 下 `Ptr == nullptr`、字节在 `Seg/Offset` 命名的 `SEG_STAGE`,或 `Seg == kMGHostSpanSegFromServerIndexMirror`(`MobileGL/MG_Pipe/MGPipeHostSpan.h:26`)表示"字节已在你那边的索引镜像里"。`MGPipeHostBytes()` 是一次可预测分支;split 解析器 `gMGPipeSegmentResolver` 由 `MG_Remote` 安装。它只进变长尾(`DrawVbo` 的用户索引、`SetShaderBuffers` 的具名 UBO 字节),永不内联进定长 payload——VBO 路径(MC/Sodium 的全部 draw)不为它付字节。
|
||||
- 变长记录(`kVarTail`)= 定长前缀 + 自描述长度的内联尾巴;`kHasBlob` 记录额外校验 `BlobRef` 落在其声明的段内。运行期边界纪律:`SEG_CMD` 是对端并发写入的区域,`static_assert` 管不到运行期损坏,违反一律 `Fatal{ProtocolCorruption}`。
|
||||
- wire 记录头 `MGPWireRecHeader{Op:u16, Flags:u16, Size:u32}`(8 B),Size 含头、8 字节倍数;**没有逐记录序号字段**——seq 就是记录序数(producer `m_emitSeq++` / consumer `m_applySeq++`)。
|
||||
- **分块上界 = ring 容量的一半**(`RingProducer::MaxRecordBytes()`):这是每个 head 偏移都能放下的最大记录(wrap pad 最多花 total−8 字节),超过它的 payload(大 `ResourceSubData`、`CreateShaderState` 归档)由发射器切成多条;ring 对更大的记录直接拒绝(nullptr + `MGLOG_E`)而不是让 producer 等一个永远不够的空闲量。
|
||||
- `MGPSubData` 的 buffer 半边:`Target == Buffer` 时没有 level 与 box,目的字节范围搭在 `UnionBox.X`(offset)与 `UnionBox.W`(size)上,`MGPipeSetSubDataBufferRange()` 是唯一拼写;单条记录上限 offset 2³¹−1 / size 2³²−1,越界由发射器拆分。
|
||||
|
||||
### 4.1 关键 payload
|
||||
|
||||
| payload | 尺寸 | 要点 |
|
||||
|---|---|---|
|
||||
| `MGPResourceDesc` | 88 | buffer / 全部纹理 target / renderbuffer 一个判别式 create/respecify 形状;`BindMask` 的 `ELEMENT_ARRAY` 位是索引镜像的开关;`ImageBindableHint` 预防性分配 image-bindable 存储;`ViewOf` 是纹理视图的存储属主(server 侧 keep-alive);`BufferForTexBuffer/BufOffset/BufSize` 实时解析(`kMGPipeWholeBuffer = ~0`)。Renderbuffer 保持独立类(自己的 format-capability target、`ComponentSizes`、twin) |
|
||||
| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | 48 / **12** / 32 | §5.3 |
|
||||
| `MGPVertexElements` | 40 | blob 同时带解析后的 `VertexAttribute[]` **和** `VertexBufferBindingPoint[]`,缺一不可(pointer 调用的 stride 0 = element size,binding 模型的 stride 0 = 每顶点读同一 element);`IsLong` 与 `Type == Float64` 分开携带;仅供查询的 `LegacyStride/LegacyPointer` 留在 client |
|
||||
| `MGPSamplerDesc` | 32 | `SamplerParameters` 逐字节过线**含 `borderColorForm`**(三种 border color 表示永远都被数值填满,没有它后端无法在 `Iiv`/`fv` 或 `VkBorderColor` 家族间选择) |
|
||||
| `MGPSamplerView` / `MGPTextureParams` | 36 / 32 | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上 |
|
||||
| `MGPProgramDesc` | 192 | 逐 stage SPIR-V blob ×6 + 反射归档 blob + `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset` + 四个状态字节,§7 |
|
||||
| `MGPFramebufferState` | 304 | 8 color + depth + stencil + **client 解析后的 `ReadSurface`**(按结构消灭 read-buffer-shared-FBO 缺陷类);`MGPSurface::InternalFormat` 内联(四个跨对象 mask 推送时零查表);`ContentHash` 既是 server 的 render-pass memo 键也是 client 的发射抑制器 |
|
||||
| `MGPSubData` / `MGPSubRegion` | 72 / 40 | §6 |
|
||||
| `MGPDrawInfo` / `MGPDrawRange` / `MGPDrawIndirect` | **56** / 12 / 40 | `Flags` 门控 `MinIndex/MaxIndex`(只在 client-memory 数组路径算)与 `XfbCpuCapturedVertices`(只在 XFB scatter 路径读)——不是每 draw 都算;`NumDraws` 个 `MGPDrawRange` 在变长尾;用户索引的 `MGHostSpan` 只在 `kDrawHasUserIndices` 时进变长尾;indirect 的 `DrawCount` 由 client 解析,server 永不读 indirect 命令块来数 draw |
|
||||
| `MGPShaderBuffers` / `MGPBufferRange` | 32 / 24 | range 不内联 host span;`kCapNeedsHostUboBytes` 下 Uniform 类带第二个变长尾 `MGHostSpan[HostSpanCount]`,与 range 数组下标对齐 |
|
||||
| `MGPPixelPackState` | 28 | 只有 PACK 方向(D5) |
|
||||
| `MGPPatchState` | 40 | 同时是 shader variant 输入 |
|
||||
| `MGPClear` | 48 | Whole / Color / Depth / Stencil / DepthStencil 判别式 |
|
||||
| `MGPGlobalConstants` | 40 | `(ShaderCso, Version)` 键控,每 program 每帧至多一次 |
|
||||
| `MGPSubDataComplete` | 24 | 纹理拉取的正向终止符,可携带零个 region |
|
||||
| `ResidualValueBlock` | **1248** | 迁移期 Track V 载体,§9.4 |
|
||||
|
||||
每条 `kVarTail` 的 `set_*`(`SetVertexBuffers`、`SetSamplerViews`、`BindSamplerStates`、`SetShaderImages`、`SetShaderBuffers`、`SetStreamOutputTargets`)都带 `ContentHash`——与 `MGPFramebufferState` 同一模式,hash 未变就不发(§5.4)。
|
||||
|
||||
## 5. 前端 state tracker(`MG_Impl/Pipe/Tracker`,P2 起)
|
||||
|
||||
### 5.1 推送发生在 verb 之前的 validate 时刻,不在 GL setter 里
|
||||
|
||||
Blaze3D 每个 batch 用 `glEnable/glDisable(GL_BLEND)` 包住(Espryt 代码自己标它为最热路径),per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium `st_validate_state`。
|
||||
|
||||
八个 validate 入口,由 `PipeCalls.def` 的 `kCtxVerb`/`kCtxObject` 条目生成:`ValidateForDraw`(20 个 draw 入口)、`ValidateForDispatch`、`ValidateForClear`、`ValidateForBlitOrCopy`、`ValidateForTextureOp`(GenerateMipmap / CopyTex* / BindImageTexture)、`ValidateForReadback`、`ValidateForXfbSpan`、`ValidateForQuery`。八个而不是四个,因为 `MG_Impl` 用到的 70 个表项里只有约 22 个是 draw/dispatch,其余 ~48 个(clear、blit、copy、回读、barrier、XFB 跨度、query/sync)很多自己就读 `pGLContext`。
|
||||
|
||||
**只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送**——即 `BufferBackendOps` 的七个 hook。纹理 subdata 不在此列(§6)。
|
||||
|
||||
### 5.2 dirty 位:值类零新增记账,对象类新增 5 个聚合世代
|
||||
|
||||
| dirty 位 | 类 | 快门来源 |
|
||||
|---|---|---|
|
||||
| `NEW_RENDER_STATE` / `NEW_PIPELINE_STATE` | 值 | `m_version` / `m_pipelineStateVersion` |
|
||||
| `NEW_PIXEL_PACK`、`NEW_PATCH_STATE`(`BitwiseEqual`,NaN 合法)、`NEW_VERTEX_ATTRIB_DEFAULTS`、`NEW_VERTEX_ELEMENTS`(VAO config version) | 值 | 既有计数器 |
|
||||
| `NEW_SHADER`、`NEW_SHADER_BINDINGS`、`NEW_GLOBAL_CONSTANTS` | 值 | link/image-unit/backend-state/block-binding/uniform-write-set/UBO-content 版本 |
|
||||
| `NEW_VERTEX_BUFFERS` | 对象 | **`VertexArrayState::m_anyVaoAttributeGeneration`**(新增)→ 命中后走 32 属性前缀 |
|
||||
| `NEW_INDEX_BUFFER` | 对象 | 索引 slot 版本 + 绑定对象 `{slot,gen}` |
|
||||
| `NEW_FRAMEBUFFER` | 对象 | **`FramebufferState::m_anyAttachmentGeneration`**(新增)+ 对象/slot 版本 → 重算 `ContentHash` |
|
||||
| `NEW_SAMPLER_VIEWS`、`NEW_SAMPLERS`、`NEW_SHADER_IMAGES` | 对象 | **`TextureState::m_anyTextureContentGeneration` + `m_anyTextureParamsGeneration`**(新增)+ bind/sampling-resolution generation → 走 `GetMaxTouchedUnit()` 前缀、重算集合 hash |
|
||||
| `NEW_CONST_BUFFERS` / `NEW_SHADER_BUFFERS` / `NEW_SO_TARGETS` | 对象 | **`BufferState::m_anyBufferChangeGeneration`**(新增)→ 走 `GetTouchedBindPointCount()` 前缀 |
|
||||
|
||||
五个聚合世代全部落在既有 bump 点上(约 20 行),把对象类组的快门从"每 validate 走查 192 单元 / 84×4 绑定点 / 32 属性 / 40 attachment"降成一次 `Uint64` 比较;对象类不能靠轮询逐对象版本(没有聚合能回答"有没有哪张已绑定纹理动了",这正是 Magma 不得不用有损 `sampledContentSum` 的原因)。
|
||||
|
||||
完整性由 `scripts/gen_pipe_dirty_surface.py` 保证:枚举 `MG_Impl/GLImpl` 里每个 mutator → 必须 bump 的聚合世代,CI 重生成 + `git diff --exit-code`,未映射即失败(P1 起成为门)。**实测规模**:926 次 mutator 调用落在 73 个不同 mutator 上,其中 92 次(7 个 mutator,绝大多数 `RecordError`)位于同函数内也会到达后端的"即时发布点",其余 834 次由紧随其后的 verb 发布——映射表是 73 条目的问题。
|
||||
|
||||
三个回绕 `Uint16` 在 tracker 边界加宽(`m_lastPushed[]` 是 tracker 自己的字段,不改 `MG_State`);回绕在 tracker 本地无害(多一次重推,永不漏推),且被集合 hash 抑制器吞掉。
|
||||
|
||||
### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集,动态状态单独走(D-B1)
|
||||
|
||||
```
|
||||
create_render_state(cso, MGPBlobRef pipelineSubsetChunks) // 只带 pipeline 子集
|
||||
bind_render_state(cso, Uint16 version, Uint16 pipelineVersion) // 稳态 12 B
|
||||
set_dynamic_state(MGPBlobRef dynamicChunks, Uint16 version) // 只带动态子集的变化 chunk
|
||||
```
|
||||
|
||||
- 整块的理由:`RenderStateParameters` 是平凡可复制 POD,Espryt 自己 `static_assert` 并做 head/blend/tail 三段 memcmp,**字段顺序承重**(`ScissorBoxWrittenMask`、`ClipDistanceEnabledMask` 故意放在 tail 段);拆成 blend/depth-stencil/rasterizer 三个 CSO 要手工维护 ~150 字段划分表且无绊线。
|
||||
- 子集身份的理由:整块内容寻址会让 `glViewport`/`glScissor`/`glBlendColor`/`glClearColor` 每次铸造新 CSO、冲掉 server 的 pipeline memo——`RenderState.h` 记录的那次回归。`RenderState.cpp` 里 viewport/scissor/line-width 族只 `++m_version`,`SET_CAPABILITY` 与 pipeline 相关 setter 才 `BumpVersions()`。
|
||||
- 动态子集:viewport、scissor、depth range、blend color、line width、polygon offset、stencil ref/write mask、clear 值、sample coverage、hints、point-size 族。
|
||||
- 划分只写在一处:`MG_Pipe/MGPipeRenderStateSpans.{h,cpp}`(P2)的 chunk 表 + `MGPipeComputePipelineSubsetHash()`,从 Magma 的 `ComputePipelineStateHash` 搬来,client 与两个后端共用;G7 的 `MG_Test` 遍历每个 `RenderState` public setter,断言 `pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变`。
|
||||
- server 侧:每 context 一份 working `RenderStateParameters`(1168 B),`bind` 与 `set_dynamic_state` 各把自己的 chunk 散射进去。**Espryt 的 `SyncRenderState`(693 行)拿到的仍是 `const RenderStateParameters&`,单 `Uint16` 早退、三段 memcmp 一行不动**;Magma 的 pipeline memo 键是 `cso.slot`,动态尾巴仍走 `ApplyDynamicDrawStateTail`。Espryt 的 head/blend/tail 划分(驱动侧增量)与 pipeline/dynamic 划分(线上与身份)是两回事,并存、各有绊线。
|
||||
- client 取值顺序:`m_pipelineStateVersion` 未变 → 复用上一个 CSO handle,零哈希;变了 → 对 pipeline 子集算 xxHash(~25-30 字,Magma 今天就在算)→ CSO map 探测 → 命中发 12 B bind,未命中发变化 chunk 的 create 再 bind;`m_version` 变而子集未变 → 只发 `set_dynamic_state`(~200 B)。
|
||||
- `FramebufferSrgb` 与 `DepthClamp` 今天**没有存储**(`glEnable` 被静默吞掉且不报错,六个后端读点恒为 false);chunk 表冻结前要补真存储并把 `FramebufferSrgb` 划进 pipeline 半边(它改变 attachment/blend 的解释)——待拍板,见 `ROADMAP.md`。
|
||||
|
||||
### 5.4 验证不变式、合并与抑制器
|
||||
|
||||
规范(D-B3):**一条 verb 的全部 `set_*`/`bind_*` 必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态惰性特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,`set_*` 之间没有顺序要求。** 推荐实现顺序(framebuffer → program → 纹理/sampler/image/buffer/global constants → render state/dynamic → vertex elements/buffers/index/attrib defaults → patch/XFB → verb)只是代码组织,不是契约。退役 Espryt 的 fragColor 重推导 workaround、`g_broadcastMemo*` 与 `ImageUnitFormatsStillMatch` 的机制是惰性特化,不是调用顺序。
|
||||
|
||||
`create_shader_state` 从编译池的终止 continuation 发出(不是从 draw),SPIR-V 在首个用到它的 draw 之前到达 server——monolith 拿不到的异步收益。
|
||||
|
||||
四条合并规则:整块结构优于逐字段;高水位标记(`GetTouchedBindPointCount`、`GetMaxTouchedUnit`)留在 tracker 走查里,直接就是 `count` 实参;只发 program 解析过的集合(`uniformSamplerOrImageUnitIndex`);**集合 hash 抑制器**——每条 `kVarTail` `set_*` 在 client 算已解析集合的 xxHash,未变不发。最后一条是从后端搬到 client 的 ~175 行去抖(`UnitBindingsSnapshot`/`PairingsIntact`/`g_fboTextureSyncList` 族)的载体:`GetTextureBindGeneration()` 在冗余重绑时也 bump(MC 26.2 每次纹理单元切换都重绑同一个 sampler),没有抑制器每个 batch 都会重发一条几百字节的变长记录并冲掉 server 的两个 memo。
|
||||
|
||||
索引绑定范围在 validate 时刻实时解析(`glBindBufferBase` 之后再 `glBufferData` 是普通应用代码)。
|
||||
|
||||
### 5.5 sampler view 在 client 侧解析
|
||||
|
||||
GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 sampler uniform 类型、mipmap 完备性(`IsMipmapCompleteForFilter`、`SamplesAsIncompleteTexture`)与 `IsUndefinedDefaultTexture`。gallium 的"每槽一个 view"就是解析后的形态,解析留在 client 并带自己的 memo(~40 行搬迁)。两处后端特定后处理留在 server、作用于已解析集合:Espryt 的 raw-depth-fetch sampler 替换、Magma 的 feedback-loop 检测。
|
||||
|
||||
### 5.6 生命周期、共享组、composite program
|
||||
|
||||
- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义;`resource_destroy` 在析构时发。三条顺序约束由 payload 表达:view 先于存储属主销毁(`ViewOf` + server keep-alive)、FBO attachment 钉住纹理(surface handle 隐含 keep-alive)、buffer texture 钉住 buffer(`BufferForTexBuffer`,范围实时解析)。
|
||||
- 共享组:v1 一个 screen、一个 context、一条 flow;`eglMakeCurrent` 是 flow 所有权转移,在既有 `EGLOperationMutex` 下发射(顺手让 `ReleaseThread` 与 `SwapInterval` 也取该锁)。
|
||||
- program pipeline 合成体:`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr<ProgramObject>` 推**一个** handle,slot 从 `ShaderCso` 保留高位段分配,pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`。合成体从不过线,server 不需要任何"解析后的 draw program"钩子;副带收益是阻塞的 `JoinLinkAndSpirv()` 离开 server 的 draw path。
|
||||
|
||||
### 5.7 emulation 的归属
|
||||
|
||||
规则:**驱动表达不了的变换在 tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。** 只有三个"读前端字节的纯 CPU 变换"下放到 client。
|
||||
|
||||
| emulation | 归属 | 过线的是什么 |
|
||||
|---|---|---|
|
||||
| client 顶点数组(`(first+count-1)*stride+elementSize`) | client | 字节(`MGHostSpan`),永不是指针 |
|
||||
| 最大索引扫描(`TryComputeMaxIndexFromHostBytes`,唯一无界的应用指针读,只有 client 同时持有两个数组) | client | `MGPDrawInfo::MinIndex/MaxIndex`(flag 门控,`~0` = 未知) |
|
||||
| client 索引数组 | client | 变长尾里的 `MGHostSpan` |
|
||||
| `*IndirectCount` 计数解析(从 parameter buffer 的 shadow 读实际 draw 数) | client | 解析后的 `MGPDrawRange[]`(几十字节) |
|
||||
| primitive-restart 重写(整 EBO 重写,`kMaxRestartRewriteBytes` = 64 MiB) | **server** | 零线上流量:从索引宿主镜像读(§10.3) |
|
||||
| multi-draw 五档分档 + 展平(`ResolveTierForBatch`,CPU 展平是回退) | **server** | 同上 |
|
||||
| viewport-array N 遍回放 | server | 无新增:16 组 viewport/scissor/depth-range 已在渲染状态里 |
|
||||
| fp64 顶点窄化 | server | 原始字节;`IsLong` 与 `Type` 分开过线 |
|
||||
| image-bindable 存储加宽/拆分 | server | 正向 `ImageBindableHint`;反向纹理拉取 + 终止符 |
|
||||
| 生成 mipmap 的前端存储 | 拆开:client 分配 level 存储,server 生成 | `MGPMipPlan`;`OnMipLevelsGenerated` 只带形状不带字节 |
|
||||
| CopyImage shadow 镜像 | client | 只回"拷贝成功",删掉一整条 server→client 字节通道 |
|
||||
| XFB CPU 图元计数 | client | `XfbCpuCapturedVertices`(flag 门控)+ `EndStreamOutput` 的 `MGPXfbAccounting` |
|
||||
| XFB scatter 的 read-modify-write | client | §8.5 |
|
||||
| 压缩纹理 / pixel unpack 规整 | client | 无 |
|
||||
|
||||
**陈旧索引纪律是逐站点表,不是一条笼统规则**(client 侧扫描/解析之前要做的 reconcile 必须逐字复现 monolith 的集合):
|
||||
|
||||
| client 侧动作 | 必须做的 reconcile |
|
||||
|---|---|
|
||||
| client 顶点数组范围计算 + 暂存 | 无(应用内存,无 GPU 写者) |
|
||||
| 最大索引扫描(EBO 源) | `SyncPersistentMappedRange()` **+** `SyncGpuWrites()` |
|
||||
| 最大索引扫描(client 指针源) | 无 |
|
||||
| `*IndirectCount` 计数解析 | **只** `SyncPersistentMappedRange()`,不加 `SyncGpuWrites()`——monolith 今天就只做这一个,加了会给 Create/Flywheel 的每 batch 平白加一次 publish-and-wait |
|
||||
| server 侧 restart 重写 / multi-draw 展平 | server 从镜像读;GPU 写者可见性由 `OnGpuWritten` 收窄集在 server 本地判定 |
|
||||
|
||||
前两条 client reconcile 的形态:publish → 等 `appliedSeq` → 排空事件 → 再碰 shadow。门:`ClientArrayAfterComputeWriteScenario`(去掉等待必须看到几何缺失);`create-indirect` fixture 上 `roundtrips-per-frame` 必须读零(P8)。
|
||||
|
||||
## 6. 纹理 subdata 与 dirty 归属
|
||||
|
||||
- `glTexSubImage*` 根本不调后端表:全部纹理上传由 Espryt 在 sync 时刻按**累积**区域做,那里跑 `MipmapStorage` 的 96-rect 级联合并与 `summedArea*4 >= unionArea*3` 的 union-box 回退,并在 unpack ring 可用时刻意塌成一个 box——Mali 按**作业数**给上传计价,~100 个精灵 rect 对一个 union box 实测 +6 ms/frame。逐 `glTexSubImage` 发一条记录会精确复现那个形状。
|
||||
- 因此:client 在自己的 `MipmapStorage` rect 模型里累积,在**下一个 validate / flush 点**把合并后的形状作为**一条** `ResourceSubData` 发出。`MOBILEGL_PIPE_STATS` 单列逐帧发射次数与上传作业数(`TextureUploadEmissions/Box/Rect/Jobs`)。
|
||||
- **同时携带 union box 与 region 列表,由 server 选上传形状**:决策留在付 GPU 代价的那一侧。实测(`MEASUREMENTS.md`):vanilla 世界同样 185 次发射,Espryt 的整 box 路径每帧 635 KB 纹素、Magma 的 rect 路径 40 KB,16×。
|
||||
- `MGPSubRegion` 显式携带 `SrcRowStride/SrcSliceStride`,`MGPSubData::SourceIsVerbatimLevelShadow` 显式携带原来由 `uploadData == mipData` 指针比较回答的问题:"这批字节是未经转换的 level shadow 吗"。split 下 client 既不发整 level 也不在 server 留整 level 镜像,指针比较不成立;Espryt 的上传路径改为从描述符取步长,`UNPACK_ROW_LENGTH` 从 `SrcRowStride/bpp` 设。形状照抄已存在的 `UnpackStagingBlock`(ring 路径本来就紧密重打包、不发 `glPixelStorei`)。
|
||||
- **dirty 归属反转**:client 保留 rect 模型、维护一份发射游标、发射后清自己的标志,server 从不碰 client 的标志。安全,因为 `MG_Impl` 里没有任何 `IsStorageDirty/GetStorageDirtyRects/GetStorageDirtyRegion` 调用点(前端从不读自己的 dirty 状态)。逐 level "server 权威位"与纹理 ack 协议因此不必存在。
|
||||
- 发射游标按**存储属主**键控 `(storageOwnerHandle, ownerUploadTarget, ownerLevel)`:`TextureObjectView` 把 dirty 查询/清除全部转发给属主并做索引重映射,view 与属主共用同一份 dirty 状态。门:通过 view 上传、经属主采样(及反向),跨 draw 边界各一次。
|
||||
- 后端真正在 shadow 里写字节的两处——CPU 回退生成 mip(RGB16F/RGB32F)与 `glCopyImageSubData` 目的地镜像——分别由 `OnTextureWriteback` 与"CopyImage 镜像搬到 client"处理。
|
||||
- Unpack PBO 完全在 client 解析;压缩纹理永不到达后端;`glCopyTexSubImage*` 与 `glClearTexImage` 整体留在 client(今天就是纯前端操作:借一次 `ReadPixels` 进 CPU scratch 再写 shadow),拆分后恰好是一次阻塞 ReadPixels round trip,脏区按普通 subdata 下发。
|
||||
|
||||
## 7. Shader state = SPIR-V + 反射归档
|
||||
|
||||
- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 client,SPIRV-Cross(`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。
|
||||
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex`、`uniformBlockBinding`、`shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings`、`xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture`、`computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples`、`uniformOffsets`。`XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
|
||||
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts`、`ResourceReflection`、`XfbVarying`、`LinkArtifacts`、`SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),更新 7 个 includer,加 CI `-H` 闭包断言。同批抽取 `MG_Pipe/MGPipeValueTypes.h`(`MAX_DRAW_BUFFERS`、`PerBufferBlendState`、`StencilFaceState`、`PixelStoreParameters`、`RenderStateParameters`、`SamplerParameters`、`BorderColorForm`、`VertexAttribute`、`VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h` 与 `RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
|
||||
- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 snorm/unorm clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度与 XFB 布局),`create_shader_state` 发布**制品**,server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法,也是 gallium `st_variant` 的做法。
|
||||
- 后端 link/compile 失败不需要同步返回:今天只是一行 `MGLOG_E` 加 bind program 0 的空 draw,`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答。`OnLog` 逐字复现——由此要求日志按严重级分级(§8.3)。
|
||||
- Magma 的两个内部 shader(blit、depth-mipmap)烘焙成签进树的 SPIR-V + uniform location + UBO 布局,用一个 `MG_Test` 重跑树内 glslang 逐字节比对守新鲜度(`MOBILEGL_BAKED_INTERNAL_SHADERS`,P7);顺带把一次 glslang 编译从 monolith 启动路径上删掉。
|
||||
|
||||
## 8. 反向通道
|
||||
|
||||
### 8.1 `MGPipeCallbacks`(P0 已落地,`MobileGL/MG_Pipe/MGPipeCallbacks.h:27-51`)
|
||||
|
||||
十个具名回调 + 一个正向终止符(`ResourceSubDataComplete`),取代今天 95 个调用点 / 17 个方法直接 poke 前端对象。gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求、default-FB 几何这些词汇(Mesa 里两者共享地址空间),具名化是有意偏离(D8)。monolith 下直调,split 下是 `SEG_EVENT` 上的记录。
|
||||
|
||||
| 回调 | 取代 |
|
||||
|---|---|
|
||||
| `OnGlError(code)` | 6 处 `RecordError`;**必须对命令流有序**,否则 `glGetError` 答错(`glGetError` 本身永远本地) |
|
||||
| `OnGpuWritten(res, ranges[])` | 6 处 `MarkGpuWritten`:client 在每个 draw/dispatch 发射点**保守自建** pending 集,这是**收窄**通道 |
|
||||
| `OnBufferWriteback(res, offset, bytes)` | PBO 回读、XFB 捕获;**按操作级批处理**(今天两处逐行循环绝不能变成每扫描线一次 IPC);必须与 epoch bump 有序 |
|
||||
| `OnTextureWriteback(res, box, bytes)` | CPU 回退生成 mip 的纹素(唯一生产者) |
|
||||
| `OnTexturePullRequest(res, target, firstLevel, levelCount, pullSerial)` | §8.4 |
|
||||
| `OnMipLevelsGenerated(res, base, count)` | 只带形状:monolith 的 `EnsureGenerateMipmapStorageAllocated` 也只 `AllocateStorage` + `MarkStorageDirty(false)` 不填内容,split 行为一致 |
|
||||
| `OnSurfaceChanged(info)` | `SwapchainObject` 写 `pDefaultFramebufferInfo` 的分层倒置;client 自己合成 default-FB 对象 |
|
||||
| `OnCapsInvalidated()` | 2 处 `InvalidateCompileEnv` |
|
||||
| `OnLog(level, text)` | ≤WARN 有损,≥ERROR 无损 + 速率限制 |
|
||||
| `OnXfbScatterReady(scratch, packedStride, vertices)` | §8.5 |
|
||||
|
||||
95 个写回点的其余归属:`MarkStorageDirty` 大多是 server 本地记账(零消息);后端凭空造的前端对象(Magma 占位纹理、swapchain default-FB 占位)→ server 原生;`SetBackendResource` 删除(server 拥有资源表);`SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除;`SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段。20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 `ResolveUniformBufferPayload` 把具名 UBO 打进自己的 UBO ring → `SetShaderBuffers` 的 host payload(D-B8)。
|
||||
|
||||
### 8.2 有序性是正确性要求
|
||||
|
||||
每一次 `WritebackFromBackend` 后面都紧跟 `BumpBufferMutationEpoch()`,否则 server 的 draw-clean memo 会在 epoch 背后变陈旧——split 里这变成反向通道上的排序规则:写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。**反向通道需要与正向通道相同的有序保证。**
|
||||
|
||||
### 8.3 错误、ack 与日志
|
||||
|
||||
- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(`glTexImage*`/`glTexStorage*` 只 `MarkStorageDirty`,Espryt 惰性分配;连 `glRenderbufferStorage*` 也在 `SyncToBackend` 里惰性做),拆分不改变可观察行为,这批不同步 ack。
|
||||
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**。`glRenderbufferStorage*` 不 ack:41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`,client 本地作答)。目录里目前没有条目携带 `kNeedsAck`(`ResourceRespecify` 是 `kNone`),标记随 P3a 的 buffer 路径落地。
|
||||
- 其余错误一律晚到,走有序的 `OnGlError`。
|
||||
- `OnLog` 分级:≤WARN 有损(覆盖最旧 + `eventDropped` 计数);≥ERROR 无损,加入触发 `eventRingFull` + 停止 apply 的语义事件集;每秒 ERROR 速率限制器,超限发一条 "N errors suppressed";`MGLOG_E_ONCE` 的 latch 变 per-server。理由:后端 link 失败只以一行 ERROR 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。
|
||||
|
||||
### 8.4 唯一的新停顿类:server 发起的纹理重铸拉取(D-B6)
|
||||
|
||||
server 不保留纹素,三个原因会要求重发已发过的 level:`RequireImageBindableStorage` 的 re-dirty、整格式再生、view 源重铸。四条缓解同时上:
|
||||
|
||||
1. **预防主因**:client 给纹理打 `everImageBound`,`ResourceCreate/Respecify` 一直携带 `ImageBindableHint`,image-bindable 存储前期分配好。
|
||||
2. **拉取异步**:server 发 `OnTexturePullRequest` 并把 twin 标 not-ready,client 下次 publish 时重发;阻塞的是 `mgl-srv-apply` 线程不是应用线程。
|
||||
3. **有上限的保留,默认关**:`MOBILEGL_PIPE_TEXEL_RETAIN_MB` 默认 0——`MipmapStorage` 保有每 level 完整 CPU 影子,拉取总能被服务,缓存买的是延迟不是正确性。只有实测拉取率非平凡才开。
|
||||
4. **显式终止符**:拉取是 request/response 对,由 `ResourceSubDataComplete(res, target, firstLevel, levelCount, pullSerial)` 终止,**可携带零个 region**——内容只来自渲染、被 `CanMirrorCopyImageShadow` 拒绝的 copy、或 GPU 侧 mip 生成的 level,client 根本没有字节;收到零 region 时 server 带着"已分配但为空"的存储继续(正是 monolith 的行为)并记 `MGLOG_W`。没有终止符 apply 线程会永久 park。
|
||||
|
||||
门:`TextureRemintPullScenario`(含无解用例,且在终止符落地前必须是红的);拉取次数逐 trace 用例发布。本设计从不声称"零 round trip",它测量并公布。
|
||||
|
||||
### 8.5 XFB scatter 搬到 client
|
||||
|
||||
Espryt 的 `ScatterCapturedRecords` 是对 client shadow 的 read-modify-write:从应用已有的字节起步,只把捕获到的 varying 补进去(`gl_SkipComponents` 的空洞保留应用原本的内容,`KHR-GL46.transform_feedback.capture_special_interleaved_test` 走到它)。server 没有 `MappedData()`,所以:server 把紧密打包的 scratch 通过 `OnBufferWriteback` 推给 client,用 `OnXfbScatterReady` 告知布局;client 拥有目的 shadow 与反射归档里的 varying/stride,原样跑补丁循环;补好的范围作为普通 `ResourceSubData` 重发并 bump change serial。不新增停顿类。
|
||||
|
||||
## 9. 后端状态机改造
|
||||
|
||||
### 9.1 原样不动的东西
|
||||
|
||||
Espryt:三条 persistent-mapped ring 与 `PersistentRing` 算法、buffer pool、7 条 fallback-repack 路径、`m_backendColorSlots` 置换表、三个 scratch FBO 及驱动侧影子、`PackState`、全部驱动绑定影子、Adreno 禁用属性 SIGSEGV workaround、Mali XFB 捕获丢失 workaround、`ScopedDefaultUnpackState`、SPIRV-Cross 会话与 post-emission ESSL 重写、驱动 POST 自检族、restart 重写与 multi-draw 五档。
|
||||
Magma:`VulkanRenderer` 全部 memo 与 scratch、`PipelineFactory`、`ProgramFactory`、`UniformManager` 的 ring 与描述符集、五个 `Vk*Manager`、`FrameContext`、`SwapchainObject`、`DynamicStateShadow`、`VertexInputStateFactory` 的 cache 本体、**D18 的节点式容器纪律**(`m_renderbufferResources`/`m_textureResources` 故意用 `std::unordered_map`,调用方跨查表缓存 `Resource*`;postmortem 注释逐字进 review checklist)。
|
||||
|
||||
从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6,从描述符取步长)。
|
||||
|
||||
唯一两处必须真改的 `MG_State` 类型内部用法(都在 Magma):占位纹理(构造真的 `TextureObject2D*` 只为复用 `SyncTextureAndGetDescriptor(ITextureObject&)` 签名,~120 行木偶戏 → ~60 行原生 `VkImage`+view+descriptor,34 个 `MOBILEGL_ASSERT(pGLContext)` 里的 9 个随之消失);两个内部 shader 烘焙(§7)。Espryt 的小号同类:`g_rawDepthFetchSamplerState` → 后端原生 sampler。
|
||||
|
||||
### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1)
|
||||
|
||||
```cpp
|
||||
// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织(~20 KB,字段集全迁移期稳定)
|
||||
struct PipeInputs {
|
||||
const RenderStateParameters& GetRenderStateParameters() const; // 阶段 A:类型与后端今天读到的完全一致
|
||||
// … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 55)
|
||||
#if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 m_filledGen[kFieldCount]; // 逐字段"上次填充的 verb 序号"
|
||||
Uint64 m_currentVerbSerial;
|
||||
#endif
|
||||
};
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
|
||||
#else
|
||||
# define MGB_CTX (::MG_State::pGLContext)
|
||||
#endif
|
||||
```
|
||||
|
||||
| 阶段 | 改什么 | 证明 |
|
||||
|---|---|---|
|
||||
| A 别名 | 机械 `sed`:`MG_State::pGLContext->` → `MGB_CTX->`(293 处)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
|
||||
| B 推送 | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 |
|
||||
| C 句柄化 | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) |
|
||||
|
||||
- 填充点逐 verb 类,不只 `PrepareForDraw`/`SetupDraw` 两处:G5 从 `PipeCalls.def` 生成"每个 `kCtxVerb`/`kCtxObject` 调用可能读哪些字段"的表,在 `MG_Impl` 的 ~93 个边界站点生成 validate/fill 调用。
|
||||
- poison 是**逐 verb 世代**不是位图:每次 verb 递增 `m_currentVerbSerial`,字段被填时记下序号,读取时断言相等(跨 verb 有效的字段显式标 sticky)。位图看不见"上一个 draw 填过、紧随的 `glTexSubImage` 读到陈旧值"。debug 与 disaggregated 构建里读一个当前 verb 未填的字段是 `Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}`。纯度门 grep 的是 `pGLContext` 不是 `pGLContext->`。
|
||||
|
||||
### 9.3 Track V / Track H
|
||||
|
||||
- Track V(值类型:`GetRenderStateParameters`、`GetPixelStoreParameters`、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma ~22 个标量 getter……约 B 类读点的 55%):机械。
|
||||
- Track H(对象类型:167 个 `SharedPtr<MG_State…>` 点):真活。
|
||||
- 读点分类实测(静态):A 探测变化 ~35(12%)、B 翻译输入 ~216(74%)、C 瞬时参数 ~4、D 身份/缓存键 ~48(与 B 重叠)、E 数据字节 3、写 8。74% 是 B 类——"bump 一个版本让 server 自己拉"行不通,值本身必须过去。
|
||||
|
||||
### 9.4 残余值块
|
||||
|
||||
Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一个**显式临时**调用 `SetResidualValueState(MGPBlobRef)`,payload `ResidualValueBlock{RenderStateParameters, PixelStoreParameters, CapabilityBits, patch 三字段}`。三条纪律:退役是编译错误(`MGL_RESIDUAL_BLOCK_SIZE` 只降不升,`MobileGL/MG_Pipe/MGPipeTypes.h:535`,P13 变成 `static_assert(sizeof == 0)`);布局逐成员 `offsetof` 断言且 split 下逐字段序列化(异质 POD 并集的 padding 差异 monolith verify 看不见);只在 P2..P13 存在,`MOBILEGL_PIPE_STATS` 单独计一类字节(`ResidualValueBlock`,P0 已占位)。
|
||||
|
||||
### 9.5 21 条身份 memo 的重键
|
||||
|
||||
统一事实:每个进入 memo 键的版本计数器要么是回绕 `Uint16`,要么根本不会被它害怕的那个 mutation bump;身份比较是堵回绕洞的补丁。`{slot, gen}` + 显式 destroy 让 **11 条直接删除**(registry 的同址 `weak_ptr` + GC ×6、`TwinLookupMemo` ×3 + `OwnerEquals`、`UnitSamplerLookupMemo` 的 `WeakPtr` 测试、`SetBackendStateMemo`、`VkTextureManager::TextureIdentity` 存活探测、`ConvertedVertexStreamKey` 的 `sourcePin`……),**2 条** server 删除但去抖搬到 client(§5.4),**7 条重键**成更便宜的比较(`StampSyncedFBO` 四元组 → `ContentHash` + server 私有 `attachmentRemintEpoch`;`ResolvedTextureBindingMemo` 9 键 → `(shaderCso.slot, viewSetSerial)`;`SetupDrawSnapshot` 的 ~14 探测字段与两个有损求和 → 三个 handle + 两个 server 纪元 + dirty mask;`VertexInputStateFactory::ComputeHash` 里的 lifetimeId → `gen` **混进** server 侧每个 content hash),**1 条**(D18)原样不动。两个顺带修掉的潜伏 bug 已先独立落地:`m_xfbCounterSlotByObject` 用裸 GL name 做键(`bd2b4158`)、`RenderbufferObject` 缺 `GetLifetimeId()`(`9c7339b2`)。
|
||||
|
||||
### 9.6 A/B 与口径收窄
|
||||
|
||||
`MOBILEGL_PIPE_PUSH` 子系统位图(含一位关闭 CSO 内容寻址,负面对照)在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时 `SnapshotFromGLContext()` 仍要合成句柄,后端仍跑重键后的 memo 代码,一个重键 bug 两臂都在。对策:**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS`(默认 ON)在 P3a/P4a 期间保留 registry / `TwinLookupMemo` 实现活在同一个 `PipeInputs` 接口之下,随 pull 路径在 P13 退役(各阶段 +1 天维护)。
|
||||
|
||||
P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;**保留 `MOBILEGL_PIPE_VERIFY` 连同它需要的 `SnapshotFromGLContext()` 与 `MG_State` include**(D-B5,verify 构建永不出货);三道纯度门在非 verify 构建上转绿。
|
||||
|
||||
## 10. server 侧
|
||||
|
||||
### 10.1 对象表与 applier
|
||||
|
||||
- `MG_Remote/Server/PipeObjectTables`:按 kind 的 slot 数组,不是对象图;server 不持有任何 buffer 的完整副本、不持有纹素、不持有前端对象图。
|
||||
- `PipeApplier`:解码 → 更新对象表与 `PipeInputs` → 调后端函数指针。debug 断言:任何传输下都不得有 `SharedPtr` 或裸前端指针跨过 applier 边界。`InProcessTransport` 走与 spawn **完全相同**的 G3 编解码路径,只在门铃/拷贝机制上不同。
|
||||
- 每 context 一份 working `RenderStateParameters`(§5.3)。
|
||||
|
||||
### 10.2 monolith 侧的净收益
|
||||
|
||||
即使 IPC 永不上线:复用地址 ABA 一整类不可表达;FBO → program 排序 hazard 消失;`SwapchainObject` 写 `MG_Impl` 的分层倒置消失;两个潜伏 bug 已修;一次 glslang 编译离开启动路径;`inproc` = 渲染线程;`MG_Test` 的 mock 后端变成 MGPipe recorder(§13.3)。monolith 净代码量是**增加**的(约 +6,650 手写 + 4,000 生成,对 ~372 行真删除),所以 monolith 论据是逐线程 CPU 数字(§13.2-④),不是删除行数。
|
||||
|
||||
### 10.3 索引宿主镜像(`MG_Remote/Server/IndexHostMirror`,P8)
|
||||
|
||||
- 覆盖:`BindMask & ELEMENT_ARRAY` 的资源,且仅当 `kCapNeedsHostIndexBytes`(split 且 server 需要索引字节做 restart 重写 / multi-draw 展平)。
|
||||
- 由 server 本来就要收的 `ResourceCreate/Respecify/SubData` 流增量维护:零额外线上流量、零 round trip。GPU 写者对镜像的影响由 `OnGpuWritten` 收窄集在 server 本地判定。
|
||||
- 预算 `MOBILEGL_PIPE_INDEX_MIRROR_MB`(默认 64),逐帧发布 `index-mirror-bytes`;超预算时该 buffer 退化为逐 draw 经 `MGHostSpan` 传送(`Seg` 指向 `SEG_STAGE`),计入 `index-bytes-shipped`。
|
||||
- 必须是它:`kMaxRestartRewriteBytes` = 64 MiB 是默认 `SEG_STAGE` 的两倍,`kMaxFlattenedIndices` = 1<<24 同量级,逐 draw 塞进 32 MiB 的段既不可行也无必要。它是本设计里唯一的"数据副本"。
|
||||
|
||||
## 11. 传输与数据面(骨架 P0 已落地,`MobileGL/MG_Remote/`)
|
||||
|
||||
### 11.1 段
|
||||
|
||||
| 段 | 拥有者 | 默认 | 内容 |
|
||||
|---|---|---|---|
|
||||
| `SEG_CMD` | client(server 只读) | 8 MiB,2 的幂 | `RingControl`(4 KiB 页)+ POD 记录 + ≤4 KiB 内联负载 |
|
||||
| `SEG_STAGE` | client | 32 MiB,上限实测定 | bulk 字节:buffer sub-data、纹理紧密重打包区域、UBO scratch、client 顶点/索引/indirect 数组、multi-draw 参数块、具名 UBO host payload、persistent-map 脏块 |
|
||||
| `SEG_REPLY` | server(client 只读) | 8 MiB,4 KiB slot | readback 像素、buffer writeback |
|
||||
| `SEG_EVENT` | server | 256 KiB SPSC ring | 十个回调的事件 + `EvQueryResult/EvFenceSignaled/EvReadbackDone` |
|
||||
| `SEG_SHADOW[n]` | client | 每对象,≥256 KiB shadow(Phase 2) | 零拷贝 buffer/texture shadow |
|
||||
| `SEG_ADOPT[n]` | server(client RW) | 每 buffer,≥16 MiB adopted store(P11) | 应用直写 GPU 内存 |
|
||||
|
||||
创建(`ShmSegment`):Android `ASharedMemory_create`(API 26;libc 的 `memfd_create` wrapper 是 API 30);桌面 Linux `syscall(SYS_memfd_create)`;其他 POSIX `shm_open`+`shm_unlink`;Windows `CreateFileMappingW`(`Local\`)。传递:POSIX `SCM_RIGHTS`(`FdPassing`,专用 `AF_UNIX SOCK_DGRAM` socketpair——消息边界保住 ancillary data 与 payload 不被拆开,sideband ≤256 B);Windows 段名走 `SegmentRef`。fd 传递在第一个 transport commit 里实现——没有它数据面在唯一重要的平台上一字节过不去。
|
||||
|
||||
不进 `SEG_STAGE` 的:restart 重写的整 EBO 与 multi-draw 展平的索引流(走索引镜像)。`SEG_SHADOW` 块的退休规则:释放的块进 pending 链表,`appliedSeq`(借入 GPU 时间线的 slot 用 `retiredSeq`)越过最后一条引用它的记录后才归还 arena。
|
||||
|
||||
### 11.2 `RingControl`(`Ring.h`)
|
||||
|
||||
一页 4 KiB,每个争用组各占一条 cache line:`SEG_CMD` 游标三元组 `cmdHead / cmdAppliedTail / cmdRetiredTail`;`SEG_STAGE` 独立三元组(`stageHead / stageAppliedTail / stageRetiredTail`——"`SEG_STAGE` 余量 < 1/4"是 publish 触发器,占用率不能从另一个 ring 算出,且 stage slot 的退休条件不同);三个严格区分的水位 `appliedSeq`(释放 `*AppliedTail`)/ `submittedSeq`(释放 staging)/ `retiredSeq` + `completedFrameSerial`(释放 `*RetiredTail` 与 `SEG_ADOPT`)+ `presentAckSerial`;`serverEpoch`(context 丢失 / server 重启 ++)、`ringGeneration`(硬 drain 后 ++,作废缓存 offset)、`consumerParked`/`producerParked`、`eventRingFull`、`eventDropped`。两个 tail 是必须的:P11 之后 server 会**借用** ring slot 而不是再拷一次,那种 slot 只能在 `completedFrameSerial` 之后回收。游标是单调字节计数、2 的幂掩码、永不重置。
|
||||
|
||||
记录头 `RingRecordHeader{kind, flags, size}`,kind 0 保留给 wrap 填充;`RingProducer::Reserve` 在记录会跨 wrap 边界时自动发 pad 记录,保证每条记录连续;`MaxRecordBytes() == Capacity()/2`;`RingConsumer::Pop` 拒绝不可能的头(非 8 对齐、小于头、大于已发布)并置 corrupt → `Fatal{ProtocolCorruption}`;`HardDrainRing` 只在两侧静默且 ring 全空时 bump generation。
|
||||
|
||||
### 11.3 双向 doorbell(`Doorbell.h`)
|
||||
|
||||
- client → server:consumer 自旋 → 置 `consumerParked=1` → 阻塞;producer release-store `cmdHead` 之后仅当 `consumerParked` 时敲(字节码 `0x01`)。
|
||||
- server → client:client 在**任何**等待(present credit、`kNeedsAck`、ring/stage 满)先自旋 `MOBILEGL_IPC_SPIN_US`(默认 50 µs)→ 置 `producerParked=1` → 阻塞;server 在 release-store 任何 watermark 之后仅当 `producerParked` 时敲(`0x02`)。没有第二个方向,每处 client 等待都退化成跨进程自旋一条 cache line——手机上一颗大核满频空转一整帧,而全库没有亲和性控制。
|
||||
- 两个实现,零 futex/eventfd/named-event 平台代码:`CondVarDoorbell`(`inproc`,带 `Kill()` 死亡态让 `Shutdown` 能 join 一个 parked 的等待者)与 `SocketDoorbell`(`spawn`,一字节;`SOCK_STREAM` 端在对端关闭时报 `POLLIN|POLLHUP` + `recv()==0`,这是死亡检测)。
|
||||
- 丢失唤醒窗口由**两个 `seq_cst` fence** 关闭(等待者置标志 → fence → 再测条件;通知者发布 watermark → fence → 读标志),标志本身的访问是 relaxed。`NotifyIfParked` 的前置条件:watermark 已发布。死亡的 doorbell 让 `Wait` 停止重新 park。
|
||||
|
||||
### 11.4 控制面(`protocol.fbs`、`Framing.h`、`ITransport.h`)
|
||||
|
||||
- 一份 schema,两种用法:热路径 → FlatBuffers `struct`(定长、无 vtable、只需边界检查)直接进 ring——即 G3 生成的记录,与 `MGPipeTypes.h` 的 POD 逐条 `static_assert` 尺寸/`offsetof` 对齐;罕见/变长/需演进 → `table` 走 CTRL socket。今天 `protocol.fbs` 只含控制面(`MobileGL/MG_Remote/Protocol/protocol.fbs:218-228` 的 `CtrlMsg`:`Hello`、`Welcome`(四个段的 `SegmentRef`)、`CapsSnapshot`、`SurfaceOp/SurfaceReply`、`ResyncRequest/Done`、`AuxRequest`(外来线程的 fence wait / query result / scalar get)、`Fatal`(`ProtocolCorruption/RingOverrun/SegmentMismatch/DeviceLost/ServerCrashed/AbiMismatch`)、`LogLine`),`file_identifier "MGLC"`;union tag 是 wire 值,只追加。
|
||||
- `protocol_generated.h` 提交进树,`scripts/gen_protocol.py` 再生成(只用 `MOBILEGL_FLATC_EXECUTABLE` 或从 pinned submodule 在仓库外构建一次的 flatc,不用 PATH 上的),CI `flatc-check`(`.github/workflows/test.yml:304`)重生成并 diff。**codegen 绝不进默认构建图**;运行时 header-only。
|
||||
- 封帧 `[u32 'MGLF'][u32 len][payload]`,64 MiB 上限,**读时校验**:坏 magic / 超长长度立即 latch 失败并报 `MOBILEGL_ERR_PROTOCOL_MISMATCH`(不是静默永久挂起);接收缓冲不足**返回所需大小并保留消息**(`MOBILEGL_ERR_BUFFER_TOO_SMALL`)。
|
||||
- `ITransport`:`SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown / Role`;热路径完全绕过它。`Shutdown` 拆掉整个连接(两端都不能再发,等待者全部解锁,已排队消息仍可读完)。`WireLog.h` 是唯一的日志入口,让 `Transport/` 的头不 include 前端 umbrella(纯度门 A 断言 `-H` 输出)。
|
||||
- `mg_protocol_base.h`:纯 C、无依赖的结果码 / span / `ShmRegion` / id 词汇,structSize-first 版本纪律(追加 = minor,改动 = major,major 不符是结构化失败)。
|
||||
|
||||
### 11.5 WAR 危害、拷贝账与背压
|
||||
|
||||
- Phase 1(P5–P8):GL 调用时刻把字节拷进 ring slot,slot 到 `stageAppliedTail` 越过它为止不可变,危害按构造消除;代价一次 memcpy,`Ops_ResidentSubData` 与 `StageBlocksIntoUnpackRing` 在 monolith 里已经在付。
|
||||
- Phase 2(shadow-in-shm,零拷贝):≥256 KiB 的 shadow 分配在 `SEG_SHADOW`(`PipeResource::MapAlignedAllocator` 增加 shm arena,保留 64 B 对齐契约;`MipmapStorage` 的 level vector 同理),`ResourceSubData` 只带 `{seg, offset, size}`。WAR 用 per-shadow 64 KiB 块发送水位:应用写某块而该块上次发送尚未被 `appliedSeq` 覆盖 → 这次写走 `SEG_STAGE`。必须整段 `#if MOBILEGL_BUILD_DISAGGREGATED` 包裹(改容器 allocator 就改了类型,option OFF 时逐字折叠回今天的 allocator)。
|
||||
|
||||
| 路径 | monolith | Phase 1 | Phase 2 |
|
||||
|---|---|---|---|
|
||||
| `glBufferSubData` → shadow store | 2 | 3 | **2** |
|
||||
| `glBufferSubData` → adopted store(P11) | 2 | 2 | 2 |
|
||||
| `glMapBufferRange(WRITE)`+unmap | 3 | 4 | 3 |
|
||||
| persistent coherent map 推送(§12) | 0 | 1/发射点 | 1/发射点(精确块) |
|
||||
| `glTexSubImage` | 2 | 2 | 2 |
|
||||
| 全局 UBO / draw | 1 | 2 | 1 |
|
||||
| adopted ≥16 MiB(P11 T1/T0) | 0 | 0 | 0 |
|
||||
|
||||
server 没有第二份 `BufferObject`,所以不存在"staging → server 侧 shadow"这次中间拷贝。字节计数器装在 wire 两侧,验收看总量。
|
||||
|
||||
- 分配与背压:逐字移植 `PersistentRing`(单调 head/tail、2 的幂掩码、frame mark)。分配失败升级:扩容(翻倍)→ 对最老未 retire 批次有界等待(默认 50 ms,走 `producerParked` doorbell)→ 硬 `Drain` + `ringGeneration` bump。硬 drain 后恢复便宜:正向流是自洽的推送流,tracker 把全部 dirty 位置为"必须重推",下一个 verb 重发完整 `set_*` 集合,纹理侧由发射游标负责,没有"重发未 apply 对象状态"的特殊协议。`SEG_CMD` 与 `SEG_STAGE` 各自独立跑这套升级。
|
||||
|
||||
### 11.6 publish、序号与 credit
|
||||
|
||||
- 不设"records ≥ 64 KiB"一类阈值(那是一整帧的流水线气泡,且否掉 `inproc` 的全部意义)。规则:每条记录(或每 8–16 条摊销)release-store `cmdHead`,仅当 `consumerParked` 时敲门铃。
|
||||
- 显式门铃点:`present`、任何 `kNeedsAck` 请求、`eglMakeCurrent`、`glFlush`(刷出不等待)、`SEG_STAGE` 余量 < 1/4、**轮询类入口**(`glClientWaitSync` 任意 timeout、`glGetSynciv(GL_SYNC_STATUS)`、`glGetQueryObject*(AVAILABLE|NO_WAIT)`——否则 `while (glClientWaitSync(s, FLUSH_COMMANDS_BIT, 0) == TIMEOUT_EXPIRED) {}` 永久自旋);带 `GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 publish。
|
||||
- 饥饿升级:同一 handle 连续 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64)本地回答"未就绪"而 watermark 毫无移动 → 升级为一次阻塞 round trip。
|
||||
- `glFinish`/`glFlush` 保持纯 no-op。
|
||||
- seq = 记录序数;两个互相独立的窗口:字节 credit(两个 ring 各自占用)与 present credit(`presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT` 时 `eglSwapBuffers` 阻塞)。server 不发 credit 消息:对 `RingControl` release store,consumer 每 64 条记录更新一次 `appliedSeq`,`producerParked` 时敲反向门铃。
|
||||
|
||||
### 11.7 事件回传与溢出
|
||||
|
||||
`SEG_EVENT` 承载十个回调加回读完成通知。client 排空点:`glGetError`、`glGetQueryObject*`、`glClientWaitSync`、`glGetSynciv`、`eglSwapBuffers`、`glMapBuffer*`/`glGetBufferSubData`/`glCopyBufferSubData`,以及**每一次等待循环的每一轮**。溢出策略(修一个双向死锁:client 卡在 present credit、server apply 线程卡在生产事件):`EvLogLine` ≤WARN 有损;语义承载事件(`EvGpuWritten`、`EvReadbackDone`、`EvFenceSignaled`、writeback、pull request、mip、scatter、`EvGlError`、surface、caps、`EvLogLine ≥ERROR`)无损——ring 满时 server 置 `eventRingFull=1`、**在记录边界停止 apply**、敲反向门铃,client 排空后清标志并敲正向门铃;ERROR 速率限制器。故障注入:client 被 credit 阻塞时灌满 `SEG_EVENT`;日志洪泛下注入一次 link 失败,那行 ERROR 必须出现且两侧恢复。server 侧 `MGLOG` 按流顺序 replay 进 client 日志流(复用 `DeferredLogLine` 机制)。
|
||||
|
||||
### 11.8 fence 与无 present 负载
|
||||
|
||||
- fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位:DirectGLES 的 `g_completedFrameSerial` 只在 `Present()` 与 `WaitForFrameSerialCompleted` 里前进,帧中 fence 会退化成帧计数推断——`DirectVulkan.cpp` 写明这是被修掉的 bug(MC 1.21.5 的 fence-paced ring 曾因此 native-heap OOM)。规则:`FenceCreate` 转成真实的后端 `FenceSync()`,server 用自己已有的逐 fence 轮询在非 present 时刻也推进并发 `EvFenceSignaled`。
|
||||
- 无 present 循环(CTS、回读循环、从不 swap 的集成场景)下 `retiredTail` 会饿死、`SEG_STAGE` 填满、每个用例都跑到硬 drain。规则:DirectGLES 的 server 加**非 present fence tick**——距上次 `Present` 超过 8 ms 或每 4096 条已 apply 记录插一个 `glFenceSync` 并轮询 fence ring;ring 占用率与升级次数进计数器;P8 加一个无 present 的 split 用例。
|
||||
|
||||
## 12. persistent map 与 ≥16 MiB 采纳
|
||||
|
||||
`AcquirePersistentMap` 是永久的地址空间捐赠(返回 host-visible coherent 指针,成为该 buffer 的唯一真相源;≥16 MiB 可变 store 由 `TryAdoptLargeStorage` 自动走到,实测 MC 26.3 p99 163→21 ms、40→115 fps、省 ~400 MB)。**整个 monolith 改造期一动不动**(D-B4),只有 IPC 那一步会打破它。
|
||||
|
||||
三档,由运行时 POST 探针选择(本项目"后端限制一律探针判定、不硬编码驱动名"的既定规则),**spike B 已在两台设备上给出答案**(`MEASUREMENTS.md` §2):
|
||||
|
||||
| 档 | 形态 | 实测 |
|
||||
|---|---|---|
|
||||
| **T0 — server 导入 client 分配**(P11 主攻) | client 分配 `AHardwareBuffer` BLOB,socket 交接;server 以 `VK_ANDROID_external_memory_android_hardware_buffer`(Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`(Espryt)导入,两侧 persistent+coherent 映射 | **Adreno 830 与 Mali 都是完整读写往返**,含 GPU 访问与两侧字节校验——唯一在两台设备、两个后端上都成立的档 |
|
||||
| T1 — server 导出自己的映射 | `VK_KHR_external_memory_fd` opaque fd,client `mmap` + 导入 | 只有 Adreno 的 Vulkan 路径可用;Adreno 的 GLES 导入 `glMapBufferRange` 全部 `GL_INVALID_OPERATION`;Mali 不可导出。**每次存储定义一次 round trip**(不是每 store 一次),`StorageBufferRegrowScenario` 发布 `map-persistent-roundtrips` |
|
||||
| T3 — host pointer 导入(`VK_EXT_external_memory_host`) | | Adreno 无扩展;Mali 只读(GPU 写对宿主映射不可见) |
|
||||
| T2 — 拒绝(永久正确回退) | `AcquirePersistentMap` 返回 `nullptr`,前端已在三处容忍 | 此档下 client 侧推送强制 |
|
||||
|
||||
`MOBILEGL_IPC_ADOPT_TIER`(`auto`/0/1/2)做负面对照;与 `MOBILEGL_IPC_RESPAWN` 互斥(被采纳的 store 是 server 拥有的内存)。
|
||||
|
||||
**client 侧 persistent map 推送三件套**(T2 档强制,P5):
|
||||
|
||||
1. 不做 map/unmap 命令对:server 唯一需要知道的是"这个资源现在有没有活的宿主写入者"(`IsBufferDrawClean` 那一行要表达的东西),所以 `ResourceRespecify/SubData` 的 payload 带一个 `hasLiveHostWrites` 位,零新增记录种类。
|
||||
2. 块粒度脏块推送:tracker 维护 `m_livePersistentMaps`(persistent+write+非 FlushExplicit+非 GpuResident),在每个 validate 点对本次操作可达的每个这类 buffer(VAO/index/indirect/UBO/SSBO/atomic/XFB target——即后端 20 个 `SyncPersistentMappedRange` 站点的并集)按 `MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(默认 64)切块发送。Phase 1 保守版(整个 mapped span 当脏,按块拆);Phase 2 精确版(shadow-in-shm 的 64 KiB 块脏位,`memcmp` 先行)。P5 验收记录 `persistent-map-push` 字节量;若保守版在 Create/Flywheel fixture 上不可接受,精确版提前——计划里唯一允许因测量改变阶段顺序的地方。
|
||||
3. 门从第一天就有:`PersistentCoherentMapScenario`(map PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验)。
|
||||
|
||||
`MOBILEGL_COHERENT_AS_FLUSH` 在拆分模式下照常生效:两个带 `coherent_as_flush: true` 的 Create fixture 在 split 与 monolith 下走同一条 buffer 路径,逐名对比才有意义。
|
||||
|
||||
## 13. 回读、roundtrip 清单与验证
|
||||
|
||||
### 13.1 稳态零 roundtrip 与不可避免的阻塞点
|
||||
|
||||
零 round trip:全部 draw/clear/blit/copy/dispatch/barrier/XFB 跨度/bind/CSO/`set_*`/上传/`present`(单向记录);全部 caps 站点(握手快照);`glGetError`/`glFinish`/`glFlush`(本地 / no-op);fence 与 query 的创建及非阻塞轮询(client 铸造 handle,未命中合法地答"未就绪");`glGetTexImage`(DirectGLES,含 GPU 生成的 mip);`glReadPixels` → pack PBO(fire-and-forget + client 侧 `MarkGpuWritten`,严格优于 monolith 的无条件停等);`glEndTransformFeedback`(取消无限 fence 等待,对 capture target 置 `MarkGpuWritten`);`eglSwapBuffers`(只查 credit);`*IndirectCount`;restart/multi-draw。
|
||||
|
||||
不可避免(全部罕见):握手一次;surface 生命周期与首次 `MakeCurrent`+`InitCapabilities` 每 surface 至多一次;`glReadPixels` → 客户内存(像素进 `SEG_REPLY`,逐行写回循环留在 server 内按操作级批成一段);`glGetTexImage`(DirectVulkan,对"无 GPU 背书"的 level 回答"请用你自己的 shadow");GPU-write pending 的 buffer 首次 CPU 读(monolith 本来就 `glFinish()`;由 `writableMask` 与 `OnGpuWritten` 收窄);`glClientWaitSync(timeout>0)`、`GL_QUERY_RESULT` 未完成、`glBeginConditionalRender`(谓词只解析一次,之后每个条件 draw 在 client 丢弃,server 永远不需要那个 query);`glBufferStorage` 的 ack;`MapPersistent`(仅 T1,每次存储定义一次);纹理拉取(§8.4);client 侧索引扫描当源 EBO 在 pending 集里;ring/stage 耗尽与 present credit(节奏,非语义)。
|
||||
|
||||
验收措辞:在全部 40 个 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数与 `index-bytes-shipped`;零 timeout 轮询循环必须在有界时间内退出。
|
||||
|
||||
### 13.2 五部分验证门(取代 monolith 的字节一致门)
|
||||
|
||||
"改前改后 `nm --defined-only` 与 `.text` size 完全相等"的门在本方案里按构造死亡(不存在能让旧字节回来的配置);替换是:
|
||||
|
||||
1. **接口纯度三道门**(只跑非 verify 构建):**A 门 include 图**——disaggregated 配置编译 `MG_Backend` 时把 `MG_State/GLState` 从 include 搜索路径移除(`nm --undefined-only` 对"只 include 不调用"是瞎的,而 `RenderState.h → FramebufferObject.h → TextureObject.h` 正是这种耦合),依赖 P0.5;**B 门符号**——`nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` 为空;**C 门未声明**——`grep -c 'pGLContext' MG_Backend/` == 0。外加 debug 断言"每个后端 memo 键都是 `{slot, gen}`,永不是裸前端指针",由 `HandleRecycleScenario` 支撑(重键前必须在至少一个后端上是红的)。
|
||||
2. **语义影子比对 `MOBILEGL_PIPE_VERIFY=1`**——决定性的一条:两套状态模型活在同一地址空间,tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 比对器逐字段、每 draw 比对,打印第一个分歧字段与 draw 序号。抓 tracker 忘推的字段、**dirty 位触发得太少**(危险方向)、两条路径变换不一致的值。第三种 CI 模式,40 个 trace + 全部集成测试,~5–10× 慢,永不出货。逐字段而非 `memcmp`(padding 会 false-DIFFER)。**保留模式**:消费即清的组(纹理 dirty rect)发射后无法重算,verify 时 tracker 保留清除前的集合并比对发射出去的 `(UnionBox, RegionCount, Regions[])`。**活过 P13**。
|
||||
3. **行为 A/B**:40 个 trace 在 `{monolith-pull, monolith-push, split}` 下 SSIM ≥ 0.99(默认阈值);`ctest -L integration-gpu` 在 `DirectGLES.` 与 `DirectGLES.Pipe.`/`DirectGLES.Split.`(DirectVulkan 同)之间逐名相同;单元测试全绿;CTS 逐后端 conformance 在 0.5 pp 内(行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail),NS 不进分母)。`TextureUploadShapeScenario` 把逐纹理逐帧的上传形状(box vs N region、作业数)录金标比对——+6 ms 悬崖由形状相等把关,SSIM 对它完全不敏感。逐名功能基线是"P1 出口的重构后 monolith"(P1 出口先用 verify 证明等价于 `81b17c0b`);`81b17c0b` 只作性能锚点。
|
||||
4. **monolith 性能不回归**:两台设备 reboot-clean、同热窗口、配对 A/B,`tools/bench.sh` + trace replay `--benchmark` 逐帧 JSON;**指标是逐线程 CPU 时间**,p50 与 p99;**绝对阈值**——tracker 每 draw 的 ns 公布并设上限(真实拉取基线只有每 draw 6.5–9.3 次 accessor,相对噪声阈值会平凡通过);Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。
|
||||
5. **覆盖 + poison + 句柄纪律**:G6 重生成 0 UNMAPPED;`gen_pipe_dirty_surface.py` 重生成 0 未映射 mutator;逐 verb 世代 poison;G7 setter 一致性测试;`ResidualValueBlock` 的 `offsetof` 断言与 P13 的 `sizeof == 0`。
|
||||
|
||||
两条幸存的字节级等式:`MOBILEGL_BUILD_DISAGGREGATED=OFF` 时 `nm --defined-only libMobileGL.so | grep MG_Remote` 为空且链接行不增加库;`nm -D libMobileGL.so | grep mobilegl_server_main` 在 RelWithDebInfo 里命中。符号与 `.text` 漂移每阶段作为信息性指标发布。
|
||||
|
||||
### 13.3 长期语义门:MGPipe recorder
|
||||
|
||||
P13 把 `MG_Test` 的 mock 后端变成 MGPipe recorder:在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖 `MG_State`,是 P13 之后不靠 verify 构建的语义门,也给 `tools/trace_replay` 一种记录**已解析**状态的、比 apitrace 精确得多的录制格式。它只覆盖推送内容,不覆盖后端对它的解释(split-only 的渲染 bug 仍无 server 侧第二意见)。
|
||||
|
||||
## 14. Present、线程与帧节奏
|
||||
|
||||
- `eglSwapBuffers` → `present{frameSerial}`(swap interval 搭在同一条记录上)→ publish + 敲门铃 → 返回,除非超出 credit。**`present` 与 `eglSwapBuffers` 严格 1:1**:两个后端的帧边界排空(Magma 四次 `OnFrameBoundary` 老化、`TryDrainFrameTransients`、`BeginFrame`;Espryt 三个 ring 与 `TrimBufferPool` 的 retire)只在 `Present` 内发生,批量会饿死它们。
|
||||
- **`MOBILEGL_IPC_PRESENT_CREDIT` 默认 1**(可配 1–4):延迟叠加,`端到端 ≈ client credit + server 帧数 + 驱动深度`;server 的 `Present` 末尾已在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 4–5 帧(60 Hz 下 66–83 ms)。P10/P12 用 `GetGpuTimestampNs` 与 `--benchmark` 逐帧 JSON 构建输入延迟直方图,只有实测吞吐收益能抵掉延迟代价才调高。
|
||||
- Magma 从不注册 `SetSwapInterval` 且偏好 `MAILBOX`/`IMMEDIATE`,IPC credit 是它唯一的显式限帧器;若需要 FIFO 作为独立 `dev` 变更。
|
||||
- 线程——client:**v1 不加线程**,编码在 GL 线程上直接写 ring(前端本就是 per-context 单线程契约);外来线程的 sync/query 读全部从 `RingControl` 无锁回答,必须发射的少数取 `ctrlMutex` 走 CTRL socket 的 `AuxRequest`(SPSC ring 不允许第二个 producer);`ShaderCompilePool` 原样在 client;可选 `mgl-client-tx` 凭测量决定。server:`mgl-srv-io`(asio、封帧、`SCM_RIGHTS`、doorbell、CTRL RPC)、`mgl-srv-apply`(**终身持有原生 context**:`g_backendContextOwnerThread` 只写一次,`MakeCurrent` 的缓存失效风暴变启动期一次性,每帧 EGL 复核恒真,off-thread 降级消失)、可选 `mgl-srv-dec`。
|
||||
- **核心放置**:拆分的全部性能主张押在两半落在两个都快的核上。全库无亲和性控制,server 是独立进程不继承 launcher 的亲和性。规则:报总 CPU 工作量差(client tracker + encode + decode + server apply vs monolith `PrepareForDraw`);复用 `ShaderCompilePool` 的大核探测把 `mgl-srv-apply` 绑到大核(`MOBILEGL_IPC_SERVER_AFFINITY`,默认 auto,解析出的 mask 打进日志);每阶段报逐线程 CPU 时间。
|
||||
- 拆机顺序:publish + server 排空并 ack → 停 apply 线程 → 关 transport → client 排空 compile pool(先于 `glslang::FinalizeProcess()` 与 `pGLContext` 析构)→ `MobileGL::Destroy()` → 释放 sync/query handle。
|
||||
|
||||
## 15. 进程、EGL 与平台
|
||||
|
||||
### 15.1 启动与握手
|
||||
|
||||
- server 定位:`MOBILEGL_IPC_SERVER_PATH`(主要)→ `dladdr(&MobileGL::Initialize)` 同目录的 `libMobileGLServer.so`(兜底;不能当主要机制,因为集成测试静态链接 `MobileGL_s`、trace replay 的可执行文件不在库目录)。配套:`MobileGLServer` 的 `RUNTIME_OUTPUT_DIRECTORY` 设为 `$<TARGET_FILE_DIR:MobileGL>`,每条新 ctest `ENVIRONMENT` 与 `add_trace_replay_test` 的 `SPLIT` 分支带 `MOBILEGL_IPC_SERVER_PATH`。
|
||||
- 启动:`socketpair(AF_UNIX, SOCK_STREAM)` + `fork`/`execve`,fd 3 = socket。无文件系统 socket 路径、无 abstract namespace、Android 上无 SELinux 争议。
|
||||
- **子进程强制 monolith**(修无界 fork 链——server stub `dlopen(libMobileGL.so)` 后必然走 `MG_Backend::Init()`,继承的 `MOBILEGL_TRANSPORT=spawn` 会再 spawn):spawn 时构造显式 envp 剔除 `MOBILEGL_TRANSPORT` 与全部 `MOBILEGL_IPC_*`;`mobilegl_server_main` 在到达 `Init()` 之前把 `MG_Config::Transport` 硬置为 `Monolith`。两条都做。`MG_Test/Wire` 测试:spawn 一个 server,进程树只多出恰好一个子进程。
|
||||
- `Hello{abi, backendType, buildFingerprint, configBlob}` → `Welcome{四个段}`。`configBlob` 转发 client 解析好的 `MG_Config::Features`,两半不可能对 quirk 开关有分歧;`buildFingerprint`(git hash + `PipeCalls.def` hash)不匹配 → 握手期 `Fatal{AbiMismatch}`。
|
||||
- `mobilegl_server_main` 声明为 `extern "C" __attribute__((visibility("default")))`:非 Debug 构建设了 hidden visibility,而 FCL/plugin 出货的是 RelWithDebInfo,否则 `dlsym` 在设备上静默失败。
|
||||
|
||||
### 15.2 Android(spike A 已证)
|
||||
|
||||
- 交付链:APK 唯一可 exec 的位置是 `lib/<abi>/`,打包器只收 `lib*.so`,所以 server 以 `add_executable` + `PREFIX "lib"/SUFFIX ".so"` 构建(真 PIE),并把 `RUNTIME_OUTPUT_DIRECTORY` 指到 AGP 收集原生产物的 `CMAKE_LIBRARY_OUTPUT_DIRECTORY`(`CMakeLists.txt:784-808`,`MOBILEGL_BUILD_SERVER_SPIKE`)。**两台设备上都已证明**:从 `TraceReplayActivity` 自身的 `untrusted_app` 进程 `fork`+`execve` `<nativeLibraryDir>/libMobileGLServer.so`,子进程落在同一域、同一 MLS category,exit 0,零 avc denial(`MEASUREMENTS.md` §1)。
|
||||
- `fork`+`execve` 而非 `posix_spawn`:bionic 从 API 28 才声明后者,minSdk 26(`android-plugin/app/src/trace/cpp/spawn_spike.cpp:63-68`)。fork 与 execve 之间只做 async-signal-safe 的 open/dup2/execve/write/_exit(父进程是多线程 JVM)。
|
||||
- 应用进程的 stdout/stderr 是 `/dev/null`:子进程用 **marker 文件** 证明自己活过,exec 被拒的 errno 经 close-on-exec pipe 回传(EACCES 与 ENOEXEC 是完全不同的判决)。
|
||||
- 生产 server 主体是 ~30 行 stub:`dlopen(libMobileGL.so)` → `dlsym("mobilegl_server_main")`。一份共享库、两个角色、版本必然匹配(Android 上那份库仍含 glslang/SPIRV-Cross,因为它同时服务 client;B 门检的是 server 侧代码有没有引用它们)。
|
||||
- minSdk 26 没有公开 NDK API 能扁平化 `ANativeWindow`(`libbinder_ndk`、`ASurfaceControl` 都是 API 29)。**P5–P11 验证路径无窗口**:pbuffer 或 `AImageReader` 的 `ANativeWindow`,trace replay 默认 pbuffer。**P12 生产路径**:Java `Surface`(Parcelable)→ Messenger/AIDL → `MobileGLServerService`(`android:process=":mgl"`)→ JNI `ANativeWindow_fromSurface`(FCLauncher 今天在 `egl_bridge.c` 做的那一次调用);仓内先例是 `android:process=":bench"` 的 `BenchService`。代价:server 进程多一个 ART(~15–25 MB)。FCL 把游戏 JVM 跑在主进程,第二个进程必须新建。
|
||||
- `HeadlessGL` 的 fork 预检会 fork 一个子进程跑完整 EGL bring-up 然后 `_exit`——拆分模式下那个子进程会 spawn 一个孤儿 server。规则:server 的 EOF 检测**即时且无条件退出**(亚秒级);client 的 socket fd 设成 `_exit` 会确定性关闭的形态;就绪握手有界重试。列为 P6 验收。
|
||||
- 通用 env 透传 `--env K=V`(`run_android_retrace_local.py` → intent extra `mobilegl_env` → `trace_replay_core.cpp` 在加载 `libMobileGL.so` 前 `setenv`)已接进 retrace 通道,取代逐 knob 加 `--es/--ez`。
|
||||
|
||||
### 15.3 Linux / Windows / 崩溃
|
||||
|
||||
- Linux/X11:`Window` 是 XID,`nativeToken:u64` 直接送,backend 自己 `XOpenDisplay(getenv("DISPLAY"))`;Wayland 维持不支持。WSL/CI 永不开窗:`EGL_PLATFORM=surfaceless` + `EnsureHeadlessPlatform()`。
|
||||
- Windows:`HWND` 进 `nativeToken`,Vulkan 可行,WGL/ANGLE-DXGI 对外进程 HWND 不受支持 → headless only。transport 默认 named pipe:asio `windows::stream_handle` 要求 overlapped 句柄,所以用 GUID 命名的 `CreateNamedPipeW(FILE_FLAG_OVERLAPPED)` + `CreateFileW(FILE_FLAG_OVERLAPPED)` 造句柄对再继承给 `CreateProcess`;AF_UNIX-everywhere 是可选简化。Windows 机器不是正确性门。macOS 不拆分(`CAMetalLayer` 无跨进程表示)。
|
||||
- server 死:client 读到 EOF/EPIPE → device-lost 闩锁(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_FALSE`+`EGL_CONTEXT_LOST`、`glGetGraphicsResetStatus` 返回 `GL_UNKNOWN_CONTEXT_RESET`);`MOBILEGL_IPC_RESPAWN=1` 时重启并全量重推(默认关,静默重启会掩盖 bug)。client 死:server 读到 EOF → 立即销毁原生 context 并退出;`MOBILEGL_IPC_IDLE_EXIT_S`(默认 30)只作最后保险。
|
||||
|
||||
## 16. 构建布局
|
||||
|
||||
```
|
||||
MobileGL/MG_Pipe/ 永远进构建(monolith 的架构,不在任何 option 之后) [P0]
|
||||
MobileGL/MG_Impl/Pipe/ Tracker、SlotAllocator、CsoCache、HostResolve、CompositeResolver [P2+]
|
||||
MobileGL/MG_Backend/MGPipe/ PipeInputs.h + MGPipeImpl_DirectGLES/DirectVulkan.cpp [P1+]
|
||||
MobileGL/MG_Remote/ 仅 MOBILEGL_BUILD_DISAGGREGATED
|
||||
Protocol/ protocol.fbs generated/protocol_generated.h mg_protocol_base.h [P0]
|
||||
Transport/ ITransport InProcessTransport Framing Ring ShmSegment(+Posix/Win32) FdPassing Doorbell WireLog [P0]
|
||||
SocketTransport [P6]
|
||||
Client/ PipeEmitter EmitTables BackendObject_Remote CapsMirror ShadowArena PersistentMapTracker GpuWritePending Surface/{X11,Win32,Android,Headless} [P5+]
|
||||
Server/ PipeApplier PipeObjectTables IndexHostMirror ServerLoop ReplyPool EventRing ServerMain [P5+]
|
||||
ServerJni.cpp [P12]
|
||||
```
|
||||
|
||||
- CMake option(`CMakeLists.txt:23`):`MOBILEGL_BUILD_DISAGGREGATED`(默认 OFF)追加 `MG_Remote/**` 进 `SOURCE_FILES`(`CMakeLists.txt:454-469`)并定义 `-DMOBILEGL_BUILD_DISAGGREGATED=1`;OFF 时 `MG_Config::Transport` 是 `constexpr Monolith`,`Init.cpp` 的分支编译期消失。`3rdparty/flatbuffers/include` 缺失时把 option 强制回 OFF 并 `message(WARNING)`(`CMakeLists.txt:440-451`)。`MobileGL` 与 `MobileGL_s` 都拿到同一份源。`MG_Test/Wire` 只在该 option 下注册(`MobileGL/MG_Test/CMakeLists.txt:93-95`)。
|
||||
- `MOBILEGL_BUILD_DISAGGREGATED_INPROC`(尚不存在):CI/调试形态,隐含开启前者,额外加角色隔离 shim。MGPipe 让需要角色分身的进程全局从四个(`pGLContext`、`gBackendFunctionsTable`、`pActiveBackendObject`、`pDefaultFramebufferInfo`)降到**两个**(pipe 表与 `pActiveBackendObject`):server 角色不再读 `pGLContext`(三道纯度门就是这个断言),`pDefaultFramebufferInfo` 由保留句柄 `{0,1}` + `OnSurfaceChanged` 取代。两个 shim 都不在 GL 热路径的每次访问上——这是 `inproc` 从"成本可疑的实验"变成"可交付形态"的直接原因(Android 上 dlopen 的库无法可靠用 initial-exec TLS,`pGLContext->` 在 `MG_Impl` 有 1494 处)。
|
||||
- `MobileGLServer`:桌面 `add_executable` 链接 `MobileGL_s`;Android `add_executable` 改名 `lib*.so` 链接共享 `MobileGL`,由 AGP 打进 `jniLibs`。
|
||||
- `MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>`(P5 起在 `ConfigLoader.cpp` 解析),免费换来 ctest `ENVIRONMENT` 变体、trace-replay 的 `setenv` 块、FCL 用户可编辑 env、plugin APK 的 V2 开关表、`/data/local/tmp` CTS 路径。
|
||||
- 测试接线陷阱:ctest `ENVIRONMENT` 是替换而非追加、`;` 必须转义、property 覆盖 job env,必须用 `mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})` 构造;`add_trace_replay_test` 加 `SPLIT` 后缀(否则与同 case+backend 重名)并加 `-DTRACE_TRANSPORT=` 给 `run_trace_case.cmake` 消费。
|
||||
- CI(`.github/workflows/test.yml:809` `pipe-gates`,P0 已落地):`gen_pipe.py` 重生成 + diff;`MG_Backend`/`MG_State` 下禁止 stdio 插桩的 grep 门;`gen_pipe_dirty_surface.py --summary`(信息性,P1 成门);`check_doc_citations.py`(警告级,文档定稿后 `--strict`)。独立 job `flatc-check`。后续:`include-graph-check`(P0.5)、`monolith-symbol-report`。
|
||||
|
||||
## 附 A:开关
|
||||
|
||||
CMake:
|
||||
|
||||
| 选项 | 默认 | 状态 |
|
||||
|---|---|---|
|
||||
| `MOBILEGL_BUILD_DISAGGREGATED` | OFF | 已落地 |
|
||||
| `MOBILEGL_BUILD_SERVER_SPIKE` | OFF(仅 Android) | 已落地(spike A,非出货) |
|
||||
| `MOBILEGL_BUILD_DISAGGREGATED_INPROC` | OFF | 计划(P5) |
|
||||
| `MOBILEGL_PIPE_VERIFY` | OFF | 计划(P1;构建期开关,编译进 `SnapshotFromGLContext()` 与 G4 比对器,P13 后保留) |
|
||||
| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON(P2..P13) | 计划(编译期臂) |
|
||||
| `MOBILEGL_FLATC_EXECUTABLE` | 空 | 已落地(只服务 `flatc-check`) |
|
||||
| `MOBILEGL_BAKED_INTERNAL_SHADERS` | ON(P7+) | 计划 |
|
||||
|
||||
运行时,MGPipe(`MobileGL/Config.h:319-358`,`MobileGL/ConfigLoader.cpp:245-256`,P0 已落地):
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `MOBILEGL_PIPE_PUSH` | 0 | 子系统位图(0 = 全 pull),含一位关闭 CSO 内容寻址;十进制或 `0x` |
|
||||
| `MOBILEGL_PIPE_VERIFY` | 0 | 逐 draw 逐字段影子比对 |
|
||||
| `MOBILEGL_PIPE_STATS` | 0 | 边界计数器(§附 B) |
|
||||
| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON | 三态读取,只有显式 falsy 才关 |
|
||||
| `MOBILEGL_PIPE_TEXEL_RETAIN_MB` | 0(0–4096) | 纹理拉取保留 LRU |
|
||||
| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64(0–4096) | 索引宿主镜像预算 |
|
||||
| `MOBILEGL_PIPE_STATS_PERIOD` | 120(1–10⁶) | 每多少帧一条汇总行 |
|
||||
| `MOBILEGL_PIPE_STATS_FILE` | 空 | teardown 时的 JSON 转储路径 |
|
||||
|
||||
运行时,传输与 IPC(计划,P5+):`MOBILEGL_TRANSPORT`(monolith)、`MOBILEGL_IPC_SERVER_PATH`、`MOBILEGL_IPC_RING_MB`(8)、`MOBILEGL_IPC_STAGE_MB`(32)、`MOBILEGL_IPC_PRESENT_CREDIT`(1)、`MOBILEGL_IPC_SPIN_US`(50)、`MOBILEGL_IPC_POLL_ESCALATE`(64)、`MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(64)、`MOBILEGL_IPC_ADOPT_TIER`(auto)、`MOBILEGL_IPC_SHADOW_SHM`(1,Phase 2 起)、`MOBILEGL_IPC_INLINE_PAYLOADS`(0,负面对照)、`MOBILEGL_IPC_SERVER_AFFINITY`(auto)、`MOBILEGL_IPC_STRICT_ERRORS`(0)、`MOBILEGL_IPC_AUDIT`(0)、`MOBILEGL_IPC_TRACE`(0)、`MOBILEGL_IPC_ATTACH`、`MOBILEGL_IPC_RESPAWN`(0)、`MOBILEGL_IPC_IDLE_EXIT_S`(30)。显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`(server 没有 `MG_Impl` 校验器)。既有负面对照开关(`MOBILEGL_ESPRYT_DISABLE_{UBO,UNPACK,UPLOAD}_RING`、`_INVALIDATE_FLUSH`、`MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION`、`MOBILEGL_COHERENT_AS_FLUSH`)全部保留。
|
||||
|
||||
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h:46-122`,P0 已落地)
|
||||
|
||||
关闭时每站点一次全局 load + 一条永不命中的分支。字节类:`stage-buffer`、`stage-texture`、`stage-ubo-global`、`stage-ubo-named`(只有 Magma 贡献,D-B8 的不对称)、`stage-vertex-client`、`stage-index-client`、`stage-indirect-cmd`(Espryt 独有)、`persistent-map-push`(P0 未接线:monolith 期不存在推送)、`residual-value-block`(占位)。调用类:`draws`、`accessor-calls`(实际执行的 GLContext accessor 次数,在约 10 个热入口做静态计数,是**下界**)、`texture-upload-emissions/box/rect/jobs`。六个 memo 门(`SyncRenderState` 早退、`SyncNeccessaryTextures` 键比较、`CurrentUnitBindingsEpoch` 快门、`TrySetupDrawFastPath`、pipeline memo、`ApplyDynamicDrawStateTail`)各计 hit/miss。每 draw payload 直方图(24 桶)已实现,等第一个发射器接入。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行(`MGLOG_I`),`TRACY_ENABLE` 下逐帧 `TracyPlot`,teardown 时可选 JSON。站点清单——哪些路径**没有**接线——写在 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`,那份清单是契约。
|
||||
@@ -0,0 +1,96 @@
|
||||
# P0 实测
|
||||
|
||||
> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74C,Adreno 830,Android 16;`3B159D009VZ00000` = Oppo PLG110,Mali,Android 16(ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。
|
||||
|
||||
## 1. Spike A — 从应用自身进程 exec 第二个原生可执行文件
|
||||
|
||||
问题:Android 上能否把 server 以 `lib*.so` 打进 APK,并从应用自己的 `untrusted_app` 域 `fork`+`execve` 它(`adb run-as` 跑在别的域,证明不了)。
|
||||
|
||||
| 设备 | 结果 |
|
||||
|---|---|
|
||||
| Adreno 830 | **OK**。父进程 `u:r:untrusted_app:s0:c173,c257,c512,c768` `fork`+`execve` `<nativeLibraryDir>/libMobileGLServer.so` → 子进程 pid 31348,exit 0;子进程 SELinux `u:r:untrusted_app:s0:c173,c257,c512,c768`(同域同 category);marker 文件、stdout 捕获、报告全在;`execErrno=0`;窗口内**零 avc denial** |
|
||||
| Mali | **OK**,同形:子进程 pid 28433,exit 0,`execErrno=0`,`u:r:untrusted_app:s0:c94,c257,c512,c768`,零 avc denial |
|
||||
|
||||
主机侧已证的三条(随 `8a239177`):AGP 会把改名成 `lib*.so` 的 `add_executable` 打进 `lib/arm64-v8a/`,前提是把 `RUNTIME_OUTPUT_DIRECTORY` 重定向到 `CMAKE_LIBRARY_OUTPUT_DIRECTORY`;`posix_spawn` 在 minSdk 26 不可用(bionic API 28 起),出货臂是 `fork`+`execve`;应用进程 stdout/stderr 是 `/dev/null`,子进程用 marker 文件证明自己活过。
|
||||
|
||||
- 代码:`tools/spikes/server_stub/main.cpp`(stub:打印并写 marker 自己的 pid/uid/SELinux 上下文)、`android-plugin/app/src/trace/cpp/spawn_spike.cpp`(`RunSpawnSpike`)、`CMakeLists.txt:784-808`(`MOBILEGL_BUILD_SERVER_SPIKE`)。
|
||||
- APK:`p0-spike-a-android/trace-debug-spike-on.apk`(在 `30d7595b` 构建,与 `7ef7c7e5` 源码相同)。
|
||||
- ColorOS 陷阱:首次 `adb install` 一个未安装的包会卡在 `com.oplus.appdetail InstallGuideActivity` 确认页,直到点"继续安装"(1272×2772 面板上 `input tap 353 2349`);同签名重装静默通过。另一台设备上一个外来签名的 trace APK(versionCode 26080769)会让 `install -r` 报 `INSTALL_FAILED_UPDATE_INCOMPATIBLE`,需先卸载。
|
||||
- 42-device.sh 的 env 透传 A/B 腿在该 ROM 上跑不了(`run-as sh -c 'cat > files/…'` 被拒);透传由下面的 stats 基线端到端证明(`--env MOBILEGL_PIPE_STATS=1` 必须在 `mobilegl.log` 里产生 `MGPipe stats` 行)。
|
||||
|
||||
## 2. Spike B — 跨进程外部内存分档
|
||||
|
||||
问题:`AcquirePersistentMap` 背后的内存能否共享给另一个进程并在那里映射,两个后端各走哪条路。探针 `tools/spikes/extmem_probe/`(`39f982e6` 源码,arm64,`adb shell` = `u:r:shell:s0` 域),4 MiB payload,64 KiB 同判决。每一行都取一次真 GPU 访问(`vkCmdCopyBuffer` + `vkCmdFillBuffer` + host-read barrier)并两侧字节校验才算 OK。
|
||||
|
||||
| 路线 | Adreno 830 | Mali |
|
||||
|---|---|---|
|
||||
| T1-opaque-fd(server 导出 `VkDeviceMemory` fd,client 裸 `mmap` + 导入) | **OK** 完整往返含 GPU 访问(`/dmabuf:system`,dedicatedOnly=1) | UNSUPPORTED(`vkCreateBuffer(external)=VK_ERROR_INVALID_EXTERNAL_HANDLE`,advertisedExportable=0) |
|
||||
| T1-dma-buf | UNSUPPORTED(`VK_EXT_external_memory_dma_buf` 缺) | UNSUPPORTED |
|
||||
| T1-gles-memobj-fd(`GL_EXT_memory_object_fd` 导入导出的 fd) | **FAIL**:导入 + `glBufferStorageMemEXT` 接受(`GL_NO_ERROR`)但每次 `glMapBufferRange` → `GL_INVALID_OPERATION`(persistent 与 plain 都是);`GL_DEVICE_UUID` 不可读 | UNSUPPORTED(扩展字符串缺,入口点可解析) |
|
||||
| **T0-ahb-blob-transfer**(client 分配 `AHardwareBuffer` BLOB → socket 交接 → server Vulkan 导入 + GL 导入) | **OK** 全链:cpu-lock、vk-import+map、GPU copy/fill、GL map persistent+coherent、写回 client 全部字节校验 | **OK** 全链,判决相同(glPersistentCoherent=1,gpuRan=1) |
|
||||
| T3-external-memory-host(`VK_EXT_external_memory_host`) | UNSUPPORTED(扩展缺) | PARTIAL:导入 + map 往返,但 **GPU 写对宿主映射不可见**(只读档) |
|
||||
| T3-memfd-cross-process / client-memfd-server-import | UNSUPPORTED | OK / PARTIAL(同样的 GPU 只读 caveat) |
|
||||
|
||||
**P11 的分档决定**:唯一在两台设备、两个后端上都是完整读写的档是 **T0**——client 分配 `AHardwareBuffer` BLOB,server 以 `VK_ANDROID_external_memory_android_hardware_buffer`(Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`(Espryt)导入,两侧 persistent+coherent 映射。Adreno 另有 T1(Vulkan 路径);Mali 无任何 server 导出路线,host-pointer 导入只读。Caveat:运行域是 `shell` 不是 `untrusted_app`;AHB 的 socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,域风险在 memfd/opaque-fd 腿上。
|
||||
|
||||
复现:
|
||||
|
||||
```sh
|
||||
ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 tools/spikes/extmem_probe/build_android.sh /tmp/extmem-build
|
||||
S=<serial>; adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/extmem_probe \
|
||||
&& adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe; echo EXIT=\$?" | tee out-$S.txt
|
||||
```
|
||||
|
||||
判决语义(OK / PARTIAL / FAIL / UNSUPPORTED)与逐腿 trace 格式见 `tools/spikes/extmem_probe/README.md`。主机构建(lavapipe)用来证明探针本身报得对:T1/T3 在 lavapipe 上全 OK;T1-gles 在 llvmpipe 上 `GL_OUT_OF_MEMORY` 是 Mesa interop 缺口,不是探针缺陷。
|
||||
|
||||
## 3. 边界计数器基线(双设备、双后端、四条 trace)
|
||||
|
||||
`MOBILEGL_PIPE_STATS=1` 经 retrace 通道的 `--env` 透传;trace APK 从 `7ef7c7e5` 构建,spike OFF。取每次运行的**最后一个完整 120 帧窗口**。accessor/draw 与 memo 门数字是软件确定的(同一 trace 在两台设备上完全相同:它们数的是代码路径不是硬件),只有墙钟/CPU 时间随设备变。
|
||||
|
||||
| trace(窗口内帧数) | 后端 | draws/f | **acc/draw** | buf B/f | tex B/f(发射 box/rect) | ubo-global B/f | **ubo-named B/f** | memo 门(hit/miss) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `minecraft-1.21.4-in-world`(360) | Espryt | 91.6 | **9.28** | 13.5 K | **635 K**(185 box / 0 rect) | 16.7 K | 0 | ers 9257/2577,etl 10538/1296,eub 10720/1114 |
|
||||
| `minecraft-1.21.4-in-world`(360) | Magma | 91.6 | **8.56** | 13.5 K | 39.9 K(97 box / 89 rect) | 16.7 K | 0 | mfp 0/10994,mpm 9240/1754,mdt 9120/1874 |
|
||||
| `minecraft-1.21.4-fabric-iris-bsl-in-world`(120,memo 冷) | Espryt | 23.2 | 21.04 | 32.6 K | 8.8 K | 1.8 K | 0 | ers 1958/1843,etl 722/3079 |
|
||||
| `minecraft-1.21.4-fabric-iris-bsl-in-world`(120,memo 冷) | Magma | 23.2 | 11.26 | 313 K | 256 K | 1.8 K | 0(vtxc 1.7 K) | mpm 1890/895,mdt 1573/1212 |
|
||||
| `improved-transparency-minecraft-26.3`(1200) | Espryt | 1320 | **8.44** | 333 K | 0 | 0 | 0 | ers 156925/2791,etl 148606/11110,eub 148246/11470 |
|
||||
| `improved-transparency-minecraft-26.3`(1200) | Magma | 1320 | **6.53** | 173 K | 0 | 0 | **331 K** | mfp 21360/137036,mpm 134421/2615,mdt 156611/1785 |
|
||||
| `minecraft-1.21.1-neoforge-create-indirect-in-world` | 两者 | — | — | — | — | — | — | 两台设备都失败(§5),且不足 120 帧 |
|
||||
|
||||
门缩写:ers = `EsprytRenderState`,etl = `EsprytTextureSyncList`,eub = `EsprytUnitBindingsEpoch`,mfp = `MagmaDrawFastPath`,mpm = `MagmaPipelineMemo`,mdt = `MagmaDynamicTail`(`MobileGL/MG_Util/Metrics/PipeStats.h:46-122`)。`accessor-calls` 是约 10 个热入口的静态计数,是每 draw accessor 数的**下界**(站点清单 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`)。
|
||||
|
||||
对设计的读法:
|
||||
|
||||
- **真机稳态动态 accessor 成本是每 draw 6.5–9.3 次**(预测区间 10–25 的下沿;llvmpipe 的 15.5/20.7 是 memo 冷的)。推送要打败的是 ~8 次 accessor + memo 探测,不是 124/169 的静态调用点数。GO/NO-GO 的 tracker 绝对 ns 上限从这里定。
|
||||
- **`stage-ubo-named`(D-B8)**:Magma 在 26.3 世界每帧重打包 **331 KB** 具名 UBO 字节,Espryt 直接绑定为 0——host payload 决定的第一个真数字。
|
||||
- **union box vs region list**:vanilla 世界同样 185 次发射,Espryt 的整 box 路径移动 **635 K** 纹素字节/帧,Magma 的 rect 路径 **40 K**,16×——"server 选上传形状"这一条的量化依据(Mali 侧的 +6 ms/frame 作业数悬崖在另一个方向)。
|
||||
|
||||
复现(一台设备一次;两台必须**串行**,见 §4):
|
||||
|
||||
```sh
|
||||
ANDROID_SERIAL=<serial> MSYS_NO_PATHCONV=1 \
|
||||
python3 tools/trace_replay/run_android_retrace_local.py \
|
||||
--case minecraft-1.21.4-in-world --backend DirectGLES \
|
||||
--env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120
|
||||
# 数字在结果目录的 mobilegl.log 里,grep 'MGPipe stats:',取最后一个完整窗口
|
||||
```
|
||||
|
||||
## 4. 桌面数据点与语料事实
|
||||
|
||||
- **llvmpipe / lavapipe 动态 accessor**(`GuiBatchScenario`,14 帧 / 26 draw,memo 冷):Espryt 20.65 / Magma 15.54 次/draw——落在预测区间内,且因场景太短偏高;真机稳态数字见 §3。
|
||||
- **dirty-surface 面**(`python3 scripts/gen_pipe_dirty_surface.py --summary`,本树):`MG_Impl/GLImpl` 41 个文件,926 次 mutator 调用,73 个不同 mutator(`RecordError` 一项就占 836 次);92 次(36 个即时发布点、7 个 mutator,绝大多数是 `RecordError`)位于同函数内也到达后端的入口,其余 834 次由紧随的 verb 发布。映射表是 73 条目的问题。
|
||||
- **读点覆盖**(`python3 scripts/gen_pipe.py`):71 条调用(11 screen / 60 context)、63 个 verify payload、61 个 `PipeInputs` 字段;477 行后端读点清单 → 299 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED**。
|
||||
- **OOM 探测惯用法**:41 个 trace fixture 中 0 例——全部语料只有 9 次 `glRenderbufferStorage` 调用散在 5 个 fixture,无一在其后 3 个调用内跟 `glGetError`;语料里真实的成功性检查是 `glCheckFramebufferStatus`。→ `glRenderbufferStorage*` 不 ack。
|
||||
- **`FramebufferSrgb` / `DepthClamp`**:`FramebufferSrgb` 的六个后端读点全部消费一个编译期常量 `false`,`DepthClamp` 零读点;两者的 `glEnable` 落到 `RenderState.cpp` 的 `default:` 分支既不存储也不报 `GL_INVALID_ENUM`;41 个 fixture 无一开启任一项(补真存储不会改动任何既有 fixture 的输出)。
|
||||
- **`GetIntegeri_v` 族**:Espryt 实现里是 `GetIntegeri_v` 的 9 个分支 + `GetInteger64i_v` 的 2 个(不是"15 个 case");`GL_COMPUTE_WORK_GROUP_SIZE` 由 `GL_Program.cpp` 用 `ProgramObject::GetComputeLocalSize` 纯前端回答。
|
||||
- **payload 尺寸**(`MG_Pipe/MGPipeTypes.h` 的 `static_assert`,arm64 与 x86-64 一致):`MGPDrawInfo` **56**、`MGHostSpan` 32、`MGPBindRenderState` **12**、`MGPResourceDesc` 88、`MGPFramebufferState` 304、`MGPProgramDesc` 192、`MGPSubData` 72、`MGPPixelPackState` 28、`ResidualValueBlock` **1248**(其中 `RenderStateParameters` 1168)。`SEG_CMD` 按 56 B 固定头定尺:MC 帧 1000–4000 draw 时每帧 56–224 KiB 头字节。
|
||||
- **persistent map 采纳的既有基线**(`dev`,MC 26.3,Adreno):≥16 MiB 可变 store 定义时采纳为 coherent persistent map 后 p99 163→21 ms、稳态 40→115 fps、省 ~400 MB。P11 的回归上限对着它。
|
||||
- **Mali 上传作业数悬崖**(Espryt 代码注释记录的既有实测):~100 个精灵 rect 对一个 union box 是 +6 ms/frame。
|
||||
|
||||
## 5. Harness 事实与陷阱
|
||||
|
||||
1. trace app 从不到达 `MobileGL::DestroyImpl`,所以 `MOBILEGL_PIPE_STATS_FILE` 的 JSON 转储在设备上永远不会写——只有 `mobilegl.log` 里的周期汇总行;短于一个周期的 trace 什么都不产出。`MOBILEGL_PIPE_STATS_PERIOD`(`458ccde1`)为此而加:需要数字的运行把它设到足够小。
|
||||
2. `run_android_retrace_local.py` 每棵树共用一个 `.trace-work/android-retrace-result` 根并在每次调用时 `rmtree`,所以两台设备必须从一棵树**串行**跑。
|
||||
3. `--env` 值里嵌入的 `/data/...` 会被 runner 的 bash.exe 做 MSYS 路径转换(`MSYS2_ARG_CONV_EXCL="/data/*"` 只覆盖开头匹配)——用 `MSYS_NO_PATHCONV=1` 跑。
|
||||
4. `coherent_as_flush` 管线完好:`--ez coherent_as_flush true` → `trace_replay_core.cpp` 的 `setenv`,独立于 `--env` 透传。
|
||||
5. **`minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败**(Adreno 830:Espryt ~4.5 分钟后黑帧,Magma 纹理上传提交时 `VK_ERROR_DEVICE_LOST`;Mali:SSIM 0.85 / 0.45)。Adreno 830 上用 `dev@81b17c0b` 基线 APK 复现,**是基线就有的问题,不是本分支造成**;它是 P3a/P8 验收清单里的用例,需先在 `dev` 修。
|
||||
@@ -0,0 +1,57 @@
|
||||
# MGPipe:MobileGL 前后端拆分
|
||||
|
||||
> 状态:**P0 已落地**(`feat/disaggregated@458ccde1`,基线 `dev@81b17c0b`)。下一步 P0.5 → P1 → P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`。
|
||||
|
||||
## 是什么
|
||||
|
||||
MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend`:Espryt = DirectGLES、Magma = DirectVulkan)之间的一份**显式接口**:gallium 形状、句柄寻址、只推不拉。它取代今天后端每 draw 直接读 `MG_State::pGLContext` 的做法,让后端拥有自己的状态机,并在此之上把前后端拆到**两个进程**。
|
||||
|
||||
接口本身是可独立交付的产物:即使 IPC 永不上线,`inproc`(同进程第二个 apply 线程)就是 monolith 的渲染线程。
|
||||
|
||||
## 架构(一段)
|
||||
|
||||
```
|
||||
应用 GL 调用
|
||||
→ MG_Impl(GL 语义、错误、shadow)
|
||||
→ MG_Impl/Pipe/Tracker:在每条 verb 之前 validate,把变化推成 MGPipe 调用
|
||||
→ MGPipeScreen / MGPipeContext(两张函数指针表,71 条调用,单一真相源 PipeCalls.def)
|
||||
monolith:直调 backend 函数 split:发射器写 SEG_CMD ring → server applier
|
||||
→ server 对象表(按 {slot, gen} 句柄索引的数组)+ PipeInputs(后端被推送的状态块)
|
||||
→ MG_Backend(Espryt / Magma),两个后端的 ring / pool / memo / lowering pass 原样不动
|
||||
← MGPipeCallbacks(10 个具名反向回调 + 1 个正向终止符)
|
||||
```
|
||||
|
||||
三种构建/运行形态共用**同一份 backend 实现**:`monolith`(默认,接口在进程内直调)、`inproc`(同进程两个线程,CI 形态与渲染线程交付物)、`spawn`(`fork`+`execve` 出 server 进程,SPSC 共享内存 ring + FlatBuffers 控制面)。
|
||||
|
||||
## 文件地图
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| `ARCHITECTURE.md` | 已定稿的设计与架构:句柄与世代、调用目录、记录约定、tracker、纹理路径、shader 制品、反向通道、后端改造、传输、persistent map 分档、进程/EGL/平台、构建与纯度门、验证策略 |
|
||||
| `ROADMAP.md` | P0…P13 阶段表、两条跑道、GO/NO-GO 清单、再基线检查点、仍然开放的问题 |
|
||||
| `MEASUREMENTS.md` | P0 实测:spike A/B 结论、双设备四条 trace 的边界计数器基线、桌面数据点、语料事实、复现命令 |
|
||||
|
||||
代码地图(P0 已落地的部分):
|
||||
|
||||
| 路径 | 作用 |
|
||||
|---|---|
|
||||
| `MobileGL/MG_Pipe/` | `PipeCalls.def`(目录)、`PipeFields.def`(比对器字段表)、`Coverage.def`(读点覆盖)、`MGPipeTypes.h`(payload POD)、`MGPipeHandles.h`、`MGPipeHostSpan.h`、`MGPipeCallbacks.h`、`MGPipe.h`、`generated/*.inc`(G1–G7 产物,提交进树) |
|
||||
| `scripts/gen_pipe.py` | 七个生成器 G1–G7;`gen_pipe_dirty_surface.py` 前端 mutator 面扫描;`gen_protocol.py` FlatBuffers 头再生成;`check_doc_citations.py` 本目录 `file:line` lint |
|
||||
| `MobileGL/MG_Remote/` | `Protocol/protocol.fbs`(控制面 schema)、`Transport/`(`Ring`、`Doorbell`、`ShmSegment`、`FdPassing`、`Framing`、`InProcessTransport`、`ITransport`);仅 `MOBILEGL_BUILD_DISAGGREGATED=ON` 编译 |
|
||||
| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | 边界计数器(字节 / 动态 accessor 调用 / 六个 memo 门 / 上传形状),`MOBILEGL_PIPE_STATS=1` 开启 |
|
||||
| `MobileGL/Config.h`、`MobileGL/ConfigLoader.cpp` | `MOBILEGL_PIPE_*` 八个开关 |
|
||||
| `tools/spikes/server_stub`、`android-plugin/app/src/trace/cpp/spawn_spike.cpp` | spike A:Android 上以 `lib*.so` 打包并从应用进程 exec 第二个原生可执行文件 |
|
||||
| `tools/spikes/extmem_probe/` | spike B:跨进程外部内存分档探针 |
|
||||
| `MobileGL/MG_Test/Pipe/`、`MG_Test/Wire/`、`MG_Test/Util/PipeStatsTest.cpp` | 目录算术、wire 层五个套件、计数器测试 |
|
||||
|
||||
## 术语
|
||||
|
||||
- **client / server**:前端进程 / 后端进程;monolith 下是同一进程的两个角色。
|
||||
- **verb**:会让 server 做事的命令(draw、dispatch、clear、blit、readback、XFB 跨度、query、纹理操作)。推送只发生在 verb 之前的 validate 时刻。
|
||||
- **CSO**:常量状态对象(render state、vertex elements、sampler、sampler view、shader),client 侧内容寻址,server 侧按句柄缓存。
|
||||
- **Track V / Track H**:值类读点的迁移(整块 POD 过线)/ 对象类读点的迁移(`SharedPtr<前端对象>` → 句柄)。
|
||||
- **`MGGen`**:server 私有的"我重铸了驱动对象"纪元,永不过线;与句柄里的 client 世代严格分开。
|
||||
|
||||
## 历史
|
||||
|
||||
本目录此前是一份 328 KB 的实施计划(`PLAN.md`)加 135 KB 的设计竞赛与三视角对抗性评审记录(`REVIEW.md`)。设计已定稿,本次改写只保留设计与架构本身;评审记录、早期草案与修订史留在 git 历史里(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`;`git show 87ee17c6:docs/Disaggregated/REVIEW.md` 可取回评审记录全文)。更早的一条已放弃分支 `Feat/CS-Delta-IPC` 的逐文件可复用判定见 `8349babe` 版 `PLAN.md` §17。
|
||||
@@ -0,0 +1,90 @@
|
||||
# MGPipe 路线图
|
||||
|
||||
> 状态:P0 已落地(`feat/disaggregated@458ccde1`)。设计见 `ARCHITECTURE.md`,实测见 `MEASUREMENTS.md`。天数是各阶段所含子系统行的求和(低端 / 高端),总计 **267–337 人天**(不含 CTS 周转);两个工程师、P7 与 P5/P6/P8 并行约 7–9 个月,真正的约束是两台设备的争用。
|
||||
|
||||
## 通用纪律(每个 commit)
|
||||
|
||||
默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门);**每个门必须能因它存在的理由变红**;Windows 机器不是正确性门;设备对比走 reboot-clean + 同热窗口配对 A/B,CPU 定频按项目协议;每阶段出口跑一次五部分门;每阶段性能判据是**逐线程 CPU 时间**。
|
||||
|
||||
两条跑道:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止且 monolith 严格好于起点;**IPC 跑道** P5 → P6 → P9 → P10 → P11 → P12。
|
||||
|
||||
## 阶段
|
||||
|
||||
| 阶段 | 天 | 落地什么 | 验收门 | 依赖 |
|
||||
|---|---|---|---|---|
|
||||
| **P0** 卫生、度量、门、骨架 | 9–11 | ✅ 边界计数器(字节 / 动态 accessor / 六个 memo 门 / 上传形状);`PipeCalls.def` 完整目录 + payload POD + 七个生成器 + CI `pipe-gates`;`gen_pipe_dirty_surface.py`;`check_doc_citations.py`;八个 `MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架(`SCM_RIGHTS` 第一优先、双 tail 双三元组的 `RingControl`、双向 doorbell、校验型 `Framing`、`ShmSegment`、`InProcessTransport`)+ `protocol.fbs` + `flatc-check` + `MG_Test/Wire` 五个套件;三个严格 no-op 收益(`GetInteger64i_v`/`GetProgramiv` 退役、`RenderbufferObject::GetLifetimeId()`、D21 XFB 计数槽重键);compute 限制进 `DynamicBackendParameters`;spike A、spike B;retrace 通道 `--env` 透传 | ✅ 单元/集成/40 trace 逐名不变;wire 层测试(fd 传递、doorbell、ring、封帧、inproc)绿;两台设备的字节/调用基线在案;spike A/B 出结论;citation lint 绿 | — |
|
||||
| **P0.5** 值头与制品头抽取 | 6–9 | `MG_Pipe/MGPipeValueTypes.h`(`RenderStateParameters`、`SamplerParameters`、`PixelStoreParameters`、`VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`,更新 7 个 includer);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言 | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** |
|
||||
| **P1** `PipeInputs` 替换与 verify harness | 10–13 | `MG_Backend/MGPipe/PipeInputs.h`(Espryt 32 / Magma 55 访问器);`sed` 293 处 + 58 行非箭头清单逐条转换(显式交付物);逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 |
|
||||
| **P2** 渲染状态 CSO + 第一片 Track H + 残余值块 | 18–26 | `MG_Impl/Pipe/Tracker`(dirty 位、5 个聚合世代、抑制器骨架);`gen_pipe_dirty_surface.py` 首轮映射成门;`MGPipeRenderStateSpans` + G7 setter 一致性测试;`CsoCache`(64 项,键 = pipeline 子集);`create/bind_render_state` + `set_dynamic_state`(Espryt `SyncRenderState` 一行不动;Magma `ComputePipelineStateHash`/`GetOrCreatePipeline`/`ApplyDynamicDrawStateTail` 改从 CSO 与动态 payload 取);`set_pixel_pack_state`、`set_patch_state`、`set_vertex_attrib_defaults`;`set_residual_value_state` + `ResidualValueBlock` 绊线;**第一片 Track H**:Espryt 0b(`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4(`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针);`MOBILEGL_PIPE_LEGACY_MEMOS`;补 `FramebufferSrgb`/`DepthClamp` 存储 | 集成 × 2 后端 × {pull, push} 逐名相同;40 trace push 下 SSIM ≥ 0.99 双后端;verify 零分歧;`HandleRecycleScenario` 绿且重键前红;G7 测试绿且拿掉一个字段能红;两台设备配对逐线程 CPU p50/p99 不差且 tracker 绝对 ns 在上限内;Blaze3D blend-toggle 微基准;CSO 内容寻址关闭的负面对照 | P1 |
|
||||
| **P3a** handle wave 1(Espryt):buffer、VAO | 18–23 | 7 个 `BufferBackendOps` → `resource_*`、`buffer_subdata_resident`(可 null)、`resource_flush_range`、`resource_readback`、`map_persistent`(不碰实现);pool 与延迟释放原样搬;vertex elements 三件(两个视图都带);`set_vertex_buffers`(`baseInstance` 显式字段);`set_index_buffer`;Adreno SIGSEGV workaround 保留 | 全套门;buffer/VAO 族场景(`LargeArenaAdoption`、`StorageBufferRegrow` 发布 `map-persistent-roundtrips`、`VertexAttribBinding`、`MultiDraw`、`PrimitiveRestart`…);Create/rd12/26.3/sodium trace;MC 26.3 在 Adreno 上 p99 不变。**再基线检查点 1:超过 27 天必须重定基线** | P2 |
|
||||
| **P4a** handle wave 2(Espryt):FBO / 纹理 / sampler / program 身份与描述符 | 26–34 | `set_framebuffer_state`(解析后的 `ReadSurface`、内联格式、`ContentHash`、`{0,1}`);sampler CSO(含 `borderColorForm`);sampler view + `set_texture_params`;`set_sampler_views`/`bind_sampler_states`/`set_shader_images`;shader CSO(SPIR-V + 归档);`set_draw/dispatch_program`;`set_global_constants`;`CompositeResolver`;纹理/renderbuffer 的 `resource_*`。emulation 在 split 下显式 Fatal 直到 P8 | 全套门;framebuffer/纹理/program 族场景;**新增"只作 attachment / image 单元 / CopyImage 端点的纹理其 `glTexParameter` 生效"场景(落地前必须红)**;两台设备 `KHR-GL46.direct_state_access.framebuffers*` 与整个 `packed_pixels` 块(~3300 例,句柄复用压力测试)。**再基线检查点 1b:超过 39 天** | P3a |
|
||||
| **P5** 传输 + inproc applier + 发射表 | 12 | `MG_Remote/Client` 发射表;`Server/PipeApplier`、`ServerLoop`(`mgl-srv-io` + `mgl-srv-apply`);`Init.cpp` 单一 hook 装 `BackendObject_Remote`;`MGPCaps` 快照;阻塞 `read_pixels`;client 侧保守 `MarkGpuWritten`;**client 侧块粒度 persistent-map 推送**;`InProcessTransport` 走与 spawn 相同的 G3 编解码;trace-replay `SPLIT` 后缀 + `-DTRACE_TRANSPORT=`;`MOBILEGL_TRANSPORT` 解析 | `DirectGLES.Split.*(ClearThenReadPixels|Triangle)` 在 `inproc` 下绿;OpenRA trace split SSIM ≥ 0.99;`PersistentCoherentMapScenario` 绿;两个角色峰值 RSS 在案;`persistent-map-push` 出数;未迁移字段读 = `Fatal{UnmigratedPipeInput}`。**第 99 天:首个 IPC 帧(缩减路径)** | P4a |
|
||||
| **P6** spawn transport | 5 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch | P5 全部测试在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。**第 104 天:首个跨进程帧** | P5 |
|
||||
| **P3b / P4b** 深化(Espryt) | 29–38 | memo 重键(`ResolvedDrawBuffers`、`ResolvedTextureBindingMemo`、`SamplerPassMemo`、image sweep、program registry…);server 删 `g_unitTextureSyncList`/`g_fboTextureSyncList`/`DirectGLES.cpp` 的 ~115 行 unit-bindings epoch 推导,**同时**在 Tracker 落地集合 hash 抑制器;dirty 归属反转(按存储属主键控的发射游标);`MGPSubRegion` 跨步描述符改造;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 原生化;回读 / pack state | ~25 个纹理场景、21 个 program 场景 + `MG_Test/ShaderTranspiler`;两台设备 `KHR-GL46.texture_*`/`internalformat.texture2d.*`/`shader_image_*`/`packed_pixels` 在 pull 基线 0.5 pp 内;每一个 Iris trace;**`TextureUploadShapeScenario`**(形状金标,Mali 帧时增量必须发布);view/owner 发射游标别名场景;verify 保留模式下 subdata 形状逐项相等;XFB 场景 + `capture_special_interleaved_test` | P4a |
|
||||
| **P7** DirectVulkan(Magma)全量迁移 | 80–104 | §5.5 其余 10 个子系统(子系统 1、4 已在 P2):`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化(~120 行删除);具名 UBO host payload(D-B8,`kCapNeedsHostUboBytes`);blit/depth-mipmap 内部 shader 烘焙 + 新鲜度测试;`VertexInputStateFactory` 裸指针写回删除;D18 容器纪律原样保留 | 集成 + 40 trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;**`nm -D libMobileGLServer.so | grep glslang` 为空**;Iris trace 上 `stage-ubo-named` 逐帧字节发布;两台设备 CTS 0.5 pp 内。**再基线检查点 2:中点(第 40–52 工作日)完成子系统 < 40% 立即重定基线** | P0.5、P2;可与 P5/P6/P8 并行 |
|
||||
| **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 12–16 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,各带逐站点 reconcile);`MGHostSpan` split 填法;`Server/IndexHostMirror`;CopyImage 镜像搬到 client;`draw_vbo` 收编 multi-draw 族(分档仍在 server);viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;G3 分块路径;无 present fence tick + 无 present split 用例;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'` 与 `'^DirectGLES\.'` 逐名相同(DirectVulkan 同);40 trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` Create fixture;`ClientArrayAfterComputeWriteScenario` 绿(去掉等待必须见几何缺失);`create-indirect` 上 `roundtrips-per-frame` 读零;`index-mirror-bytes`/`index-bytes-shipped` 逐用例发布。**第 145 天:全功能 split** | P6、P3b/P4b |
|
||||
| **P9** 反向通道 | 10 | `SEG_REPLY` slot 池;阻塞 `read_pixels`;PBO 回读 fire-and-forget;`OnGpuWritten` 收窄;`OnBufferWriteback` 按操作级批处理 + epoch 排序;`OnXfbScatterReady` + client scatter;`OnTextureWriteback`;`OnMipLevelsGenerated`;纹理拉取四条缓解 + 终止符;`OnGlError` 有序 + `glBufferStorage` 的 ack;`OnCapsInvalidated`;`OnSurfaceChanged`;`OnLog` 分级 + 速率限制;`SEG_EVENT` 溢出策略 | 回读/XFB 场景在 split 下绿;`TextureRemintPullScenario` 绿且含无解用例(终止符前表现为 apply 线程挂死/超时);拉取计数逐 trace 发布;故障注入:credit 阻塞时灌满 `SEG_EVENT`、日志洪泛下注入 link 失败 | P8 |
|
||||
| **P10** sync / query / present 节奏 | 6 | client 铸造 sync/query handle;轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE`;fence 完成度来自真的逐 fence 退休;DirectGLES 非 present fence tick;`present` 1:1;credit 默认 1 + 叠加公式;roundtrip 计数器与输入延迟直方图;三个独立 `dev` monolith 修复(`glEndTransformFeedback` 无限 `ClientWaitSync` → 推迟到首次读;`glDispatchCompute` 三次 `GetIntegeri_v` 校验 → 读 `CompileEnv`;D21 已落地) | query/XFB/`AsyncCompile` 场景在 split 下绿;40 个用例上 draw/state/upload 路径 roundtrip 读零,条件渲染与阻塞 query 次数逐用例发布;零 timeout 轮询在有界时间退出;`bench.sh` 配对 A/B:两侧都关采纳时 split 帧时在 monolith 10% 内,输入延迟 p50/p99 在案 | P9 |
|
||||
| **P11** persistent map 与 ≥16 MiB 采纳 | 8 | POST 探针档位选择(T0 主攻,Adreno 可选 T1,T2 回退);`SEG_ADOPT` 生命周期绑 `completedFrameSerial`;`MOBILEGL_IPC_ADOPT_TIER` 负面对照 | `LargeArenaAdoptionScenario` 在所选档下绿;26.3 与两个 Create fixture SSIM ≥ 0.99;`StorageBufferRegrowScenario` 发布 `map-persistent-roundtrips`;Adreno 830 上 p99 帧时与峰值 RSS 对 monolith 采纳基线(163→21 ms / 40→115 fps / ~400 MB)**回归不超过 10%**;若 T2 成为某设备的永久答案,其实测代价写进文档 | P10、spike B(已答) |
|
||||
| **P12** Android 生产窗口路径 | 10 | `android:process=":mgl"` Service 收 Java `Surface` → `ANativeWindow_fromSurface`;server 生命周期绑 Activity;FCL 用户 env 与 plugin APK V2 开关表接线 | Minecraft 经 FCL 在 spawn 模式下于 Adreno 830 双后端入世界;配对 reboot-clean bench + 输入延迟直方图;杀 server 产生干净 device-lost latch;SIGKILL 故障注入 | P11 |
|
||||
| **P13** 退役 pull 路径 | 8–12 | 删 `SnapshotFromGLContext()` 非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;保留 `MOBILEGL_PIPE_VERIFY`;MGPipe recorder 金标模式;删 `set_residual_value_state`;`MG_Backend` 的 `MG_State` include 收缩到 `MGPipeValueTypes.h`;在计数器活着的情况下重调幸存缓存容量(`VaoDrawMemo` 2048、`SetupDrawSnapshot` 4、pipeline memo 8、`syncedTextureMemo` 8)并变成带 env 覆盖的调优参数;最终符号/尺寸/CPU 报告 | `static_assert(sizeof(ResidualValueBlock) == 0)` 编译通过;三道纯度门在非 verify 构建上转绿;verify 构建仍零分歧;recorder 金标在 40 trace 上建立;全套门(集成 × 2 后端 × {monolith, split}、单元、40 trace、两台设备 CTS 在 `81b17c0b` 基线 0.5 pp 内);**monolith 逐线程 CPU 在两台设备 p50/p99 上不差于 P0 基线** | P7、P8、P12 |
|
||||
|
||||
累计(低端):P0 9 → P0.5 15 → P1 25 → P2 43 → P3a 61 → P4a 87 → P5 99 → P6 104 → P3b/P4b 133 → P8 145 → P9 155 → P10 161 → P11 169 → P12 179 → P13 187;P7 另 80–104,单跑道累计 267。
|
||||
|
||||
**CTS 周转单独计价**:`gl44to46` 约 56,271 例。逐阶段只跑该阶段可能影响的具名块(P4a `packed_pixels`、P3b/P4b `texture_*`/`shader_image_*`、P9 `transform_feedback*`);完整 caselist 只在五个架构边界(P0.5、P3a、P4a、P3b/P4b、P13)与每次合并 `dev` 之前跑,放 CI 不放关键路径。若周转仍主导排期,加宽估时而不是削弱门。
|
||||
|
||||
## 里程碑
|
||||
|
||||
- **第 25 天(P1 出口)**:verify harness 逐 draw 逐字段证明"推送等价于拉取"。零产品风险,**不是** GO/NO-GO。
|
||||
- **第 43 天(P2 出口):GO/NO-GO**。
|
||||
- 第 99 天:首个 `inproc` IPC 帧(缩减路径);第 104 天:首个跨进程帧;第 145 天:全功能 split;第 187 / 267 天:三道纯度门转绿。
|
||||
|
||||
## 第 43 天 GO/NO-GO 清单
|
||||
|
||||
手上必须有:
|
||||
|
||||
- [ ] P1 交付的逐 draw 逐字段语义等价证明(40 trace + 全部集成测试零分歧)
|
||||
- [ ] 两个后端上都已推送的渲染状态,`SyncRenderState` 693 行一行未动
|
||||
- [ ] 两片 Track H 的实测单位成本(Espryt 0b、Magma 子系统 4)
|
||||
- [ ] 两台设备(Adreno 830 `35d0befa`、Mali `3B159D009VZ00000`)reboot-clean 配对的逐线程 CPU 时间增量,p50 与 p99
|
||||
- [ ] tracker 每 draw 的**绝对 ns**(上限从设备基线定:稳态每 draw 6.5–9.3 次 accessor + memo 探测,见 `MEASUREMENTS.md`)
|
||||
- [ ] Blaze3D blend-toggle 微基准(enable/draw/disable/draw,MC batch 速率)
|
||||
- [ ] 负面对照:关掉 CSO 内容寻址重跑,把"推送更慢"与"CSO 设计更慢"分开
|
||||
|
||||
判据与出口:
|
||||
|
||||
- **继续**:两台设备 p50 与 p99 逐线程 CPU 增量都不为负;tracker 绝对 ns 在上限内;Track H 单位成本不超出估计的 50%。按两条跑道推进。
|
||||
- **收缩为 headless 工装用途或重新评估**:任一判据落空。**不回滚**:P0/P0.5/P1/P2 的产物(句柄基建与重键、两个头文件抽取、计数器、verify harness、渲染状态 CSO)全是自洽的 monolith 交付物,留在 `dev`;MGPipe 收缩为 `MG_Test` mock 后端 → MGPipe recorder(给 trace_replay 一种记录已解析状态的录制格式)+ `inproc` 渲染线程实验;IPC 跑道搁置到出现新判据。
|
||||
- 沉没成本:P0 与 P0.5 无论走哪条路都要花(后者本身是 monolith 净收益);真正只为 MGPipe 押上的是 P1 + P2 ≈ 28–39 天,NO-GO 分支下仍留下上述产物。
|
||||
|
||||
## 再基线检查点
|
||||
|
||||
| 触发 | 动作 |
|
||||
|---|---|
|
||||
| P3a > 27 天 | "窄句柄化"的前提错了,P4a 开始前重定基线 |
|
||||
| P4a > 39 天 | 同上 |
|
||||
| P7 中点(第 40–52 工作日)完成子系统 < 40% | 立即重定基线(P3a 的检查点发现不了 Magma 特有的超期) |
|
||||
|
||||
任一触发,先跑 `inproc` 的证伪数字再决定是否继续。
|
||||
|
||||
## 仍然开放的问题
|
||||
|
||||
P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn` 不可用、OOM 探测惯用法、`GetInteger64i_v`/`GetProgramiv` 退役、D21 与 `RenderbufferObject` lifetime id、动态 accessor 基线)。
|
||||
|
||||
1. **client 侧 dirty 走查的真实每 draw CPU 代价。** 拉取基线已实测为每 draw 6.5–9.3 次 accessor + memo 探测;推送要在这个数字下净减少。P2 的头号数字,逐线程 CPU + 绝对 ns,两台设备。
|
||||
2. **真实语料上纹理重铸拉取的发生率。** `ImageBindableHint` 预防主因,但整格式再生在普通 `glTexImage` 格式变更上就触发。若 MC/Iris fixture 上非平凡,保留 LRU 从默认 0 升为强制并拿真预算。
|
||||
3. **spike B 的 `untrusted_app` 域复核。** 两台设备的分档在 `shell` 域测得;T0 的 AHB socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,风险在 memfd/opaque-fd 腿上。从应用进程再跑一次 `extmem_probe`(spike A 的 exec 钩子已可用)。
|
||||
4. **渲染状态的 wire 粒度。** chunk 划分定下来后,CSO LRU 容量(暂定 64)与 `set_dynamic_state` 的 chunk 粒度由计数器定。
|
||||
5. **`FramebufferSrgb` / `DepthClamp` 的拍板。** 事实已清(无存储、`glEnable` 静默吞掉、六个读点恒 false、41 个 fixture 无一开启);建议在 chunk 表冻结前补真存储并把 `FramebufferSrgb` 划进 pipeline 半边。由计划所有者拍板,**拍板前不冻结 chunk 表**。
|
||||
6. **具名 UBO host payload 的形状(D-B8)。** 第一个数字已有:Magma 在 26.3 世界每帧重打包 331 KB 具名 UBO 字节,Espryt 为 0。要么冻结现在的第二变长尾形状,要么走备选(Magma 直接描述符绑定常驻 `VkBuffer` range,独立 `dev` PR + Iris 性能门)。
|
||||
7. **`MG_Util` 的切割缝。** server 需要 SPIRV-Cross pass 流水线、ESSL 转译缓存、格式处理器、POST 探针;client 需要 glslang phase A/B 与反射层。P0.5 解决了 `ProgramObject.h` 一处,`MG_Util` 内部是否有干净的 Transpile-vs-Reflect 缝未审计。
|
||||
8. **一份反射归档能否服务三个消费者**(Espryt 读前端表、Magma 跑 SPIRV-Reflect、`DirectVulkan.cpp` 为 `glGetProgramResource*` 又反射一遍)。
|
||||
9. **viewport-array 回放能否塞进一次 `draw_vbo`**:`EndViewportRoutingPasses` 会 `InvalidateSyncedRenderState`,各遍之间观察到的状态是否与今天一致未验证。
|
||||
10. **`ResidentSubData` 的不对称怎么收口。** null 项保住今天的行为;给 Magma 补真实现是行为变更,独立 `dev` PR。
|
||||
11. **`SEG_STAGE` 的上限。** 六类新字节需要 P8 之后用 MC in-world 与 Create 两类 fixture 的 `stage-*` 计数器给 p99 占用;G3 分块路径需要设计与测试。
|
||||
12. **P13 之后 split-only 渲染 bug 的 server 侧第二意见。** verify 构建 + recorder 只覆盖推送内容,不覆盖后端对它的解释。
|
||||
13. **烘焙后的内部 shader 能否在没有活 `ProgramObject` 的情况下表达 uniform location 与 UBO 布局。** 未做原型。
|
||||
14. **推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。
|
||||
15. **monolith 的 `*IndirectCount` 不调 `SyncGpuWrites()` 是不是潜在缺口**(compute 写的 indirect buffer)。独立 `dev` 问题,拆分不得借机顺手修。
|
||||
16. **索引宿主镜像的实际内存占用。** MC/Sodium/Iris 语料里 element-array buffer 总量未测;若显著超 64 MiB,退化路径的频率与代价必须实测。
|
||||
17. **create-indirect fixture 在 Adreno 830 上的失败**是 `dev@81b17c0b` 就有的(基线 APK 复现),不是本分支造成;它是 P3a/P8 验收清单里的用例,需要先在 `dev` 上修。
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
# MobileGL - scripts/check_doc_citations.py
|
||||
# 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
|
||||
"""Resolve every `path:line` citation in a set of markdown documents.
|
||||
|
||||
A design document that cites the code is only as good as its line numbers, and a wrong one
|
||||
is worse than none: it sends the next reader to a function that does something else. The
|
||||
disaggregation plan's first draft cited SamplerObject.h:468-492 for a struct that lives at
|
||||
:72-96 in a 160-line file, and nothing caught it.
|
||||
|
||||
So every `File.h:123` and `File.cpp:123-456` in the given documents is resolved against a
|
||||
git revision - the file must exist there and must have at least that many lines. Bare file
|
||||
names are resolved by basename, which is how the plan spells most of its citations; an
|
||||
ambiguous basename is reported rather than guessed.
|
||||
|
||||
python3 scripts/check_doc_citations.py docs/Disaggregated/*.md
|
||||
python3 scripts/check_doc_citations.py --rev 81b17c0b --strict docs/Disaggregated/*.md
|
||||
|
||||
Exits non-zero only with --strict, so it can be wired into CI as a warning first.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# `Managers.cpp:4340-4390`, `MobileGL/MG_State/.../RenderState.h:263`, `:12-13` is NOT
|
||||
# matched on purpose: a citation with no file name cannot be checked, only guessed.
|
||||
CITATION_RE = re.compile(
|
||||
r"(?<![\w/.-])((?:[\w.-]+/)*[\w.-]+\.(?:h|hpp|cpp|cc|c|py|def|inc|md|json|yml|yaml|txt|fbs))"
|
||||
r":(\d+)(?:-(\d+))?")
|
||||
|
||||
|
||||
def git(args, rev_root=REPO_ROOT):
|
||||
return subprocess.run(["git", "-C", rev_root] + args, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, check=False).stdout.decode("utf-8", "replace")
|
||||
|
||||
|
||||
class Tree(object):
|
||||
def __init__(self, rev):
|
||||
self.Rev = rev
|
||||
listing = git(["ls-tree", "-r", "--name-only", rev]).splitlines()
|
||||
if not listing:
|
||||
sys.exit("check_doc_citations: revision %s has no files (bad revision?)" % rev)
|
||||
self.Paths = set(listing)
|
||||
self.ByBasename = {}
|
||||
for path in listing:
|
||||
self.ByBasename.setdefault(os.path.basename(path), []).append(path)
|
||||
self.LineCounts = {}
|
||||
|
||||
def Resolve(self, cited):
|
||||
if cited in self.Paths:
|
||||
return [cited]
|
||||
candidates = self.ByBasename.get(os.path.basename(cited), [])
|
||||
if len(candidates) > 1 and "/" in cited:
|
||||
candidates = [p for p in candidates if p.endswith(cited)] or candidates
|
||||
return candidates
|
||||
|
||||
def LineCount(self, path):
|
||||
if path not in self.LineCounts:
|
||||
blob = git(["show", "%s:%s" % (self.Rev, path)])
|
||||
# A file with no trailing newline still has that last line.
|
||||
count = blob.count("\n") + (1 if blob and not blob.endswith("\n") else 0)
|
||||
self.LineCounts[path] = count
|
||||
return self.LineCounts[path]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("documents", nargs="+", help="markdown files to check")
|
||||
parser.add_argument("--rev", default="HEAD", help="git revision the citations point into")
|
||||
parser.add_argument("--strict", action="store_true", help="exit 1 when a citation does not resolve")
|
||||
args = parser.parse_args()
|
||||
|
||||
tree = Tree(args.rev)
|
||||
checked = 0
|
||||
problems = []
|
||||
for document in args.documents:
|
||||
if not os.path.exists(document):
|
||||
problems.append("%s: no such document" % document)
|
||||
continue
|
||||
with open(document, "r", encoding="utf-8", errors="replace") as handle:
|
||||
lines = handle.read().splitlines()
|
||||
for number, line in enumerate(lines, start=1):
|
||||
for match in CITATION_RE.finditer(line):
|
||||
cited, first, last = match.group(1), int(match.group(2)), match.group(3)
|
||||
last = int(last) if last else first
|
||||
checked += 1
|
||||
where = "%s:%d: `%s`" % (document, number, match.group(0))
|
||||
candidates = tree.Resolve(cited)
|
||||
if not candidates:
|
||||
problems.append("%s -> no such file at %s" % (where, args.rev))
|
||||
continue
|
||||
if len(candidates) > 1:
|
||||
problems.append("%s -> ambiguous: %s" % (where, ", ".join(sorted(candidates))))
|
||||
continue
|
||||
if last < first:
|
||||
problems.append("%s -> inverted line range" % where)
|
||||
continue
|
||||
count = tree.LineCount(candidates[0])
|
||||
if last > count:
|
||||
problems.append("%s -> %s has %d lines at %s"
|
||||
% (where, candidates[0], count, args.rev))
|
||||
|
||||
print("check_doc_citations: %d citations in %d document(s) against %s, %d problem(s)"
|
||||
% (checked, len(args.documents), args.rev, len(problems)))
|
||||
for problem in problems:
|
||||
print(" %s" % problem)
|
||||
if problems and args.strict:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,626 @@
|
||||
# Backend read inventory -> delta catalog matrix
|
||||
|
||||
> GENERATED by `scripts/extract_backend_read_inventory.py` - do not edit by hand.
|
||||
> Acceptance rule: zero UNMAPPED rows before the delta path goes live (P5).
|
||||
|
||||
## Summary
|
||||
|
||||
- scanned files: 57 (MG_Backend/**.cpp|.h)
|
||||
- total read points: 477
|
||||
- pGLContext accesses: 293
|
||||
- SharedPtr<MG_State wire args: 167
|
||||
- BufferBackendOps touchpoints: 17
|
||||
- UNMAPPED: 0
|
||||
|
||||
## Delta kind distribution
|
||||
|
||||
| delta kind | count |
|
||||
|---|---|
|
||||
| handle-ify (wire handle) | 167 |
|
||||
| RenderStateBlob | 99 |
|
||||
| ObjectBind(Texture) | 33 |
|
||||
| ObjectBind(Buffer) | 29 |
|
||||
| ObjectBind(BufferRange) | 24 |
|
||||
| FboAttach | 19 |
|
||||
| Buffer ops delta | 17 |
|
||||
| XfbOp | 15 |
|
||||
| ObjectBind(Image) | 14 |
|
||||
| ObjectBind(VAO) | 12 |
|
||||
| ObjectBind(Program) | 10 |
|
||||
| TexParam | 9 |
|
||||
| Texture state | 7 |
|
||||
| PixelStoreBlob | 6 |
|
||||
| client-resolved (error queue) | 6 |
|
||||
| ProgramPublish | 3 |
|
||||
| client-resolved (validation) | 3 |
|
||||
| CurrentAttrib | 2 |
|
||||
| client-resolved (compile env) | 2 |
|
||||
|
||||
## Per-file hits
|
||||
|
||||
### `MobileGL/MG_Backend/BackendObject.h` (9 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 34 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> Texture;` |
|
||||
| 35 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;` |
|
||||
| 158 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferfv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 160 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 162 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 164 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 168 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
|
||||
| 169 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
|
||||
| 186 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*GetTextureImage)(const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp` (1 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 849 | BufferBackendOps | - | Buffer ops delta | `BufferImpl::RegisterBufferBackendOps();` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp` (142 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 55 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static SharedPtr<MG_State::GLState::SamplerObject> g_rawDepthFetchSamplerState;` |
|
||||
| 142 | pGLContext | GetFramebufferBindingSlot | FboAttach | `std::remove_reference_t<decltype(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;` |
|
||||
| 182 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool NeedsRawDepthFetchSampler(const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject,` |
|
||||
| 260 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 356 | pGLContext | GetTouchedBufferBindingPointCount | ObjectBind(BufferRange) | `auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);` |
|
||||
| 369 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);` |
|
||||
| 428 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);` |
|
||||
| 461 | pGLContext | GetTouchedBufferBindingPointCount | ObjectBind(BufferRange) | `MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);` |
|
||||
| 464 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();` |
|
||||
| 473 | pGLContext | GetBufferBindingPointCount | ObjectBind(BufferRange) | `const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);` |
|
||||
| 480 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,` |
|
||||
| 517 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto& bufferObject = MG_State::pGLContext->GetBufferBindingSlot(target).GetBoundObject();` |
|
||||
| 537 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,` |
|
||||
| 661 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 712 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::BufferObject> buffer;` |
|
||||
| 739 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ProgramObject> scatterProgram;` |
|
||||
| 891 | pGLContext | GetTransformFeedbackCapturedVertices | XfbOp | `static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());` |
|
||||
| 986 | pGLContext | GetTransformFeedbackProgram | XfbOp | `const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();` |
|
||||
| 1006 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,` |
|
||||
| 1199 | SharedPtr<MG_State | - | handle-ify (wire handle) | `BackendVertexArrayObject* ResolveVaoTwin(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao) {` |
|
||||
| 1215 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncCurrentVAO(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,` |
|
||||
| 1237 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& program) {` |
|
||||
| 1243 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 1274 | pGLContext | GetCurrentVertexAttribute | CurrentAttrib | `const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);` |
|
||||
| 1298 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,` |
|
||||
| 1372 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 1385 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 1413 | pGLContext | GetTextureContextId | Texture state | `const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();` |
|
||||
| 1414 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
|
||||
| 1457 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>* slot = nullptr;` |
|
||||
| 1503 | pGLContext | GetTextureContextId | Texture state | `keys.contextId = MG_State::pGLContext->GetTextureContextId();` |
|
||||
| 1505 | pGLContext | GetMaxTouchedTextureUnit | ObjectBind(Texture) | `keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();` |
|
||||
| 1506 | pGLContext | GetSamplingResolutionGeneration | TexParam | `keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 1552 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& unit = MG_State::pGLContext->GetTextureUnitObject(index);` |
|
||||
| 1700 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));` |
|
||||
| 1792 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `const auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));` |
|
||||
| 1998 | pGLContext | GetRenderStateParametersVersion | RenderStateBlob | `Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();` |
|
||||
| 2012 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 2041 | pGLContext | GetViewport | RenderStateBlob | `IntVec4 backendViewport = MG_State::pGLContext->GetViewport();` |
|
||||
| 2124 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);` |
|
||||
| 2680 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {` |
|
||||
| 2800 | pGLContext | GetPatchVertices | RenderStateBlob | `static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) \|\|` |
|
||||
| 2802 | pGLContext | GetPatchDefaultOuterLevel | RenderStateBlob | `MG_State::pGLContext->GetPatchDefaultOuterLevel()) \|\|` |
|
||||
| 2804 | pGLContext | GetPatchDefaultInnerLevel | RenderStateBlob | `MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {` |
|
||||
| 2853 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncAndBindFramebufferObject(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 2902 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,` |
|
||||
| 2905 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);` |
|
||||
| 2915 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 2926 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();` |
|
||||
| 2973 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static Bool ResolveAndBindUnitTextures(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,` |
|
||||
| 2998 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 3113 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Int unit, const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject) {` |
|
||||
| 3162 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();` |
|
||||
| 3250 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {` |
|
||||
| 3312 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MG_State::pGLContext->GetProgramForDraw());` |
|
||||
| 3322 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,` |
|
||||
| 3423 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding);` |
|
||||
| 3521 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 3547 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject =` |
|
||||
| 3603 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();` |
|
||||
| 3653 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();` |
|
||||
| 3762 | pGLContext | IsTransformFeedbackActive | XfbOp | `if (MG_State::pGLContext->IsTransformFeedbackActive() \|\|` |
|
||||
| 3763 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {` |
|
||||
| 3767 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 3862 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,` |
|
||||
| 3941 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,` |
|
||||
| 4003 | pGLContext | GetProgramForDispatch | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();` |
|
||||
| 4029 | pGLContext | ValidateProgramName | client-resolved (validation) | `if (!MG_State::pGLContext->ValidateProgramName(program)) {` |
|
||||
| 4034 | pGLContext | GetProgramObject | ProgramPublish | `auto& programObject = MG_State::pGLContext->GetProgramObject(program);` |
|
||||
| 4097 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor;` |
|
||||
| 4156 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 4313 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {` |
|
||||
| 4314 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static const SharedPtr<MG_State::GLState::BufferObject> none;` |
|
||||
| 4315 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 4331 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) \|\|` |
|
||||
| 4332 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {` |
|
||||
| 4337 | pGLContext | GetPrimitiveRestartIndex | RenderStateBlob | `const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();` |
|
||||
| 4377 | pGLContext | GetPrimitiveRestartIndex | RenderStateBlob | `const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();` |
|
||||
| 4505 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 4544 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 4615 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 4646 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 4647 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
|
||||
| 4717 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 4748 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 4749 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
|
||||
| 4908 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 4949 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 5194 | pGLContext | IsTransformFeedbackActive | XfbOp | `if (MG_State::pGLContext->IsTransformFeedbackActive() &&` |
|
||||
| 5195 | pGLContext | IsTransformFeedbackPaused | XfbOp | `!MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {` |
|
||||
| 5811 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
|
||||
| 5812 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer, GLint srcX0, GLint srcY0,` |
|
||||
| 5979 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),` |
|
||||
| 5980 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,` |
|
||||
| 5990 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
|
||||
| 5991 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
|
||||
| 6042 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto unit = MG_State::pGLContext->GetActiveTextureUnit();` |
|
||||
| 6043 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 6120 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
|
||||
| 6267 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
|
||||
| 6310 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
|
||||
| 6335 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
|
||||
| 6561 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
|
||||
| 6586 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
|
||||
| 6634 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `Uint activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();` |
|
||||
| 6635 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject((Int)activeTextureUnit)` |
|
||||
| 6729 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();` |
|
||||
| 6730 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)` |
|
||||
| 6794 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
|
||||
| 6867 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto unitIndex = MG_State::pGLContext->GetActiveTextureUnit();` |
|
||||
| 6868 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& unit = MG_State::pGLContext->GetTextureUnitObject(unitIndex);` |
|
||||
| 6994 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {` |
|
||||
| 7059 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
|
||||
| 7260 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 7266 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 7271 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 7288 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 7297 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 7306 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 7315 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 7324 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 7333 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 7352 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 7357 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 7408 | pGLContext | ValidateProgramName | client-resolved (validation) | `if (!MG_State::pGLContext->ValidateProgramName(program)) return;` |
|
||||
| 7409 | pGLContext | GetProgramObject | ProgramPublish | `auto& programObject = MG_State::pGLContext->GetProgramObject(program);` |
|
||||
| 7456 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool IsWidenedNamedDrawBuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 7514 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 7534 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 7551 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 7571 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 7605 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
|
||||
| 7613 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 8595 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 8825 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 9092 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;` |
|
||||
| 9135 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 9245 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();` |
|
||||
| 9248 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)` |
|
||||
| 9471 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
|
||||
| 9561 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 10114 | BufferBackendOps | - | Buffer ops delta | `BufferImpl::RegisterBufferBackendOps();` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/DirectGLES.h` (6 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 60 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 62 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 64 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 66 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 70 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
|
||||
| 71 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/Managers.cpp` (39 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 443 | SharedPtr<MG_State | - | handle-ify (wire handle) | `String source, const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {` |
|
||||
| 495 | BufferBackendOps | - | Buffer ops delta | `using MG_State::GLState::BufferBackendOps;` |
|
||||
| 1333 | BufferBackendOps | - | Buffer ops delta | `const BufferBackendOps g_glesBufferBackendOps = {` |
|
||||
| 1356 | BufferBackendOps | - | Buffer ops delta | `void RegisterBufferBackendOps() {` |
|
||||
| 1357 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);` |
|
||||
| 1363 | BufferBackendOps | - | Buffer ops delta | `void UnregisterBufferBackendOps() {` |
|
||||
| 1364 | BufferBackendOps | - | Buffer ops delta | `if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) {` |
|
||||
| 1365 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(nullptr);` |
|
||||
| 1379 | BufferBackendOps | - | Buffer ops delta | `UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch` |
|
||||
| 1453 | SharedPtr<MG_State | - | handle-ify (wire handle) | `GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {` |
|
||||
| 2264 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject) {` |
|
||||
| 2496 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count) {` |
|
||||
| 2787 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
|
||||
| 3604 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
|
||||
| 3606 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();` |
|
||||
| 3607 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 3614 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
|
||||
| 3707 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
|
||||
| 3735 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId == MG_State::pGLContext->GetTextureContextId() &&` |
|
||||
| 3736 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration == MG_State::pGLContext->GetSamplingResolutionGeneration() &&` |
|
||||
| 3806 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();` |
|
||||
| 3807 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 4661 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();` |
|
||||
| 4662 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 4670 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
|
||||
| 4781 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
|
||||
| 5280 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
|
||||
| 5385 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
|
||||
| 5409 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {` |
|
||||
| 5520 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {` |
|
||||
| 6155 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);` |
|
||||
| 7118 | pGLContext | GetPatchVertices | RenderStateBlob | `? MG_State::pGLContext->GetPatchVertices()` |
|
||||
| 7126 | pGLContext | GetPatchDefaultOuterLevel | RenderStateBlob | `? MG_State::pGLContext->GetPatchDefaultOuterLevel()` |
|
||||
| 7129 | pGLContext | GetPatchDefaultInnerLevel | RenderStateBlob | `? MG_State::pGLContext->GetPatchDefaultInnerLevel()` |
|
||||
| 7229 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {` |
|
||||
| 8262 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {` |
|
||||
| 8450 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {` |
|
||||
| 8620 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject) {` |
|
||||
| 8676 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/Managers.h` (18 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 501 | BufferBackendOps | - | Buffer ops delta | `void RegisterBufferBackendOps();` |
|
||||
| 502 | BufferBackendOps | - | Buffer ops delta | `void UnregisterBufferBackendOps();` |
|
||||
| 511 | SharedPtr<MG_State | - | handle-ify (wire handle) | `GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);` |
|
||||
| 679 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject);` |
|
||||
| 681 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);` |
|
||||
| 953 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
|
||||
| 959 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
|
||||
| 960 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
|
||||
| 966 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
|
||||
| 967 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
|
||||
| 973 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
|
||||
| 1127 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,` |
|
||||
| 1151 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,` |
|
||||
| 1156 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);` |
|
||||
| 1531 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);` |
|
||||
| 1639 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);` |
|
||||
| 1817 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);` |
|
||||
| 1846 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp` (8 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 44 | pGLContext | GetPrimitiveRestartIndex | RenderStateBlob | `return MG_State::pGLContext->GetPrimitiveRestartIndex();` |
|
||||
| 50 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) \|\|` |
|
||||
| 51 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);` |
|
||||
| 86 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 92 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {` |
|
||||
| 93 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static const SharedPtr<MG_State::GLState::BufferObject> none;` |
|
||||
| 94 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 355 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectGLES/Utils.cpp` (2 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 2297 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 2301 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp` (2 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 389 | pGLContext | InvalidateCompileEnv | client-resolved (compile env) | `MG_State::pGLContext->InvalidateCompileEnv();` |
|
||||
| 789 | pGLContext | InvalidateCompileEnv | client-resolved (compile env) | `MG_State::pGLContext->InvalidateCompileEnv();` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp` (25 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 280 | pGLContext | ValidateProgramName | client-resolved (validation) | `if (!MG_State::pGLContext->ValidateProgramName(program)) {` |
|
||||
| 283 | pGLContext | GetProgramObject | ProgramPublish | `auto& programObject = MG_State::pGLContext->GetProgramObject(program);` |
|
||||
| 288 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 369 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 376 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 383 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 390 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 412 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 475 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
|
||||
| 543 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 596 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 706 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 712 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 717 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 739 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
|
||||
| 765 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 770 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
|
||||
| 816 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
|
||||
| 849 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,` |
|
||||
| 886 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 1010 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 1112 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
|
||||
| 1113 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
|
||||
| 1337 | pGLContext | GetTransformFeedbackPausedPrimitiveCounter | XfbOp | `primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -` |
|
||||
| 1384 | pGLContext | GetTransformFeedbackPausedPrimitiveCounter | XfbOp | `MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h` (7 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 36 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 38 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 40 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 42 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
|
||||
| 76 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
|
||||
| 77 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
|
||||
| 103 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp` (23 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 161 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::TextureObjectMipmap> MakePlaceholderTextureObject(TextureTarget target,` |
|
||||
| 505 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 508 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;` |
|
||||
| 554 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 781 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();` |
|
||||
| 809 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
|
||||
| 820 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 846 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 873 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> texture;` |
|
||||
| 1015 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);` |
|
||||
| 1188 | pGLContext | GetBufferBindingPointCount | ObjectBind(BufferRange) | `static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));` |
|
||||
| 1193 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);` |
|
||||
| 1294 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);` |
|
||||
| 1303 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> placeholder;` |
|
||||
| 1392 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(` |
|
||||
| 1433 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(` |
|
||||
| 1477 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;` |
|
||||
| 1597 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetUnboundStorageImageTexture(` |
|
||||
| 1661 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
|
||||
| 1849 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();` |
|
||||
| 1937 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);` |
|
||||
| 2012 | pGLContext | GetBufferBindingPointCount | ObjectBind(BufferRange) | `static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform));` |
|
||||
| 2017 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h` (7 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 165 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
|
||||
| 184 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(` |
|
||||
| 191 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(` |
|
||||
| 213 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,` |
|
||||
| 305 | SharedPtr<MG_State | - | handle-ify (wire handle) | `mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;` |
|
||||
| 308 | SharedPtr<MG_State | - | handle-ify (wire handle) | `mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;` |
|
||||
| 317 | SharedPtr<MG_State | - | handle-ify (wire handle) | `mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp` (9 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 50 | BufferBackendOps | - | Buffer ops delta | `using MG_State::GLState::BufferBackendOps;` |
|
||||
| 104 | BufferBackendOps | - | Buffer ops delta | `const BufferBackendOps g_vulkanBufferBackendOps = {` |
|
||||
| 130 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);` |
|
||||
| 137 | BufferBackendOps | - | Buffer ops delta | `if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) {` |
|
||||
| 138 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(nullptr);` |
|
||||
| 255 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {` |
|
||||
| 496 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void VkBufferManager::OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource) {` |
|
||||
| 566 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
|
||||
| 614 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h` (4 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 146 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
|
||||
| 151 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
|
||||
| 165 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);` |
|
||||
| 184 | SharedPtr<MG_State | - | handle-ify (wire handle) | `VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp` (11 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 57 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;` |
|
||||
| 238 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
|
||||
| 261 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
|
||||
| 319 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
|
||||
| 325 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;` |
|
||||
| 350 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;` |
|
||||
| 372 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
|
||||
| 392 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
|
||||
| 404 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
|
||||
| 409 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
|
||||
| 461 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h` (4 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 127 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture);` |
|
||||
| 135 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
|
||||
| 148 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
|
||||
| 150 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp` (5 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 342 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {` |
|
||||
| 613 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);` |
|
||||
| 965 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);` |
|
||||
| 1111 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));` |
|
||||
| 1595 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h` (1 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 353 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp` (2 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 808 | pGLContext | GetTextureObject | Texture state | `const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());` |
|
||||
| 951 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp` (140 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 462 | pGLContext | GetViewportIndexed | RenderStateBlob | `const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);` |
|
||||
| 467 | pGLContext | GetDepthRangeIndexed | RenderStateBlob | `const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);` |
|
||||
| 522 | pGLContext | GetBlendColor | RenderStateBlob | `const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();` |
|
||||
| 555 | pGLContext | GetPolygonOffsetUnits | RenderStateBlob | `const Float constantFactor = MG_State::pGLContext->GetPolygonOffsetUnits();` |
|
||||
| 556 | pGLContext | GetPolygonOffsetFactor | RenderStateBlob | `const Float slopeFactor = MG_State::pGLContext->GetPolygonOffsetFactor();` |
|
||||
| 569 | pGLContext | GetLineWidth | RenderStateBlob | `Float lineWidth = MG_State::pGLContext->GetLineWidth();` |
|
||||
| 648 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);` |
|
||||
| 649 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);` |
|
||||
| 1242 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));` |
|
||||
| 1246 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));` |
|
||||
| 1302 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
|
||||
| 2770 | pGLContext | GetClampReadColor | RenderStateBlob | `const GLenum clampMode = MG_State::pGLContext->GetClampReadColor();` |
|
||||
| 2987 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 3419 | SharedPtr<MG_State | - | handle-ify (wire handle) | `? SharedPtr<MG_State::GLState::BufferObject>{}` |
|
||||
| 3446 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) \|\|` |
|
||||
| 3447 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);` |
|
||||
| 3924 | pGLContext | GetCurrentVertexAttribute | CurrentAttrib | `const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);` |
|
||||
| 4058 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 4074 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& indexBufferShared =` |
|
||||
| 4813 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;` |
|
||||
| 4814 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;` |
|
||||
| 4815 | pGLContext | GetRenderStateParameters | RenderStateBlob | `return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;` |
|
||||
| 4826 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 4938 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 4982 | pGLContext | GetPipelineStateVersion | RenderStateBlob | `const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();` |
|
||||
| 5160 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace);` |
|
||||
| 5161 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest);` |
|
||||
| 5163 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) &&` |
|
||||
| 5166 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard);` |
|
||||
| 5168 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;` |
|
||||
| 5169 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `auto stencilTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);` |
|
||||
| 5175 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 5187 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);` |
|
||||
| 5188 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);` |
|
||||
| 5190 | pGLContext | GetPolygonModeFront | RenderStateBlob | `MG_Util::ConvertPolygonModeToVkEnum(MG_State::pGLContext->GetPolygonModeFront());` |
|
||||
| 5257 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),` |
|
||||
| 5258 | pGLContext | GetMinSampleShadingValue | RenderStateBlob | `.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),` |
|
||||
| 5264 | pGLContext | GetPatchVertices | RenderStateBlob | `.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),` |
|
||||
| 5268 | pGLContext | GetCullFaceMode | RenderStateBlob | `? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)` |
|
||||
| 5282 | pGLContext | GetDepthMask | RenderStateBlob | `.depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(),` |
|
||||
| 5287 | pGLContext | GetDepthFunc | RenderStateBlob | `.depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MG_State::pGLContext->GetDepthFunc()),` |
|
||||
| 5288 | pGLContext | GetLogicOp | RenderStateBlob | `.logicOp = MG_Util::ConvertLogicOperationToVkEnum(MG_State::pGLContext->GetLogicOp()),` |
|
||||
| 5324 | pGLContext | GetPatchDefaultOuterLevel | RenderStateBlob | `const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();` |
|
||||
| 5325 | pGLContext | GetPatchDefaultInnerLevel | RenderStateBlob | `const FloatVec2& defaultInnerLevel = MG_State::pGLContext->GetPatchDefaultInnerLevel();` |
|
||||
| 5377 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 5405 | pGLContext | GetBlendFuncIndexed | RenderStateBlob | `MG_State::pGLContext->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);` |
|
||||
| 5406 | pGLContext | GetBlendEquationIndexed | RenderStateBlob | `MG_State::pGLContext->GetBlendEquationIndexed(i, colorEquation, alphaEquation);` |
|
||||
| 5407 | pGLContext | IsCapabilityEnabledIndexed | RenderStateBlob | `const Bool blendEnabled = MG_State::pGLContext->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);` |
|
||||
| 5412 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `MG_State::pGLContext->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);` |
|
||||
| 5828 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 5889 | pGLContext | GetRenderStateParametersVersion | RenderStateBlob | `const Uint paramsVersion = MG_State::pGLContext->GetRenderStateParametersVersion();` |
|
||||
| 5903 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 5979 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 6003 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDraw();` |
|
||||
| 6051 | pGLContext | IsTransformFeedbackActive | XfbOp | `MG_State::pGLContext->IsTransformFeedbackActive() &&` |
|
||||
| 6065 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 6070 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 6081 | pGLContext | GetPipelineStateVersion | RenderStateBlob | `const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();` |
|
||||
| 6082 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
|
||||
| 6090 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 6264 | pGLContext | GetSamplingResolutionGeneration | TexParam | `const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 6385 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& drawProgram = *MG_State::pGLContext->GetProgramForDraw();` |
|
||||
| 6397 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 6404 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 6405 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDraw();` |
|
||||
| 6445 | pGLContext | IsTransformFeedbackActive | XfbOp | `if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&` |
|
||||
| 6459 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
|
||||
| 6465 | pGLContext | GetSamplingResolutionGeneration | TexParam | `const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 6583 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
|
||||
| 6602 | pGLContext | GetSamplingResolutionGeneration | TexParam | `const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 6763 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) \|\|` |
|
||||
| 6764 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);` |
|
||||
| 6905 | pGLContext | GetPipelineStateVersion | RenderStateBlob | `snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();` |
|
||||
| 6906 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
|
||||
| 6932 | pGLContext | GetSamplingResolutionGeneration | TexParam | `snap.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
|
||||
| 6969 | pGLContext | GetProgramForDispatch | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDispatch();` |
|
||||
| 7021 | pGLContext | GetProgramForDispatch | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDispatch();` |
|
||||
| 7065 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto indirectBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();` |
|
||||
| 7139 | pGLContext | GetScissorBox | RenderStateBlob | `? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(),` |
|
||||
| 7142 | pGLContext | GetScissorBox | RenderStateBlob | `: MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent);` |
|
||||
| 7190 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {` |
|
||||
| 7193 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();` |
|
||||
| 7201 | pGLContext | GetClearColor | RenderStateBlob | `.color = MG_State::pGLContext->GetClearColor(),` |
|
||||
| 7202 | pGLContext | GetClearDepth | RenderStateBlob | `.depth = MG_State::pGLContext->GetClearDepth(),` |
|
||||
| 7203 | pGLContext | GetClearStencil | RenderStateBlob | `.stencil = MG_State::pGLContext->GetClearStencil()` |
|
||||
| 7210 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {` |
|
||||
| 7233 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);` |
|
||||
| 7260 | pGLContext | GetDepthMask | RenderStateBlob | `if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) {` |
|
||||
| 7273 | pGLContext | GetStencilState | RenderStateBlob | `MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
|
||||
| 7302 | pGLContext | GetDepthMask | RenderStateBlob | `if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MG_State::pGLContext->GetDepthMask()) {` |
|
||||
| 7306 | pGLContext | GetStencilState | RenderStateBlob | `const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
|
||||
| 7322 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);` |
|
||||
| 7343 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);` |
|
||||
| 7372 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {` |
|
||||
| 7414 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {` |
|
||||
| 7445 | pGLContext | GetDepthMask | RenderStateBlob | `const auto depthClearAllowed = [&]() -> Bool { return MG_State::pGLContext->GetDepthMask(); };` |
|
||||
| 7447 | pGLContext | GetStencilState | RenderStateBlob | `const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
|
||||
| 7459 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));` |
|
||||
| 7516 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));` |
|
||||
| 7534 | pGLContext | GetDepthMask | RenderStateBlob | `if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() &&` |
|
||||
| 7542 | pGLContext | GetStencilState | RenderStateBlob | `MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
|
||||
| 7561 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();` |
|
||||
| 7598 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
|
||||
| 7622 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
|
||||
| 7645 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
|
||||
| 7660 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
|
||||
| 8042 | SharedPtr<MG_State | - | handle-ify (wire handle) | `VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {` |
|
||||
| 8519 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
|
||||
| 8520 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
|
||||
| 8524 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void VulkanRenderer::BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,` |
|
||||
| 8525 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,` |
|
||||
| 8552 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {` |
|
||||
| 8553 | pGLContext | GetScissorBox | RenderStateBlob | `const IntVec4& scissor = MG_State::pGLContext->GetScissorBox();` |
|
||||
| 9147 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
|
||||
| 9147 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
|
||||
| 9155 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
|
||||
| 9866 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
|
||||
| 10621 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
|
||||
| 10622 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
|
||||
| 10652 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
|
||||
| 10652 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
|
||||
| 10657 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void VulkanRenderer::GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,` |
|
||||
| 10896 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
|
||||
| 10896 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
|
||||
| 11137 | pGLContext | GetBoundTransformFeedbackName | XfbOp | `const Uint name = MG_State::pGLContext->GetBoundTransformFeedbackName();` |
|
||||
| 11151 | pGLContext | IsTransformFeedbackActive | XfbOp | `!MG_State::pGLContext->IsTransformFeedbackActive()) {` |
|
||||
| 11157 | pGLContext | IsTransformFeedbackPaused | XfbOp | `if (MG_State::pGLContext->IsTransformFeedbackPaused()) {` |
|
||||
| 11160 | pGLContext | GetTransformFeedbackProgram | XfbOp | `const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();` |
|
||||
| 11199 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,` |
|
||||
| 11230 | pGLContext | GetTransformFeedbackGeneration | XfbOp | `const Uint64 generation = MG_State::pGLContext->GetTransformFeedbackGeneration();` |
|
||||
| 11253 | pGLContext | GetTransformFeedbackProgram | XfbOp | `const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();` |
|
||||
| 11901 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
|
||||
| 11979 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 11989 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 11995 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
|
||||
| 12080 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
|
||||
| 12090 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 12160 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
|
||||
| 12650 | pGLContext | GetProvokingVertexMode | RenderStateBlob | `MG_State::pGLContext->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)` |
|
||||
| 14813 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
|
||||
|
||||
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h` (12 hits)
|
||||
|
||||
| line | kind | member | delta | snippet |
|
||||
|---|---|---|---|---|
|
||||
| 184 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 186 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 188 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 190 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
|
||||
| 195 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,` |
|
||||
| 196 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,` |
|
||||
| 256 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
|
||||
| 378 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ProgramObject> program;` |
|
||||
| 379 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::SamplerObject> nearestSampler;` |
|
||||
| 380 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::SamplerObject> linearSampler;` |
|
||||
| 388 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ProgramObject> program;` |
|
||||
| 1390 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);` |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user