mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-14 13:33:23 +09:00
Compare commits
220
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd7f71d6c | ||
|
|
353daa265e | ||
|
|
69728a4f9b | ||
|
|
c6f828532a | ||
|
|
5bb28140cb | ||
|
|
bf7fc6bf6b | ||
|
|
b72d4c5dff | ||
|
|
c88c6ff196 | ||
|
|
16488d5801 | ||
|
|
c16a51da62 | ||
|
|
08e45dc9c6 | ||
|
|
b2ce268c86 | ||
|
|
df58f21d83 | ||
|
|
29a89cc10a | ||
|
|
8e3db2cb87 | ||
|
|
1ba7b1a96f | ||
|
|
1a7b4b6a2b | ||
|
|
55e5165e2f | ||
|
|
2860d09051 | ||
|
|
c9206dd955 | ||
|
|
dfbda7ccb4 | ||
|
|
6a702afa72 | ||
|
|
ab685fb0e9 | ||
|
|
1ae7a8eaa4 | ||
|
|
ead7a10b68 | ||
|
|
035f1b07ae | ||
|
|
c7ab618ab0 | ||
|
|
2e43df6125 | ||
|
|
015b91f08d | ||
|
|
987b7ad13c | ||
|
|
d26cf1e31b | ||
|
|
8196818e9f | ||
|
|
648800a2da | ||
|
|
b4bee9dd38 | ||
|
|
494305381e | ||
|
|
fb106a682e | ||
|
|
c8c4397475 | ||
|
|
9c24a14d2b | ||
|
|
ac9ab078cc | ||
|
|
377d90d9f1 | ||
|
|
4cfb29a553 | ||
|
|
51e95c9260 | ||
|
|
982958ebb1 | ||
|
|
3717424229 | ||
|
|
f973008c4d | ||
|
|
edcb9328e2 | ||
|
|
e61060fbdf | ||
|
|
3ce8ce3cb1 | ||
|
|
394d255487 | ||
|
|
0222852b94 | ||
|
|
2edb2aa82a | ||
|
|
fc415aee76 | ||
|
|
307267c188 | ||
|
|
cbbaf77f6d | ||
|
|
39cd8fb73b | ||
|
|
92eab9f3b7 | ||
|
|
9441071567 | ||
|
|
2f530ad8a2 | ||
|
|
c00f5eabae | ||
|
|
24b8aef616 | ||
|
|
8133cd52ae | ||
|
|
06f28b0a1b | ||
|
|
b9feb9d7be | ||
|
|
2b4ae539da | ||
|
|
e815ce8d33 | ||
|
|
00d6c2480e | ||
|
|
f823422bfa | ||
|
|
e52a997f34 | ||
|
|
c7a14ed790 | ||
|
|
d639e54616 | ||
|
|
32f222dbc2 | ||
|
|
a2c81a2f64 | ||
|
|
8dac1fd8a4 | ||
|
|
b71cb9cc2c | ||
|
|
efd3533bd1 | ||
|
|
67890f523a | ||
|
|
976fbe4cd4 | ||
|
|
8d3e2317a6 | ||
|
|
0b55b55d4d | ||
|
|
642ac891af | ||
|
|
592558f5fc | ||
|
|
0a117243d5 | ||
|
|
344e2f8439 | ||
|
|
91900bdc5e | ||
|
|
b32140e614 | ||
|
|
7030d307be | ||
|
|
853c5f1264 | ||
|
|
e44d6ac7da | ||
|
|
8571e0c001 | ||
|
|
7784d6bbf7 | ||
|
|
4aec5621a8 | ||
|
|
4c02279f22 | ||
|
|
e869d18ce8 | ||
|
|
9d1e788597 | ||
|
|
ce5753c883 | ||
|
|
5e65907d63 | ||
|
|
98bf1925aa | ||
|
|
17f1c0b59b | ||
|
|
2522f1803c | ||
|
|
7cde55b1c1 | ||
|
|
2a69f61c88 | ||
|
|
590d74525d | ||
|
|
b57276b88a | ||
|
|
ac25e3323a | ||
|
|
96881f639c | ||
|
|
16ea23c927 | ||
|
|
b9b14c4775 | ||
|
|
37c7aede31 | ||
|
|
64b5794bc9 | ||
|
|
ce4dca2835 | ||
|
|
df826f426f | ||
|
|
7ccb920ad2 | ||
|
|
582adb0dd6 | ||
|
|
578795c84d | ||
|
|
9af4c8aefd | ||
|
|
5a97eb8d1b | ||
|
|
fc427a14d6 | ||
|
|
54e7c92cf4 | ||
|
|
1abddf0d00 | ||
|
|
0819f9d410 | ||
|
|
952a1ca4a7 | ||
|
|
bd05e061fc | ||
|
|
23a9470a28 | ||
|
|
53098bc57f | ||
|
|
76f75f3a0c | ||
|
|
d587d394fe | ||
|
|
de996d539f | ||
|
|
b4b1825951 | ||
|
|
ec5f6ead3f | ||
|
|
f636a4cdae | ||
|
|
bd2e43ede3 | ||
|
|
e2333d98a8 | ||
|
|
5c9ad5b499 | ||
|
|
9145433890 | ||
|
|
c371599811 | ||
|
|
b34eed580d | ||
|
|
c14283ee2d | ||
|
|
9dccabf03c | ||
|
|
53fa16c2c2 | ||
|
|
c2de6576ad | ||
|
|
35e1c861a3 | ||
|
|
9ebcf5a73d | ||
|
|
05dc94fa38 | ||
|
|
abe29eae2d | ||
|
|
0a6af63655 | ||
|
|
32b81dae42 | ||
|
|
9c32d9fa68 | ||
|
|
4d3d677a7f | ||
|
|
d20c2d01ae | ||
|
|
2fcde6eb9b | ||
|
|
21c7e8cd63 | ||
|
|
9524c688ca | ||
|
|
6481691d1e | ||
|
|
9176dcff3d | ||
|
|
26de2b60fc | ||
|
|
82467823ef | ||
|
|
2a902cbd79 | ||
|
|
3c72e5c637 | ||
|
|
1797db0070 | ||
|
|
01a33dfc58 | ||
|
|
93a72678ca | ||
|
|
3cb3e32b87 | ||
|
|
89f1e6cfba | ||
|
|
2c7d99d5e8 | ||
|
|
9cde08064c | ||
|
|
48a306025d | ||
|
|
e0102bad8c | ||
|
|
ffeed0da3a | ||
|
|
86dc38a07e | ||
|
|
c60639db1d | ||
|
|
74ae652ecd | ||
|
|
414dd09bda | ||
|
|
47bbf5e6fd | ||
|
|
73089cf2b3 | ||
|
|
80d205c401 | ||
|
|
4373964443 | ||
|
|
2271f6c1ec | ||
|
|
8d0a1b6ca5 | ||
|
|
625f4db47d | ||
|
|
b1d60523e4 | ||
|
|
8966d5c294 | ||
|
|
bcff9b00cc | ||
|
|
de33d40173 | ||
|
|
7aa00761f8 | ||
|
|
aa0c242b51 | ||
|
|
6c2f8a560c | ||
|
|
568090f09f | ||
|
|
738b29f3e4 | ||
|
|
a7a82ffee5 | ||
|
|
24560ffe81 | ||
|
|
6bcfb1fba1 | ||
|
|
362daafeed | ||
|
|
6485ae1dc0 | ||
|
|
ad705a7392 | ||
|
|
acfd367852 | ||
|
|
743f4e8e6e | ||
|
|
6e35ea2c02 | ||
|
|
c932af7f3c | ||
|
|
8a71ea5954 | ||
|
|
984e2b69f4 | ||
|
|
f2ad2fae9b | ||
|
|
27cbc53241 | ||
|
|
b60da6729d | ||
|
|
b4f0c53426 | ||
|
|
429b4472bb | ||
|
|
221a8144c2 | ||
|
|
1987bdff5c | ||
|
|
2fa58940e4 | ||
|
|
35dddc0c89 | ||
|
|
3bdd565dc6 | ||
|
|
884f6aac96 | ||
|
|
b9a89967b0 | ||
|
|
141227e8ba | ||
|
|
930c29e9e0 | ||
|
|
c16fb5a8ac | ||
|
|
e3e890ed5d | ||
|
|
bf6feebb22 | ||
|
|
f10db410e8 | ||
|
|
aaa50c88e1 | ||
|
|
1f72597dd8 |
@@ -61,15 +61,40 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The render server is not trace-only: both flavors carry the control screen, the offscreen
|
||||
# service, the on-screen display activity, and (split native builds) the supervisor executable.
|
||||
# The plugin APK ships to users, so its build keeps the three components private (manifest
|
||||
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
|
||||
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
|
||||
require "$component" "$plugin_manifest" "plugin render server component ($component)"
|
||||
require "$component" "$trace_manifest" "trace render server component ($component)"
|
||||
done
|
||||
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
|
||||
trace_contents=$(unzip -Z1 "$trace_apk")
|
||||
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
|
||||
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
|
||||
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
|
||||
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
|
||||
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
|
||||
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
|
||||
echo '::error::Trace APK must not expose a launcher activity' >&2
|
||||
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
|
||||
# entry icon and the bottom navigation bar switches to the render server screen. What is
|
||||
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
|
||||
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
|
||||
# reds here instead of shipping two icons.
|
||||
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
|
||||
if [[ "$main_count" -ne 1 ]]; then
|
||||
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
|
||||
|
||||
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
|
||||
|
||||
@@ -1461,6 +1461,22 @@ jobs:
|
||||
- name: Spawn/split lane parity (9.2)
|
||||
run: python3 scripts/ci/spawn_lane_parity.py build-split
|
||||
|
||||
# P8-A (ID-P8-2): every monolith case, tails normalised, is on a gated split arm or is named
|
||||
# with a reason class in scripts/data/split_coverage_exemptions.txt. The same `ctest -N`
|
||||
# cost as the parity step above, and the question it cannot answer: parity compares the
|
||||
# split arms with EACH OTHER, so a case missing from all three passes it.
|
||||
- name: Split coverage against monolith (P8-A)
|
||||
run: |
|
||||
python3 scripts/ci/split_coverage.py --self-test
|
||||
python3 scripts/ci/split_coverage.py build-split
|
||||
|
||||
# P8-SE: PipeCatalogueTest's unmigrated-emulation list equals the MGPipeUnmigratedEmulation
|
||||
# call sites in the tree, one site per name. A source lint: no build needed.
|
||||
- name: Unmigrated-emulation list against its call sites (P8-SE)
|
||||
run: |
|
||||
python3 scripts/ci/unmigrated_emulation_sites.py --self-test
|
||||
python3 scripts/ci/unmigrated_emulation_sites.py
|
||||
|
||||
- name: Unit tests on the split runtime
|
||||
working-directory: build-split
|
||||
env:
|
||||
@@ -2042,6 +2058,13 @@ jobs:
|
||||
"PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
|
||||
"PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
|
||||
"StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding",
|
||||
# P11 A2: the >= 16 MiB arena and its tier assertion (emulated).
|
||||
"LargeArenaAdoptionScenario.SubDataAfterAnInFlightDrawReachesTheNextDraw",
|
||||
"LargeArenaAdoptionScenario.RespecifiedVertexArenaKeepsVaoBindings",
|
||||
"LargeArenaAdoptionScenario.RespecifiedIndexArenaKeepsVaoBinding",
|
||||
"LargeArenaAdoptionScenario.ReadbackSeesTheLatestCpuWrite",
|
||||
"LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack",
|
||||
"LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares",
|
||||
]
|
||||
slp.require_green(json.load(open(sys.argv[2])), sys.argv[3],
|
||||
["DirectVulkan.Split.Buffers." + name for name in cases])
|
||||
@@ -2104,6 +2127,12 @@ jobs:
|
||||
"PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
|
||||
"PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
|
||||
"StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding",
|
||||
"LargeArenaAdoptionScenario.SubDataAfterAnInFlightDrawReachesTheNextDraw",
|
||||
"LargeArenaAdoptionScenario.RespecifiedVertexArenaKeepsVaoBindings",
|
||||
"LargeArenaAdoptionScenario.RespecifiedIndexArenaKeepsVaoBinding",
|
||||
"LargeArenaAdoptionScenario.ReadbackSeesTheLatestCpuWrite",
|
||||
"LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack",
|
||||
"LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares",
|
||||
]
|
||||
required = [sys.argv[4] + name for name in cases]
|
||||
if sys.argv[5] == "tcp":
|
||||
|
||||
@@ -28,3 +28,4 @@ tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
/.gradle
|
||||
/.trace-work/
|
||||
|
||||
Vendored
+538
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* Copyright © 2011 Intel Corporation
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice (including the next
|
||||
* paragraph) shall be included in all copies or substantial portions of the
|
||||
* Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* Authors:
|
||||
* Benjamin Franzke <benjaminfranzke@googlemail.com>
|
||||
*/
|
||||
|
||||
#ifndef _GBM_H_
|
||||
#define _GBM_H_
|
||||
|
||||
#define __GBM__ 1
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* \file gbm.h
|
||||
* \brief Generic Buffer Manager
|
||||
*/
|
||||
|
||||
struct gbm_device;
|
||||
struct gbm_bo;
|
||||
struct gbm_surface;
|
||||
|
||||
/**
|
||||
* \mainpage The Generic Buffer Manager
|
||||
*
|
||||
* This module provides an abstraction that the caller can use to request a
|
||||
* buffer from the underlying memory management system for the platform.
|
||||
*
|
||||
* This allows the creation of portable code whilst still allowing access to
|
||||
* the underlying memory manager.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstraction representing the handle to a buffer allocated by the
|
||||
* manager
|
||||
*/
|
||||
union gbm_bo_handle {
|
||||
void *ptr;
|
||||
int32_t s32;
|
||||
uint32_t u32;
|
||||
int64_t s64;
|
||||
uint64_t u64;
|
||||
};
|
||||
|
||||
/** Format of the allocated buffer */
|
||||
enum gbm_bo_format {
|
||||
/** RGB with 8 bits per channel in a 32 bit value */
|
||||
GBM_BO_FORMAT_XRGB8888,
|
||||
/** ARGB with 8 bits per channel in a 32 bit value */
|
||||
GBM_BO_FORMAT_ARGB8888
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The FourCC format codes are taken from the drm_fourcc.h definition, and
|
||||
* re-namespaced. New GBM formats must not be added, unless they are
|
||||
* identical ports from drm_fourcc.
|
||||
*/
|
||||
#define __gbm_fourcc_code(a,b,c,d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \
|
||||
((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
|
||||
|
||||
#define GBM_FORMAT_BIG_ENDIAN (1<<31) /* format is big endian instead of little endian */
|
||||
|
||||
/* color index */
|
||||
#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
|
||||
|
||||
/* 8 bpp Red */
|
||||
#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
|
||||
|
||||
/* 16 bpp Red */
|
||||
#define GBM_FORMAT_R16 __gbm_fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */
|
||||
|
||||
/* 16 bpp RG */
|
||||
#define GBM_FORMAT_GR88 __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
|
||||
|
||||
/* 32 bpp RG */
|
||||
#define GBM_FORMAT_RG1616 __gbm_fourcc_code('R', 'G', '3', '2') /* [31:0] R:G 16:16 little endian */
|
||||
#define GBM_FORMAT_GR1616 __gbm_fourcc_code('G', 'R', '3', '2') /* [31:0] G:R 16:16 little endian */
|
||||
|
||||
/* 8 bpp RGB */
|
||||
#define GBM_FORMAT_RGB332 __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */
|
||||
#define GBM_FORMAT_BGR233 __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */
|
||||
|
||||
/* 16 bpp RGB */
|
||||
#define GBM_FORMAT_XRGB4444 __gbm_fourcc_code('X', 'R', '1', '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_XBGR4444 __gbm_fourcc_code('X', 'B', '1', '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_RGBX4444 __gbm_fourcc_code('R', 'X', '1', '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_BGRX4444 __gbm_fourcc_code('B', 'X', '1', '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB4444 __gbm_fourcc_code('A', 'R', '1', '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_ABGR4444 __gbm_fourcc_code('A', 'B', '1', '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_RGBA4444 __gbm_fourcc_code('R', 'A', '1', '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_BGRA4444 __gbm_fourcc_code('B', 'A', '1', '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */
|
||||
|
||||
#define GBM_FORMAT_XRGB1555 __gbm_fourcc_code('X', 'R', '1', '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_XBGR1555 __gbm_fourcc_code('X', 'B', '1', '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_RGBX5551 __gbm_fourcc_code('R', 'X', '1', '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */
|
||||
#define GBM_FORMAT_BGRX5551 __gbm_fourcc_code('B', 'X', '1', '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB1555 __gbm_fourcc_code('A', 'R', '1', '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_ABGR1555 __gbm_fourcc_code('A', 'B', '1', '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_RGBA5551 __gbm_fourcc_code('R', 'A', '1', '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */
|
||||
#define GBM_FORMAT_BGRA5551 __gbm_fourcc_code('B', 'A', '1', '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */
|
||||
|
||||
#define GBM_FORMAT_RGB565 __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */
|
||||
#define GBM_FORMAT_BGR565 __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */
|
||||
|
||||
/* 24 bpp RGB */
|
||||
#define GBM_FORMAT_RGB888 __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */
|
||||
#define GBM_FORMAT_BGR888 __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */
|
||||
|
||||
/* 32 bpp RGB */
|
||||
#define GBM_FORMAT_XRGB8888 __gbm_fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_XBGR8888 __gbm_fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_RGBX8888 __gbm_fourcc_code('R', 'X', '2', '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_BGRX8888 __gbm_fourcc_code('B', 'X', '2', '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB8888 __gbm_fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_ABGR8888 __gbm_fourcc_code('A', 'B', '2', '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_RGBA8888 __gbm_fourcc_code('R', 'A', '2', '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_BGRA8888 __gbm_fourcc_code('B', 'A', '2', '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_XRGB2101010 __gbm_fourcc_code('X', 'R', '3', '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_XBGR2101010 __gbm_fourcc_code('X', 'B', '3', '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_RGBX1010102 __gbm_fourcc_code('R', 'X', '3', '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */
|
||||
#define GBM_FORMAT_BGRX1010102 __gbm_fourcc_code('B', 'X', '3', '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB2101010 __gbm_fourcc_code('A', 'R', '3', '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_ABGR2101010 __gbm_fourcc_code('A', 'B', '3', '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_RGBA1010102 __gbm_fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
|
||||
#define GBM_FORMAT_BGRA1010102 __gbm_fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
|
||||
|
||||
/* 64 bpp RGB */
|
||||
#define GBM_FORMAT_XBGR16161616 __gbm_fourcc_code('X', 'B', '4', '8') /* [63:0] x:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
#define GBM_FORMAT_ABGR16161616 __gbm_fourcc_code('A', 'B', '4', '8') /* [63:0] A:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
/*
|
||||
* Floating point 64bpp RGB
|
||||
* IEEE 754-2008 binary16 half-precision float
|
||||
* [15:0] sign:exponent:mantissa 1:5:10
|
||||
*/
|
||||
#define GBM_FORMAT_XBGR16161616F __gbm_fourcc_code('X', 'B', '4', 'H') /* [63:0] x:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
#define GBM_FORMAT_ABGR16161616F __gbm_fourcc_code('A', 'B', '4', 'H') /* [63:0] A:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
/* packed YCbCr */
|
||||
#define GBM_FORMAT_YUYV __gbm_fourcc_code('Y', 'U', 'Y', 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_YVYU __gbm_fourcc_code('Y', 'V', 'Y', 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_UYVY __gbm_fourcc_code('U', 'Y', 'V', 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_VYUY __gbm_fourcc_code('V', 'Y', 'U', 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_AYUV __gbm_fourcc_code('A', 'Y', 'U', 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */
|
||||
|
||||
/*
|
||||
* 2 plane YCbCr
|
||||
* index 0 = Y plane, [7:0] Y
|
||||
* index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian
|
||||
* or
|
||||
* index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian
|
||||
*/
|
||||
#define GBM_FORMAT_NV12 __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */
|
||||
#define GBM_FORMAT_NV21 __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */
|
||||
#define GBM_FORMAT_NV16 __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */
|
||||
#define GBM_FORMAT_NV61 __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */
|
||||
|
||||
/*
|
||||
* 3 plane YCbCr
|
||||
* index 0: Y plane, [7:0] Y
|
||||
* index 1: Cb plane, [7:0] Cb
|
||||
* index 2: Cr plane, [7:0] Cr
|
||||
* or
|
||||
* index 1: Cr plane, [7:0] Cr
|
||||
* index 2: Cb plane, [7:0] Cb
|
||||
*/
|
||||
#define GBM_FORMAT_YUV410 __gbm_fourcc_code('Y', 'U', 'V', '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU410 __gbm_fourcc_code('Y', 'V', 'U', '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV411 __gbm_fourcc_code('Y', 'U', '1', '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU411 __gbm_fourcc_code('Y', 'V', '1', '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV420 __gbm_fourcc_code('Y', 'U', '1', '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU420 __gbm_fourcc_code('Y', 'V', '1', '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV422 __gbm_fourcc_code('Y', 'U', '1', '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU422 __gbm_fourcc_code('Y', 'V', '1', '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV444 __gbm_fourcc_code('Y', 'U', '2', '4') /* non-subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU444 __gbm_fourcc_code('Y', 'V', '2', '4') /* non-subsampled Cr (1) and Cb (2) planes */
|
||||
|
||||
struct gbm_format_name_desc {
|
||||
char name[5];
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags to indicate the intended use for the buffer - these are passed into
|
||||
* gbm_bo_create(). The caller must set the union of all the flags that are
|
||||
* appropriate
|
||||
*
|
||||
* \sa Use gbm_device_is_format_supported() to check if the combination of format
|
||||
* and use flags are supported
|
||||
*/
|
||||
enum gbm_bo_flags {
|
||||
/**
|
||||
* Buffer is going to be presented to the screen using an API such as KMS
|
||||
*/
|
||||
GBM_BO_USE_SCANOUT = (1 << 0),
|
||||
/**
|
||||
* Buffer is going to be used as cursor
|
||||
*/
|
||||
GBM_BO_USE_CURSOR = (1 << 1),
|
||||
/**
|
||||
* Deprecated
|
||||
*/
|
||||
GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
|
||||
/**
|
||||
* Buffer is to be used for rendering - for example it is going to be used
|
||||
* as the storage for a color buffer
|
||||
*/
|
||||
GBM_BO_USE_RENDERING = (1 << 2),
|
||||
/**
|
||||
* Buffer can be used for gbm_bo_write. This is guaranteed to work
|
||||
* with GBM_BO_USE_CURSOR, but may not work for other combinations.
|
||||
*/
|
||||
GBM_BO_USE_WRITE = (1 << 3),
|
||||
/**
|
||||
* Buffer is linear, i.e. not tiled.
|
||||
*/
|
||||
GBM_BO_USE_LINEAR = (1 << 4),
|
||||
/**
|
||||
* Buffer is protected, i.e. encrypted and not readable by CPU or any
|
||||
* other non-secure / non-trusted components nor by non-trusted OpenGL,
|
||||
* OpenCL, and Vulkan applications.
|
||||
*/
|
||||
GBM_BO_USE_PROTECTED = (1 << 5),
|
||||
|
||||
/**
|
||||
* The buffer will be used for front buffer rendering. On some
|
||||
* platforms this may (for example) disable framebuffer compression
|
||||
* to avoid problems with compression flags data being out of sync
|
||||
* with pixel data.
|
||||
*/
|
||||
GBM_BO_USE_FRONT_RENDERING = (1 << 6),
|
||||
|
||||
/**
|
||||
* Allow the driver to select fixed-rate compression parameters.
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_DEFAULT = (1 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 1bpc, less than 2bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_1BPC = (2 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 2bpc, less than 3bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_2BPC = (3 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 3bpc, less than 4bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_3BPC = (4 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 4bpc, less than 5bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_4BPC = (5 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 5bpc, less than 6bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_5BPC = (6 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 6bpc, less than 7bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_6BPC = (7 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 7bpc, less than 8bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_7BPC = (8 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 8bpc, less than 9bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_8BPC = (9 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 9bpc, less than 10bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_9BPC = (10 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 10bpc, less than 11bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_10BPC = (11 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 11bpc, less than 12bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_11BPC = (12 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 12bpc, no maximum rate
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_12BPC = (13 << 7),
|
||||
|
||||
/* next available value is (1 << 11) */
|
||||
};
|
||||
|
||||
#define GBM_BO_FIXED_COMPRESSION_MASK (((1 << 11) - 1) & ~((1 << 7) - 1))
|
||||
|
||||
int
|
||||
gbm_device_get_fd(struct gbm_device *gbm);
|
||||
|
||||
const char *
|
||||
gbm_device_get_backend_name(struct gbm_device *gbm);
|
||||
|
||||
int
|
||||
gbm_device_is_format_supported(struct gbm_device *gbm,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
int
|
||||
gbm_device_get_format_modifier_plane_count(struct gbm_device *gbm,
|
||||
uint32_t format,
|
||||
uint64_t modifier);
|
||||
|
||||
void
|
||||
gbm_device_destroy(struct gbm_device *gbm);
|
||||
|
||||
struct gbm_device *
|
||||
gbm_create_device(int fd);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create_with_modifiers(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create_with_modifiers2(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count,
|
||||
uint32_t flags);
|
||||
|
||||
#define GBM_BO_IMPORT_WL_BUFFER 0x5501
|
||||
#define GBM_BO_IMPORT_EGL_IMAGE 0x5502
|
||||
#define GBM_BO_IMPORT_FD 0x5503
|
||||
#define GBM_BO_IMPORT_FD_MODIFIER 0x5504
|
||||
|
||||
struct gbm_import_fd_data {
|
||||
int fd;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
uint32_t format;
|
||||
};
|
||||
|
||||
#define GBM_MAX_PLANES 4
|
||||
|
||||
struct gbm_import_fd_modifier_data {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t format;
|
||||
uint32_t num_fds;
|
||||
int fds[GBM_MAX_PLANES];
|
||||
int strides[GBM_MAX_PLANES];
|
||||
int offsets[GBM_MAX_PLANES];
|
||||
uint64_t modifier;
|
||||
};
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_import(struct gbm_device *gbm, uint32_t type,
|
||||
void *buffer, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Flags to indicate the type of mapping for the buffer - these are
|
||||
* passed into gbm_bo_map(). The caller must set the union of all the
|
||||
* flags that are appropriate.
|
||||
*
|
||||
* These flags are independent of the GBM_BO_USE_* creation flags. However,
|
||||
* mapping the buffer may require copying to/from a staging buffer.
|
||||
*
|
||||
* See also: pipe_map_flags
|
||||
*/
|
||||
enum gbm_bo_transfer_flags {
|
||||
/**
|
||||
* Buffer contents read back (or accessed directly) at transfer
|
||||
* create time.
|
||||
*/
|
||||
GBM_BO_TRANSFER_READ = (1 << 0),
|
||||
/**
|
||||
* Buffer contents will be written back at unmap time
|
||||
* (or modified as a result of being accessed directly).
|
||||
*/
|
||||
GBM_BO_TRANSFER_WRITE = (1 << 1),
|
||||
/**
|
||||
* Read/modify/write
|
||||
*/
|
||||
GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
|
||||
};
|
||||
|
||||
void *
|
||||
gbm_bo_map(struct gbm_bo *bo,
|
||||
uint32_t x, uint32_t y, uint32_t width, uint32_t height,
|
||||
uint32_t flags, uint32_t *stride, void **map_data);
|
||||
|
||||
void
|
||||
gbm_bo_unmap(struct gbm_bo *bo, void *map_data);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_width(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_height(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_stride(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_stride_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_format(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_bpp(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_offset(struct gbm_bo *bo, int plane);
|
||||
|
||||
struct gbm_device *
|
||||
gbm_bo_get_device(struct gbm_bo *bo);
|
||||
|
||||
union gbm_bo_handle
|
||||
gbm_bo_get_handle(struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_bo_get_fd(struct gbm_bo *bo);
|
||||
|
||||
uint64_t
|
||||
gbm_bo_get_modifier(struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_bo_get_plane_count(struct gbm_bo *bo);
|
||||
|
||||
union gbm_bo_handle
|
||||
gbm_bo_get_handle_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
int
|
||||
gbm_bo_get_fd_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
int
|
||||
gbm_bo_write(struct gbm_bo *bo, const void *buf, size_t count);
|
||||
|
||||
void
|
||||
gbm_bo_set_user_data(struct gbm_bo *bo, void *data,
|
||||
void (*destroy_user_data)(struct gbm_bo *, void *));
|
||||
|
||||
void *
|
||||
gbm_bo_get_user_data(struct gbm_bo *bo);
|
||||
|
||||
void
|
||||
gbm_bo_destroy(struct gbm_bo *bo);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create_with_modifiers(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create_with_modifiers2(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count,
|
||||
uint32_t flags);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_surface_lock_front_buffer(struct gbm_surface *surface);
|
||||
|
||||
void
|
||||
gbm_surface_release_buffer(struct gbm_surface *surface, struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_surface_has_free_buffers(struct gbm_surface *surface);
|
||||
|
||||
void
|
||||
gbm_surface_destroy(struct gbm_surface *surface);
|
||||
|
||||
char *
|
||||
gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc *desc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# glvnd vendor ABI headers
|
||||
|
||||
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
|
||||
headers intact.
|
||||
|
||||
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
|
||||
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
|
||||
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
|
||||
smaller dependency than a build-time download of a source tree.
|
||||
|
||||
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2013, NVIDIA CORPORATION.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and/or associated documentation files (the
|
||||
* "Materials"), to deal in the Materials without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
* permit persons to whom the Materials are furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* unaltered in all copies or substantial portions of the Materials.
|
||||
* Any additions, deletions, or changes to the original source files
|
||||
* must be clearly indicated in accompanying documentation.
|
||||
*
|
||||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#include <GL/gl.h>
|
||||
|
||||
#if !defined(__GL_DISPATCH_ABI_H)
|
||||
#define __GL_DISPATCH_ABI_H
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \defgroup gldispatchabi GL dispatching ABI
|
||||
*
|
||||
* This is not a complete ABI, but rather a fragment common to the libEGL and
|
||||
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
|
||||
* these client ABIs.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Thread-local implementation used by libglvnd. This is passed into the patch
|
||||
* function callback via the type parameter.
|
||||
*
|
||||
* For most architectures, the vendor library can ignore this parameter, since
|
||||
* it will always be the same value. It's used for systems like ARM, where the
|
||||
* stubs might be use the ARM or Thumb instruction sets.
|
||||
*
|
||||
* The stub type does not make any distinction between TLS and TSD stubs. The
|
||||
* entire purpose of entrypoint rewriting is to skip the dispatch table in
|
||||
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
|
||||
*/
|
||||
enum {
|
||||
/*!
|
||||
* Indicates that the stubs aren't defined in assembly. For example, if the
|
||||
* dispatch stubs are written in C. Vendor libraries generally won't see
|
||||
* this value.
|
||||
*/
|
||||
__GLDISPATCH_STUB_UNKNOWN,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x86 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_X86,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x86-64 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_X86_64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv7, using the Thumb instruction set.
|
||||
*/
|
||||
__GLDISPATCH_STUB_ARMV7_THUMB,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv7, using the normal ARM instruction set.
|
||||
*/
|
||||
__GLDISPATCH_STUB_ARMV7_ARM,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv8/aarch64.
|
||||
*/
|
||||
__GLDISPATCH_STUB_AARCH64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
|
||||
*/
|
||||
__GLDISPATCH_STUB_X32,
|
||||
|
||||
/*!
|
||||
* Used for stubs on PPC64 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_PPC64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
|
||||
*/
|
||||
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on LOONGARCH/loongarch64.
|
||||
*/
|
||||
__GLDISPATCH_STUB_LOONGARCH64,
|
||||
};
|
||||
|
||||
/*!
|
||||
* A callback function called by the vendor library to fetch the address of an
|
||||
* entrypoint.
|
||||
*
|
||||
* The function returns two pointers, one writable and one executable. The two
|
||||
* pointers may or may not be the same virtual address, but they will both be
|
||||
* mappings of the same physical memory.
|
||||
*
|
||||
* The vendor library should write its entrypoint to the address returned by
|
||||
* \p writePtr, but should use the address from \p execPtr for things like
|
||||
* calculating PC-relative offsets.
|
||||
*
|
||||
* Note that if this function fails, then the vendor library can still try to
|
||||
* patch other entrypoints.
|
||||
*
|
||||
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
|
||||
* be zero, even if the stub uses the thumb instruction set. The vendor library
|
||||
* should use the \c type parameter of \c initiatePatch to determine which
|
||||
* instruction set to use.
|
||||
*
|
||||
* \param funcName The function name.
|
||||
* \param[out] writePtr The pointer that the vendor library can write to.
|
||||
* \param[out] execPtr The pointer to the executable code.
|
||||
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
|
||||
*/
|
||||
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
|
||||
void **writePtr, const void **execPtr);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // __GL_DISPATCH_ABI_H
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* Copyright (c) 2013, NVIDIA CORPORATION.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and/or associated documentation files (the
|
||||
* "Materials"), to deal in the Materials without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
* permit persons to whom the Materials are furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* unaltered in all copies or substantial portions of the Materials.
|
||||
* Any additions, deletions, or changes to the original source files
|
||||
* must be clearly indicated in accompanying documentation.
|
||||
*
|
||||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#if !defined(__LIB_EGL_ABI_H)
|
||||
#define __LIB_EGL_ABI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#include "glvnd/GLdispatchABI.h"
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \defgroup eglvendorabi EGL Vendor ABI
|
||||
*
|
||||
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
|
||||
*
|
||||
* Each vendor is associated with three distinct dispatch table types:
|
||||
*
|
||||
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
|
||||
* provided by the vendor at load time during the initial handshake.
|
||||
* - dynamic EGL dispatch table: this is a structure allocated by the API
|
||||
* library at runtime used to manage EGL extension functions which are not
|
||||
* present in the static table.
|
||||
* - core GL dispatch table: this is a structure maintained by the API library
|
||||
* which contains both GL core (static) and GL extension (dynamic) functions.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/*
|
||||
* Rendering API handling:
|
||||
*
|
||||
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
|
||||
* other API is added, then the major version number will be incremented.
|
||||
*
|
||||
* When the application calls eglBindAPI, libEGL will forward the call to every
|
||||
* vendor library. In addition, a vendor library can query the current API from
|
||||
* libEGL using the getCurrentApi callback.
|
||||
*
|
||||
* Vendor libraries are not required to support both GL and GLES, but they must
|
||||
* be able to deal with either one as the current rendering API. If a vendor
|
||||
* doesn't support the current API, then it should return an error from
|
||||
* eglCreateContext.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Current version of the ABI.
|
||||
*
|
||||
* This version number contains a major number in the high-order 16 bits, and
|
||||
* a minor version number in the low-order 16 bits.
|
||||
*
|
||||
* The major version number is incremented when an interface change will break
|
||||
* backwards compatibility with existing vendor libraries. The minor version
|
||||
* number is incremented when there's a change but existing vendor libraries
|
||||
* will still work.
|
||||
*/
|
||||
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
|
||||
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
|
||||
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
|
||||
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
|
||||
{
|
||||
return version >> 16;
|
||||
}
|
||||
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
|
||||
{
|
||||
return version & 0xFFFF;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* This opaque structure stores function pointers for EGL extension functions.
|
||||
* It is allocated at runtime by the API library. Vendor-provided dispatch
|
||||
* functions retrieve and operate on this structure using the API below.
|
||||
*/
|
||||
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
|
||||
|
||||
/****************************************************************************
|
||||
* API library exports *
|
||||
****************************************************************************/
|
||||
|
||||
typedef struct __EGLapiExportsRec {
|
||||
/************************************************************************
|
||||
* The following routines are used by vendor-implemented EGL dispatch
|
||||
* functions to lookup and call into the right vendor.
|
||||
************************************************************************/
|
||||
|
||||
/*!
|
||||
* This function must be called at the start of every EGL dispatch stub. It
|
||||
* performs any necessary per-call bookkeeping.
|
||||
*/
|
||||
void (* threadInit) (void);
|
||||
|
||||
EGLenum (* getCurrentApi)(void);
|
||||
|
||||
/*!
|
||||
* This function retrieves the appropriate current dynamic dispatch table,
|
||||
* if a GL context is current. Otherwise, this returns NULL.
|
||||
*/
|
||||
__EGLvendorInfo *(*getCurrentVendor)(void);
|
||||
|
||||
/*!
|
||||
* This retrieves the current context for this thread.
|
||||
*/
|
||||
EGLContext (*getCurrentContext)(void);
|
||||
|
||||
/*!
|
||||
* Returns the current display for this thread.
|
||||
*/
|
||||
EGLDisplay (*getCurrentDisplay)(void);
|
||||
|
||||
/*!
|
||||
* Returns the current drawable for this thread.
|
||||
*/
|
||||
EGLSurface (*getCurrentSurface)(EGLint readDraw);
|
||||
|
||||
/*!
|
||||
* This function retrieves an entry point from the dynamic dispatch table
|
||||
* given an index into the table.
|
||||
*/
|
||||
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
|
||||
|
||||
/*!
|
||||
* Sets the last error for the current thread. The error will be returned
|
||||
* the next time the app calls eglGetError().
|
||||
*
|
||||
* This function will override a call to \c setLastVendor, and vice-versa.
|
||||
*/
|
||||
void (* setEGLError) (EGLint errorCode);
|
||||
|
||||
/*!
|
||||
* Notifies libEGL about the vendor library that an EGL function is
|
||||
* dispatched to. This is used to look up the last EGL error code from a
|
||||
* vendor.
|
||||
*
|
||||
* A vendor library may generate an EGL error asynchronously, if it
|
||||
* offloads some EGL functions onto a worker thread. In that case, calling
|
||||
* \c setEGLError to set the last error would not work.
|
||||
*
|
||||
* When an EGL dispatch stubs finds the vendor library, it must call this
|
||||
* function before it dispatches the function call.
|
||||
*
|
||||
* This function will override a call to \c setEGLError, and vice-versa.
|
||||
*
|
||||
* \param vendor The vendor library of the current EGL function call.
|
||||
* \return EGL_TRUE on success, or EGL_FALSE on failure.
|
||||
*/
|
||||
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
|
||||
|
||||
/************************************************************************
|
||||
* These routines are used by vendor dispatch functions to look up
|
||||
* and add mappings between various objects and vendors.
|
||||
************************************************************************/
|
||||
|
||||
// As far as I can tell, all EGL functions that take a context or drawable
|
||||
// also take an EGLDisplay. So, we don't need any extra functions for
|
||||
// mapping a context/surface/whatever to a EGLDisplay or vendor.
|
||||
|
||||
/*!
|
||||
* Returns the EGL vendor for an EGLDisplay handle.
|
||||
*/
|
||||
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
|
||||
|
||||
/*!
|
||||
* Returns the EGL vendor for an EGLDeviceEXT handle.
|
||||
*/
|
||||
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
|
||||
|
||||
/**
|
||||
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
|
||||
* any function that returns an EGLDeviceEXT handle should call this.
|
||||
*
|
||||
* Supported since ABI version 1.2.
|
||||
*/
|
||||
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
|
||||
} __EGLapiExports;
|
||||
|
||||
/*****************************************************************************
|
||||
* API library imports *
|
||||
*****************************************************************************/
|
||||
|
||||
/*!
|
||||
* The enum values accepted by \c __EGLapiImports::getVendorString.
|
||||
*/
|
||||
enum
|
||||
{
|
||||
/*!
|
||||
* The set of platform extensions that the vendor supports.
|
||||
*
|
||||
* Platform extensions typically just add enums to the existing
|
||||
* eglGetPlatformDisplay function, so we don't need any additional logic
|
||||
* or special handling from libEGL itself.
|
||||
*
|
||||
* As a result, vendor libraries can add these extensions without having
|
||||
* to modify libEGL to match.
|
||||
*/
|
||||
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
|
||||
};
|
||||
|
||||
/*!
|
||||
* This structure stores required and optional vendor library callbacks.
|
||||
*/
|
||||
typedef struct __EGLapiImportsRec {
|
||||
/**
|
||||
* Creates an EGLDisplay. This function is used to handle both
|
||||
* \c eglGetDisplay and \c eglGetPlatformDisplay.
|
||||
*
|
||||
* For some platform types, libEGL may be able to identify a specific
|
||||
* vendor to use.
|
||||
*
|
||||
* If libEGL can't figure out which vendor to use on its own, then it will
|
||||
* go through the list of available vendor libraries and call this function
|
||||
* until one succeeds.
|
||||
*
|
||||
* If the application calls eglGetPlatformDisplay, then the parameters are
|
||||
* passed through from the application.
|
||||
*
|
||||
* If the application calls eglGetDisplay, and the native display handle is
|
||||
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
|
||||
* type, and will pass that platform to the vendor library.
|
||||
*
|
||||
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
|
||||
* libEGL will call into each vendor library with \p platform set to
|
||||
* \c EGL_NONE. The vendor library can then select a default display to
|
||||
* return.
|
||||
*
|
||||
* In all cases, if the vendor library can't find a matching EGLDisplay,
|
||||
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
|
||||
* through the vendor's \c eglGetError function.
|
||||
*/
|
||||
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
|
||||
const EGLAttrib *attrib_list);
|
||||
|
||||
/*!
|
||||
* Checks if the vendor library supports a given client API (that is, the
|
||||
* API value passed to \c eglBindAPI).
|
||||
*/
|
||||
EGLBoolean (* getSupportsAPI) (EGLenum api);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Returns a string from the vendor library. This is used for
|
||||
* anything that isn't available from eglQueryString.
|
||||
*/
|
||||
const char * (* getVendorString) (int name);
|
||||
|
||||
/*
|
||||
* NOTE: libEGL will load all of the vendor libraries up front, so it
|
||||
* handles extension functions slightly differently than EGL.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* This retrieves the pointer to the real EGL or core GL function.
|
||||
*
|
||||
* \param procName The name of the function.
|
||||
* \return A pointer to a function, or \c NULL if the vendor does not
|
||||
* support the function.
|
||||
*/
|
||||
void * (* getProcAddress) (const char *procName);
|
||||
|
||||
/*!
|
||||
* This retrieves vendor-neutral functions which use the
|
||||
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
|
||||
*
|
||||
* A vendor library must provide a dispatch function for all EGL display
|
||||
* extension functions that it supports.
|
||||
*
|
||||
* Client extension functions cannot be dispatched based on an EGDisplay,
|
||||
* so they must be handled in libEGL itself.
|
||||
*
|
||||
* \param procName The name of the function.
|
||||
* \return A pointer to a function, or \c NULL if the vendor does not
|
||||
* support the function or \p procName is not a EGL display function.
|
||||
*/
|
||||
void * (*getDispatchAddress) (const char *procName);
|
||||
|
||||
/*!
|
||||
* This notifies the vendor library which dispatch table index is
|
||||
* assigned to a particular EGL extension function.
|
||||
*/
|
||||
void (*setDispatchIndex) (const char *procName, int index);
|
||||
|
||||
/*
|
||||
* The vendor library may use the isPatchSupported, initiatePatch,
|
||||
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
|
||||
* entrypoints at make current time, provided no other contexts are current
|
||||
* and the TLS model supports this functionality. This is a performance
|
||||
* optimization that may not be available at runtime; the vendor library
|
||||
* must not depend on this functionality for correctness.
|
||||
*
|
||||
* To use this optimization, the vendor library must provide at least the
|
||||
* isPatchSupported and initiatePatch entrypoints.
|
||||
*
|
||||
* Note that these functions are identical to the ones used in GLX.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Checks to see if the vendor library supports patching the
|
||||
* given stub type and size.
|
||||
*
|
||||
* \param type The type of entrypoints. This will be a one of the
|
||||
* __GLDISPATCH_STUB_* values.
|
||||
* \param stubSize The maximum size of the stub that the vendor library can
|
||||
* write, in bytes.
|
||||
* \param lookupStubOffset A callback into libglvnd to look up the address
|
||||
* of each entrypoint.
|
||||
*/
|
||||
GLboolean (* isPatchSupported)(int type, int stubSize);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
|
||||
* top-level entrypoints.
|
||||
*
|
||||
* The vendor library should use the \p lookupStubOffset callback to find
|
||||
* the addresses of each entrypoint.
|
||||
*
|
||||
* This function may be called more than once to patch multiple sets of
|
||||
* entrypoints. For example, depending on how they're built, libOpenGL.so
|
||||
* or libGL.so may have their own entrypoints that are separate functions
|
||||
* from the ones in libGLdispatch.
|
||||
*
|
||||
* Note that during this call is the only time that the entrypoints can be
|
||||
* modified. After the call to \c initiatePatch returns, the vendor library
|
||||
* should treat the entrypoints as read-only.
|
||||
*
|
||||
* \param type The type of entrypoints. This will be a one of the
|
||||
* __GLDISPATCH_STUB_* values.
|
||||
* \param stubSize The maximum size of the stub that the vendor library can
|
||||
* write, in bytes.
|
||||
* \param lookupStubOffset A callback into libglvnd to look up the address
|
||||
* of each entrypoint.
|
||||
*
|
||||
* \return GL_TRUE if the vendor library supports patching with this type
|
||||
* and size.
|
||||
*/
|
||||
GLboolean (*initiatePatch)(int type,
|
||||
int stubSize,
|
||||
DispatchPatchLookupStubOffset lookupStubOffset);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
|
||||
* longer owns the top-level entrypoints.
|
||||
*
|
||||
* Libglvnd will take care of the restoring the entrypoints back to their
|
||||
* original state. The vendor library must not try to modify them.
|
||||
*/
|
||||
void (*releasePatch)(void);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
|
||||
* This callback allows vendor libraries to perform any per-thread
|
||||
* initialization.
|
||||
*
|
||||
* This is basically a workaround for broken applications. A lot of apps
|
||||
* will make one or more invalid GLX/EGL calls on a thread (often including
|
||||
* a MakeCurrent with invalid parameters), and then will try to call an
|
||||
* OpenGL function.
|
||||
*
|
||||
* A non-libglvnd-based driver would be able to initialize any thread state
|
||||
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
|
||||
* past libGLX.
|
||||
*
|
||||
* This function is optional. If it's \c NULL, then libGLdispatch will
|
||||
* simply ignore it.
|
||||
*
|
||||
* \note This function may be called concurrently from multiple threads.
|
||||
*/
|
||||
void (*patchThreadAttach)(void);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Tries to determine the platform type for a native display.
|
||||
*
|
||||
* If the vendor library provides this function, then libglvnd will call it
|
||||
* to determine which platform to use for a native display handle in
|
||||
* eglGetDisplay.
|
||||
*
|
||||
* If no vendor library identifies the platform, then libglvnd will fall
|
||||
* back to its own platform detection logic.
|
||||
*
|
||||
* Libglvnd can call this function for any native display handle except
|
||||
* \c EGL_DEFAULT_DISPLAY.
|
||||
*
|
||||
* No matter what the value of \p native_display, the vendor library must
|
||||
* not crash, and must not return a false match. If the vendor library
|
||||
* can't identify the display, then it must return \c EGL_NONE.
|
||||
*
|
||||
* In particular, that means that a vendor library must not return any sort
|
||||
* of default or fallback platform.
|
||||
*
|
||||
* \param native_display The native display handle passed to eglGetDisplay.
|
||||
* \return Either a platform type enum or EGL_NONE.
|
||||
*/
|
||||
EGLenum (* findNativeDisplayPlatform) (void *native_display);
|
||||
} __EGLapiImports;
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
|
||||
|
||||
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
|
||||
__EGLvendorInfo *vendor, __EGLapiImports *imports);
|
||||
|
||||
/*!
|
||||
* Vendor libraries must export a function called __egl_Main() with the
|
||||
* following prototype.
|
||||
*
|
||||
* This function also performs a handshake based on the ABI version number.
|
||||
* Vendor libraries can optionally use the version number to support older
|
||||
* versions of the ABI.
|
||||
*
|
||||
* \param[in] version The ABI version. The upper 16 bits contains the major version
|
||||
* number, and the lower 16 bits contains the minor version number.
|
||||
*
|
||||
* \param[in] exports The table of functions provided by libEGL. This pointer will
|
||||
* remain valid for as long as the vendor is loaded.
|
||||
*
|
||||
* \param[in] vendor The opaque pointer used to identify this vendor library. This
|
||||
* may be used in future versions to provide additional per-vendor information.
|
||||
*
|
||||
* \param[out] imports The function table that the vendor library should fill
|
||||
* in. The vendor library must assign every non-optional function in the
|
||||
* struct.
|
||||
*
|
||||
* \return True on success. If the vendor library does not support the
|
||||
* requested ABI version or if some other error occurs, then it should return
|
||||
* False.
|
||||
*/
|
||||
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
|
||||
__EGLvendorInfo *vendor, __EGLapiImports *imports);
|
||||
|
||||
/*!
|
||||
* @}
|
||||
*/
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __LIB_EGL_ABI_H */
|
||||
+171
-1
@@ -29,6 +29,8 @@ option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-pr
|
||||
# only decides whether the shims are compiled in.
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
|
||||
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
|
||||
option(MOBILEGL_BUILD_EXTMEM_PROBE "Package tools/spikes/extmem_probe as libMobileGLExtmemProbe.so for the trace APK's exec hook (P11 A3, Android only)" OFF)
|
||||
option(MOBILEGL_EXTMEM_PROBE_AS_SERVER "Spike only (P11 B2 step 1): also package the probe as libMobileGLServer.so, the real server as libMobileGLServerReal.so" OFF)
|
||||
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
|
||||
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
||||
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
||||
@@ -401,7 +403,10 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
|
||||
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
|
||||
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
|
||||
|
||||
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
|
||||
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
|
||||
@@ -432,10 +437,14 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Backend/Init.cpp
|
||||
MobileGL/MG_Backend/BackendObject.cpp
|
||||
|
||||
MobileGL/MG_Backend/HostFrameBridge.cpp
|
||||
MobileGL/MG_Backend/GbmFrameChannel.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||
@@ -607,9 +616,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/SocketTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/ControlInbox.cpp
|
||||
# P9 W3: the server's log forward - a queue and a sender thread, graded by severity
|
||||
# (<= WARN lossy, >= ERROR lossless and paced), so no logging thread writes the socket.
|
||||
MobileGL/MG_Remote/Transport/LogForward.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
|
||||
# P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by
|
||||
# both roles (so SERVER-partition code in the link ratchet, not the client's tracker).
|
||||
MobileGL/MG_Remote/Transport/AdoptTier.cpp
|
||||
# P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles).
|
||||
MobileGL/MG_Remote/Transport/AdoptT0.cpp
|
||||
# ---- P5: the three new directories ------------------------------
|
||||
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
|
||||
# var-tails in and out of SEG_STAGE. [w1]
|
||||
@@ -630,6 +647,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
MobileGL/MG_Remote/Client/DmaBufImport.cpp
|
||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
|
||||
@@ -650,6 +668,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
|
||||
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
|
||||
MobileGL/MG_Remote/Server/PairAcceptor.cpp
|
||||
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
|
||||
MobileGL/MG_Remote/Server/AdoptInbox.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSpawn.cpp
|
||||
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
|
||||
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
|
||||
@@ -682,7 +705,7 @@ if (ANDROID)
|
||||
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||
)
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
|
||||
# P12 (on-screen server window): the APK's MobileGLDisplayActivity - the in-process
|
||||
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
|
||||
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
|
||||
list(APPEND SOURCE_FILES
|
||||
@@ -763,6 +786,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# 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)
|
||||
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
|
||||
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
@@ -800,6 +826,17 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
# A LINUX BUILD COMPILES THE DirectVulkan BACKEND BUT HAS NO VULKAN LOADER TO LINK AGAINST, so the
|
||||
# resulting shared library carries undefined vk* symbols - and that is not theoretical: glvnd dlopens
|
||||
# this file as the EGL vendor with RTLD_NOW, so the load fails outright with "undefined symbol:
|
||||
# vkGetQueryPoolResults" (measured on the phone's container). Android links the loader already; on
|
||||
# Linux the caller passes the runtime's own libvulkan.so.1, and leaving the variable empty keeps the
|
||||
# previous behaviour for every build that has no Vulkan runtime to point at.
|
||||
set(MOBILEGL_VULKAN_LOADER "" CACHE FILEPATH "Vulkan loader (libvulkan.so.1) to link into the GL library")
|
||||
if (UNIX AND NOT ANDROID AND MOBILEGL_VULKAN_LOADER)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${MOBILEGL_VULKAN_LOADER})
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
@@ -866,6 +903,68 @@ if(NOT ANDROID)
|
||||
)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MobileGLGbm: the GBM drop-in the container's compositor loads.
|
||||
#
|
||||
# kwin - the Wayland compositor in the container half of the split - allocates the
|
||||
# buffers it renders into and scans out through libgbm, and the dma-buf fds it gets
|
||||
# back are what the Anland display producer ships to the phone. Mesa's libgbm cannot
|
||||
# serve that: its buffers belong to the host GPU, and the phone would receive a copy
|
||||
# through a path that was never designed to carry one. This target is the library
|
||||
# that is loaded in its place, under the name a loader looks for.
|
||||
#
|
||||
# It is deliberately NOT part of libMobileGL.so. The compositor needs one file named
|
||||
# libgbm.so that it loads instead of Mesa's, and folding this code into the GL
|
||||
# library would put a second GL frontend, a second Vulkan instance and a second
|
||||
# glslang in the compositor's address space to serve an API that shares none of them.
|
||||
#
|
||||
# The include paths below are borrowed rather than linked. Includes.h - which this
|
||||
# library includes for the MobileGL types and the MGLOG macros - pulls in the GL,
|
||||
# EGL, Vulkan, glslang, SPIRV-Cross and xxHash HEADERS transitively, and headers are
|
||||
# all this library wants from them: linking any of those static libraries into a
|
||||
# libgbm would embed a copy of each in a process that already has one.
|
||||
#
|
||||
# -Bsymbolic-functions, for the same reason the main target carries it: the point of
|
||||
# this library is to REPLACE a libgbm that may already be loaded in the process, so
|
||||
# every internal reference has to bind to this file's own definitions rather than to
|
||||
# whatever else the process has under the same names.
|
||||
# ---------------------------------------------------------------------------
|
||||
add_library(MobileGLGbm SHARED
|
||||
MobileGL/MG_Gbm/GbmDevice.cpp
|
||||
# The display host's socket protocol, kept in its own translation unit so that the
|
||||
# allocation paths never have to know how a frame arrives - only that one did.
|
||||
MobileGL/MG_Gbm/GbmHostChannel.cpp
|
||||
MobileGL/MG_Gbm/GbmApi.cpp
|
||||
# MGLOG_I/W/E expand to a call to MobileGL::MG_Util::Debug::Log, and this is the
|
||||
# one translation unit that defines it. It is compiled in rather than linked from
|
||||
# libMobileGL.so because this library has to load into a compositor that has no
|
||||
# libMobileGL in it at all - and because a libgbm with an undefined symbol is a
|
||||
# library that fails on the first diagnostic rather than at a link step somebody
|
||||
# could have read.
|
||||
MobileGL/MG_Util/Debug/Log.cpp
|
||||
)
|
||||
|
||||
# The vendored gbm.h first: this library implements THAT declaration set, and a
|
||||
# system <gbm.h> from another Mesa version on the include path ahead of it would
|
||||
# change the ABI under the implementations in GbmApi.cpp.
|
||||
target_include_directories(MobileGLGbm PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/gbm/include
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/glslang
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Cross
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/xxHash
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
|
||||
)
|
||||
|
||||
set_target_properties(MobileGLGbm PROPERTIES
|
||||
OUTPUT_NAME gbm
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_options(MobileGLGbm PRIVATE "LINKER:-Bsymbolic-functions")
|
||||
endif()
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
|
||||
@@ -873,6 +972,19 @@ if (TRACY_ENABLE)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
|
||||
endif ()
|
||||
|
||||
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
|
||||
#
|
||||
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
|
||||
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
|
||||
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
|
||||
# With default visibility every reference inside this library resolves against the whole process,
|
||||
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
|
||||
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
|
||||
# the two visibilities do not mix; this flag is that promise made by the linker instead.
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
android
|
||||
@@ -982,6 +1094,12 @@ if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
# The replay runner also builds for Android: an adb-shell executable that can
|
||||
# dial a render server on the device (unix or TCP) without any Activity.
|
||||
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P0 spike A: the Android delivery chain for a second native executable.
|
||||
#
|
||||
@@ -1090,3 +1208,55 @@ if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
|
||||
# P11 A3 (open question 3): the spike-B external-memory probe, exec'd from the trace
|
||||
# app's own process by the spike hook (`--es mobilegl_spike_spawn
|
||||
# libMobileGLExtmemProbe.so`), so its rows answer for the untrusted_app domain. The
|
||||
# same PIE-executable-named-like-a-library shape as MobileGLServerSpike above; it
|
||||
# links nothing from MobileGL.
|
||||
add_executable(MobileGLExtmemProbe
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
|
||||
set_target_properties(MobileGLExtmemProbe PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLExtmemProbe"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLExtmemProbe PRIVATE -fPIE)
|
||||
target_link_options(MobileGLExtmemProbe PRIVATE -pie)
|
||||
target_link_libraries(MobileGLExtmemProbe PRIVATE vulkan EGL GLESv3 android log)
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLExtmemProbe PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
if (MOBILEGL_EXTMEM_PROBE_AS_SERVER)
|
||||
# P11 B2 step 1, spike APKs only: MobileGLServerService execs `libMobileGLServer.so
|
||||
# <endpoint> --serve` and B1's broker connects to whatever that listens on, so the probe
|
||||
# takes the server's file name and answers "does a client's AHardwareBuffer cross this
|
||||
# route into the server app" on the real route (tools/spikes/extmem_probe, route mode).
|
||||
if (TARGET MobileGLServer)
|
||||
set_target_properties(MobileGLServer PROPERTIES OUTPUT_NAME "MobileGLServerReal")
|
||||
endif()
|
||||
add_executable(MobileGLExtmemProbeAsServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
|
||||
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServer"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLExtmemProbeAsServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLExtmemProbeAsServer PRIVATE -pie)
|
||||
target_link_libraries(MobileGLExtmemProbeAsServer PRIVATE vulkan EGL GLESv3 android log)
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The GBM library carries the tree's logger, which reaches Android's own log. The main target gets
|
||||
# these libraries in its own Android block; a library built from the same sources needs them for the
|
||||
# same reason, and without them it is the LINK that fails rather than anything in the code.
|
||||
if (ANDROID)
|
||||
target_link_libraries(MobileGLGbm PRIVATE log android)
|
||||
endif()
|
||||
|
||||
+58
-8
@@ -132,6 +132,18 @@ namespace MobileGL::MG_Config {
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
|
||||
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
|
||||
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
|
||||
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
|
||||
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
|
||||
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES: how many consecutive frames (strictly, BeginFrame
|
||||
// epochs) a frame slot's descriptor pools must stay under a quarter full before Magma
|
||||
// hands the slot's whole set cache back and lets it re-grow. 0 = never trim. A loading
|
||||
// frame otherwise keeps its burst of sets (rd12: ~15k, where steady frames use a few dozen)
|
||||
// for the life of the process. See UniformManager::TrimFrameDescriptorPools.
|
||||
Uint32 MagmaDescriptorTrimFrames = 120;
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
@@ -202,9 +214,12 @@ namespace MobileGL::MG_Config {
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
|
||||
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
|
||||
// tore the frames still reading the old bytes) - still no driver-mediated write
|
||||
// into the busy mutable store, which on this class of Mali driver stalls the
|
||||
// thread or ghost-copies the whole arena, and the arena stops costing its size
|
||||
// again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
@@ -484,7 +499,11 @@ namespace MobileGL::MG_Config {
|
||||
enum class IpcSurface : Uint8 {
|
||||
Offscreen = 0, // a window surface is the client's own window, as before (the default)
|
||||
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
|
||||
Host = 2, // a window surface draws into frames the DISPLAY HOST owns; the host presents
|
||||
};
|
||||
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
|
||||
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
|
||||
inline constexpr Uint32 kAdoptTierUnset = 3;
|
||||
struct IpcTable {
|
||||
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
|
||||
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
|
||||
@@ -595,11 +614,33 @@ namespace MobileGL::MG_Config {
|
||||
// flag keyed on wantReply would disable the very mechanism it exists to enable
|
||||
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
|
||||
Uint32 CreateWindow = 10;
|
||||
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
|
||||
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
|
||||
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
|
||||
// negative control has a spelling the day P11 writes it.
|
||||
Uint32 AdoptTier = 2;
|
||||
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels
|
||||
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
|
||||
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
|
||||
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
|
||||
// nothing, and marks the buffer GPU-written. The 1 arm is the A/B control and the
|
||||
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
|
||||
// wait per read.
|
||||
Uint32 PboReadbackSync = 0;
|
||||
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
|
||||
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
|
||||
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
|
||||
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
|
||||
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
|
||||
// arm and the red-once of the no-reply gate (FencePollScenario).
|
||||
Uint32 PollEscalate = 64;
|
||||
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
|
||||
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
|
||||
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
|
||||
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
|
||||
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
|
||||
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
|
||||
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
|
||||
Uint32 AdoptTier = kAdoptTierUnset;
|
||||
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
|
||||
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
|
||||
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
|
||||
Uint32 AllowAdoptT0 = 1;
|
||||
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
|
||||
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
|
||||
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
|
||||
@@ -689,6 +730,15 @@ namespace MobileGL::MG_Config {
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
|
||||
// P12: the other on-screen shape, and the mirror image of the one above. The display host
|
||||
// (the Android side that owns the screen) allocates the frames, offers each one over the
|
||||
// host frame bridge, and puts it on the glass when this side says it is drawn - so a
|
||||
// client that asks for this has no window of its own either, and the server takes the
|
||||
// frames rather than making a window.
|
||||
inline Bool HostFrameWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Host;
|
||||
}
|
||||
#else
|
||||
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||
|
||||
+29
-13
@@ -230,6 +230,9 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.MagmaMaxDrawsPerCommandBuffer =
|
||||
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
|
||||
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
@@ -421,14 +424,21 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
|
||||
// StreamLink's crossing log are the checks on this number.
|
||||
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
|
||||
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
|
||||
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
|
||||
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
|
||||
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
|
||||
// The verify harness compares the pushed block against the applier per verb; a
|
||||
// batched queue lets the comparer read a supplied field mid-apply, which is a
|
||||
// torn read rather than a divergence. The batch is therefore off whenever the
|
||||
// shadow comparer is armed.
|
||||
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
|
||||
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
|
||||
// point of use, which is where the "P11" in the message belongs.
|
||||
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
|
||||
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
|
||||
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
|
||||
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
|
||||
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
|
||||
// P11 B2: the server's own T0 allow switch (Config.h).
|
||||
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
|
||||
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
|
||||
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
|
||||
@@ -484,8 +494,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
}
|
||||
}
|
||||
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
|
||||
// the semantics). An unknown value is named and read as the default, like every knob here.
|
||||
// P12 (on-screen): MOBILEGL_IPC_SURFACE = offscreen | server | host (Config.h has the
|
||||
// semantics). An unknown value is named and read as the default, like every knob here.
|
||||
{
|
||||
String surface;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
|
||||
@@ -495,9 +505,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
} else if (surface == "server") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Server;
|
||||
} else if (surface == "host") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Host;
|
||||
} else {
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
|
||||
"offscreen|server, using offscreen",
|
||||
"offscreen|server|host, using offscreen",
|
||||
surface.c_str());
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
}
|
||||
@@ -505,12 +517,14 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// parses this before RunSession sets its own transport, and has no client half anyway.)
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
|
||||
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
|
||||
if (ipc.Surface != MG_Config::IpcSurface::Offscreen && !serverProcess &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// Said, not silently honoured: there is no remote server here to own a window.
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
|
||||
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
|
||||
"and this run has none. Window surfaces stay the client's own");
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=%s is IGNORED - it asks for an on-screen "
|
||||
"surface (the server's own window, or the frames the display host owns) and "
|
||||
"this run has no remote server to provide one: MOBILEGL_TRANSPORT is not "
|
||||
"spawn. Window surfaces stay the client's own",
|
||||
ipc.Surface == MG_Config::IpcSurface::Server ? "server" : "host");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,11 +536,13 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
|
||||
// log that does not name it makes every run ambiguous about its own configuration.
|
||||
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
|
||||
"persistent-block=%uKiB create-window=%u "
|
||||
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
|
||||
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
|
||||
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
|
||||
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
|
||||
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.AdoptTier,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
|
||||
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
|
||||
ipc.AllowAdoptT0,
|
||||
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
|
||||
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
|
||||
|
||||
+9
-2
@@ -8,6 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
// ============== Platform-specific definitions and macros ============== //
|
||||
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
|
||||
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
|
||||
@@ -34,6 +36,11 @@
|
||||
#ifdef _WIN32
|
||||
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
|
||||
// points with. The one place where that is not enough - a vendor library handing glvnd its own
|
||||
// entry points, where a reference resolving against the process instead of this library would be a
|
||||
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
|
||||
// here, because a second visibility on the same declaration does not compile.
|
||||
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
@@ -105,8 +112,8 @@
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
|
||||
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
|
||||
fprintf(stderr, "Assertion failed" __VA_OPT__(": ") "%s", "" __VA_ARGS__); \
|
||||
fprintf(stderr, " at %s:%d (%s)\n", __FILE__, __LINE__, __func__); \
|
||||
TRAP; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -172,11 +172,15 @@ namespace MobileGL::MG_Backend {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
|
||||
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
|
||||
return false;
|
||||
}
|
||||
|
||||
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
|
||||
// refused a caller for a reason that is not there: the surface and context tables are keyed
|
||||
// by the display each was created on, every operation carries its own display handle, and
|
||||
// this field has no reader anywhere - it was the whole of the rule.
|
||||
//
|
||||
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
|
||||
// probing another, so the second eglInitialize is the one that matters, and a refusal there
|
||||
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
|
||||
// and the real display having failed, with nothing in between that says which was refused.
|
||||
m_eglDisplay = dpy;
|
||||
m_eglDisplayInitialized = true;
|
||||
if (major) {
|
||||
@@ -209,6 +213,28 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The frame itself is the backend's business (InitHostFrameSurface); what happens here is
|
||||
// the registration every surface kind shares, so the swap path and the state mirrors see
|
||||
// a host-framed surface exactly like any other.
|
||||
Bool BackendObject::CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglDisplayInitialized) {
|
||||
MGLOG_E("CreateEGLHostFrameSurface failed: EGL display is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("CreateEGLHostFrameSurface failed: invalid EGLSurface");
|
||||
return false;
|
||||
}
|
||||
auto& state = m_eglSurfaces[surface];
|
||||
state = EGLSurfaceState{
|
||||
.Kind = SurfaceKind::HostFrame,
|
||||
.Width = static_cast<EGLint>(std::max<Uint32>(width, 1)),
|
||||
.Height = static_cast<EGLint>(std::max<Uint32>(height, 1)),
|
||||
};
|
||||
return ActivateEGLSurface(surface);
|
||||
}
|
||||
|
||||
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
|
||||
@@ -290,6 +316,11 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
|
||||
return false;
|
||||
}
|
||||
} else if (surfaceState->Kind == SurfaceKind::HostFrame) {
|
||||
if (!InitHostFrameSurface(surfaceState->Width, surfaceState->Height)) {
|
||||
MGLOG_E("ActivateEGLSurface failed: backend InitHostFrameSurface failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
|
||||
return false;
|
||||
@@ -315,6 +346,29 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
|
||||
return false;
|
||||
}
|
||||
// EGL_KHR_surfaceless_context. A context whose draw and read surfaces are both absent has
|
||||
// no framebuffer of its own to be bound to: the caller draws into framebuffers it binds
|
||||
// itself, and this side only has to have its own GL context current on this thread for the
|
||||
// records that follow. Everything below this point describes a surface, so none of it
|
||||
// applies - and what is recorded is exactly what was asked for, so a later swap on a
|
||||
// surface that does not exist is never made to look like one that does.
|
||||
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
|
||||
if (!m_backendCapabilitiesInitialized) {
|
||||
if (!InitCapabilities()) {
|
||||
MGLOG_E("MakeEGLCurrent failed: InitCapabilities failed");
|
||||
return false;
|
||||
}
|
||||
m_backendCapabilitiesInitialized = true;
|
||||
}
|
||||
ReleaseEGLCurrentThread(threadKey);
|
||||
m_eglCurrentThreads[threadKey] = EGLCurrentState{
|
||||
.Display = dpy,
|
||||
.DrawSurface = EGL_NO_SURFACE,
|
||||
.ReadSurface = EGL_NO_SURFACE,
|
||||
.Context = ctx,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
if (draw != read || !ActivateEGLSurface(draw)) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
@@ -387,6 +441,12 @@ namespace MobileGL::MG_Backend {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A host-framed surface has no swapchain: presenting it means telling the host the frame
|
||||
// is drawn, because the host is what puts it on the glass.
|
||||
if (m_eglSurfaceKind == SurfaceKind::HostFrame) {
|
||||
return PresentHostFrame();
|
||||
}
|
||||
|
||||
const auto& backendFunctions = GetBackendFunctions();
|
||||
if (!backendFunctions.Present) {
|
||||
MGLOG_E("SwapEGLBuffers failed: backend Present function is null");
|
||||
@@ -404,6 +464,27 @@ namespace MobileGL::MG_Backend {
|
||||
}
|
||||
}
|
||||
|
||||
// A backend that has no host-framed surfaces says so by name: the server may ask for one
|
||||
// (MOBILEGL_IPC_SURFACE=host) against a build that cannot serve it, and that has to be a
|
||||
// refusal with a reason rather than a surface that draws nowhere.
|
||||
Bool BackendObject::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
(void)width;
|
||||
(void)height;
|
||||
MGLOG_E("InitHostFrameSurface: this backend has no host-framed surfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject::PresentHostFrame() {
|
||||
MGLOG_E("PresentHostFrame: this backend has no host-framed surfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject::PresentHostFrameIfCurrent() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglSurfaceInitialized || m_eglSurfaceKind != SurfaceKind::HostFrame) return true;
|
||||
return PresentHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
return false;
|
||||
|
||||
@@ -595,6 +595,18 @@ namespace MobileGL {
|
||||
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
|
||||
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
||||
// The host owns the screen: this side never gets a window of the client's to draw
|
||||
// into, so the surface it draws into is a frame the host allocated and offered over
|
||||
// the host frame bridge. Each swap tells the host the frame is done and takes the
|
||||
// next; presentation is the host's, not a swapchain's.
|
||||
virtual Bool CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
virtual Bool InitHostFrameSurface(EGLint width, EGLint height);
|
||||
virtual Bool PresentHostFrame();
|
||||
// The present RECORD's half of a host-framed swap. A split client's swap reaches the
|
||||
// server as a present record (ServerVerbSink::OnPresent), never as SwapEGLBuffers, so
|
||||
// without this the host was never told a frame was drawn. True when the current
|
||||
// surface is not host-framed: there is nothing to present then.
|
||||
Bool PresentHostFrameIfCurrent();
|
||||
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
// Forwards the app-requested eglSwapInterval to the backend's native
|
||||
@@ -616,7 +628,11 @@ namespace MobileGL {
|
||||
enum class SurfaceKind {
|
||||
None,
|
||||
Window,
|
||||
Pbuffer
|
||||
Pbuffer,
|
||||
// A surface whose drawing target is a frame the display host owns. There is no
|
||||
// swapchain behind it: the host puts the frame on the glass when this side says
|
||||
// it is drawn, which is why presenting one is its own call and not a Present.
|
||||
HostFrame
|
||||
};
|
||||
|
||||
struct EGLCurrentState {
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
// The role's own backend is resolved the way Utils.cpp (C6 / ID-52) resolves it; that needs this
|
||||
// header, and it has to be included at file scope - a header included inside this file's namespace
|
||||
// would nest its own namespace under it.
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
@@ -1028,6 +1034,241 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
namespace {
|
||||
// The host may still be starting when the client asks for its surface: it is a separate
|
||||
// app that has to come up and allocate its pool. Waiting is better than refusing a
|
||||
// surface a client is entitled to, and the wait is answered by name if it expires.
|
||||
constexpr Int kHostFrameWaitMs = 30000;
|
||||
}
|
||||
|
||||
Bool TakeHostFrameForExternalTexture(Uint width, Uint height) {
|
||||
// THIS ROLE'S OWN BACKEND, resolved exactly the way ActiveBackendFormatCaps (Utils.cpp,
|
||||
// C6 / ID-52) resolves it: under a live split session the SERVER's private backend owns the
|
||||
// context on the apply thread, and pActiveBackendObject is the CLIENT's mirror there.
|
||||
MG_Backend::BackendObject* backend = nullptr;
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
} else {
|
||||
backend = MG_Backend::pActiveBackendObject.get();
|
||||
}
|
||||
if (backend == nullptr) return false;
|
||||
return backend->InitHostFrameSurface(static_cast<EGLint>(width), static_cast<EGLint>(height));
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
// The EGL context comes from the same place a pbuffer session's does. What changes is not
|
||||
// how this side draws but what it draws INTO: a frame the display host allocated, bound as
|
||||
// this context's framebuffer. The pbuffer stays as the draw target for the moment before
|
||||
// the first frame arrives, which is the only moment it is used.
|
||||
if (!DirectGLES::InitPbufferSurface(width, height)) {
|
||||
MGLOG_E("InitHostFrameSurface: the context the host frames are drawn in did not come up");
|
||||
return false;
|
||||
}
|
||||
if (!m_hostFrameBridgeOpen) {
|
||||
String why;
|
||||
if (!m_hostFrameBridge.Open(MG_Backend::kHostFrameSocket, kHostFrameWaitMs, why)) {
|
||||
MGLOG_E("InitHostFrameSurface: no display host: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
m_hostFrameBridgeOpen = true;
|
||||
}
|
||||
return TakeHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::TakeHostFrame() {
|
||||
String why;
|
||||
struct AHardwareBuffer* frame = nullptr;
|
||||
MG_Backend::HostFrameOffer offer{};
|
||||
if (!m_hostFrameBridge.Acquire(&frame, offer, why)) {
|
||||
MGLOG_E("TakeHostFrame: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
HostFrameTarget target;
|
||||
if (!HostFrameTargetCreate(frame, target, why)) {
|
||||
MGLOG_E("TakeHostFrame: the host frame %ux%u did not become a draw target: %s", offer.Width,
|
||||
offer.Height, why.c_str());
|
||||
AHardwareBuffer_release(frame);
|
||||
return false;
|
||||
}
|
||||
DestroyHostFrame();
|
||||
m_hostFrameTarget = target;
|
||||
m_hostFrameBuffer = frame;
|
||||
m_hostFrameOffer = offer;
|
||||
// The canvas is made at the first frame's size and kept; a failure leaves the frame itself
|
||||
// as the draw target, which is how this path behaved before the canvas existed.
|
||||
if (!EnsureHostFrameCanvas(target.Width, target.Height))
|
||||
MGLOG_E_ONCE("TakeHostFrame: no canvas in front of the host frames; drawing into the frame itself");
|
||||
const HostFrameTarget& drawTarget = HostFrameDrawTarget();
|
||||
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
|
||||
// resolve to this target, and that resolution is what this global is.
|
||||
g_hostFrameFramebufferId = drawTarget.Framebuffer;
|
||||
// Published together with the framebuffer, because they describe the same target: the STORAGE
|
||||
// an attachment is bound to when the client's texture has none of its own, and the SIZE that
|
||||
// decides whether binding it is right at all.
|
||||
g_hostFrameRenderbuffer = drawTarget.Renderbuffer;
|
||||
g_hostFrameWidth = drawTarget.Width;
|
||||
g_hostFrameHeight = drawTarget.Height;
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
HostFrameTargetBind(drawTarget);
|
||||
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
|
||||
// over there renders into THESE frames - it runs this project's own GBM, whose
|
||||
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
|
||||
// every frame taken here is useless to it until its descriptor has crossed. Open() binds
|
||||
// the listening socket a container dials, and the IsUp() test keeps the ordinary frame
|
||||
// down to an integer compare: the socket outlives every container and every connection,
|
||||
// while OfferFrame accepts whoever dialled since the last frame and opens the socket
|
||||
// itself when the first frame arrives before any container has.
|
||||
//
|
||||
// NEITHER ANSWER IS EXAMINED, and that is the channel's contract rather than an
|
||||
// oversight: no container running, a container that has died, or one that has taken the
|
||||
// descriptor and stopped reading must all leave this frame drawn and presented exactly as
|
||||
// it would have been without a channel - the descriptor is a copy of the pixels' route,
|
||||
// not a step in producing them.
|
||||
if (!m_gbmFrameChannel.IsUp()) m_gbmFrameChannel.Open();
|
||||
(void)m_gbmFrameChannel.OfferFrame(frame, offer);
|
||||
MGLOG_I("host frame %u is the draw target: %ux%u", offer.Index, target.Width, target.Height);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::DestroyHostFrame() {
|
||||
// The canvas outlives the frame, so with one the globals keep naming it: the attachments
|
||||
// made over it stay valid between this frame being handed back and the next one arriving.
|
||||
g_hostFrameFramebufferId = m_hostFrameCanvas.Framebuffer;
|
||||
g_hostFrameRenderbuffer = m_hostFrameCanvas.Renderbuffer;
|
||||
g_hostFrameWidth = m_hostFrameCanvas.Width;
|
||||
g_hostFrameHeight = m_hostFrameCanvas.Height;
|
||||
HostFrameTargetDestroy(m_hostFrameTarget);
|
||||
if (m_hostFrameBuffer != nullptr) {
|
||||
AHardwareBuffer_release(m_hostFrameBuffer);
|
||||
m_hostFrameBuffer = nullptr;
|
||||
}
|
||||
// AND NOTHING IS SAID TO THE CHANNEL HERE, which is the fix rather than an omission. This
|
||||
// runs once per presented frame: it used to Close() the channel (unlinking the container's
|
||||
// socket node and dropping its connection sixty times a second - measured: connect()
|
||||
// answered EAGAIN(11) on every attempt) and it used to tell the channel to forget the frame,
|
||||
// which left the ONE window a compositor's first allocations arrive in - between this frame
|
||||
// being presented and the host handing the next one over, i.e. inside Complete() above -
|
||||
// with nothing to answer a dialer with. A compositor allocates its swapchain ONCE and keeps
|
||||
// those buffers for the life of the process, so a dialer that finds nothing here renders
|
||||
// its whole life into buffers nobody displays. The frame the channel remembers is the last
|
||||
// one the host handed over: the next offer replaces it and Close() releases it.
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::PresentHostFrame() {
|
||||
// When this returns, the pixels are complete: glFinish is the fence the host is told
|
||||
// about, and the host's own readback - which Complete reads - is what proves the drawing
|
||||
// landed where the screen will look.
|
||||
BlitHostFrameCanvas();
|
||||
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
|
||||
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
|
||||
// server's apply loop passes through the host channel - the swap it runs lands here for a
|
||||
// host-framed surface - and a release is the container's compositor saying it is done
|
||||
// reading a frame this side handed over. Poll() is a bare recvmsg on a non-blocking
|
||||
// socket: with no container connected it returns on its first test, and a container that
|
||||
// is between frames answers EAGAIN, so a pass costs nothing when there is nothing to
|
||||
// read. Nothing here is waited for and nothing is required to arrive; the count of
|
||||
// frames offered and not released is a number the channel keeps for the log, never a
|
||||
// frame the render path waits on.
|
||||
m_gbmFrameChannel.Poll();
|
||||
String why;
|
||||
if (!m_hostFrameBridge.Complete(m_hostFrameOffer, /*imported=*/true, /*drawn=*/true, /*fenceOk=*/true, why)) {
|
||||
MGLOG_E("PresentHostFrame: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
// The answer has been read, so the host has already put this frame on the glass: it is
|
||||
// this side's to drop, and the next one becomes the drawing target.
|
||||
DestroyHostFrame();
|
||||
return TakeHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint width, Uint height) {
|
||||
if (m_hostFrameCanvas.Framebuffer != 0) return true;
|
||||
if (width == 0 || height == 0 || !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glBlitFramebuffer ||
|
||||
!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glCheckFramebufferStatus)
|
||||
return false;
|
||||
// RAW binds, as HostFrameTargetCreate makes them: the caller invalidates the engine's
|
||||
// framebuffer shadow and binds through it right after.
|
||||
GLuint renderbuffer = 0;
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
// The frames are R8G8B8A8_UNORM (the display host allocates them so), and so is this.
|
||||
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height));
|
||||
GLuint framebuffer = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("host frame canvas %ux%u: framebuffer status 0x%04x", width, height, status);
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
return false;
|
||||
}
|
||||
m_hostFrameCanvas.Renderbuffer = renderbuffer;
|
||||
m_hostFrameCanvas.Framebuffer = framebuffer;
|
||||
m_hostFrameCanvas.Width = width;
|
||||
m_hostFrameCanvas.Height = height;
|
||||
MGLOG_I("host frame canvas: %ux%u as framebuffer %u (renderbuffer %u), copied into each frame at present",
|
||||
width, height, framebuffer, renderbuffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::BlitHostFrameCanvas() {
|
||||
if (m_hostFrameCanvas.Framebuffer == 0 || m_hostFrameTarget.Framebuffer == 0) return;
|
||||
// Whatever the compositor had bound is put back as it was, raw for raw, so the engine's
|
||||
// shadow (which never saw this) is still right afterwards. The scissor test and
|
||||
// rasterizer discard both apply to a blit, so they are lifted for it.
|
||||
GLint readBinding = 0;
|
||||
GLint drawBinding = 0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readBinding);
|
||||
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawBinding);
|
||||
const GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
|
||||
const GLboolean discard = g_GLESFuncs.glIsEnabled(GL_RASTERIZER_DISCARD);
|
||||
if (scissor) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
if (discard) g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_hostFrameCanvas.Framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_hostFrameTarget.Framebuffer);
|
||||
const Bool sameSize = m_hostFrameCanvas.Width == m_hostFrameTarget.Width &&
|
||||
m_hostFrameCanvas.Height == m_hostFrameTarget.Height;
|
||||
g_GLESFuncs.glBlitFramebuffer(0, 0, static_cast<GLint>(m_hostFrameCanvas.Width),
|
||||
static_cast<GLint>(m_hostFrameCanvas.Height), 0, 0,
|
||||
static_cast<GLint>(m_hostFrameTarget.Width),
|
||||
static_cast<GLint>(m_hostFrameTarget.Height), GL_COLOR_BUFFER_BIT,
|
||||
sameSize ? GL_NEAREST : GL_LINEAR);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readBinding));
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawBinding));
|
||||
if (scissor) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
||||
if (discard) g_GLESFuncs.glEnable(GL_RASTERIZER_DISCARD);
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
// The other arm's half: on a build with no host frames there is no frame to take, and the
|
||||
// attach site's ordinary path (a storage-less external texture, exactly as before) is the honest
|
||||
// behaviour there rather than a link error.
|
||||
Bool TakeHostFrameForExternalTexture(Uint, Uint) { return false; }
|
||||
|
||||
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
(void)width;
|
||||
(void)height;
|
||||
MGLOG_E("InitHostFrameSurface: host frames are a disaggregated Android path");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
|
||||
void BackendObject_DirectGLES::DestroyHostFrame() {}
|
||||
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint, Uint) { return false; }
|
||||
void BackendObject_DirectGLES::BlitHostFrameCanvas() {}
|
||||
|
||||
Bool BackendObject_DirectGLES::PresentHostFrame() {
|
||||
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
|
||||
return DirectGLES::InitPbufferSurface(width, height);
|
||||
}
|
||||
@@ -1052,13 +1293,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
|
||||
return false;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
return false;
|
||||
// EGL_KHR_surfaceless_context, WHICH IS THE SHAPE A COMPOSITOR ASKS FOR. Its context has no
|
||||
// draw surface of its own - every frame goes into a framebuffer it binds itself, and under
|
||||
// this project that framebuffer is one of the display host's frames - so the surface checks
|
||||
// below describe a surface that legitimately does not exist. What the server still needs is
|
||||
// its own GL context current on this thread for the records that follow, and that is what
|
||||
// the native bind underneath them does.
|
||||
const Bool surfaceless = draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT;
|
||||
if (surfaceless && !m_eglSurfaceInitialized) {
|
||||
// A DRAWABLE OF THIS SIDE'S OWN, WHICH IS WHAT A SURFACELESS CLIENT LEAVES TO US. The
|
||||
// backend's native context has to be current on a surface of some kind before a single
|
||||
// record of the client's can run - that is what the Init*Surface calls establish - and a
|
||||
// client that makes its context current with no surface at all supplies none of its own.
|
||||
// A pbuffer is the one that costs nothing to keep: it is never scanned out and never
|
||||
// read, because every frame the client draws goes into a framebuffer it binds itself.
|
||||
// The client is NOT told it has a surface - m_eglSurface stays EGL_NO_SURFACE - so a
|
||||
// swap on one is still the refusal it was.
|
||||
if (!InitPbufferSurface(1, 1)) {
|
||||
MGLOG_E("MakeEGLCurrent failed: the backend's own drawable for a surfaceless context could not "
|
||||
"be created");
|
||||
return false;
|
||||
}
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = SurfaceKind::Pbuffer;
|
||||
m_eglCurrentThreads.clear();
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
|
||||
return false;
|
||||
if (!surfaceless) {
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -1086,9 +1356,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple
|
||||
// (the ID-67 control's different tuple stays at 1 native bind where 2 are required).
|
||||
const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
DirectGLES::IsBackendContextCurrentOnThisThread() &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
|
||||
(surfaceless || (m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx)));
|
||||
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
|
||||
NoteNativeContextGone();
|
||||
return false;
|
||||
@@ -1107,6 +1377,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
// A HOST-FRAMED SURFACE HAS TO BE PUT BACK AFTER EVERY MAKE-CURRENT. The native call
|
||||
// binds the surface's own default framebuffer - the pbuffer this path makes its context
|
||||
// with - and that is not where these pixels go. Both the driver and the engine's binding
|
||||
// shadow have to be told again, or the next default-framebuffer bind is deduped away and
|
||||
// every draw lands in a pbuffer nobody sees.
|
||||
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
HostFrameTargetBind(HostFrameDrawTarget());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1263,7 +1542,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
E_GL_ARB_get_program_binary,
|
||||
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
|
||||
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
|
||||
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
|
||||
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
|
||||
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
|
||||
// missing checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
|
||||
// rendering context" and quit before drawing a frame.
|
||||
//
|
||||
// The three are true statements about this backend now rather than a test unlock: every
|
||||
// procedure they define is implemented in MG_Impl/GLImpl/Exporting/Definitions.cpp over
|
||||
// the same shader and program objects the core entry points drive - the 39 of
|
||||
// ARB_shader_objects, ARB_vertex_shader's three attrib-location entry points, and none
|
||||
// at all for ARB_fragment_shader ("New Procedures and Functions: None" - it is the
|
||||
// capability half, and the reason kwin's conjunction names it).
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
// NPOT textures: core since GL 2.0, and every host this backend can run on provides
|
||||
// them. Full non-power-of-two support - mipmapping and every wrap mode, not ES 2.0's
|
||||
// CLAMP_TO_EDGE-only exception - is required from ES 3.0 on, and the entries this list
|
||||
// already advertises without any gate (compute shaders, shader storage buffers,
|
||||
// indirect draws) need an ES 3.1 host. Nothing in the texture path rejects or pads a
|
||||
// non-power-of-two size - storage and sampling are forwarded to that host - so the
|
||||
// string describes what an application actually gets.
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
|
||||
// and this name is what makes them reachable - libepoxy in kwin resolves
|
||||
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
|
||||
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
|
||||
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
|
||||
// The render-server half of the import (fd over the GBM socket, then the server-side
|
||||
// EGLImage) is the next change; until it lands an imported texture has no storage behind
|
||||
// it, which is logged once at the entry point rather than left to look like a black frame.
|
||||
E_GL_OES_EGL_image};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "HostFrameTarget.h"
|
||||
#include "MG_Backend/HostFrameBridge.h"
|
||||
#include "MG_Backend/GbmFrameChannel.h"
|
||||
#include "../BackendObject.h"
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
@@ -56,8 +59,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void UpdateDynamicBackendParameters();
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
// A host-framed surface draws into a frame the display host owns; presenting one
|
||||
// tells the host the frame is drawn, because the host is what puts it on the glass.
|
||||
// NOT CreateEGLHostFrameSurface: the base class registers the surface and activates it,
|
||||
// and activation is what reaches InitHostFrameSurface below. There is nothing about the
|
||||
// EGL side of a host-framed surface this backend needs to do its own way.
|
||||
Bool InitHostFrameSurface(EGLint width, EGLint height) override;
|
||||
Bool PresentHostFrame() override;
|
||||
// Takes the next frame from the host and makes it this context's framebuffer.
|
||||
Bool TakeHostFrame();
|
||||
void DestroyHostFrame();
|
||||
// The canvas the compositor draws into, made once at the first frame's size.
|
||||
Bool EnsureHostFrameCanvas(Uint width, Uint height);
|
||||
// Copies the canvas into the frame about to be presented.
|
||||
void BlitHostFrameCanvas();
|
||||
// What draws resolve to: the canvas when there is one, the frame itself otherwise.
|
||||
const HostFrameTarget& HostFrameDrawTarget() const {
|
||||
return m_hostFrameCanvas.Framebuffer != 0 ? m_hostFrameCanvas : m_hostFrameTarget;
|
||||
}
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: the frames the display host owns, one at a time, which
|
||||
// is what the host offers - it hands one over and waits for the answer before it
|
||||
// presents it and offers the next.
|
||||
MG_Backend::HostFrameBridge m_hostFrameBridge;
|
||||
// The frames the bridge just took, on to the container: the Wayland compositor over there
|
||||
// renders into this same memory, and the only route to it that end has is the dma-buf
|
||||
// descriptor this channel offers per frame and reads a release back from.
|
||||
MG_Backend::GbmFrameChannel m_gbmFrameChannel;
|
||||
HostFrameTarget m_hostFrameTarget;
|
||||
// A STABLE TARGET IN FRONT OF THE FRAMES. Every present hands the frame back and takes
|
||||
// a different one, and its renderbuffer is deleted with it - but a compositor attaches
|
||||
// its framebuffers ONCE (kwin: one per daemon buffer, for the life of the process), so
|
||||
// attachments bound to a frame's own renderbuffer named a deleted object from the second
|
||||
// frame on. The attachments name this instead, and each present copies it into the
|
||||
// frame. No EGLImage behind it (Image stays null).
|
||||
HostFrameTarget m_hostFrameCanvas;
|
||||
struct AHardwareBuffer* m_hostFrameBuffer = nullptr;
|
||||
MG_Backend::HostFrameOffer m_hostFrameOffer{};
|
||||
Bool m_hostFrameBridgeOpen = false;
|
||||
|
||||
Bool m_initialized = false;
|
||||
MG_External::EGLFunctionsTable m_EGLFunctions;
|
||||
MG_External::GLESFunctionsTable m_GLESFunctions;
|
||||
|
||||
@@ -87,6 +87,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
|
||||
MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
Uint g_hostFrameFramebufferId = 0;
|
||||
Uint g_hostFrameRenderbuffer = 0;
|
||||
Uint g_hostFrameWidth = 0;
|
||||
Uint g_hostFrameHeight = 0;
|
||||
MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -483,11 +487,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
|
||||
const Uint8* const base = resource ? resource->hostBytes : nullptr;
|
||||
const Uint8* base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr;
|
||||
// P8-C: a store nothing ever staged but a shader wrote (the GPU-culling shape)
|
||||
// takes its first copy from this server's own GL buffer. A store that HAS a copy
|
||||
// is not refreshed here: most indirect draws execute natively and read no field
|
||||
// on the CPU, so ExecuteIndexed/ArraysIndirectCommands refresh the fields only
|
||||
// when they consume them.
|
||||
if (base == nullptr && resource != nullptr) {
|
||||
base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_bytes");
|
||||
}
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (base == nullptr) {
|
||||
MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged "
|
||||
"shadow on this side (GPU-written indirect commands are P8's)",
|
||||
"shadow on this side and no GPU write to take one from",
|
||||
label, handle.Slot, handle.Gen);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -769,6 +781,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
entry.Res.Gen);
|
||||
continue;
|
||||
}
|
||||
// P8-C: the SERVER's half of the mark MarkShaderStorageBuffersGpuWritten no longer
|
||||
// makes under a transport - on this side's twin, for this side's CPU readers
|
||||
// (SplitHostBytesForCpuRead). Every bound storage point, which is the client's
|
||||
// own conservative set (ShaderBufferEmit.h marks every one writable); a uniform
|
||||
// point is read-only by definition.
|
||||
if (shaderBufferClass != MG_Pipe::kMGPipeShaderBufferClassUniform) {
|
||||
backendResource->serverGpuWritten = true;
|
||||
}
|
||||
const auto backendBufferId = backendResource->id;
|
||||
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
|
||||
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), backendBufferId);
|
||||
@@ -997,6 +1017,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched
|
||||
// counter point rather than only the ones this program declares, which is
|
||||
// the safe direction for an over-approximate set.
|
||||
//
|
||||
// P8-C: the server's own twin is marked instead, for the server's own CPU
|
||||
// readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a
|
||||
// parameter or element buffer is read on this side.
|
||||
backendResource->serverGpuWritten = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1284,6 +1309,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
ProcessDeferredBufferReleases();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
ProcessDeferredT0Retires(); // P11 B2
|
||||
#endif
|
||||
|
||||
// All buffers we need are:
|
||||
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
|
||||
@@ -1531,6 +1559,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
ProcessDeferredBufferReleases();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
ProcessDeferredT0Retires(); // P11 B2
|
||||
#endif
|
||||
// THE COMPUTE PATH IS THE ONE THAT NEEDS THE FRONTEND-INDEXED UNIFORM PASS, and it
|
||||
// is why the uniform class is emitted at all: compute does NOT go through the
|
||||
// per-program block remap in BindCurrentProgramWithResources, so these are the
|
||||
@@ -1797,7 +1828,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const MG_Pipe::MGPipeHandle res = target.handle;
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr || resource->persistentMapped) continue;
|
||||
if (resource == nullptr) continue;
|
||||
if (resource->persistentMapped) {
|
||||
// P8-C: nothing to copy - the capture reaches a coherent map -
|
||||
// but this side's CPU readers must finish before reading it.
|
||||
resource->serverGpuWritten = true;
|
||||
continue;
|
||||
}
|
||||
const SizeT size = target.end - target.start;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
|
||||
void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget,
|
||||
@@ -1838,6 +1875,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"captured bytes of buffer {%u,%u} cannot reach the client shadow",
|
||||
res.Slot, res.Gen);
|
||||
}
|
||||
// P8-C: the server's own staged copy takes the same bytes, from the
|
||||
// same mapping, so this side's CPU readers see the capture too.
|
||||
BufferImpl::StageServerWrittenRange(*resource, res, mapped, target.start, size);
|
||||
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
|
||||
// Ops_H_Readback's order: writeback, unmap, THEN the serial
|
||||
// stamp, so the next draw does not re-upload the server's stale
|
||||
@@ -1998,7 +2038,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
splitRes.Slot, splitRes.Gen);
|
||||
continue;
|
||||
}
|
||||
const Uint8* hostBytes = splitResource->hostBytes;
|
||||
// P8-C: made current first - a store a shader wrote here is refreshed.
|
||||
const Uint8* hostBytes = BufferImpl::SplitHostBytesForCpuRead(*splitResource, splitRes,
|
||||
"xfb_scatter_pre_capture");
|
||||
// A NULL SHADOW IS NOT A LOST CAPTURE. It is the ORPHANED store
|
||||
// (glBufferData(size, NULL) with no resource_subdata since), which
|
||||
// M-3 declares legal: every byte the application has not staged is
|
||||
@@ -2063,9 +2105,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"scattered capture of buffer {%u,%u} cannot reach the client shadow",
|
||||
splitRes.Slot, splitRes.Gen);
|
||||
}
|
||||
// Ops_H_Readback's stamp, for its reason: the GL store now holds
|
||||
// bytes the server's staged shadow does not, and the next draw must
|
||||
// not re-upload the stale shadow over the capture.
|
||||
// P8-C: the staged copy takes the composed range too, so this side's CPU
|
||||
// readers see the capture (before P8 it kept the pre-capture bytes).
|
||||
BufferImpl::StageServerWrittenRange(*splitResource, splitRes, staged.data(), target.start,
|
||||
rangeBytes);
|
||||
// Ops_H_Readback's stamp, for its reason: the GL store now holds the
|
||||
// capture, and the next draw must not re-upload an older range over it.
|
||||
splitResource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(splitRes);
|
||||
} else
|
||||
#endif
|
||||
@@ -4345,12 +4390,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
const auto textureFormat = static_cast<TextureInternalFormat>(resource->Desc.InternalFormat);
|
||||
const TextureTarget textureTarget = PipeTextureTargetForHandle(view.Res);
|
||||
const GLenum access = GLAccessForImageView(view);
|
||||
// Before the sync, for the frontend arm's reason: the note may adopt a twin, and
|
||||
// `backendTexture` below is a reference into the same registry.
|
||||
if (access != GL_READ_ONLY) {
|
||||
NoteDriverSideTextureWriteByHandle(view.Res);
|
||||
}
|
||||
auto& backendTexture = SyncTextureToBackendByHandle(view.Res, /*imageBindableStorageRequired=*/true);
|
||||
if (!backendTexture) return;
|
||||
IssueImageTextureBind(unit, *backendTexture, textureTarget, textureFormat,
|
||||
static_cast<GLenum>(view.InternalFormat), static_cast<GLint>(view.Level),
|
||||
view.Layered != 0, static_cast<GLint>(view.Layer),
|
||||
GLAccessForImageView(view));
|
||||
view.Layered != 0, static_cast<GLint>(view.Layer), access);
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -4375,6 +4425,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before the sync: `backendTexture` below is a reference into the registry, which the
|
||||
// lookup inside the note may reshuffle.
|
||||
if (imageBinding.Access != GL_READ_ONLY) {
|
||||
NoteDriverSideTextureWrite(imageBinding.Texture);
|
||||
}
|
||||
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
|
||||
if (!backendTexture) return;
|
||||
IssueImageTextureBind(unit, *backendTexture, imageBinding.Texture->GetTarget(),
|
||||
@@ -4409,7 +4464,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// reach of an apply thread entirely.
|
||||
void MarkWritableImageBufferTexturesGpuWritten() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// P8-C: the SERVER's twin of the buffer behind each writable buffer image, for
|
||||
// this side's CPU readers (SplitHostBytesForCpuRead). Every read is a record:
|
||||
// the unit set SyncImageTextureBinding(view) tracked, the applier's image view,
|
||||
// and the texture descriptor's backing-store handle.
|
||||
if (g_writableImageBufferUnitCount == 0) return;
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
for (Uint unit = 0; unit < g_writableImageBufferUnits.size() && unit < st.BoundShaderImages.size();
|
||||
++unit) {
|
||||
if (!g_writableImageBufferUnits[unit]) continue;
|
||||
const MG_Pipe::MGPImageView& view = st.BoundShaderImages[unit];
|
||||
if (!IsWritableImageBufferView(view)) continue;
|
||||
const auto* record = PipeTextureRecordForHandle(view.Res);
|
||||
if (record == nullptr) continue;
|
||||
if (auto* buffer = BufferImpl::FindBufferResourceForHandle(record->Desc.BufferForTexBuffer)) {
|
||||
buffer->serverGpuWritten = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_writableImageBufferUnitCount == 0) return;
|
||||
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
|
||||
@@ -6445,7 +6519,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Through the shadow: a raw bind here would leave the shadow claiming
|
||||
// the previous user FBO, false-skipping its next re-bind.
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -6514,7 +6588,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Through the shadow: a raw bind here would leave the shadow claiming
|
||||
// the previous user FBO, false-skipping its next re-bind.
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6534,7 +6608,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::g_integerColorDrawBufferMask = 0;
|
||||
}
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6575,7 +6649,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::g_integerColorDrawBufferMask = 0;
|
||||
}
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8475,6 +8549,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-C: the command bytes an indirect executor is about to CONSUME on the CPU, made current.
|
||||
// Consumed means: every field on the CPU path (no native indirect), or on the native path the
|
||||
// uniforms fed from the command - gl_BaseVertex whenever the program reads it, and
|
||||
// gl_BaseInstance when the program reads it without the mg_IndirectParams storage view that
|
||||
// would give it the GPU's word. A native draw that feeds neither reads nothing and keeps its
|
||||
// pointer, so a compute-culled indirect buffer drawn every frame costs no readback.
|
||||
static const Uint8* CurrentIndirectCommandBytes(MG_Pipe::MGPipeHandle handle, const Uint8* commandBytes,
|
||||
SizeT commandOffset, Bool useNative, Bool indexed) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return commandBytes;
|
||||
if (useNative) {
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
if (program == nullptr) return commandBytes;
|
||||
const Bool baseVertexFromCpu = indexed && program->ReadsBaseVertex();
|
||||
const Bool baseInstanceFromCpu = program->GetIndirectParamsBinding() < 0 && program->ReadsBaseInstance();
|
||||
if (!baseVertexFromCpu && !baseInstanceFromCpu) return commandBytes;
|
||||
}
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
|
||||
if (resource == nullptr) return commandBytes;
|
||||
const Uint8* base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_fields");
|
||||
return base != nullptr ? base + commandOffset : commandBytes;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
||||
// execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's
|
||||
// culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted
|
||||
@@ -8521,6 +8619,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
:
|
||||
#endif
|
||||
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
|
||||
/*indexed=*/true);
|
||||
#endif
|
||||
if (useNative) {
|
||||
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
|
||||
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
|
||||
@@ -8619,6 +8721,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
:
|
||||
#endif
|
||||
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
|
||||
/*indexed=*/false);
|
||||
#endif
|
||||
if (useNative) {
|
||||
const auto backendProgram = GetCurrentBackendProgram();
|
||||
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
||||
@@ -9203,9 +9309,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
// The staged shadow is the source of truth for CPU reads on this side: a
|
||||
// persistent map's blocks and a shader write's writeback were consumed by the
|
||||
// applier before this verb ran.
|
||||
const Uint8* hostBytes = elementResource ? elementResource->hostBytes : nullptr;
|
||||
// persistent map's blocks were consumed by the applier before this verb ran, and
|
||||
// (P8-C) a store a shader wrote on this side is read back from this server's own
|
||||
// GL buffer first - no writeback ever reaches the server's copy.
|
||||
const Uint8* hostBytes =
|
||||
elementResource
|
||||
? BufferImpl::SplitHostBytesForCpuRead(*elementResource, elements,
|
||||
"primitive_restart_substitution")
|
||||
: nullptr;
|
||||
if (hostBytes == nullptr) {
|
||||
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
|
||||
"the bound element array buffer and none is available.",
|
||||
@@ -9522,8 +9633,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
|
||||
auto* paramResource =
|
||||
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
|
||||
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
|
||||
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
|
||||
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
|
||||
// (a store a shader wrote on this side is read back); the command buffer only gets a
|
||||
// first copy when it has none - its fields are made current by the executor, and
|
||||
// only when it consumes them (CurrentIndirectCommandBytes).
|
||||
const Uint8* const parameterBytes =
|
||||
paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource,
|
||||
applierState.VerbIndirectParameterBuffer,
|
||||
"indirect_count_parameter")
|
||||
: nullptr;
|
||||
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
||||
if (drawBytes == nullptr && drawResource != nullptr) {
|
||||
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
|
||||
"indirect_count_commands");
|
||||
}
|
||||
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
@@ -9676,8 +9799,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
|
||||
auto* paramResource =
|
||||
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
|
||||
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
|
||||
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
|
||||
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
|
||||
// (a store a shader wrote on this side is read back); the command buffer only gets a
|
||||
// first copy when it has none - its fields are made current by the executor, and
|
||||
// only when it consumes them (CurrentIndirectCommandBytes).
|
||||
const Uint8* const parameterBytes =
|
||||
paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource,
|
||||
applierState.VerbIndirectParameterBuffer,
|
||||
"indirect_count_parameter")
|
||||
: nullptr;
|
||||
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
|
||||
if (drawBytes == nullptr && drawResource != nullptr) {
|
||||
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
|
||||
"indirect_count_commands");
|
||||
}
|
||||
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
@@ -10929,6 +11064,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
continue;
|
||||
}
|
||||
|
||||
TextureImpl::NoteDriverSideTextureWrite(destinationTexture);
|
||||
auto backendSource = TextureImpl::SyncTextureObjectToBackend(sourceTexture);
|
||||
auto backendDestination = TextureImpl::SyncTextureObjectToBackend(destinationTexture);
|
||||
if (!backendSource || !backendDestination) continue;
|
||||
@@ -11608,51 +11744,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// P5c (hd), P5e (tx2): the verification half, with the RECORD passed in instead of re-read
|
||||
// from the verb stash - the caller has it and the two must not disagree about which texture
|
||||
// is being verified.
|
||||
static void EnsureGenerateMipmapStorageDescribed(const MG_Pipe::MGPipeResourceRecord& record) {
|
||||
// P8-B1: THE LEVELS A generate_mipmap RECORD GENERATES, end-exclusive. GL generates levels
|
||||
// BASE_LEVEL+1 .. q, q = min(p, MAX_LEVEL) with p the last level the base image's own chain
|
||||
// reaches, and an immutable texture's storage bounds the chain too (GL 4.6 core 8.14.4). The
|
||||
// client already computed that window (ComputeMipmapGenerationRange) and defined its levels
|
||||
// before emitting the verb, so the record's plan is the window and the descriptor is what
|
||||
// holds it: Base = VerbMipBaseLevel, End = min(VerbMipLevelCount, Desc.Levels, MaxLevel + 1,
|
||||
// Base + the base extent's chain) - the same clip Magma's GenerateWireMipmap makes.
|
||||
//
|
||||
// What stood here instead demanded the full chain from LEVEL 0 and died with
|
||||
// Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the descriptor held less: a
|
||||
// MAX_LEVEL below the chain, or glTexStorage2D with fewer levels, both legal GL.
|
||||
struct RecordMipWindow {
|
||||
Uint32 Base = 0;
|
||||
Uint32 End = 0; // end-exclusive; End <= Base + 1 generates nothing
|
||||
};
|
||||
|
||||
static RecordMipWindow GenerateMipmapWindowByRecord(const MG_Pipe::MGPipeResourceRecord& record) {
|
||||
const auto& applier = MG_Pipe::MGPipeApplier();
|
||||
const auto& desc = record.Desc;
|
||||
if (desc.Width == 0 || desc.Levels == 0) {
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
|
||||
}
|
||||
Uint maxDimension = desc.Width;
|
||||
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(desc.Target);
|
||||
if (target != MG_Pipe::MGPipeResourceTarget::Tex1D &&
|
||||
target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) {
|
||||
maxDimension = std::max(maxDimension, desc.Height);
|
||||
}
|
||||
if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) {
|
||||
maxDimension = std::max(maxDimension, desc.Depth);
|
||||
}
|
||||
Uint requiredLevels = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension /= 2;
|
||||
++requiredLevels;
|
||||
}
|
||||
if (desc.Levels < requiredLevels) {
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
|
||||
RecordMipWindow window{applier.VerbMipBaseLevel, applier.VerbMipBaseLevel};
|
||||
if (desc.Width == 0 || desc.Levels == 0 || window.Base >= desc.Levels) return window;
|
||||
Uint32 end = std::min<Uint32>(applier.VerbMipLevelCount, desc.Levels);
|
||||
if (record.ParamsSerial) end = std::min<Uint32>(end, static_cast<Uint32>(record.Params.MaxLevel) + 1u);
|
||||
const IntVec3 baseExtent = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
|
||||
desc.Depth, window.Base);
|
||||
const Int shrinking = MG_Remote::Server::StagedTextureShrinkingAxisCount(desc.Target);
|
||||
Int largest = 1;
|
||||
for (Int axis = 0; axis < shrinking && axis < 3; ++axis) largest = std::max<Int>(largest, baseExtent[axis]);
|
||||
Uint32 chain = 1;
|
||||
while (largest > 1) {
|
||||
largest /= 2;
|
||||
++chain;
|
||||
}
|
||||
window.End = std::max(window.Base, std::min(end, window.Base + chain));
|
||||
return window;
|
||||
}
|
||||
#endif
|
||||
|
||||
static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// The frontend has already defined the generated chain before emitting this verb.
|
||||
// Its resource_respecify carries that shape, so the server only verifies the
|
||||
// descriptor; it must not allocate or dirty the client's level shadows again.
|
||||
// P5c (hd): identity is the handle the generate_mipmap record carried
|
||||
// (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2).
|
||||
const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes;
|
||||
const auto* record = PipeTextureRecordForHandle(handle);
|
||||
if (record == nullptr) {
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
|
||||
}
|
||||
EnsureGenerateMipmapStorageDescribed(*record);
|
||||
return false; // No server-side shadow allocation was necessary.
|
||||
}
|
||||
#endif
|
||||
// P8-B1: the transport arm that stood here (a descriptor check under the same
|
||||
// `generate-mipmap-storage` name) was unreachable - GenerateMipmap returns through
|
||||
// GenerateMipmapByRecord before this call on every split arm - and the record arm's
|
||||
// window above replaces the check it made.
|
||||
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
||||
MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage.");
|
||||
Bool allocatedStorage = false;
|
||||
@@ -12386,11 +12520,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as
|
||||
// barriered), and the handle is now simply passed in - the extent derivation, the per-level
|
||||
// blits and the format question are §1's descriptor reads exactly as P5c wrote them.
|
||||
//
|
||||
// P8-B1: both chains run over the record's WINDOW (GenerateMipmapWindowByRecord), not from
|
||||
// level 1 to the end of the descriptor: a BASE_LEVEL above 0 is the source and a level past
|
||||
// MAX_LEVEL is left as it is, which is what the monolith twins below still get wrong.
|
||||
static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
const RecordMipWindow& window) {
|
||||
const auto& desc = record.Desc;
|
||||
const GLuint textureId = backendTexture->GetBackendTextureId();
|
||||
for (Uint32 level = 1; level < desc.Levels; ++level) {
|
||||
for (Uint32 level = window.Base + 1; level < window.End; ++level) {
|
||||
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
|
||||
desc.Depth, level - 1);
|
||||
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
|
||||
@@ -12403,12 +12542,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
const RecordMipWindow& window) {
|
||||
const auto& desc = record.Desc;
|
||||
const GLenum filter =
|
||||
IsIntegerColorFormat(static_cast<TextureInternalFormat>(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR;
|
||||
const GLuint textureId = backendTexture->GetBackendTextureId();
|
||||
for (Uint32 level = 1; level < desc.Levels; ++level) {
|
||||
for (Uint32 level = window.Base + 1; level < window.End; ++level) {
|
||||
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
|
||||
desc.Depth, level - 1);
|
||||
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
|
||||
@@ -12591,6 +12731,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// touched unit happened to sync it as a side effect. The named narrowing (P5e-5) says
|
||||
// the union that still covers every texture includes "the waited texture ops", and
|
||||
// this is one of them: it syncs its own endpoint by handle.
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle);
|
||||
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle);
|
||||
if (!backendTexture || dstRecord == nullptr) {
|
||||
MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin "
|
||||
@@ -12606,6 +12747,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit)
|
||||
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
||||
.GetBoundObject();
|
||||
if (textureObject) TextureImpl::NoteDriverSideTextureWrite(textureObject);
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
if (!backendTextureSlot || !*backendTextureSlot) {
|
||||
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
|
||||
@@ -12708,6 +12850,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
|
||||
// P5e (tx2): synced by handle - see CopyTexImage2D's note. A copy destination is not
|
||||
// in the sampler-view window, so nothing else on this path would build its twin.
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle);
|
||||
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle);
|
||||
if (!backendTexture) {
|
||||
MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no "
|
||||
@@ -12722,6 +12865,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
|
||||
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
||||
.GetBoundObject();
|
||||
if (textureObject) TextureImpl::NoteDriverSideTextureWrite(textureObject);
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
if (!backendTextureSlot || !*backendTextureSlot) {
|
||||
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
|
||||
@@ -12858,65 +13002,268 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL.
|
||||
//
|
||||
// ES generates a chain only for a colour-renderable, filterable format, and whether the
|
||||
// driver's storage for these two is one depends on the driver: Espryt stores them natively
|
||||
// where the driver renders the three-channel form and widens them to RGBA16F / RGBA32F where
|
||||
// it does not (TextureImpl::BackendTextureFormatAddsAlpha), and a widened RGBA32F still needs
|
||||
// OES_texture_float_linear to be filterable. So GenerateMipmapByRecord asks the driver first
|
||||
// and comes here only when it refuses - or when MGITEST_ESPRYT_FORCE_CPU_MIPMAP says to, which
|
||||
// is the only way a host whose driver never refuses reaches this arm.
|
||||
//
|
||||
// THE SOURCE IS THE SERVER'S LEVEL, NEVER THE CLIENT'S. The base level is read back from the
|
||||
// driver (ReadTextureLevelTight), because a render target's texels exist only there - the
|
||||
// monolith arm (GenerateThreeChannelFloatMipmapOnCpu) filters the frontend's level shadow
|
||||
// instead and reads a stale base after a draw. Only when the driver cannot read the level back
|
||||
// (storage it cannot attach) does the staged store answer, and a store that does not cover the
|
||||
// level declines by name rather than inventing texels.
|
||||
static Bool ForcedServerCpuMipmap() {
|
||||
// The R-16 control. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
|
||||
static const Bool s_forced = std::getenv("MGITEST_ESPRYT_FORCE_CPU_MIPMAP") != nullptr;
|
||||
return s_forced;
|
||||
}
|
||||
|
||||
// WireTextureReadback.inc (included further down) defines it: E's extent-scoped stand-in for a
|
||||
// driver that refuses to read a colour level back, asked here too so a host reaches the store
|
||||
// route below (P8-SE).
|
||||
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent);
|
||||
|
||||
// The base level as tight RGB floats: the driver's texels first, the staged store second.
|
||||
//
|
||||
// P8-SE: THE STORE ANSWERS ONLY FOR A TEXTURE THE DRIVER NEVER WROTE (P8-E's sticky mark). A
|
||||
// render target's level 0 lives on the driver; the store still holds whatever crossed before
|
||||
// the draw, and filtering it would hand the chain stale texels. `driverWrittenBefore` is the
|
||||
// mark as it stood BEFORE this generation noted its own write (GenerateMipmap's split arm),
|
||||
// because that note is unconditional and would otherwise decline every store read. Anything
|
||||
// the store cannot stand for is a named decline, `declined` says why.
|
||||
static Bool ReadThreeChannelFloatLevelOnServer(MG_Pipe::MGPipeHandle mipRes,
|
||||
const MG_Pipe::MGPipeResourceRecord& record,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
TextureTarget textureTarget, TextureUploadTarget face, Uint32 level,
|
||||
const IntVec3& extent, Bool driverWrittenBefore,
|
||||
Vector<Float>& rgb, const char*& declined) {
|
||||
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
|
||||
const SizeT texels = static_cast<SizeT>(extent.x()) * static_cast<SizeT>(extent.y());
|
||||
Vector<Uint8> bytes;
|
||||
if (!WireTextureReadbackRefusedForTesting(extent) &&
|
||||
ReadTextureLevelTight(backendTexture->GetBackendTextureId(), textureTarget, face, format,
|
||||
static_cast<GLint>(level), extent, backendTexture->RequiresImageBindableStorage(),
|
||||
GL_RGB, GL_FLOAT, bytes) &&
|
||||
bytes.size() == texels * 3 * sizeof(Float)) {
|
||||
rgb.resize(texels * 3);
|
||||
Memcpy(rgb.data(), bytes.data(), bytes.size());
|
||||
return true;
|
||||
}
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(mipRes);
|
||||
const auto faceCode = static_cast<Uint16>(face);
|
||||
const auto levelCode = static_cast<Uint16>(level);
|
||||
const Bool half = format == TextureInternalFormat::RGB16F;
|
||||
const SizeT componentBytes = half ? sizeof(Uint16) : sizeof(Float);
|
||||
// A view's staged bytes live under its storage owner's key, in the owner's levels; a view
|
||||
// whose level the driver cannot read has no answer here.
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) {
|
||||
declined = "a view has no staged level of its own";
|
||||
} else if (!store.CopiesIntoServerStorage()) {
|
||||
declined = "this arm keeps no staged store";
|
||||
} else if (driverWrittenBefore) {
|
||||
declined = "the driver has written the texture since its bytes crossed";
|
||||
} else if (!store.IsCovered(key, faceCode, levelCode)) {
|
||||
declined = "not every byte of the level crossed";
|
||||
} else if (store.LevelByteSize(key, faceCode, levelCode) != texels * 3 * componentBytes) {
|
||||
declined = "the staged level is not the size of the level";
|
||||
}
|
||||
if (declined != nullptr) return false;
|
||||
const Uint8* staged = store.RequireLevelBytes(key, faceCode, levelCode, "generate-mipmap-server-cpu");
|
||||
rgb.resize(texels * 3);
|
||||
for (SizeT i = 0; i < texels * 3; ++i) {
|
||||
if (half) {
|
||||
Uint16 bits = 0;
|
||||
Memcpy(&bits, staged + i * componentBytes, sizeof(bits));
|
||||
rgb[i] = MG_Util::DecodeHalfBitsToFloat(bits);
|
||||
} else {
|
||||
Memcpy(&rgb[i], staged + i * componentBytes, sizeof(Float));
|
||||
}
|
||||
}
|
||||
MGLOG_W_ONCE("MGPipe: generate-mipmap-server-cpu: texture {%u, %u} level %u could not be read back from "
|
||||
"the driver; filtering the staged store's copy of it instead",
|
||||
mipRes.Slot, mipRes.Gen, level);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void GenerateThreeChannelFloatMipmapOnServer(MG_Pipe::MGPipeHandle mipRes,
|
||||
const MG_Pipe::MGPipeResourceRecord& record,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
Bool driverWrittenBefore, const char* why) {
|
||||
const auto& desc = record.Desc;
|
||||
const auto format = static_cast<TextureInternalFormat>(desc.InternalFormat);
|
||||
const auto textureTarget = TextureTargetForPipeResourceTarget(desc.Target);
|
||||
const RecordMipWindow window = GenerateMipmapWindowByRecord(record);
|
||||
if (window.End <= window.Base + 1) return;
|
||||
Vector<TextureUploadTarget> faces;
|
||||
if (textureTarget == TextureTarget::Texture2D) {
|
||||
faces.push_back(TextureUploadTarget::Texture2D);
|
||||
} else if (textureTarget == TextureTarget::TextureCubeMap) {
|
||||
for (Int face = 0; face < 6; ++face)
|
||||
faces.push_back(static_cast<TextureUploadTarget>(
|
||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face));
|
||||
}
|
||||
// The upload spelling the driver's storage takes: GL_FLOAT is legal for all four float
|
||||
// internal formats (ES 3.2 table 8.2), and the channel count follows the storage.
|
||||
GLenum esInternal = GL_NONE, esFormat = GL_NONE, esType = GL_NONE;
|
||||
TextureImpl::GenerateTextureFormatInfo(format, &esInternal, &esFormat, &esType, textureTarget);
|
||||
const Bool fourChannel = esInternal == GL_RGBA16F || esInternal == GL_RGBA32F;
|
||||
if (faces.empty() || (!fourChannel && esInternal != GL_RGB16F && esInternal != GL_RGB32F)) {
|
||||
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: target %s stored as %s has "
|
||||
"no server CPU arm (%s); levels %u..%u are left as they are",
|
||||
mipRes.Slot, mipRes.Gen, MG_Util::ConvertTextureTargetToString(textureTarget).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(esInternal).c_str(), why, window.Base + 1, window.End - 1);
|
||||
return;
|
||||
}
|
||||
MGLOG_I_ONCE("MGPipe: generate-mipmap-server-cpu: %s levels filtered on the server's CPU from its own "
|
||||
"level (%s)", MG_Util::ConvertTextureInternalFormatToString(format).c_str(), why);
|
||||
|
||||
const GLuint textureId = backendTexture->GetBackendTextureId();
|
||||
const GLenum bindTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget);
|
||||
for (const TextureUploadTarget face : faces) {
|
||||
IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
|
||||
desc.Depth, window.Base);
|
||||
Vector<Float> level;
|
||||
const char* declined = nullptr;
|
||||
if (!ReadThreeChannelFloatLevelOnServer(mipRes, record, backendTexture, textureTarget, face, window.Base,
|
||||
extent, driverWrittenBefore, level, declined)) {
|
||||
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: base level %u could not "
|
||||
"be read back from the driver and the staged store cannot stand for it (%s); levels "
|
||||
"%u..%u are left as they are - DECLINED (generate-mipmap-store-declined)",
|
||||
mipRes.Slot, mipRes.Gen, window.Base, declined != nullptr ? declined : "?",
|
||||
window.Base + 1, window.End - 1);
|
||||
return;
|
||||
}
|
||||
for (Uint32 dst = window.Base + 1; dst < window.End; ++dst) {
|
||||
const IntVec3 dstExtent =
|
||||
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, dst);
|
||||
// The monolith's box filter: the 2x2 source footprint, clamped where a dimension
|
||||
// is already 1 (GL 4.6 core 8.14.4 leaves the filter to the implementation).
|
||||
Vector<Float> next(static_cast<SizeT>(dstExtent.x()) * static_cast<SizeT>(dstExtent.y()) * 3);
|
||||
const auto at = [&](Int x, Int y, Int c) {
|
||||
return level[(static_cast<SizeT>(y) * static_cast<SizeT>(extent.x()) + static_cast<SizeT>(x)) * 3 +
|
||||
static_cast<SizeT>(c)];
|
||||
};
|
||||
for (Int y = 0; y < dstExtent.y(); ++y) {
|
||||
for (Int x = 0; x < dstExtent.x(); ++x) {
|
||||
const Int x0 = std::min(x * 2, extent.x() - 1), x1 = std::min(x * 2 + 1, extent.x() - 1);
|
||||
const Int y0 = std::min(y * 2, extent.y() - 1), y1 = std::min(y * 2 + 1, extent.y() - 1);
|
||||
for (Int c = 0; c < 3; ++c)
|
||||
next[(static_cast<SizeT>(y) * static_cast<SizeT>(dstExtent.x()) + static_cast<SizeT>(x)) *
|
||||
3 + static_cast<SizeT>(c)] =
|
||||
0.25f * (at(x0, y0, c) + at(x1, y0, c) + at(x0, y1, c) + at(x1, y1, c));
|
||||
}
|
||||
}
|
||||
Vector<Float> upload;
|
||||
if (fourChannel) {
|
||||
const SizeT count = next.size() / 3;
|
||||
upload.resize(count * 4);
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
upload[i * 4] = next[i * 3];
|
||||
upload[i * 4 + 1] = next[i * 3 + 1];
|
||||
upload[i * 4 + 2] = next[i * 3 + 2];
|
||||
upload[i * 4 + 3] = 1.0f;
|
||||
}
|
||||
}
|
||||
// Tight float rows are a multiple of four bytes, which the resting UNPACK state
|
||||
// (4/0/0/0, and the unpack-PBO binding at 0) parses as they are - CopyR32FTexture2D's
|
||||
// discipline, including the re-bind that keeps the binding cache truthful.
|
||||
ClearGLErrors();
|
||||
BufferImpl::BindPixelUnpackBufferId(0);
|
||||
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
||||
g_GLESFuncs.glBindTexture(bindTarget, textureId);
|
||||
g_GLESFuncs.glTexSubImage2D(TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(face),
|
||||
static_cast<GLint>(dst), 0, 0, static_cast<GLsizei>(dstExtent.x()),
|
||||
static_cast<GLsizei>(dstExtent.y()), fourChannel ? GL_RGBA : GL_RGB,
|
||||
GL_FLOAT, fourChannel ? upload.data() : next.data());
|
||||
const GLenum err = g_GLESFuncs.glGetError();
|
||||
auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
|
||||
[static_cast<SizeT>(textureTarget)];
|
||||
g_GLESFuncs.glBindTexture(bindTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0);
|
||||
if (err != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu: uploading level %u of texture {%u, %u} "
|
||||
"failed: %s", dst, mipRes.Slot, mipRes.Gen,
|
||||
MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
return;
|
||||
}
|
||||
level = Move(next);
|
||||
extent = dstExtent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from
|
||||
// VerbMipRes. Same four arms as the frontend body, with every shape question answered from
|
||||
// the descriptor.
|
||||
//
|
||||
// RULING 14, AND WHY THE ROW IS NOT FLIPPED HERE. The unit read this arm retires is what the
|
||||
// wait existed for, but ONE frontend contact is left on the path and it is not tx2's to
|
||||
// retire: the RGB16F/RGB32F CPU filter reaches into the client's level shadows
|
||||
// (GenerateThreeChannelFloatMipmapOnCpu, kimi audit row 61) and P8/P9 own the pull. So the
|
||||
// brief's escape applies - the arm is not complete, the row stays kWaitApplied and the flip
|
||||
// is listed as trailing - and the record stays barriered, which is what keeps the CPU arm's
|
||||
// refusal a refusal rather than a wrong picture.
|
||||
// RULING 14's TRAILING FLIP. tx2 kept this verb's PipeCalls.def row at kWaitApplied because
|
||||
// one frontend contact was left on the path: the RGB16F/RGB32F CPU filter reached into the
|
||||
// client's level shadows. P8-B2 retired it (GenerateThreeChannelFloatMipmapOnServer reads the
|
||||
// server's own level), so no arm here reads client memory any more; the WaitClass flip itself
|
||||
// is a protocol change and is left to the integrator.
|
||||
//
|
||||
// `driverWrittenBefore`: P8-E's driver-written mark on the texture's staged store as it stood
|
||||
// before this call noted its own write - the CPU arm's store route reads it (P8-SE).
|
||||
static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes,
|
||||
const MG_Pipe::MGPipeResourceRecord& record,
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
Bool driverWrittenBefore) {
|
||||
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
|
||||
const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target);
|
||||
const Bool threeChannelFloat =
|
||||
format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F;
|
||||
|
||||
if (format == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(format) ||
|
||||
format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) {
|
||||
// The storage verification EnsureGenerateMipmapStorageAllocated's disaggregated arm
|
||||
// already makes, with the record passed in rather than re-read from VerbMipRes: the
|
||||
// frontend defined the generated chain before emitting this verb, so the server only
|
||||
// checks that the descriptor it was given can carry it.
|
||||
EnsureGenerateMipmapStorageDescribed(record);
|
||||
}
|
||||
if (format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) {
|
||||
// The CPU filter reads and writes the CLIENT's level shadows. It already aborts
|
||||
// three frames deep at the MipmapStorage guard under a transport; naming it here
|
||||
// says which emulation, which is what the guard cannot.
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-filter");
|
||||
}
|
||||
// A view's layer window is the one bound the native arm cannot express, so it is asked
|
||||
// for before the format arms - those two are 2D-only emulations of the same generation
|
||||
// and a view reaches them too.
|
||||
if (GenerateMipmapThroughViewWindowByRecord(mipRes, record, format)) return;
|
||||
if (IsDepthOnlyFormat(format)) {
|
||||
GenerateDepthTexture2DMipmapByRecord(record, backendTexture);
|
||||
GenerateDepthTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record));
|
||||
return;
|
||||
}
|
||||
if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) {
|
||||
GenerateColorTexture2DMipmapByRecord(record, backendTexture);
|
||||
GenerateColorTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record));
|
||||
return;
|
||||
}
|
||||
if (threeChannelFloat && ForcedServerCpuMipmap()) {
|
||||
GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore,
|
||||
"forced by MGITEST_ESPRYT_FORCE_CPU_MIPMAP");
|
||||
return;
|
||||
}
|
||||
|
||||
const GLenum backendTarget =
|
||||
TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target));
|
||||
// TempTextureUnit, not the active unit: the scratch bind is the backend's own and
|
||||
// BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit
|
||||
// was never anything but the frontend's way of naming the texture, and the record names
|
||||
// it now.
|
||||
backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit);
|
||||
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the
|
||||
// source texture is detached from every framebuffer that attaches it - by handle, over
|
||||
// the applier's own framebuffer records.
|
||||
ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes);
|
||||
ScopedCompleteFramebufferBinding completeFramebuffer;
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glGenerateMipmap(backendTarget);
|
||||
RecordGLError("glGenerateMipmap", backendTarget, format);
|
||||
GLenum refused = GL_NO_ERROR;
|
||||
{
|
||||
// TempTextureUnit, not the active unit: the scratch bind is the backend's own and
|
||||
// BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit
|
||||
// was never anything but the frontend's way of naming the texture, and the record
|
||||
// names it now.
|
||||
backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit);
|
||||
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the
|
||||
// source texture is detached from every framebuffer that attaches it - by handle,
|
||||
// over the applier's own framebuffer records.
|
||||
ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes);
|
||||
ScopedCompleteFramebufferBinding completeFramebuffer;
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glGenerateMipmap(backendTarget);
|
||||
if (!threeChannelFloat) {
|
||||
RecordGLError("glGenerateMipmap", backendTarget, format);
|
||||
return;
|
||||
}
|
||||
// The driver's refusal of a three-channel float chain is not the application's
|
||||
// error: the CPU arm below answers it.
|
||||
refused = g_GLESFuncs.glGetError();
|
||||
}
|
||||
if (refused == GL_NO_ERROR) return;
|
||||
GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore,
|
||||
"the driver refused glGenerateMipmap");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -12943,6 +13290,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// for GenerateMipmap move to kWaitNone.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes;
|
||||
// P8-SE: whether the staged store still stands for the texture is read BEFORE the note
|
||||
// below sets the mark, or the CPU arm's store route could never answer at all.
|
||||
const Bool driverWrittenBefore = MG_Remote::Server::ServerStagedTexture().IsDriverWritten(
|
||||
MG_Remote::Server::StagedTextureStore::KeyForHandle(mipRes));
|
||||
// Every arm GenerateMipmapByRecord takes writes the levels above the base on the
|
||||
// driver only (P8-B2's CPU arm uploads its levels there too), so the note is
|
||||
// unconditional.
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(mipRes);
|
||||
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes);
|
||||
const auto* record = PipeTextureRecordForHandle(mipRes);
|
||||
if (!backendTexture || record == nullptr) {
|
||||
@@ -12951,7 +13306,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
mipRes.Slot, mipRes.Gen);
|
||||
return;
|
||||
}
|
||||
GenerateMipmapByRecord(target, mipRes, *record, backendTexture);
|
||||
GenerateMipmapByRecord(target, mipRes, *record, backendTexture, driverWrittenBefore);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -12970,6 +13325,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TextureImpl::SyncTextureObjectToBackend(texture);
|
||||
return;
|
||||
}
|
||||
// Every path below writes the levels above the base on the driver only.
|
||||
TextureImpl::NoteDriverSideTextureWrite(texture);
|
||||
auto& backendTexture = TextureImpl::SyncTextureObjectToBackend(texture);
|
||||
|
||||
// Asked for before the format arms below, which are 2D-only emulations of the same
|
||||
@@ -13083,25 +13440,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
|
||||
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
|
||||
// reachable from a path that has a renderbuffer but no framebuffer.
|
||||
// reachable from a path that has a renderbuffer but no framebuffer. The MONOLITH arm's only:
|
||||
// under a transport MakeGLESCopyImageEndpoint resolves a renderbuffer endpoint by handle and
|
||||
// never calls this. (P8-E removed the `CopyImageSubData+RENDERBUFFER` latch that stood here -
|
||||
// its transport condition could not hold at the only call site - and
|
||||
// CopyImageStoreReadbackScenario's renderbuffer case runs the handle arm on every transport.)
|
||||
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
|
||||
if (!renderbufferObject) return nullptr;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (fb, CONTRACT-P5E.md §4.4): the scope at this site is DELETED, and with it the
|
||||
// arm that needed it. The only caller is the glCopyImageSubData endpoint builder, and a
|
||||
// RENDERBUFFER endpoint is refused on the wire before it is emitted and again at the
|
||||
// sink (PipeApplier.cpp's ServerUnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER")),
|
||||
// so under a transport nothing can reach here with one. Saying so by name is better
|
||||
// than a find-or-MINT against the frontend address that the refusal already proved
|
||||
// unreachable - if it ever becomes reachable, the name is where to start.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"CopyImageSubData+RENDERBUFFER\"} a renderbuffer "
|
||||
"endpoint reached the backend under an active transport, where both the client "
|
||||
"emitter and the sink refuse one");
|
||||
std::abort();
|
||||
}
|
||||
#endif
|
||||
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
|
||||
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
|
||||
backendRenderbufferObject = *slot;
|
||||
@@ -13222,19 +13568,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
|
||||
if (!srcMipmap || !dstMipmap) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5b package i1's ONE backend edit, and the contract names this site
|
||||
// (MG_Remote/CONTRACT-P5B.md §2 i1 "the copy-image-shadow-mirror emulation", ruling
|
||||
// §6.7). Migrating glCopyImageSubData moves the first blocker off the client's
|
||||
// Fatal{UnmigratedVerb} and onto the Fatal below, on every Espryt copy between two
|
||||
// textures with CPU shadows - so the ruling is: UNDER A REAL TRANSPORT THE SERVER SKIPS
|
||||
// THE MIRROR, and the client-side mirror ROADMAP P8 names ("CopyImage 镜像搬到 client")
|
||||
// stays P8's.
|
||||
//
|
||||
// WHAT THE SKIP LOSES IS BOUNDED BY TWO FATALS, which is the whole reason it is allowed
|
||||
// to be a skip rather than a port: a later glGetTexImage of the destination served from
|
||||
// the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9) and a texture
|
||||
// re-mint that re-uploads the level is Fatal{UnmigratedEmulation, "texture-remint-pull"}
|
||||
// (Managers.cpp:5634). Neither can silently read the un-mirrored shadow.
|
||||
// UNDER A REAL TRANSPORT THE SERVER HAS NO FRONTEND SHADOW TO MIRROR INTO (P5b i1, ruling
|
||||
// §6.7), and P8-E settled where the move lives instead - not on the client (ROADMAP P8's
|
||||
// original row) but in the server's staged store: CopyImageSubData's handle arm makes the
|
||||
// same texel-block move there (TextureImpl::FollowCopyImageInStagedStore) for EVERY
|
||||
// format, not only the redundant-encoding ones this mirror serves, and marks the store
|
||||
// driver-written where it cannot. The store is what answers a split glGetTexImage the
|
||||
// driver refuses to read back (WireTextureReadback.inc's store route); every other split
|
||||
// read of the destination reads the GPU level itself (notes/p8/E.md).
|
||||
//
|
||||
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
|
||||
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
|
||||
@@ -13242,14 +13583,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ordinary correct path and must still run.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-M). glCopyImageSubData's CPU-shadow mirror copies the source level's shadow
|
||||
// rows into the DESTINATION's shadow so a later readback of the destination sees what
|
||||
// the GPU copy put there. Both shadows are the client's, so this is the clearest case
|
||||
// in the family of an emulation that cannot survive a split; ROADMAP puts the move
|
||||
// itself in P8 and this names the site until then.
|
||||
MG_Pipe::MGPipeUnmigratedEmulation("copy-image-shadow-mirror");
|
||||
#endif
|
||||
// P4a (D-M) named this site `copy-image-shadow-mirror` with MGPipeUnmigratedEmulation.
|
||||
// P8-SE removed the call: the return above means no transport reaches it, and on a
|
||||
// monolith arm the call was a no-op (PipeCatalogueTest's list and the lint
|
||||
// scripts/ci/unmigrated_emulation_sites.py follow).
|
||||
|
||||
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
|
||||
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
|
||||
@@ -13316,11 +13653,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// dereference. The frontend validator is what keeps this unreachable and what reports
|
||||
// the error the application is owed; declining is only how a future gap up there stops
|
||||
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
||||
if (!dstEndpoint.IsRenderbuffer() && dstEndpoint.Texture) {
|
||||
TextureImpl::NoteDriverSideTextureWrite(dstEndpoint.Texture);
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The handle arm's endpoint names its texture by handle, and the server has no frontend
|
||||
// object to note. The staged store's half of the note waits for the copy (P8-E, below).
|
||||
const Bool handleArmDestination =
|
||||
!dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle);
|
||||
if (handleArmDestination) {
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle, /*storeFollowsTheWrite=*/true);
|
||||
}
|
||||
#endif
|
||||
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
|
||||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
|
||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-E (notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced,
|
||||
// so every byte the store holds for them is on the driver, and the copy below is a raw
|
||||
// texel-block move - the store makes the same move, which keeps the destination level
|
||||
// answerable from here when the driver will not read it back (ReadTextureImageWire's store
|
||||
// route) and keeps a whole-level replay of it (a re-mint's refused readback, a full-level
|
||||
// re-upload) from putting the pre-copy texels back. Where the store cannot follow (a
|
||||
// renderbuffer, a cube or 1D-array endpoint, a source the driver has written, a level whose
|
||||
// bytes did not all cross) it is marked driver-written instead, so it declines rather than
|
||||
// answering with stale bytes. Nothing moves to the client (ROADMAP P8's original row).
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && handleArmDestination) {
|
||||
const Bool followed =
|
||||
!srcEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(srcEndpoint.TextureHandle) &&
|
||||
TextureImpl::FollowCopyImageInStagedStore(
|
||||
srcEndpoint.TextureHandle, static_cast<Uint32>(srcLevel), IntVec3{srcX, srcY, srcZ},
|
||||
dstEndpoint.TextureHandle, static_cast<Uint32>(dstLevel), IntVec3{dstX, dstY, dstZ},
|
||||
IntVec3{srcWidth, srcHeight, srcDepth});
|
||||
if (!followed) TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto
|
||||
// wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint.
|
||||
@@ -13394,6 +13763,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
||||
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-E: the store already made the move the driver just refused.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && handleArmDestination) {
|
||||
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
|
||||
@@ -14952,10 +15327,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Session::Fail entirely, which is the funnel the census gate watches.
|
||||
//
|
||||
// THE FUNNEL KEEPS ITS TEETH FOR THE OTHER SITES: MGPipeUnmigratedEmulation still
|
||||
// aborts for generate-mipmap-storage, generate-mipmap-cpu-fallback,
|
||||
// generate-mipmap-cpu-filter and texture-remint-pull, which really do reach into a
|
||||
// client address space. Only this call site is retired, and only because its own
|
||||
// premise did not hold.
|
||||
// names generate-mipmap-storage and generate-mipmap-cpu-fallback, on monolith arms only
|
||||
// (P8-SE dropped copy-image-shadow-mirror, a site no transport reached). Only this call
|
||||
// site is retired here, and only because its own premise did not hold (texture-remint-pull
|
||||
// went the same way in P9 W2, and P8-B the two split generate-mipmap names, likewise).
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
if (shadow == nullptr) {
|
||||
MGLOG_E_ONCE("GetTexImage: no level shadow on this side for target=0x%x level=%d, and under an "
|
||||
@@ -15169,9 +15544,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "WireTextureReadback.inc"
|
||||
#endif
|
||||
|
||||
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
|
||||
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
|
||||
@@ -15859,7 +16232,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// resizing depth/stencil without colour would report the default framebuffer incomplete.
|
||||
static void PublishDefaultFramebufferDepthStencilFormat() {
|
||||
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P8-SE: THE PLACEHOLDER IS ONLY THE MONOLITH ARM'S TARGET. With a transport the answer is
|
||||
// posted (OnSurfaceChanged, below) and the client writes its own placeholder; a spawn or tcp
|
||||
// server process has no frontend at all, so pDefaultFramebufferInfo is null there. Gating on
|
||||
// it returned before the post, the client kept Init.cpp's GL_DEPTH32F_STENCIL8 guess for a
|
||||
// D24S8 pbuffer, and a blit into a buffer allocated from that guess lost depth and stencil
|
||||
// (notes/p8/SE.md). Inproc only passed because client and server share the process.
|
||||
if (!g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return;
|
||||
if (!defaultFBOInfo && MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == nullptr) return;
|
||||
#else
|
||||
if (!defaultFBOInfo || !g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return;
|
||||
#endif
|
||||
|
||||
// GL_DEPTH / GL_STENCIL are the default framebuffer's spellings; a user framebuffer
|
||||
// would need GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT and answers GL_INVALID_ENUM
|
||||
@@ -15940,8 +16324,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stencilFormat = TextureInternalFormat::StencilIndex8;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Null in a spawn / tcp server (above); only the monolith arm below writes through them.
|
||||
auto* depthTexture = defaultFBOInfo ? defaultFBOInfo->depthAttachment.get() : nullptr;
|
||||
auto* stencilTexture = defaultFBOInfo ? defaultFBOInfo->stencilAttachment.get() : nullptr;
|
||||
#else
|
||||
auto* depthTexture = defaultFBOInfo->depthAttachment.get();
|
||||
auto* stencilTexture = defaultFBOInfo->stencilAttachment.get();
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the attachments are CLIENT
|
||||
|
||||
@@ -23,15 +23,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
|
||||
// Native/server-only color level readback. Returns tightly packed, owned
|
||||
// Driver-side color/depth level readback. Returns tightly packed, owned
|
||||
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
|
||||
// The caller resolves identity/extent from its resource record and supplies
|
||||
// whether the SOURCE allocation already uses an image carrier.
|
||||
// The caller supplies identity and extent, and whether the SOURCE allocation
|
||||
// already uses an image carrier.
|
||||
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
|
||||
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
|
||||
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
|
||||
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
|
||||
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
|
||||
// refuses answers true, keeping the caller on its readback path.
|
||||
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
GLint level, const IntVec3& logicalExtent);
|
||||
#endif
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
@@ -243,6 +249,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void InvalidateSyncedRenderState();
|
||||
} // namespace RenderStateImpl
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: what the client's DEFAULT framebuffer means while a host-framed
|
||||
// surface is current. Zero everywhere else, because the driver's own 0 IS the EGL surface
|
||||
// then - but on a host-framed surface the pixels belong to a frame the display host owns,
|
||||
// and drawing into the driver's 0 would draw into a pbuffer nobody ever sees.
|
||||
extern Uint g_hostFrameFramebufferId;
|
||||
// WHAT THE HOST FRAME'S STORAGE IS, for the one caller that has to bind it in place of a
|
||||
// storage-less attachment: an imported dma-buf is the display host's frame (same memory, same
|
||||
// size), and the frame's storage is a RENDERBUFFER (HostFrameTarget.cpp binds it with
|
||||
// glFramebufferRenderbuffer). A texture argument cannot name it, which is why these exist and
|
||||
// why g_hostFrameFramebufferId alone was not enough. Zero whenever no frame is held.
|
||||
extern Uint g_hostFrameRenderbuffer;
|
||||
extern Uint g_hostFrameWidth;
|
||||
extern Uint g_hostFrameHeight;
|
||||
// TAKE THE FRAME WHEN NOTHING ELSE WILL. On the anland route the container's compositor never
|
||||
// creates a window surface, so ActivateEGLSurface never runs and InitHostFrameSurface is never
|
||||
// reached: the FIRST proof that the display host's frame is wanted is an ATTACHMENT of an
|
||||
// imported external texture (Managers.cpp's 0x8D65 arm), and that is where the frame has to be
|
||||
// taken. The host's own offer carries the width/height, so the numbers passed here are only
|
||||
// what the bridge logs. Answers false (and changes nothing) whenever no backend is up.
|
||||
Bool TakeHostFrameForExternalTexture(Uint width, Uint height);
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.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 "HostFrameTarget.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "Managers.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
// The four entry points this is spelled in are optional everywhere: the loader's own
|
||||
// tables do not carry them (a missing EGL function there is MGLOG_F), so the extension
|
||||
// strings decide first and the pointers are taken only once they have.
|
||||
using GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
|
||||
using CreateImageFn = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
using DestroyImageFn = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
|
||||
using ImageTargetRenderbufferFn = void (*)(GLenum, GLeglImageOES);
|
||||
|
||||
Bool HasExtensionWord(const char* list, const char* word) {
|
||||
if (list == nullptr) return false;
|
||||
const SizeT n = std::strlen(word);
|
||||
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
|
||||
const Bool startOk = at == list || at[-1] == ' ';
|
||||
const Bool endOk = !at[n] || at[n] == ' ';
|
||||
if (startOk && endOk) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct EntryPoints {
|
||||
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
|
||||
CreateImageFn CreateImage = nullptr;
|
||||
DestroyImageFn DestroyImage = nullptr;
|
||||
ImageTargetRenderbufferFn TargetRenderbuffer = nullptr;
|
||||
};
|
||||
|
||||
// Resolved once per process and cached together with the refusal: a driver that lacks one
|
||||
// of these does not grow it later, and the answer names what is missing.
|
||||
const EntryPoints& Resolved(String& why) {
|
||||
static EntryPoints points;
|
||||
static Bool resolved = false;
|
||||
static String refusal;
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
|
||||
!g_GLESFuncs.glGetString) {
|
||||
refusal = "the loader has no eglGetProcAddress/eglQueryString/eglGetCurrentDisplay/glGetString";
|
||||
} else {
|
||||
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
|
||||
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
|
||||
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
|
||||
refusal = "EGL_ANDROID_get_native_client_buffer is not advertised";
|
||||
} else if (!HasExtensionWord(egl, "EGL_KHR_image_base")) {
|
||||
refusal = "EGL_KHR_image_base is not advertised";
|
||||
} else if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
|
||||
refusal = "GL_OES_EGL_image is not advertised";
|
||||
} else {
|
||||
points.GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
|
||||
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
|
||||
points.CreateImage =
|
||||
reinterpret_cast<CreateImageFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
|
||||
points.DestroyImage =
|
||||
reinterpret_cast<DestroyImageFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
|
||||
points.TargetRenderbuffer = reinterpret_cast<ImageTargetRenderbufferFn>(
|
||||
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
|
||||
if (points.GetNativeClientBuffer == nullptr || points.CreateImage == nullptr ||
|
||||
points.DestroyImage == nullptr || points.TargetRenderbuffer == nullptr) {
|
||||
refusal = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / eglCreateImageKHR / "
|
||||
"eglDestroyImageKHR / glEGLImageTargetRenderbufferStorageOES";
|
||||
points = EntryPoints{};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
why = refusal;
|
||||
return points;
|
||||
}
|
||||
|
||||
void DrainGLErrors() {
|
||||
if (!g_GLESFuncs.glGetError) return;
|
||||
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool HostFrameTargetSupported(String& why) {
|
||||
const EntryPoints& points = Resolved(why);
|
||||
return points.GetNativeClientBuffer != nullptr;
|
||||
}
|
||||
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why) {
|
||||
target = HostFrameTarget{};
|
||||
if (buffer == nullptr) {
|
||||
why = "no buffer";
|
||||
return false;
|
||||
}
|
||||
const EntryPoints& points = Resolved(why);
|
||||
if (points.GetNativeClientBuffer == nullptr) return false;
|
||||
|
||||
if (!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glBindRenderbuffer || !g_GLESFuncs.glGenFramebuffers ||
|
||||
!g_GLESFuncs.glBindFramebuffer || !g_GLESFuncs.glFramebufferRenderbuffer ||
|
||||
!g_GLESFuncs.glCheckFramebufferStatus) {
|
||||
why = "the loader has no glGenRenderbuffers/glBindRenderbuffer/glGenFramebuffers/glBindFramebuffer/"
|
||||
"glFramebufferRenderbuffer/glCheckFramebufferStatus";
|
||||
return false;
|
||||
}
|
||||
|
||||
AHardwareBuffer_Desc desc{};
|
||||
AHardwareBuffer_describe(buffer, &desc);
|
||||
|
||||
DrainGLErrors();
|
||||
const EGLClientBuffer clientBuffer = points.GetNativeClientBuffer(buffer);
|
||||
if (clientBuffer == nullptr) {
|
||||
why = "eglGetNativeClientBufferANDROID returned null";
|
||||
return false;
|
||||
}
|
||||
const EGLint imageAttribs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
|
||||
const EGLImageKHR image =
|
||||
points.CreateImage(g_EGLFuncs.eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
|
||||
clientBuffer, imageAttribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) {
|
||||
why = "eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) refused the host frame";
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint renderbuffer = 0;
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
points.TargetRenderbuffer(GL_RENDERBUFFER, reinterpret_cast<GLeglImageOES>(image));
|
||||
const GLenum renderbufferError = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
|
||||
if (renderbufferError != GL_NO_ERROR) {
|
||||
why = std::format("glEGLImageTargetRenderbufferStorageOES -> 0x{:04x}", renderbufferError);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
why = std::format("the host frame as a color attachment -> framebuffer status 0x{:04x}", status);
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
|
||||
return false;
|
||||
}
|
||||
|
||||
target.Image = image;
|
||||
target.Renderbuffer = renderbuffer;
|
||||
target.Framebuffer = framebuffer;
|
||||
target.Width = desc.width;
|
||||
target.Height = desc.height;
|
||||
MGLOG_I("host frame target: %ux%d as framebuffer %u (stride %u, format 0x%x)", desc.width, desc.height,
|
||||
framebuffer, desc.stride, desc.format);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostFrameTargetBind(const HostFrameTarget& target) {
|
||||
if (target.Framebuffer == 0) return;
|
||||
// THROUGH THE ENGINE'S SHADOW, never raw: a raw bind would leave the shadow claiming
|
||||
// the previous framebuffer and false-skip the next re-bind (FramebufferImpl, Managers.h).
|
||||
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, target.Framebuffer);
|
||||
}
|
||||
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target) {
|
||||
if (target.Framebuffer != 0 && g_GLESFuncs.glDeleteFramebuffers)
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &target.Framebuffer);
|
||||
if (target.Renderbuffer != 0 && g_GLESFuncs.glDeleteRenderbuffers)
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
|
||||
if (target.Image != nullptr) {
|
||||
String why;
|
||||
const EntryPoints& points = Resolved(why);
|
||||
if (points.DestroyImage != nullptr && g_EGLFuncs.eglGetCurrentDisplay)
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), static_cast<EGLImageKHR>(target.Image));
|
||||
}
|
||||
target = HostFrameTarget{};
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool HostFrameTargetSupported(String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer*, HostFrameTarget&, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
void HostFrameTargetBind(const HostFrameTarget&) {}
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target) { target = HostFrameTarget{}; }
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.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 <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// THE FRAME THE DISPLAY HOST IS ABOUT TO SCAN OUT, AS A DRAW TARGET.
|
||||
//
|
||||
// The display host (the Android side that owns the screen) allocates its frames itself and
|
||||
// offers each one over a socket as an AHardwareBuffer. That is not a convenience: measured
|
||||
// on the device, AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this side
|
||||
// allocated and refuses one the display queue handed over (gralloc BAD_VALUE), so the frames
|
||||
// a render server can draw into are the ones the host made for it.
|
||||
//
|
||||
// What this makes of such a frame: an EGLImage the driver accepts as a renderbuffer, and a
|
||||
// framebuffer that is the drawing target while that frame is the current one. None of
|
||||
// Android's window machinery is involved - no ANativeWindow, no SurfaceFlinger - which is
|
||||
// the point: the host puts the frame on the glass when this side says it is drawn.
|
||||
struct HostFrameTarget {
|
||||
void* Image = nullptr; // EGLImageKHR; opaque here so this header stays free of EGL types
|
||||
Uint Renderbuffer = 0;
|
||||
Uint Framebuffer = 0;
|
||||
Uint Width = 0;
|
||||
Uint Height = 0;
|
||||
};
|
||||
|
||||
// Whether the driver advertises what this is spelled in. False is answered with the missing
|
||||
// name in `why`, never an abort: a server that cannot take host frames has to be able to
|
||||
// say so.
|
||||
Bool HostFrameTargetSupported(String& why);
|
||||
|
||||
// Imports `buffer` as a render target. On failure `target` is left empty and `why` says
|
||||
// which call refused; no GL name is leaked.
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why);
|
||||
|
||||
// Binds the target as the current draw framebuffer, called when its frame becomes current.
|
||||
void HostFrameTargetBind(const HostFrameTarget& target);
|
||||
|
||||
// Drops the GL names and the EGLImage. The AHardwareBuffer belongs to the bridge, which
|
||||
// releases it; this never touches it.
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1004,6 +1004,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (the UBO binding walk) skip the probe while their pre-pass epoch read
|
||||
// matches, exactly like the per-VAO memo stamps.
|
||||
Uint64 drawCleanEpoch = 0;
|
||||
// Frame serial whose completion retires the last glCopyBufferSubData the upload
|
||||
// ring staged INTO this store (the flush ladder's ring tier: FlushPendingRangesNow
|
||||
// on the pull arm, FlushPendingRangesFrom on the push arm); 0 = none. Until it
|
||||
// retires, both keep every partial range off the range-invalidating map: on
|
||||
// Adreno 830 that map, on a store with a queued copy, loses the copy (see
|
||||
// FlushPendingRangesNow and InvalidateFlushAccessFor). Draw thread only, like the
|
||||
// flush.
|
||||
Uint64 ringCopyRetireSerial = 0;
|
||||
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
|
||||
// immutable, persistently+coherently mapped, and persistentPtr is what the app
|
||||
// (and the frontend PipeResource) write into directly. While set, draw-time
|
||||
@@ -1042,6 +1050,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the legacy arm did; this member exists for the drains that have no object, which
|
||||
// in P3a is the readback flush and the fp64 narrowing.
|
||||
const Uint8* hostBytes = nullptr;
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P11 B2 (T0): the client's AHardwareBuffer this store IS - imported with
|
||||
// glBufferStorageExternalEXT, so `id` is immutable, persistentMapped and
|
||||
// persistentPtr is this server's own coherent map of the client's pages. The
|
||||
// reference is this twin's own, released only after a fence that follows the
|
||||
// store's last use has signalled (RetireT0Import). Survives an ES context loss:
|
||||
// the next ensure re-imports it. Null for every store T0 did not adopt.
|
||||
void* externalAhb = nullptr;
|
||||
// P8-C: A SHADER WROTE THIS STORE ON THIS SIDE since the R-11 staged copy (or, for
|
||||
// T0, the coherent map) was last known to match it - set by the draw / dispatch that
|
||||
// bound it writable (storage block, atomic counter, writable buffer image), cleared
|
||||
// by SplitHostBytesForCpuRead's refresh and by a respecify. Only the server's own
|
||||
// CPU readers consult it; the client keeps its own set (GpuWritePending.h).
|
||||
Bool serverGpuWritten = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1104,6 +1127,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
|
||||
SizeT end, const char* site);
|
||||
|
||||
// P11 B2: THE CPU-READABLE BYTES OF A SPLIT SERVER'S BUFFER TWIN, for the handle-arm
|
||||
// readers that have no frontend object (indirect commands, primitive-restart
|
||||
// substitution, the multi-draw index rebase): the R-11 staged copy, or - for a T0 store,
|
||||
// which has none - this server's own coherent map of the client's AHardwareBuffer, read
|
||||
// exactly as the monolith arm reads the adopted map through MappedData().
|
||||
inline const Uint8* SplitHostBytes(const GLESBufferResource& resource) {
|
||||
if (resource.hostBytes != nullptr) return resource.hostBytes;
|
||||
return resource.externalAhb != nullptr ? static_cast<const Uint8*>(resource.persistentPtr) : nullptr;
|
||||
}
|
||||
|
||||
// P8-C: THE SAME BYTES, MADE CURRENT FIRST - for a reader that CONSUMES them (the restart
|
||||
// rewrite, the multi-draw rebase, the *IndirectCount count, an indirect command's
|
||||
// CPU-fed fields, the XFB scatter's pre-capture bytes). SplitHostBytes above is only
|
||||
// "what uploads staged": a store a shader wrote on this side (serverGpuWritten) is
|
||||
// refreshed from this server's own GL buffer - queued uploads flushed into it first,
|
||||
// then the whole store read back into the staged copy, which also gives an orphaned
|
||||
// store its first copy - and a T0 store lands its queued resident writes and finishes
|
||||
// before its coherent map is read. A clean staged store costs one flag test.
|
||||
// `res` names the record whose descriptor width the copy takes; `site` names the
|
||||
// reader in the one-time diagnostic when the refresh cannot run.
|
||||
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
|
||||
const char* site);
|
||||
// P8-C: bytes the SERVER itself put into a store's GL buffer and already holds on the CPU
|
||||
// (an XFB capture it mapped for the client's writeback, a scatter it composed) go into
|
||||
// the staged copy as well, so no refresh is owed for them. A T0 store has no copy.
|
||||
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
|
||||
SizeT offset, SizeT size);
|
||||
|
||||
// M-3's WHOLE-STORE predicate, and the GUARD every caller of RequireStagedCoverage over
|
||||
// a whole-store read has to carry: true when the application DECLARED the store's
|
||||
// content (glBufferData(size, data)), so a coverage gap is a missing record; false when
|
||||
@@ -1143,10 +1194,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ("do not take tier 1"), which drops the range onto the staging ring and costs a copy
|
||||
// rather than a corruption.
|
||||
//
|
||||
// `ringCopyInFlight`: the store still has an upload-ring glCopyBufferSubData queued that the
|
||||
// GPU may not have run (GLESBufferResource::ringCopyRetireSerial not yet retired).
|
||||
//
|
||||
// Returns the glMapBufferRange access bits, or 0 when tier 1 must not be taken.
|
||||
inline constexpr SizeT kEsprytInvalidateRangeMinBytes = 128u * 1024u;
|
||||
constexpr GLbitfield InvalidateFlushAccessFor(SizeT queuedStart, SizeT queuedEnd, SizeT mapStart,
|
||||
SizeT mapEnd, SizeT limit, SizeT storageSize) {
|
||||
SizeT mapEnd, SizeT limit, SizeT storageSize,
|
||||
Bool ringCopyInFlight) {
|
||||
if (mapEnd <= mapStart) return 0u;
|
||||
// THE WIDENING REFUSAL. Not >=, not "covers": exactly, in both directions. A map
|
||||
// narrower than the queued range leaves bytes unwritten inside a range it has just
|
||||
@@ -1158,6 +1213,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// page-substitution fast path needs a page-coverable range to engage, and below it
|
||||
// the driver falls back to waiting out the WAR hazard on the CPU.
|
||||
if (!wholeBuffer && size < kEsprytInvalidateRangeMinBytes) return 0u;
|
||||
// A partial range-invalidating map must not overtake a ring copy into this store the GPU
|
||||
// has not run yet. Adreno 830 answers such a map on a store with a copy still queued by
|
||||
// losing the copy (dev f973008c, the pull arm's FlushPendingRangesNow: create-instancing
|
||||
// drew whole Embeddium terrain sections empty, SSIM 0.870). The range goes to the ring
|
||||
// instead, GPU-ordered behind the copies. The whole-buffer orphan-map is exempt: it
|
||||
// rewrites every byte from the shadow the queued copies were taken from.
|
||||
if (!wholeBuffer && ringCopyInFlight) return 0u;
|
||||
return GL_MAP_WRITE_BIT |
|
||||
(wholeBuffer ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT);
|
||||
}
|
||||
@@ -1267,6 +1329,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
||||
// thread without a current ES context). Called from draw-time sync.
|
||||
void ProcessDeferredBufferReleases();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P11 B2: the T0 half of that drain - run right after it (retired AHardwareBuffers).
|
||||
void ProcessDeferredT0Retires();
|
||||
#endif
|
||||
|
||||
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
||||
void BindBufferId(GLenum target, Uint id);
|
||||
@@ -1813,6 +1879,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
// An external texture's storage is an imported EGLImage; to the driver it is 2D.
|
||||
case TextureTarget::TextureExternalOES:
|
||||
return TextureTarget::Texture2D;
|
||||
default:
|
||||
return target;
|
||||
}
|
||||
@@ -1963,7 +2032,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// allocation, or capture the old native contents, merge pending
|
||||
// upload regions and re-arm the server's whole-level replay set.
|
||||
// First allocations use staged bytes directly. No frontend dirty
|
||||
// flag, allocator or pixel-store object participates.
|
||||
// flag, allocator or pixel-store object participates. P9 W2: a level
|
||||
// the driver does not hold (defined after the last sync) is never read
|
||||
// back, and a readback the driver refuses falls back to the staged
|
||||
// level when the store covers it (notes/p9/W2-REMINT.md).
|
||||
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
|
||||
const MG_Pipe::MGPipeResourceRecord& record);
|
||||
#endif
|
||||
@@ -1972,6 +2044,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
|
||||
// the application floats.
|
||||
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
|
||||
// See m_driverMayHoldGpuWrites. Callers holding the STATE texture use
|
||||
// NoteDriverSideTextureWrite, which also reaches the storage behind a view.
|
||||
void NoteDriverSideWrite() { m_driverMayHoldGpuWrites = true; }
|
||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||
Uint GetBackendTextureId() const;
|
||||
|
||||
@@ -2088,6 +2163,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_imageBindableStorageRequired = false;
|
||||
Bool m_backendStorageImmutable = false;
|
||||
// Whether the driver texture may hold texels the shadow never saw. Set the first time
|
||||
// anything other than a texel upload can write it - an application framebuffer
|
||||
// attachment, a writable image binding, a copy or blit into it, a driver-side mipmap
|
||||
// generation - and never cleared. Until then the driver holds, outside a level's
|
||||
// pending writes, exactly the shadow bytes it was last sent, so those writes may go up
|
||||
// as their union box (one unpack-ring job, which is what Mali needs); from then on
|
||||
// only the texels the client wrote may go up, since the shadow is stale everywhere
|
||||
// else. See SyncMipmapsToBackend.
|
||||
Bool m_driverMayHoldGpuWrites = false;
|
||||
// Latches the "this driver has no buffer textures" report to once per texture. The
|
||||
// report is emitted from the respecify path, which bails before recording the state
|
||||
// it was asked to apply - so without the latch the texture stays permanently dirty
|
||||
@@ -2257,6 +2341,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
Bool imageBindableStorageRequired = false);
|
||||
// Every path that lets the DRIVER write a texture's texels calls this (see
|
||||
// BackendTextureObject::m_driverMayHoldGpuWrites): an application framebuffer attachment,
|
||||
// a writable image binding, a copy, blit or mipmap generation into it. A view writes its
|
||||
// storage texture's image, so the storage texture is the one marked.
|
||||
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
|
||||
// id/tx2) --------------------------------------------------------------------------
|
||||
@@ -2273,6 +2362,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(
|
||||
MG_Pipe::MGPipeHandle texture, Bool imageBindableStorageRequired = false);
|
||||
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle texture);
|
||||
// NoteDriverSideTextureWrite's HANDLE ARM, for the same paths when the caller holds a
|
||||
// Texture handle (a framebuffer surface's Res, an image unit's Res, VerbMipRes,
|
||||
// VerbCopyTexDst, a copy-image endpoint): the twin the handle's record names is marked, or
|
||||
// its storage owner's (Desc.ViewOf) for a view. Nothing on the apply thread reads a
|
||||
// frontend object for it.
|
||||
//
|
||||
// P8-E: under a transport the same storage owner's staged store is marked driver-written
|
||||
// too (StagedTextureStore::MarkDriverWritten), so a refused readback does not answer from
|
||||
// bytes the driver has since overwritten - unless `storeFollowsTheWrite`, which only
|
||||
// glCopyImageSubData passes: it tries FollowCopyImageInStagedStore first and marks the store
|
||||
// itself when that fails.
|
||||
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle texture, Bool storeFollowsTheWrite = false);
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-E (notes/p8/E.md): make glCopyImageSubData's texel-block move in the server's staged
|
||||
// store too (StagedTextureStore::FollowCopy), so the destination level's bytes stay its
|
||||
// content. Box in each image's own texel grid. False when the store cannot follow: a cube
|
||||
// map or 1D array endpoint (copy-image puts faces / layers on z, the store does not), a view,
|
||||
// two different internal formats, a level whose bytes did not all cross, a source the
|
||||
// driver has written, or a monolith arm.
|
||||
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
|
||||
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
|
||||
const IntVec3& destinationOrigin, const IntVec3& size);
|
||||
#endif
|
||||
// Brings every texture the next draw reads - the touched units' bindings and the draw
|
||||
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
|
||||
@@ -3030,6 +3142,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||
// uniform write, and only such a program needs the reset after one.
|
||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-C: same for the gl_BaseInstance uniform - which an indirect draw feeds from the
|
||||
// command on the CPU only when the mg_IndirectParams view is absent.
|
||||
Bool ReadsBaseInstance() const { return m_baseInstanceUniformLocation >= 0; }
|
||||
#endif
|
||||
// Which viewport indices the next draw's fragments may keep, one bit each. Written
|
||||
// once per replay pass; see ForEachViewportRoutingPass.
|
||||
void SetViewportPassMask(Uint32 indexMask) const;
|
||||
|
||||
@@ -191,15 +191,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (question == IndexBufferQuestion::Presence) return view;
|
||||
|
||||
if (question == IndexBufferQuestion::HostBytes) {
|
||||
// No Sync* pair here and none is missing: on this side the applier has
|
||||
// already consumed the persistent-map blocks and the shader writebacks for
|
||||
// this resource before the draw verb replayed, so the server's staged shadow
|
||||
// IS the synced copy. That is also why this arm cannot be expressed as "find
|
||||
// the object and run the monolith body".
|
||||
// The applier has already consumed the persistent-map blocks for this
|
||||
// resource before the draw verb replayed; the one Sync* this arm owes is the
|
||||
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
|
||||
// a shader wrote on this side is read back from this server's own GL buffer,
|
||||
// because no writeback ever reaches the server's staged copy. That is also why
|
||||
// this arm cannot be expressed as "find the object and run the monolith body".
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
view.HostBytes = resource->hostBytes;
|
||||
view.HostBytes = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
|
||||
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
|
||||
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
|
||||
"multidraw_index_rebase");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Included in DirectGLES.cpp after the native packed-word readback helper.
|
||||
// The server needs owned tight rows for a storage promotion, not client PACK
|
||||
// layout or a fallback into a frontend texture's potentially stale shadow.
|
||||
// A storage promotion needs the driver's own texels as owned tight rows, not
|
||||
// client PACK layout or a fallback into a frontend texture's potentially stale
|
||||
// shadow.
|
||||
class ScopedWireTextureReadLevel {
|
||||
public:
|
||||
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
|
||||
@@ -280,6 +281,144 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
|
||||
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
|
||||
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
|
||||
// its FBO attachment is incomplete - which used to end the session at
|
||||
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
|
||||
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
GLint level, const IntVec3& logicalExtent) {
|
||||
if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true;
|
||||
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
|
||||
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
GLenum binding = GL_NONE;
|
||||
switch (nativeTarget) {
|
||||
case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break;
|
||||
case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break;
|
||||
case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
|
||||
case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
|
||||
default: return true;
|
||||
}
|
||||
// A cube's levels are per face; every other target answers for the whole level.
|
||||
const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP
|
||||
? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget;
|
||||
DrainESErrors();
|
||||
GLint previous = 0;
|
||||
g_GLESFuncs.glGetIntegerv(binding, &previous);
|
||||
g_GLESFuncs.glBindTexture(nativeTarget, texture);
|
||||
GLint width = 0, height = 0, depth = 0;
|
||||
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width);
|
||||
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height);
|
||||
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth);
|
||||
const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
g_GLESFuncs.glBindTexture(nativeTarget, static_cast<GLuint>(previous));
|
||||
if (!answered) return true;
|
||||
const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
|
||||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
|
||||
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
|
||||
}
|
||||
|
||||
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
|
||||
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
|
||||
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
|
||||
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
|
||||
// the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it.
|
||||
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) {
|
||||
static const IntVec3 s_refused = [] {
|
||||
const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT");
|
||||
if (value == nullptr) return IntVec3{0, 0, 0};
|
||||
char* end = nullptr;
|
||||
const long width = std::strtol(value, &end, 10);
|
||||
if (end == value || *end != 'x') return IntVec3{0, 0, 0};
|
||||
const long height = std::strtol(end + 1, nullptr, 10);
|
||||
if (width <= 0 || height <= 0) return IntVec3{0, 0, 0};
|
||||
return IntVec3{static_cast<Int>(width), static_cast<Int>(height), 1};
|
||||
}();
|
||||
return s_refused.x() > 0 && extent == s_refused;
|
||||
}
|
||||
|
||||
// P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store
|
||||
// is the only CPU copy of the level on this side, and it answers only while its bytes ARE the
|
||||
// level: every byte crossed (IsCovered), at the extent asked for, and the texture was never
|
||||
// written by the driver except by copies the store followed (!IsDriverWritten - see
|
||||
// StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is
|
||||
// GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type
|
||||
// takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row.
|
||||
// Anything else DECLINES BY NAME: the reply is an error, and the client's
|
||||
// Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has
|
||||
// since overwritten.
|
||||
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
|
||||
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
|
||||
GLenum format, GLenum type, Vector<Uint8>& bytes) {
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
|
||||
const auto target = static_cast<Uint16>(uploadTarget);
|
||||
const auto mip = static_cast<Uint16>(level);
|
||||
ReadbackChannelMapping mapping{};
|
||||
const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0;
|
||||
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(extent.z(), 0));
|
||||
const char* declined = nullptr;
|
||||
if (!store.CopiesIntoServerStorage()) {
|
||||
declined = "this arm keeps no staged store";
|
||||
} else if (store.IsDriverWritten(key)) {
|
||||
declined = "the driver has written the texture since its bytes crossed";
|
||||
} else if (!store.IsCovered(key, target, mip)) {
|
||||
declined = "not every byte of the level crossed";
|
||||
} else if (store.LevelExtentOrUndefined(key, target, mip) != extent) {
|
||||
declined = "the staged level is not the extent asked for";
|
||||
} else if (pixelBytes == 0 || texels == 0) {
|
||||
declined = "the format/type pair has no readback mapping";
|
||||
}
|
||||
Vector<Uint8> out;
|
||||
if (declined == nullptr) {
|
||||
const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store");
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
|
||||
if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) {
|
||||
declined = "the staged words are not the client's word size";
|
||||
} else {
|
||||
out.assign(staged, staged + texels * pixelBytes);
|
||||
}
|
||||
} else {
|
||||
Vector<Uint8> wide;
|
||||
Bool isInteger = false;
|
||||
Bool isSigned = false;
|
||||
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide,
|
||||
isInteger, isSigned)) {
|
||||
declined = "the staged format has no wide decoding";
|
||||
} else if (mapping.isInteger != isInteger) {
|
||||
declined = "an integer read of a normalized level, or the reverse";
|
||||
} else {
|
||||
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
|
||||
const SizeT rowTexels = static_cast<SizeT>(extent.x());
|
||||
const SizeT rows = texels / rowTexels;
|
||||
const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType);
|
||||
out.resize(texels * pixelBytes);
|
||||
for (SizeT row = 0; row < rows; ++row) {
|
||||
ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes,
|
||||
out.data() + row * rowTexels * pixelBytes, rowTexels,
|
||||
wideType, mapping, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (declined != nullptr) {
|
||||
MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store "
|
||||
"cannot answer it (%s) - DECLINED (texture-readback-store-declined)",
|
||||
res.Slot, res.Gen, level, declined);
|
||||
return false;
|
||||
}
|
||||
MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); "
|
||||
"answered from the server's staged store (texture-readback-from-store)",
|
||||
res.Slot, res.Gen, level, static_cast<unsigned>(logicalFormat));
|
||||
bytes = Move(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
|
||||
const auto* record = PipeTextureRecordForHandle(info.Res);
|
||||
if (!record) return false;
|
||||
@@ -293,17 +432,32 @@
|
||||
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
|
||||
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
|
||||
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
|
||||
const auto logicalFormat = static_cast<TextureInternalFormat>(record->Desc.InternalFormat);
|
||||
// The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is
|
||||
// ReadTextureLevelTight's first choice for exactly these reads).
|
||||
const Bool packedWordRead =
|
||||
!twin->RequiresImageBindableStorage() &&
|
||||
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) &&
|
||||
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32);
|
||||
const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent);
|
||||
Vector<Uint8> result;
|
||||
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
|
||||
const auto uploadTarget = wholeCube
|
||||
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
|
||||
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
|
||||
Vector<Uint8> slice;
|
||||
if (!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget,
|
||||
static_cast<TextureInternalFormat>(record->Desc.InternalFormat), info.Level, extent,
|
||||
twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) return false;
|
||||
if (refusedForTesting ||
|
||||
!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level,
|
||||
extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) {
|
||||
if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent,
|
||||
info.Format, info.Type, slice)) return false;
|
||||
}
|
||||
result.insert(result.end(), slice.begin(), slice.end());
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -345,6 +345,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
pVulkanRenderer.reset();
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
// And the swap interval it asked for (DirectVulkan.h).
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
ForgetRequestedSwapInterval();
|
||||
}
|
||||
}
|
||||
#else
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
||||
@@ -363,7 +367,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Any renderer instance this assignment replaces is destroyed here;
|
||||
// fence/timer-query handles stamped with the old generation go stale.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
|
||||
VulkanRendererConfig config;
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
return true;
|
||||
@@ -486,6 +492,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return BackendObject::SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) {
|
||||
// Same lock as SwapEGLBuffers: the renderer reads the request in Present.
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
BackendObject::SetEGLSwapInterval(interval);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
BackendObject::ReleaseEGLSurface(surface);
|
||||
@@ -647,7 +659,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
E_GL_ARB_get_program_binary,
|
||||
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
|
||||
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
|
||||
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
|
||||
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
|
||||
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
|
||||
// missing checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
|
||||
// rendering context" and quit before drawing a frame.
|
||||
//
|
||||
// The three are true statements about this backend too, and the entry points behind
|
||||
// them are the same ones the DirectGLES half of this list is advertised with: the
|
||||
// frontend implements all 39 ARB_shader_objects procedures, ARB_vertex_shader's three
|
||||
// attrib-location entry points, and nothing for ARB_fragment_shader, whose "New
|
||||
// Procedures and Functions" section reads "None". The shader and program objects live in
|
||||
// MG_Impl/MG_State, above the backend that draws with them, so a name that is true on
|
||||
// one backend is true on the other.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
// NPOT textures: Vulkan puts no power-of-two restriction on an image, its mip chain or
|
||||
// its samplers, so this is the strongest form of the promise the string makes. Kept
|
||||
// identical to the DirectGLES group so the two backends do not disagree about what
|
||||
// MobileGL is.
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
|
||||
// and this name is what makes them reachable - libepoxy in kwin resolves
|
||||
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
|
||||
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
|
||||
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
|
||||
// The render-server half of the import (fd over the GBM socket, then the server-side
|
||||
// EGLImage) is the next change; until it lands an imported texture has no storage behind
|
||||
// it, which is logged once at the entry point rather than left to look like a black frame.
|
||||
E_GL_OES_EGL_image};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
@@ -727,6 +770,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool funcsTableInitialized = false;
|
||||
if (!funcsTableInitialized) {
|
||||
funcsTable.Present = Present;
|
||||
funcsTable.SetSwapInterval = SetSwapInterval;
|
||||
funcsTable.GL.DrawArrays = DrawArrays;
|
||||
funcsTable.GL.DrawElements = DrawElements;
|
||||
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
|
||||
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
|
||||
void SetEGLSwapInterval(Int interval) override;
|
||||
void ReleaseEGLSurface(EGLSurface surface) override;
|
||||
void ReleaseEGLResources() override;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DescriptorPoolCensus.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 <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A count of Magma's per-frame descriptor pools and the sets they hold (UniformManager), for
|
||||
// tests. Dependency-free on purpose: the integration scenarios include it next to the system GL
|
||||
// headers. They link the static library on desktop; the shipping shared library is built with
|
||||
// hidden visibility, so on Android nothing outside the library can call GetDescriptorPoolCensus.
|
||||
struct DescriptorPoolCensus {
|
||||
bool available = false; // false when no Magma renderer is live (e.g. the DirectGLES backend)
|
||||
uint32_t frameSlots = 0;
|
||||
uint32_t pools = 0; // descriptor pools across every frame slot
|
||||
uint32_t maxPoolsInOneSlot = 0;
|
||||
uint64_t capacitySets = 0; // sum of maxSets over those pools
|
||||
uint64_t cachedSets = 0; // sets allocated from them and held in the per-layout caches
|
||||
uint64_t slotTrims = 0; // whole-slot trims since the renderer came up
|
||||
};
|
||||
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -370,16 +370,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#endif // !MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The verb's handles identify server stores. Readback orders GPU-produced
|
||||
// command/count bytes before CPU tier expansion; never inspect a client binding.
|
||||
// The verb's handles identify server stores; never inspect a client binding.
|
||||
//
|
||||
// P8-D: THE GPU READS THE WORDS (VulkanRenderer::DrawWireIndirectNative, WireDraw.inc), as
|
||||
// on the monolith arm. The CPU expansion below is kept for the one shape the monolith arm
|
||||
// also expands - a COUNT form the device cannot issue natively (no VK_KHR_draw_indirect_count,
|
||||
// or maxdrawcount > 1 without multiDrawIndirect: VulkanRenderer.cpp's
|
||||
// MultiDrawElementsIndirectCount) - where ReadWireBuffer orders GPU-produced command/count
|
||||
// bytes before the expansion.
|
||||
void DrawWireIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
|
||||
GLsizei stride, Bool indexed, Bool counted = false, GLintptr countOffset = 0) {
|
||||
if (drawcount <= 0) return;
|
||||
if (drawcount <= 0 || countOffset < 0) return;
|
||||
if (pVulkanRenderer->DrawWireIndirectNative(mode, type, reinterpret_cast<Uint64>(indirect), drawcount,
|
||||
stride, indexed, counted, static_cast<Uint64>(countOffset)))
|
||||
return;
|
||||
auto& buffers = pVulkanRenderer->GetWireBufferManager();
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
// P8-D: this call reads its words on the CPU; the waits those reads take are its own.
|
||||
struct ExpansionCount {
|
||||
VkBufferManager& buffers;
|
||||
const Uint64 waitsBefore = buffers.GetWireHostWaitCount();
|
||||
~ExpansionCount() {
|
||||
if (!MG_Util::PipeStats::Enabled()) return;
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectCpuExpansions, 1);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitsIndirect,
|
||||
buffers.GetWireHostWaitCount() - waitsBefore);
|
||||
}
|
||||
} expansionCount{buffers};
|
||||
if (counted) {
|
||||
Uint32 count = 0;
|
||||
if (countOffset < 0 || !buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
|
||||
if (!buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
|
||||
static_cast<Uint64>(countOffset), sizeof(count), &count)) return;
|
||||
drawcount = static_cast<GLsizei>(std::min<Uint32>(count, static_cast<Uint32>(drawcount)));
|
||||
if (!drawcount) return;
|
||||
@@ -917,12 +937,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void DispatchComputeIndirect(GLintptr indirect) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-SV: THE GPU READS THE GROUP COUNTS on the wire arm too - VulkanRenderer's
|
||||
// DispatchComputeIndirect takes its transport branch into DispatchWireComputeIndirect
|
||||
// (WireDraw.inc), which issues vkCmdDispatchIndirect from the verb's store. The CPU read
|
||||
// that was here (ReadWireBuffer, a whole-GPU wait once a shader had written the store) is gone.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
Uint32 groups[3]{};
|
||||
if (indirect >= 0 && pVulkanRenderer->GetWireBufferManager().ReadWireBuffer(
|
||||
MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer,
|
||||
static_cast<Uint64>(indirect), sizeof(groups), groups))
|
||||
pVulkanRenderer->DispatchCompute(groups[0], groups[1], groups[2]);
|
||||
pVulkanRenderer->DispatchComputeIndirect(indirect);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -1741,4 +1761,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_Util::PipeStats::OnPresent();
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
Optional<Int> g_requestedSwapInterval;
|
||||
} // namespace
|
||||
|
||||
void SetSwapInterval(Int interval) {
|
||||
g_requestedSwapInterval = interval;
|
||||
if (pVulkanRenderer) {
|
||||
pVulkanRenderer->SetSwapInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Int> GetRequestedSwapInterval() {
|
||||
return g_requestedSwapInterval;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void ForgetRequestedSwapInterval() {
|
||||
g_requestedSwapInterval.reset();
|
||||
}
|
||||
#endif
|
||||
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P11 M2: UNDER A TRANSPORT THE POOLS ARE THE SERVER RENDERER'S, and its apply thread
|
||||
// grows, rewinds and trims them while the test thread would be reading - a census taken
|
||||
// after a swap the client ran ahead of, or mid-burst, walks a set cache BeginFrame may be
|
||||
// clearing. Inproc, the server is a thread of this process: the read is posted to it as
|
||||
// the test probe, which runs between drain batches, so it sees every record published
|
||||
// before it and none half-applied - without waiting on the GPU, which is the point of the
|
||||
// trim case (it gives a burst back with other slots' work in flight). Spawn and tcp keep
|
||||
// their pools in another process; the client has no renderer and answers unavailable.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::InProcess) return DescriptorPoolCensus{};
|
||||
DescriptorPoolCensus census{};
|
||||
const MobileGLResult result = MG_Remote::Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
*static_cast<DescriptorPoolCensus*>(user) =
|
||||
pVulkanRenderer ? pVulkanRenderer->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
&census);
|
||||
return result == MOBILEGL_OK ? census : DescriptorPoolCensus{};
|
||||
}
|
||||
#endif
|
||||
return pVulkanRenderer ? pVulkanRenderer->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -147,4 +147,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
|
||||
Int64 GetGpuTimestampNs();
|
||||
void Present();
|
||||
// eglSwapInterval. Remembered across renderers: one created for a later window surface
|
||||
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
|
||||
void SetSwapInterval(Int interval);
|
||||
Optional<Int> GetRequestedSwapInterval();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The swap interval an ended server session asked for is not the next one's, the same rule as
|
||||
// DirectGLES::ForgetRequestedSwapInterval: under a transport ~BackendObject_DirectVulkan forgets
|
||||
// it, so the next session starts at the default until it asks.
|
||||
void ForgetRequestedSwapInterval();
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "SwapchainObject.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
|
||||
@@ -140,12 +142,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
|
||||
const Vector<VkPresentModeKHR>& availablePresentModes) {
|
||||
for (auto desiredPresentMode : s_desiredPresentModes) {
|
||||
for (const auto& presentMode : availablePresentModes) {
|
||||
if (presentMode == desiredPresentMode) {
|
||||
return presentMode;
|
||||
}
|
||||
const Vector<VkPresentModeKHR>& availablePresentModes, Optional<Int> swapInterval) {
|
||||
static constexpr VkPresentModeKHR s_unthrottledPresentModes[]{
|
||||
VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR};
|
||||
static constexpr VkPresentModeKHR s_vsyncPresentModes[]{VK_PRESENT_MODE_FIFO_KHR};
|
||||
static constexpr VkPresentModeKHR s_adaptiveVsyncPresentModes[]{VK_PRESENT_MODE_FIFO_RELAXED_KHR,
|
||||
VK_PRESENT_MODE_FIFO_KHR};
|
||||
std::span<const VkPresentModeKHR> ranked = s_desiredPresentModes;
|
||||
if (swapInterval) {
|
||||
if (*swapInterval == 0) {
|
||||
ranked = s_unthrottledPresentModes;
|
||||
} else if (*swapInterval > 0) {
|
||||
ranked = s_vsyncPresentModes;
|
||||
} else {
|
||||
ranked = s_adaptiveVsyncPresentModes;
|
||||
}
|
||||
}
|
||||
for (auto desiredPresentMode : ranked) {
|
||||
if (std::find(availablePresentModes.begin(), availablePresentModes.end(), desiredPresentMode) !=
|
||||
availablePresentModes.end()) {
|
||||
return desiredPresentMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,9 +169,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return availablePresentModes[0];
|
||||
}
|
||||
|
||||
const char* SwapchainObject::GetPresentModeName(VkPresentModeKHR presentMode) {
|
||||
return string_VkPresentModeKHR(presentMode);
|
||||
}
|
||||
|
||||
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
|
||||
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
|
||||
VkExtent2D desiredExtent) {
|
||||
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
|
||||
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
|
||||
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
|
||||
"SwapchainObject::Create failed: incomplete swapchain capabilities");
|
||||
@@ -174,8 +194,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes, swapInterval);
|
||||
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
|
||||
m_presentMode = presentMode;
|
||||
m_supportedPresentModes = swapchainCapabilities.presentModes;
|
||||
|
||||
const auto& swapchainCaps = swapchainCapabilities.capabilities;
|
||||
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
|
||||
|
||||
@@ -26,13 +26,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
|
||||
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
|
||||
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
|
||||
// swapInterval is the app's eglSwapInterval, empty until it asks (s_desiredPresentModes
|
||||
// ranks the modes then). 0 wants IMMEDIATE, else MAILBOX, else FIFO; >= 1 wants FIFO,
|
||||
// which cannot skip vblanks, so 2 and up pace like 1; a negative interval (adaptive
|
||||
// vsync) wants FIFO_RELAXED, else FIFO.
|
||||
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes,
|
||||
Optional<Int> swapInterval = Nullopt);
|
||||
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
|
||||
|
||||
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
|
||||
Optional<Int> swapInterval);
|
||||
void Shutdown(VkDevice device);
|
||||
|
||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||
VkPresentModeKHR GetPresentMode() const { return m_presentMode; }
|
||||
// The surface's present modes as queried when this swapchain was created.
|
||||
const Vector<VkPresentModeKHR>& GetSupportedPresentModes() const { return m_supportedPresentModes; }
|
||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||
VkExtent2D GetExtent() const { return m_extent; }
|
||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
||||
@@ -79,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||
VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||
Vector<VkPresentModeKHR> m_supportedPresentModes;
|
||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||
VkExtent2D m_extent{};
|
||||
VkExtent2D m_surfaceExtent{};
|
||||
|
||||
@@ -591,6 +591,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_samplerResolveMemoHighWater = 0;
|
||||
m_setsPerFrame = setsPerFrame;
|
||||
m_peakDescriptorSetsObserved = 0;
|
||||
m_trimQuietEpochs = MG_Config::Features.MagmaDescriptorTrimFrames;
|
||||
// The renderer records its first frame into slot 0 before any BeginFrame runs.
|
||||
m_epochSlot = 0;
|
||||
m_epochOpen = true;
|
||||
m_descriptorSlotTrims = 0;
|
||||
m_textureManager = textureManager;
|
||||
m_samplerManager = samplerManager;
|
||||
|
||||
@@ -600,6 +605,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.activeDescriptorPoolIndex = 0;
|
||||
frame.allocatedSetsThisFrame = 0;
|
||||
frame.peakAllocatedSetsThisFrame = 0;
|
||||
frame.capacitySets = m_setsPerFrame;
|
||||
frame.quietEpochs = 0;
|
||||
frame.descriptorPools.clear();
|
||||
|
||||
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
||||
@@ -654,6 +661,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.activeDescriptorPoolIndex = 0;
|
||||
frame.allocatedSetsThisFrame = 0;
|
||||
frame.peakAllocatedSetsThisFrame = 0;
|
||||
frame.capacitySets = 0;
|
||||
frame.quietEpochs = 0;
|
||||
}
|
||||
m_frames.clear();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -662,6 +671,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_wirePrivateStoragePlaceholders = 0;
|
||||
m_wireInvalidStorageImagesBindNull = false;
|
||||
#endif
|
||||
m_epochSlot = 0;
|
||||
m_epochOpen = false;
|
||||
|
||||
m_bufferManager = nullptr;
|
||||
m_programFactory = nullptr;
|
||||
@@ -701,6 +712,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"UniformDescriptorBinder: new descriptor set peak observed=%u (base setsPerFrame=%u, frame=%u, pools=%zu)",
|
||||
m_peakDescriptorSetsObserved, m_setsPerFrame, frameIndex, frame.descriptorPools.size());
|
||||
}
|
||||
// The epoch that just ended is the one of the slot begun last - this slot again on the
|
||||
// drain path, the previous frame's slot on the Present path. Its peak is still unreset.
|
||||
if (m_epochOpen && m_epochSlot < m_frames.size()) {
|
||||
NoteEndedEpochForTrim(m_frames[m_epochSlot].peakAllocatedSetsThisFrame);
|
||||
}
|
||||
m_epochSlot = frameIndex;
|
||||
m_epochOpen = true;
|
||||
// No pending command buffer references this slot's sets here (see TrimFrameDescriptorPools
|
||||
// for why that holds at both of BeginFrame's call sites), so a quiet grown slot can be
|
||||
// handed back whole.
|
||||
if (m_trimQuietEpochs != 0 && frame.descriptorPools.size() > 1 &&
|
||||
frame.capacitySets >= kDescriptorTrimMinCapacity && frame.quietEpochs >= m_trimQuietEpochs) {
|
||||
TrimFrameDescriptorPools(frame, frameIndex);
|
||||
}
|
||||
frame.activeDescriptorPoolIndex = 0;
|
||||
frame.allocatedSetsThisFrame = 0;
|
||||
frame.peakAllocatedSetsThisFrame = 0;
|
||||
@@ -709,11 +734,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
// The frame's descriptor sets are recycled above, so last frame's reuse targets
|
||||
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||
for (auto& entry : m_descriptorReuseMemo) {
|
||||
entry.valid = false;
|
||||
}
|
||||
m_fastRebindMemo.valid = false;
|
||||
m_lastBindValid = false;
|
||||
InvalidateDescriptorSetMemos();
|
||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||
// reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a
|
||||
// resolve has actually written can be valid, so the high-water mark bounds the
|
||||
@@ -747,10 +768,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.second.cursor = 0;
|
||||
}
|
||||
frame.allocatedSetsThisFrame = 0;
|
||||
frame.peakAllocatedSetsThisFrame = 0;
|
||||
for (auto& entry : m_descriptorReuseMemo) entry.valid = false;
|
||||
m_fastRebindMemo.valid = false;
|
||||
m_lastBindValid = false;
|
||||
// The PEAK is not reset: a rewind ends a window of the epoch, not the epoch. The trim scores
|
||||
// an epoch by the most sets the slot held at once in it (NoteEndedEpochForTrim at the next
|
||||
// BeginFrame), and on this arm that is the largest window - resetting here would score a
|
||||
// burst frame by its last window alone and trim a slot that re-grows every frame.
|
||||
InvalidateDescriptorSetMemos();
|
||||
const Uint32 touchedBindings =
|
||||
std::min<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
|
||||
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
|
||||
@@ -761,6 +783,68 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
#endif
|
||||
|
||||
void UniformManager::InvalidateDescriptorSetMemos() {
|
||||
// Every VkDescriptorSet handle held outside the per-layout caches - in this backend they all
|
||||
// live in this class. A trim can hand a destroyed set's handle value to a fresh allocation,
|
||||
// so the bind-dedup shadow must drop too, not only the two reuse memos.
|
||||
for (auto& entry : m_descriptorReuseMemo) {
|
||||
entry.valid = false;
|
||||
}
|
||||
m_fastRebindMemo.valid = false;
|
||||
m_lastBindValid = false;
|
||||
}
|
||||
|
||||
void UniformManager::NoteEndedEpochForTrim(Uint32 epochPeakSets) {
|
||||
for (auto& slot : m_frames) {
|
||||
if (static_cast<Uint64>(epochPeakSets) * kDescriptorTrimHeadroom > slot.capacitySets) {
|
||||
slot.quietEpochs = 0;
|
||||
} else if (slot.quietEpochs < std::numeric_limits<Uint32>::max()) {
|
||||
++slot.quietEpochs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UniformManager::TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex) {
|
||||
const SizeT poolsBefore = frame.descriptorPools.size();
|
||||
const Uint64 capacityBefore = frame.capacitySets;
|
||||
// Holders first: the per-layout cache and every memo that may name one of these sets.
|
||||
frame.descriptorSetCacheByLayout.clear();
|
||||
InvalidateDescriptorSetMemos();
|
||||
for (SizeT i = 1; i < frame.descriptorPools.size(); ++i) {
|
||||
if (frame.descriptorPools[i].handle != VK_NULL_HANDLE) {
|
||||
vkDestroyDescriptorPool(m_device, frame.descriptorPools[i].handle, nullptr);
|
||||
}
|
||||
}
|
||||
frame.descriptorPools.resize(1);
|
||||
auto& base = frame.descriptorPools[0];
|
||||
// Frees every set allocated from it; the spec defines no failure for vkResetDescriptorPool.
|
||||
vkResetDescriptorPool(m_device, base.handle, 0);
|
||||
base.allocatedSets = 0;
|
||||
frame.activeDescriptorPoolIndex = 0;
|
||||
frame.capacitySets = base.maxSets;
|
||||
frame.quietEpochs = 0;
|
||||
++m_descriptorSlotTrims;
|
||||
MGLOG_D("UniformDescriptorBinder: frame %u trimmed its descriptor pools (%zu pools / %llu sets -> 1 / %u)",
|
||||
frameIndex, poolsBefore, static_cast<unsigned long long>(capacityBefore), base.maxSets);
|
||||
}
|
||||
|
||||
DescriptorPoolCensus UniformManager::GetDescriptorPoolCensus() const {
|
||||
DescriptorPoolCensus census;
|
||||
census.available = true;
|
||||
census.frameSlots = static_cast<uint32_t>(m_frames.size());
|
||||
for (const auto& frame : m_frames) {
|
||||
const auto poolCount = static_cast<uint32_t>(frame.descriptorPools.size());
|
||||
census.pools += poolCount;
|
||||
census.maxPoolsInOneSlot = std::max(census.maxPoolsInOneSlot, poolCount);
|
||||
census.capacitySets += frame.capacitySets;
|
||||
for (const auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
|
||||
census.cachedSets += cacheEntryPair.second.sets.size();
|
||||
}
|
||||
}
|
||||
census.slotTrims = m_descriptorSlotTrims;
|
||||
return census;
|
||||
}
|
||||
|
||||
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
|
||||
SizeT purgedSets = 0;
|
||||
for (auto& frame : m_frames) {
|
||||
@@ -2700,9 +2784,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
MOBILEGL_ASSERT(bufferObject != nullptr,
|
||||
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
|
||||
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
||||
static thread_local Vector<Uint8> paddedUbo;
|
||||
if (bufferObject == nullptr) {
|
||||
// A block the program declares at a point with nothing bound - never bound, or its
|
||||
// buffer was deleted while bound, which unbinds it (UnboundUniformBlockScenario,
|
||||
// DeletedBoundBufferScenario). GL leaves what the shader READS undefined and raises no
|
||||
// error; this used to dereference the null object. Answer it with a zeroed block of the
|
||||
// reflected size, as the wire arm does (ResolveWireUniformBufferPayload).
|
||||
const VkDeviceSize blockSize =
|
||||
static_cast<VkDeviceSize>(program.GetUBOSizeAt(static_cast<Uint32>(blockIndex)));
|
||||
paddedUbo.assign(static_cast<SizeT>(blockSize > 0 ? blockSize : 16), 0);
|
||||
out.payload = paddedUbo.data();
|
||||
out.payloadSize = static_cast<VkDeviceSize>(paddedUbo.size());
|
||||
return true;
|
||||
}
|
||||
bufferObject->SyncPersistentMappedRange();
|
||||
|
||||
MOBILEGL_ASSERT(bufferObject->MappedData() != nullptr && bufferObject->GetSize() != 0,
|
||||
@@ -2735,7 +2830,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outSize = blockSize;
|
||||
outData = bufferObject->MappedData() + static_cast<SizeT>(rangeStart);
|
||||
if (available < blockSize) {
|
||||
static thread_local Vector<Uint8> paddedUbo;
|
||||
paddedUbo.assign(static_cast<SizeT>(blockSize), 0);
|
||||
Memcpy(paddedUbo.data(), outData, static_cast<SizeT>(available));
|
||||
outData = paddedUbo.data();
|
||||
@@ -2831,32 +2925,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Uint32 UniformManager::NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
|
||||
Uint32 initialMaxSets) {
|
||||
const Uint32 minSets = std::max<Uint32>(1, initialMaxSets);
|
||||
const Uint32 maxSets = std::max(minSets, kMaxDescriptorPoolSets);
|
||||
Uint32 largest = 0;
|
||||
for (const auto& pool : pools) {
|
||||
if (pool.updateAfterBind == updateAfterBind) {
|
||||
largest = std::max(largest, pool.maxSets);
|
||||
}
|
||||
}
|
||||
if (largest == 0) {
|
||||
return minSets;
|
||||
}
|
||||
const Uint64 doubled = static_cast<Uint64>(largest) * 2;
|
||||
return static_cast<Uint32>(std::clamp<Uint64>(doubled, minSets, maxSets));
|
||||
}
|
||||
|
||||
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
|
||||
if (frame.descriptorPools.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto matchingBucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
|
||||
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
|
||||
? std::max<Uint32>(1, matchingBucket->maxSets)
|
||||
: m_setsPerFrame;
|
||||
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
|
||||
: currentMaxSets;
|
||||
|
||||
Uint32 grownMaxSets = NextDescriptorPoolMaxSets(frame.descriptorPools, updateAfterBind, m_setsPerFrame);
|
||||
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||
currentMaxSets, grownMaxSets);
|
||||
return false;
|
||||
// A large pool is a large driver allocation; if the device refuses one, settle for less
|
||||
// rather than failing the draw - halving down to the base size, where the old policy lived.
|
||||
while (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
||||
if (grownMaxSets <= m_setsPerFrame) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u sets)",
|
||||
grownMaxSets);
|
||||
return false;
|
||||
}
|
||||
grownMaxSets = std::max(m_setsPerFrame, grownMaxSets / 2);
|
||||
}
|
||||
|
||||
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
|
||||
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
||||
MGLOG_D(
|
||||
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
|
||||
frameIndex, currentMaxSets, grownMaxSets, frame.descriptorPools.size());
|
||||
frame.capacitySets += grownMaxSets;
|
||||
// Growing is the opposite of quiet: a trim must wait a full quiet run from here.
|
||||
frame.quietEpochs = 0;
|
||||
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool exhausted, grew a %u-set pool (updateAfterBind=%d), "
|
||||
"poolCount=%zu",
|
||||
frameIndex, grownMaxSets, updateAfterBind ? 1 : 0, frame.descriptorPools.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "VkSamplerManager.h"
|
||||
#include "VkTextureManager.h"
|
||||
#include "WirePlaceholderKey.h"
|
||||
#include "../DescriptorPoolCensus.h"
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
|
||||
@@ -84,9 +85,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
||||
// references its sets. This is the only eviction path for the per-layout
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
// references its sets. This is the only PER-LAYOUT eviction path for the
|
||||
// caches - a live layout's entry must never be purged on its own (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here. The
|
||||
// other path, TrimFrameDescriptorPools, drops a slot's whole cache together with
|
||||
// the pools behind it.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
|
||||
// in binding order, and within a binding in array-element order): the resolved texture
|
||||
@@ -158,13 +161,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
private:
|
||||
// ---- per-frame descriptor pools ----------------------------------------------------
|
||||
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
|
||||
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
|
||||
// added by GrowFrameDescriptorPool when an allocation found no room. A pool serves one
|
||||
// updateAfterBind flavour, because a set can only come from a pool created with the flag
|
||||
// its layout was.
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
Uint32 maxSets = 0;
|
||||
Uint32 allocatedSets = 0;
|
||||
Bool updateAfterBind = false;
|
||||
};
|
||||
// One pool's maxSets never exceeds this. It bounds one pool's driver memory: CreateDescriptorPool
|
||||
// declares 8 descriptors of each of 6 types per set, so a 4096-set pool asks for 196,608
|
||||
// descriptors - about 12 MiB at 64 bytes each on a driver that backs every declared one.
|
||||
static constexpr Uint32 kMaxDescriptorPoolSets = 4096;
|
||||
// The growth policy, pure so the host tests can drive it: the size of the next pool a slot
|
||||
// adds for `updateAfterBind` layouts, given the slot's existing pools. Twice the LARGEST pool
|
||||
// of that flavour, capped at kMaxDescriptorPoolSets; `initialMaxSets` when the slot has no
|
||||
// pool of that flavour yet. So a frame that needs N sets adds O(log N) pools while N stays
|
||||
// under the cap, and one more pool per kMaxDescriptorPoolSets sets beyond it. Taking the
|
||||
// largest (not the first) pool is the point: the first same-flavour pool is the oldest,
|
||||
// smallest one, and doubling IT made every grown pool the same size.
|
||||
static Uint32 NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
|
||||
Uint32 initialMaxSets);
|
||||
// Trim policy (see TrimFrameDescriptorPools). A slot is "quiet" in an epoch whose peak set
|
||||
// count times kDescriptorTrimHeadroom still fits its capacity; a slot that has been quiet for
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES consecutive epochs, and holds at least
|
||||
// kDescriptorTrimMinCapacity sets, is trimmed. The headroom is what keeps the trim from
|
||||
// thrashing: geometric growth leaves a re-grown slot at least a third full (the pool a growth
|
||||
// adds is at most twice the largest before it, so at most twice everything the slot held, and
|
||||
// the epoch that grew it needed more than that), so the epoch that grew a slot is never quiet
|
||||
// for it. The floor keeps a slot sized for ordinary
|
||||
// frames from being trimmed at all - the trim exists to give back a burst.
|
||||
static constexpr Uint32 kDescriptorTrimHeadroom = 4;
|
||||
static constexpr Uint32 kDescriptorTrimMinCapacity = 1024;
|
||||
// For tests (DescriptorPoolCensus.h): pools, capacity, cached sets and trims over every slot.
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus() const;
|
||||
|
||||
private:
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
||||
@@ -239,6 +275,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
|
||||
#endif
|
||||
|
||||
// descriptorSetCacheByLayout is the per-frame set cache: BeginFrame rewinds every cursor, so
|
||||
// a slot re-writes the sets it allocated in earlier frames instead of allocating new ones, and
|
||||
// the cache only ever grows - except through TrimFrameDescriptorPools, which gives a slot's
|
||||
// whole burst back at once (and OnDescriptorSetLayoutDestroyed, one dead layout at a time).
|
||||
struct FrameResources {
|
||||
Vector<DescriptorPoolBucket> descriptorPools;
|
||||
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
|
||||
@@ -252,6 +292,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 activeDescriptorPoolIndex = 0;
|
||||
Uint32 allocatedSetsThisFrame = 0;
|
||||
Uint32 peakAllocatedSetsThisFrame = 0;
|
||||
// Sum of maxSets over descriptorPools, kept in step by Initialize, Grow and Trim.
|
||||
Uint64 capacitySets = 0;
|
||||
// Consecutive epochs (see NoteEndedEpochForTrim) that were quiet for THIS slot's capacity.
|
||||
Uint32 quietEpochs = 0;
|
||||
};
|
||||
|
||||
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
|
||||
@@ -395,6 +439,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Vector<Uint32>& dynamicOffsets);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||
// An epoch is the stretch between two BeginFrame calls: one frame, or the part of one between
|
||||
// two drains (VulkanRenderer::TryDrainFrameTransients). Called by BeginFrame with the epoch
|
||||
// that just ended, it updates every slot's quiet count - a burst in any slot counts against
|
||||
// all of them, so bursts that land in the other slots still hold a slot's trim off.
|
||||
void NoteEndedEpochForTrim(Uint32 epochPeakSets);
|
||||
// Gives a slot's whole descriptor burst back: clears its per-layout cache, destroys every
|
||||
// grown pool and resets the base pool, so the slot re-grows geometrically from setsPerFrame.
|
||||
// Whole-slot rather than per-set: vkFreeDescriptorSets fragments a pool and can never empty
|
||||
// a grown one. Called only from BeginFrame, which runs only when no pending command buffer
|
||||
// references the slot's sets:
|
||||
// - Present path (VulkanRenderer::Present, after WaitAndAcquireNextImage): the slot's
|
||||
// imageInFlightFence has been waited. That fence rides the slot's LAST submission - the
|
||||
// Present submit - and every mid-frame submission of the slot (FlushPendingCommands,
|
||||
// SplitOversizedRecording, readbacks) went to the same queue earlier, so the fence's
|
||||
// signal covers them (vkQueueSubmit's fence scope includes all earlier submissions).
|
||||
// - Drain path (VulkanRenderer::TryDrainFrameTransients): every submission has been
|
||||
// observed complete and no recording is open.
|
||||
// Sets are per slot: a slot's sets are only ever bound into recordings made while it is the
|
||||
// current slot, so trimming one slot never touches another slot's in-flight work.
|
||||
void TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex);
|
||||
// Drops every handle this class holds to a descriptor set outside the per-layout caches.
|
||||
void InvalidateDescriptorSetMemos();
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||
@@ -412,6 +478,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_maxBindings = 0;
|
||||
Uint32 m_setsPerFrame = 0;
|
||||
Uint32 m_peakDescriptorSetsObserved = 0;
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES, latched at Initialize (0 = never trim).
|
||||
Uint32 m_trimQuietEpochs = 0;
|
||||
// The slot whose epoch is open (the last one BeginFrame was called for); valid once
|
||||
// m_epochOpen is set. Its peak is the ended epoch's peak at the next BeginFrame.
|
||||
Uint32 m_epochSlot = 0;
|
||||
Bool m_epochOpen = false;
|
||||
Uint64 m_descriptorSlotTrims = 0;
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
|
||||
@@ -15,7 +15,14 @@
|
||||
#include "MG_Pipe/MGPipeCallbacks.h"
|
||||
#include "MG_Pipe/PipeApply.h"
|
||||
#include "MG_Remote/Server/StagedShadow.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "MG_Remote/Transport/AdoptT0.h"
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
#include <android/hardware_buffer.h>
|
||||
#endif
|
||||
#include <Config.h>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
|
||||
@@ -206,6 +213,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// modified ranges. A server pointer is never donated across the wire.
|
||||
.MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; },
|
||||
.UnmapPersistent = [](MG_Pipe::MGPipeHandle) {},
|
||||
#if defined(__ANDROID__)
|
||||
// P11 B2 (T0): the one door a server pointer is still never donated through stays shut;
|
||||
// what T0 adds is the other direction - the CLIENT's pages imported as the store.
|
||||
.ImportExternal = [](MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) -> Bool {
|
||||
return WireManager().ImportWireBuffer(res, ahb, size);
|
||||
},
|
||||
.SelfTestExternal = [](char* why, Uint64 whyBytes) -> Bool {
|
||||
return VkBufferManager::SelfTestWireImport(why, whyBytes);
|
||||
},
|
||||
#endif
|
||||
};
|
||||
#endif
|
||||
} // namespace
|
||||
@@ -245,11 +262,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// to wait for a submission (DeferredWireRelease), and three lines from now the record
|
||||
// no longer carries it.
|
||||
const Uint64 orphanedUseSerial = resource->lastUseSerial;
|
||||
DeferWireRelease(std::move(resource->buffer), orphanedUseSerial);
|
||||
// P11 B2: a T0 store is ALWAYS released through the serial-gated list (never "at once"),
|
||||
// so the client's AHardwareBuffer outlives every submission that can still name it however
|
||||
// the client's own release (at this respecify) raced the frames in flight.
|
||||
DeferWireRelease(std::move(resource->buffer),
|
||||
resource->imported ? T0ReleaseSerial(orphanedUseSerial) : orphanedUseSerial);
|
||||
resource->imported = false;
|
||||
resource->size = desc.Width;
|
||||
resource->lastUseSerial = 0;
|
||||
resource->lastUseSubmitIndex = 0; // M2 r2: the new store carries no submission yet (B3's stamp is per store)
|
||||
resource->gpuWritesPending = false;
|
||||
resource->indirectReadBarrierPending = false;
|
||||
resource->stagedCoverage.clear();
|
||||
++m_sliceEpochCounter;
|
||||
if (resource->size == 0) return;
|
||||
@@ -294,7 +317,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &barrier, 0, nullptr, 0, nullptr);
|
||||
if (!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
|
||||
// P8-D: counted here, past the idle early-out, so `whw` is waits TAKEN, not calls.
|
||||
++m_wireHostWaits;
|
||||
const Bool timed = MG_Util::PipeStats::Enabled();
|
||||
const auto waitStart = timed ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
const Bool waited =
|
||||
pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true);
|
||||
if (timed) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaits, 1);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitMicros,
|
||||
static_cast<Uint64>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - waitStart).count()));
|
||||
}
|
||||
if (!waited) {
|
||||
return false;
|
||||
}
|
||||
resource.lastUseSerial = 0;
|
||||
@@ -537,11 +572,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferManager::ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) {
|
||||
Vector<Uint8> bytes(static_cast<SizeT>(size));
|
||||
if (!ReadWireBuffer(res, offset, size, bytes.data())) {
|
||||
// P11 B2: A T0 STORE'S READBACK IS "DONE, NO BYTES". Its pages are the client's
|
||||
// AHardwareBuffer, so what the client needs before it reads them is this server's GPU
|
||||
// having finished every write to them - the resident copies WriteWireBuffer recorded and
|
||||
// any shader write - and nothing crossing SEG_EVENT. The wait is the store's own host-access
|
||||
// wait (a barrier to host, then the sync point's submission); the reply then says "done".
|
||||
// One death for both shapes: the store could not be made readable.
|
||||
auto* resource = FindWireBuffer(res);
|
||||
const Bool t0 = resource != nullptr && resource->imported;
|
||||
Vector<Uint8> bytes(t0 ? 0 : static_cast<SizeT>(size));
|
||||
if (t0 ? !WaitForWireBufferHostAccess(*resource) : !ReadWireBuffer(res, offset, size, bytes.data())) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
if (t0) {
|
||||
++m_sliceEpochCounter;
|
||||
return;
|
||||
}
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-writeback-callback\"}");
|
||||
std::abort();
|
||||
@@ -561,6 +608,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
resource->stagedCoverage.clear();
|
||||
resource->gpuWritesPending = true;
|
||||
resource->indirectReadBarrierPending = true;
|
||||
++m_sliceEpochCounter;
|
||||
// The existing client dirty-state protocol accepts one whole-store
|
||||
// notification. The store remains on the GPU and exact later SubData
|
||||
@@ -573,10 +621,312 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range);
|
||||
}
|
||||
|
||||
Bool VkBufferManager::TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || !resource->indirectReadBarrierPending) return false;
|
||||
resource->indirectReadBarrierPending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A WIRE STORE -----------------------------
|
||||
|
||||
// The serial a T0 store is released under: its last use, or - when no GPU command has named it
|
||||
// since a host-access wait - the current frame's, so the release still waits for the sync
|
||||
// point rather than taking DeferWireRelease's "never named, destroy now" arm. Conservative by
|
||||
// at most one submission.
|
||||
Uint64 VkBufferManager::T0ReleaseSerial(Uint64 lastUseSerial) const {
|
||||
if (lastUseSerial != 0) return lastUseSerial;
|
||||
return m_frameSerial != 0 ? m_frameSerial : 1;
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
namespace {
|
||||
Int32 T0PickMemoryType(const VkPhysicalDeviceMemoryProperties& props, Uint32 bits,
|
||||
VkMemoryPropertyFlags want) {
|
||||
for (Uint32 i = 0; i < props.memoryTypeCount; ++i) {
|
||||
if ((bits & (1u << i)) != 0 && (props.memoryTypes[i].propertyFlags & want) == want)
|
||||
return static_cast<Int32>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct T0ImportedBuffer {
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
void* mapped = nullptr;
|
||||
Bool coherent = true;
|
||||
};
|
||||
|
||||
void T0DestroyImported(VkDevice device, T0ImportedBuffer& imported) {
|
||||
if (imported.mapped != nullptr) vkUnmapMemory(device, imported.memory);
|
||||
if (imported.buffer != VK_NULL_HANDLE) vkDestroyBuffer(device, imported.buffer, nullptr);
|
||||
if (imported.memory != VK_NULL_HANDLE) vkFreeMemory(device, imported.memory, nullptr);
|
||||
imported = T0ImportedBuffer{};
|
||||
}
|
||||
|
||||
// A VkBuffer on a dedicated allocation IMPORTED from `ahb`, in a host-visible memory type
|
||||
// (coherent preferred), mapped whole. The server's own host map is what WriteWireBuffer's
|
||||
// idle path and every CPU reader of a wire store use, so a type without HOST_VISIBLE is a
|
||||
// refusal, not a degraded import. Nothing is left behind on failure.
|
||||
Bool T0ImportAhb(const VulkanRenderer::WireAhbImport& ctx, void* ahb, Uint64 size, VkBufferUsageFlags usage,
|
||||
T0ImportedBuffer& out, String& why) {
|
||||
out = T0ImportedBuffer{};
|
||||
const auto getProperties =
|
||||
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
|
||||
VkAndroidHardwareBufferPropertiesANDROID properties{
|
||||
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
|
||||
VkResult result = getProperties(ctx.device, static_cast<const AHardwareBuffer*>(ahb), &properties);
|
||||
if (result != VK_SUCCESS) {
|
||||
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
|
||||
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
|
||||
VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
bufferInfo.pNext = &external;
|
||||
bufferInfo.size = size;
|
||||
bufferInfo.usage = usage;
|
||||
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
result = vkCreateBuffer(ctx.device, &bufferInfo, nullptr, &out.buffer);
|
||||
if (result != VK_SUCCESS) {
|
||||
out.buffer = VK_NULL_HANDLE;
|
||||
why = "vkCreateBuffer(external AHB) -> " + std::to_string(static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
Int32 type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
out.coherent = type >= 0;
|
||||
if (type < 0)
|
||||
type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
|
||||
if (type < 0) {
|
||||
why = "no host-visible memory type can import it (memoryTypeBits 0x" +
|
||||
std::to_string(properties.memoryTypeBits) + ")";
|
||||
T0DestroyImported(ctx.device, out);
|
||||
return false;
|
||||
}
|
||||
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
||||
dedicated.buffer = out.buffer;
|
||||
VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
|
||||
import.buffer = static_cast<AHardwareBuffer*>(ahb);
|
||||
import.pNext = &dedicated;
|
||||
VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocateInfo.pNext = &import;
|
||||
allocateInfo.allocationSize = properties.allocationSize;
|
||||
allocateInfo.memoryTypeIndex = static_cast<Uint32>(type);
|
||||
result = vkAllocateMemory(ctx.device, &allocateInfo, nullptr, &out.memory);
|
||||
if (result != VK_SUCCESS) {
|
||||
out.memory = VK_NULL_HANDLE;
|
||||
why = "vkAllocateMemory(import AHB) -> " + std::to_string(static_cast<int>(result));
|
||||
T0DestroyImported(ctx.device, out);
|
||||
return false;
|
||||
}
|
||||
result = vkBindBufferMemory(ctx.device, out.buffer, out.memory, 0);
|
||||
if (result == VK_SUCCESS) result = vkMapMemory(ctx.device, out.memory, 0, VK_WHOLE_SIZE, 0, &out.mapped);
|
||||
if (result != VK_SUCCESS || out.mapped == nullptr) {
|
||||
out.mapped = nullptr;
|
||||
why = "vkBindBufferMemory/vkMapMemory -> " + std::to_string(static_cast<int>(result));
|
||||
T0DestroyImported(ctx.device, out);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
Bool VkBufferManager::ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
|
||||
#if defined(__ANDROID__)
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (resource == nullptr || ahb == nullptr || size == 0 || size != resource->size) return false;
|
||||
VulkanRenderer::WireAhbImport ctx;
|
||||
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) {
|
||||
MGLOG_W_ONCE("Magma T0: this device did not take VK_ANDROID_external_memory_android_hardware_buffer");
|
||||
return false;
|
||||
}
|
||||
VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
|
||||
T0ImportedBuffer imported;
|
||||
String why;
|
||||
if (!T0ImportAhb(ctx, ahb, size, usage, imported, why)) {
|
||||
MGLOG_W("Magma T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen,
|
||||
static_cast<unsigned long long>(size), why.c_str());
|
||||
return false;
|
||||
}
|
||||
// The T2 store it replaces goes the way a respecify's does (and any draw still naming it
|
||||
// keeps it until its submission completes).
|
||||
DeferWireRelease(std::move(resource->buffer), resource->lastUseSerial);
|
||||
MG_Remote::Transport::AdoptT0::AcquireImported(ahb);
|
||||
resource->buffer.AdoptExternal(ctx.device, imported.buffer, imported.memory, size, imported.mapped,
|
||||
imported.coherent, ahb);
|
||||
resource->imported = true;
|
||||
resource->lastUseSerial = 0;
|
||||
resource->lastUseSubmitIndex = 0;
|
||||
resource->gpuWritesPending = false;
|
||||
// The whole store holds bytes the client supplied (its AHB, seeded from its shadow).
|
||||
resource->stagedCoverage.clear();
|
||||
MG_Remote::Server::StagedShadowStore::CoverageAdd(resource->stagedCoverage, 0, static_cast<SizeT>(size));
|
||||
++m_sliceEpochCounter;
|
||||
++m_wireStoreCount;
|
||||
NoteWireStorePeaks();
|
||||
PublishWireReclaimGauges();
|
||||
MGLOG_D("Magma T0: imported {slot=%u, gen=%u} (%llu bytes, %s memory)", res.Slot, res.Gen,
|
||||
static_cast<unsigned long long>(size), imported.coherent ? "coherent" : "non-coherent");
|
||||
return true;
|
||||
#else
|
||||
(void)res;
|
||||
(void)ahb;
|
||||
(void)size;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE POST (B2; decided by probe, never by driver name). The AHB API promises CPU coherence only
|
||||
// across an unlock/lock, and T0 holds the client's lock for the store's life, so this exercises
|
||||
// that pattern on THIS device before kCapAdoptT0 may be published:
|
||||
// hold - a 64 KiB BLOB allocated and locked by the test, never unlocked while it runs;
|
||||
// read - this server's own host map of the import reads what the CPU wrote through the lock;
|
||||
// gpu - one submission copies that pattern out of the import (the GPU read) and fills another
|
||||
// region of it (the GPU write), then a transfer->host barrier and a fence wait;
|
||||
// back - the fill is read through the HELD pointer, the copy from a host-visible staging buffer.
|
||||
Bool VkBufferManager::SelfTestWireImport(char* whyOut, Uint64 whyBytes) {
|
||||
const auto answer = [&](Bool ok, const String& text) {
|
||||
std::snprintf(whyOut, static_cast<SizeT>(whyBytes), "%s", text.c_str());
|
||||
return ok;
|
||||
};
|
||||
#if defined(__ANDROID__)
|
||||
VulkanRenderer::WireAhbImport ctx;
|
||||
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx))
|
||||
return answer(false, "the device did not take VK_ANDROID_external_memory_android_hardware_buffer");
|
||||
constexpr Uint64 kBytes = 64 * 1024;
|
||||
constexpr Uint32 kWords = 1024; // one 4 KiB region
|
||||
constexpr Uint64 kFillAt = 4 * 4096; // region 4
|
||||
constexpr Uint32 kFillWord = 0x6C0FFEE0u;
|
||||
Vector<Uint32> pattern(kBytes / 4);
|
||||
for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast<Uint32>(i * 2654435761u);
|
||||
MG_Remote::Transport::AdoptT0::HeldStore held;
|
||||
String why;
|
||||
if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why))
|
||||
return answer(false, "hold: " + why);
|
||||
T0ImportedBuffer imported, staging;
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
String verdict;
|
||||
Bool ok = false;
|
||||
do {
|
||||
if (!T0ImportAhb(ctx, held.ahb, kBytes,
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
|
||||
imported, why)) {
|
||||
verdict = "import: " + why;
|
||||
break;
|
||||
}
|
||||
if (!imported.coherent) {
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = imported.memory;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
vkInvalidateMappedMemoryRanges(ctx.device, 1, &range);
|
||||
}
|
||||
if (std::memcmp(imported.mapped, pattern.data(), kWords * 4) != 0) {
|
||||
verdict = "read: the server's host map of the import does not hold the CPU's pattern";
|
||||
break;
|
||||
}
|
||||
// A host-visible staging buffer for the GPU read's destination.
|
||||
VkBufferCreateInfo stagingInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
stagingInfo.size = kWords * 4;
|
||||
stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (vkCreateBuffer(ctx.device, &stagingInfo, nullptr, &staging.buffer) != VK_SUCCESS) {
|
||||
staging.buffer = VK_NULL_HANDLE;
|
||||
verdict = "staging buffer";
|
||||
break;
|
||||
}
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetBufferMemoryRequirements(ctx.device, staging.buffer, &requirements);
|
||||
const Int32 stagingType = T0PickMemoryType(ctx.memory, requirements.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
VkMemoryAllocateInfo stagingAlloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
stagingAlloc.allocationSize = requirements.size;
|
||||
stagingAlloc.memoryTypeIndex = static_cast<Uint32>(stagingType < 0 ? 0 : stagingType);
|
||||
if (stagingType < 0 || vkAllocateMemory(ctx.device, &stagingAlloc, nullptr, &staging.memory) != VK_SUCCESS ||
|
||||
vkBindBufferMemory(ctx.device, staging.buffer, staging.memory, 0) != VK_SUCCESS ||
|
||||
vkMapMemory(ctx.device, staging.memory, 0, VK_WHOLE_SIZE, 0, &staging.mapped) != VK_SUCCESS) {
|
||||
verdict = "staging memory";
|
||||
break;
|
||||
}
|
||||
std::memset(staging.mapped, 0, kWords * 4);
|
||||
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
poolInfo.queueFamilyIndex = ctx.queueFamily;
|
||||
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
commandInfo.commandBufferCount = 1;
|
||||
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
if (vkCreateCommandPool(ctx.device, &poolInfo, nullptr, &pool) != VK_SUCCESS) {
|
||||
pool = VK_NULL_HANDLE;
|
||||
verdict = "command pool";
|
||||
break;
|
||||
}
|
||||
commandInfo.commandPool = pool;
|
||||
if (vkAllocateCommandBuffers(ctx.device, &commandInfo, &commands) != VK_SUCCESS ||
|
||||
vkCreateFence(ctx.device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
|
||||
verdict = "command buffer / fence";
|
||||
break;
|
||||
}
|
||||
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
vkBeginCommandBuffer(commands, &begin);
|
||||
VkMemoryBarrier hostWrites{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
hostWrites.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
|
||||
hostWrites.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
|
||||
&hostWrites, 0, nullptr, 0, nullptr);
|
||||
VkBufferCopy region{0, 0, kWords * 4};
|
||||
vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, ®ion);
|
||||
vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kFillWord);
|
||||
VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost,
|
||||
0, nullptr, 0, nullptr);
|
||||
vkEndCommandBuffer(commands);
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &commands;
|
||||
if (vkQueueSubmit(ctx.queue, 1, &submit, fence) != VK_SUCCESS ||
|
||||
vkWaitForFences(ctx.device, 1, &fence, VK_TRUE, 2'000'000'000ull) != VK_SUCCESS) {
|
||||
verdict = "gpu: the submission did not complete within 2 s";
|
||||
break;
|
||||
}
|
||||
Uint32 gpuRead = 0, heldSaw = 0;
|
||||
const auto* copied = static_cast<const Uint32*>(staging.mapped);
|
||||
const auto* heldWords = static_cast<const Uint32*>(held.ptr);
|
||||
for (Uint32 i = 0; i < kWords; ++i) {
|
||||
if (copied[i] == pattern[i]) ++gpuRead;
|
||||
if (heldWords[kFillAt / 4 + i] == kFillWord) ++heldSaw;
|
||||
}
|
||||
ok = gpuRead == kWords && heldSaw == kWords;
|
||||
verdict = "64 KiB AHB, lock held throughout: host map read ok (" +
|
||||
String(imported.coherent ? "coherent" : "non-coherent") + " memory), GPU read " +
|
||||
std::to_string(gpuRead) + "/1024 words of the CPU's pattern, GPU write seen through the "
|
||||
"held lock " + std::to_string(heldSaw) + "/1024 words";
|
||||
} while (false);
|
||||
if (fence != VK_NULL_HANDLE) vkDestroyFence(ctx.device, fence, nullptr);
|
||||
if (pool != VK_NULL_HANDLE) vkDestroyCommandPool(ctx.device, pool, nullptr);
|
||||
T0DestroyImported(ctx.device, staging);
|
||||
T0DestroyImported(ctx.device, imported);
|
||||
MG_Remote::Transport::AdoptT0::ReleaseHeld(held);
|
||||
return answer(ok, verdict);
|
||||
#else
|
||||
return answer(false, "no AHardwareBuffer on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) {
|
||||
const auto found = m_wireBuffers.find(WireBufferKey(res));
|
||||
if (found == m_wireBuffers.end()) return;
|
||||
DeferWireRelease(std::move(found->second.buffer), found->second.lastUseSerial);
|
||||
DeferWireRelease(std::move(found->second.buffer),
|
||||
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
|
||||
: found->second.lastUseSerial); // P11 B2, as Respecify
|
||||
m_wireBuffers.erase(found);
|
||||
++m_sliceEpochCounter;
|
||||
PublishWireReclaimGauges();
|
||||
|
||||
@@ -125,6 +125,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
|
||||
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
// P8-D: whether a shader write was marked on this store since the last indirect draw
|
||||
// read it natively, clearing the mark. The caller records the INDIRECT_COMMAND_READ
|
||||
// barrier before the draw when this answers true. Transfer writes need no mark: their own
|
||||
// after-barriers already make them visible to MEMORY_READ at ALL_COMMANDS.
|
||||
Bool TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res);
|
||||
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
|
||||
// the attribution base a caller reads before and after its own ReadWireBuffer.
|
||||
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
|
||||
|
||||
// Resource-op entry points. All run on the server apply owner.
|
||||
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
|
||||
@@ -134,6 +142,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
void DestroyWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
// P11 B2 (T0): make the client's AHardwareBuffer `ahb` (a BLOB of `size` bytes) buffer
|
||||
// `res`'s store - a VkBuffer on memory imported through
|
||||
// VK_ANDROID_external_memory_android_hardware_buffer, host-visible and mapped like every
|
||||
// wire store, so every wire path (draw binds, WriteWireBuffer's ordered copy, CPU readers)
|
||||
// applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not
|
||||
// imported (the T2 store is untouched and the client is DECLINED).
|
||||
Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size);
|
||||
// The POST self-test of the sustained-lock pattern on this device (see the definition).
|
||||
static Bool SelfTestWireImport(char* why, Uint64 whyBytes);
|
||||
#endif
|
||||
|
||||
// Recreate all per-frame transient arenas
|
||||
@@ -245,12 +262,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Only ranges actually submitted by resource_subdata are covered. No
|
||||
// shadow is retained: flush cannot replay stale bytes over GPU writes.
|
||||
Vector<Range1D> stagedCoverage;
|
||||
// P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts
|
||||
// no bytes (the client reads its own pages once the wait below it is done).
|
||||
Bool imported = false;
|
||||
// P8-D: a shader write was marked (MarkWireBufferGpuWritten) and no indirect draw has
|
||||
// recorded its INDIRECT_COMMAND_READ barrier since (TakeWireIndirectReadBarrier).
|
||||
Bool indirectReadBarrierPending = false;
|
||||
};
|
||||
// P8-D: see GetWireHostWaitCount.
|
||||
Uint64 m_wireHostWaits = 0;
|
||||
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
|
||||
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
|
||||
}
|
||||
WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
Bool WaitForWireBufferHostAccess(WireBufferResource& resource);
|
||||
// P11 B2: the serial a T0 store's release waits behind (never "destroy at once").
|
||||
Uint64 T0ReleaseSerial(Uint64 lastUseSerial) const;
|
||||
|
||||
// P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME
|
||||
// BOUNDARY.
|
||||
|
||||
@@ -8,13 +8,45 @@
|
||||
|
||||
#include "VkBufferObject.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Transport/AdoptT0.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void VkBufferObject::MoveExternalFrom(VkBufferObject& other) {
|
||||
m_device = other.m_device;
|
||||
m_externalMemory = other.m_externalMemory;
|
||||
m_externalAhb = other.m_externalAhb;
|
||||
m_externalCoherent = other.m_externalCoherent;
|
||||
other.m_device = VK_NULL_HANDLE;
|
||||
other.m_externalMemory = VK_NULL_HANDLE;
|
||||
other.m_externalAhb = nullptr;
|
||||
other.m_externalCoherent = true;
|
||||
}
|
||||
|
||||
void VkBufferObject::AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
|
||||
void* mapped, Bool coherent, void* ahb) {
|
||||
Destroy();
|
||||
m_device = device;
|
||||
m_buffer = buffer;
|
||||
m_externalMemory = memory;
|
||||
m_mappedData = mapped;
|
||||
m_size = size;
|
||||
m_externalCoherent = coherent;
|
||||
m_externalAhb = ahb;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
|
||||
m_allocator = other.m_allocator;
|
||||
m_buffer = other.m_buffer;
|
||||
m_allocation = other.m_allocation;
|
||||
m_mappedData = other.m_mappedData;
|
||||
m_size = other.m_size;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MoveExternalFrom(other);
|
||||
#endif
|
||||
|
||||
other.m_allocator = nullptr;
|
||||
other.m_buffer = VK_NULL_HANDLE;
|
||||
@@ -35,6 +67,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_allocation = other.m_allocation;
|
||||
m_mappedData = other.m_mappedData;
|
||||
m_size = other.m_size;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MoveExternalFrom(other);
|
||||
#endif
|
||||
|
||||
other.m_allocator = nullptr;
|
||||
other.m_buffer = VK_NULL_HANDLE;
|
||||
@@ -89,6 +124,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferObject::Destroy() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) {
|
||||
// P11 B2 (T0): an imported AHardwareBuffer store. Reached through the wire arm's
|
||||
// deferred release, i.e. once no submission can still name it.
|
||||
if (m_mappedData != nullptr) vkUnmapMemory(m_device, m_externalMemory);
|
||||
if (m_buffer != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_buffer, nullptr);
|
||||
vkFreeMemory(m_device, m_externalMemory, nullptr);
|
||||
MG_Remote::Transport::AdoptT0::ReleaseImported(m_externalAhb);
|
||||
m_mappedData = nullptr;
|
||||
m_buffer = VK_NULL_HANDLE;
|
||||
m_externalMemory = VK_NULL_HANDLE;
|
||||
m_externalAhb = nullptr;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_externalCoherent = true;
|
||||
m_size = 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
Unmap();
|
||||
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
|
||||
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
|
||||
@@ -117,6 +170,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferObject::Unmap() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// An imported store stays mapped for its life (Destroy unmaps it).
|
||||
if (m_externalMemory != VK_NULL_HANDLE) return;
|
||||
#endif
|
||||
if (!IsValid() || m_mappedData == nullptr) {
|
||||
m_mappedData = nullptr;
|
||||
return;
|
||||
@@ -143,6 +200,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) {
|
||||
if (m_externalCoherent) return true;
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = m_externalMemory;
|
||||
range.offset = 0;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||
if (flushResult != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
@@ -167,6 +234,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resolvedSize == 0) {
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) {
|
||||
if (m_externalCoherent) return true;
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = m_externalMemory;
|
||||
range.offset = 0;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
|
||||
@@ -64,7 +64,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void* GetMappedData() const { return m_mappedData; }
|
||||
Bool IsMapped() const { return m_mappedData != nullptr; }
|
||||
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
||||
Bool IsValid() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE;
|
||||
#endif
|
||||
return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer
|
||||
// imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a
|
||||
// dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves
|
||||
// like any mapped store (GetSlice, Upload, Invalidate), and Destroy frees the buffer and
|
||||
// the memory and drops `ahb` - which is why the store goes through the wire arm's
|
||||
// serial-gated deferred release like every other: the AHardwareBuffer is let go only once
|
||||
// no submission can still name it. `coherent` = the memory type is HOST_COHERENT.
|
||||
void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
|
||||
void* mapped, Bool coherent, void* ahb);
|
||||
Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
@@ -72,5 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaAllocation m_allocation = nullptr;
|
||||
void* m_mappedData = nullptr;
|
||||
VkDeviceSize m_size = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkDeviceMemory m_externalMemory = VK_NULL_HANDLE;
|
||||
void* m_externalAhb = nullptr;
|
||||
Bool m_externalCoherent = true;
|
||||
void MoveExternalFrom(VkBufferObject& other);
|
||||
#endif
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -1333,6 +1333,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::FindTextureResource(
|
||||
MG_State::GLState::ITextureObject& textureOrView) {
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
|
||||
return it != m_textureResources.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const {
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
|
||||
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const {
|
||||
auto& storage = StorageTextureOf(textureOrView);
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&storage));
|
||||
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
return it->second.syncedShapeVersion == storage.GetShapeVersion() &&
|
||||
it->second.syncedContentVersion != storage.GetContentVersion();
|
||||
}
|
||||
|
||||
Bool VkTextureManager::WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const {
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
|
||||
return it != m_textureResources.end() && it->second.lastRecordingWriteGeneration == m_recordingGeneration;
|
||||
}
|
||||
|
||||
void VkTextureManager::StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(*texture)));
|
||||
if (it != m_textureResources.end()) {
|
||||
StampResourceRecordingWrite(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
@@ -1361,7 +1397,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
|
||||
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
|
||||
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
|
||||
StampResourceRecordingUse(resource);
|
||||
StampResourceRecordingWrite(resource);
|
||||
|
||||
if (resource.layout != newLayout && resource.mipLevels > 1) {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
@@ -1475,8 +1511,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded, and an
|
||||
// image unit may store to it.
|
||||
StampResourceRecordingWrite(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -4077,18 +4114,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDeviceSize offset = 0;
|
||||
// Sub-region upload (a small sprite in a big atlas): only the dirty box
|
||||
// is staged and copied. texelSize keeps the LEVEL extent - the staging
|
||||
// row copy needs it for the shadow's stride. Plain color formats only;
|
||||
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
|
||||
// levels and stay full-size.
|
||||
// row copy needs it for the stride. Color formats only (an RGB-expanded
|
||||
// level is cut from its expanded bytes); the depth(+stencil) conversion
|
||||
// passes rewrite whole levels and stay full-size.
|
||||
Bool subRegion = false;
|
||||
IntVec3 regionLo = {0, 0, 0};
|
||||
IntVec3 regionSize = {0, 0, 0};
|
||||
SizeT texelBytes = 0;
|
||||
// Scatter refinement of the single dirty box: when the storage's rect
|
||||
// list reports the writes' true footprint (~100 sprites whose union box
|
||||
// spans the whole atlas), each rect is staged tightly and copied with
|
||||
// its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. Empty means
|
||||
// "stage the one box above". Only set while subRegion.
|
||||
// The writes' exact footprint when it is more than one box (~100 sprites
|
||||
// whose union box spans the whole atlas): each rect is staged tightly and
|
||||
// copied with its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage.
|
||||
// Empty means "stage the one box above". Only set while subRegion.
|
||||
Vector<MG_State::GLState::MipmapDirtyRegion> rects;
|
||||
};
|
||||
|
||||
@@ -4100,6 +4136,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
targets.push_back(uploadTarget);
|
||||
}
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(mipmapTexture.GetFormat());
|
||||
Vector<MG_State::GLState::MipmapDirtyRegion> dirtyFootprint;
|
||||
|
||||
VkDeviceSize stagingSize = 0;
|
||||
for (const TextureUploadTarget target : targets) {
|
||||
@@ -4142,34 +4179,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
uploadItem.source = source;
|
||||
uploadItem.offset = stagingSize;
|
||||
uploadItem.uploadByteSize = byteSize;
|
||||
if (!formatInfo.expandRgbToRgba &&
|
||||
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||
// The RGB -> RGBA expansion rewrites the level texel for texel into a tight buffer
|
||||
// of its own, so it runs first and the dirty region below is taken from whichever
|
||||
// bytes are staged. Uploading an expanded level WHOLE put the shadow's stale texels
|
||||
// over whatever the GPU had drawn into the rest of it.
|
||||
if (formatInfo.expandRgbToRgba) {
|
||||
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
|
||||
uploadItem.expandedData);
|
||||
MOBILEGL_ASSERT(expanded,
|
||||
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
|
||||
mipmapTexture.GetExternalIndex(),
|
||||
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
|
||||
uploadItem.uploadByteSize = uploadItem.expandedData.size();
|
||||
}
|
||||
const SizeT levelBytes = uploadItem.uploadByteSize;
|
||||
if (GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
|
||||
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
|
||||
static_cast<SizeT>(texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(texelSize.z(), 1));
|
||||
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
|
||||
byteSize % texelCount == 0) {
|
||||
// Exactly the texels the client wrote, never their union box or the storage's
|
||||
// rect list (which merges touching writes into their bounding box, folds when
|
||||
// full and is withheld past 3/4 of the box): the image may hold what the GPU
|
||||
// drew or cleared anywhere else, and the shadow is stale there. Each box is one
|
||||
// more VkBufferImageCopy in the same vkCmdCopyBufferToImage, which costs next
|
||||
// to nothing, so there is no cheaper shape worth being wrong for. A union box
|
||||
// that spans the level with gaps in it is not a whole-level write either.
|
||||
mipmapTexture.GetStorageDirtyFootprint(target, level, dirtyFootprint);
|
||||
if (!region.Empty() && (dirtyFootprint.size() >= 2 || !region.CoversWholeLevel(texelSize)) &&
|
||||
texelCount > 0 && levelBytes % texelCount == 0) {
|
||||
uploadItem.subRegion = true;
|
||||
uploadItem.regionLo = region.lo;
|
||||
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
|
||||
region.hi.z() - region.lo.z()};
|
||||
uploadItem.texelBytes = byteSize / texelCount;
|
||||
uploadItem.texelBytes = levelBytes / texelCount;
|
||||
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
|
||||
static_cast<SizeT>(uploadItem.regionSize.y()) *
|
||||
static_cast<SizeT>(uploadItem.regionSize.z()) *
|
||||
uploadItem.texelBytes;
|
||||
// Scatter refinement: the storage only hands out its rect list
|
||||
// when the rects' summed area is materially smaller than the
|
||||
// union box (0 otherwise), so taking it always stages fewer
|
||||
// bytes than the box - the very amplification this path exists
|
||||
// to avoid paying twice.
|
||||
MG_State::GLState::MipmapDirtyRegion
|
||||
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
const SizeT dirtyRectCount = mipmapTexture.GetStorageDirtyRects(
|
||||
target, level, dirtyRects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||
if (dirtyRectCount >= 2) {
|
||||
uploadItem.rects.assign(dirtyRects, dirtyRects + dirtyRectCount);
|
||||
// Several boxes: each is staged tightly and copied on its own. One box is
|
||||
// the union box above.
|
||||
if (dirtyFootprint.size() >= 2) {
|
||||
uploadItem.rects.assign(dirtyFootprint.begin(), dirtyFootprint.end());
|
||||
SizeT rectTexels = 0;
|
||||
for (const auto& rect : uploadItem.rects) {
|
||||
rectTexels += rect.TexelCount();
|
||||
@@ -4195,15 +4246,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formatInfo.expandRgbToRgba) {
|
||||
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
|
||||
uploadItem.expandedData);
|
||||
MOBILEGL_ASSERT(expanded,
|
||||
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
|
||||
mipmapTexture.GetExternalIndex(),
|
||||
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
|
||||
uploadItem.uploadByteSize = uploadItem.expandedData.size();
|
||||
}
|
||||
uploadItems.push_back(Move(uploadItem));
|
||||
if (!uploadItems.back().expandedData.empty()) {
|
||||
uploadItems.back().source = uploadItems.back().expandedData.data();
|
||||
@@ -4438,6 +4480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
Vector<VkBufferImageCopy> rectCopies;
|
||||
for (const auto& item : uploadItems) {
|
||||
if (!item.rects.empty()) {
|
||||
// Multi-rect item: one VkBufferImageCopy per rect, all submitted in a
|
||||
@@ -4445,7 +4488,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// construction, so no two copies write the same texels. Multi-rect
|
||||
// implies subRegion, which implies a plain color aspect - the combined
|
||||
// depth-stencil split below can never see one of these.
|
||||
VkBufferImageCopy rectCopies[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
rectCopies.resize(item.rects.size());
|
||||
Uint32 rectCopyCount = 0;
|
||||
VkDeviceSize runningOffset = item.offset;
|
||||
for (const auto& rect : item.rects) {
|
||||
@@ -4476,7 +4519,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
runningOffset += static_cast<VkDeviceSize>(rect.TexelCount() * item.texelBytes);
|
||||
}
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies);
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies.data());
|
||||
continue;
|
||||
}
|
||||
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
||||
|
||||
@@ -332,6 +332,11 @@ public:
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// The same, for the last recorded command that WRITES the image (a render pass it is an
|
||||
// attachment of, a clear, a blit/copy into it, a mip generation, a storage-image binding).
|
||||
// An upload into an image written this recording has to be ordered after that write
|
||||
// (VulkanRenderer::OrderPendingUploadAfterRecording).
|
||||
Uint64 lastRecordingWriteGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -381,6 +386,7 @@ public:
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->lastRecordingWriteGeneration, that.lastRecordingWriteGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||
@@ -623,6 +629,26 @@ public:
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// A write is a use too. See TextureResource::lastRecordingWriteGeneration.
|
||||
void StampResourceRecordingWrite(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
resource.lastRecordingWriteGeneration = m_recordingGeneration;
|
||||
}
|
||||
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
|
||||
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
|
||||
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
|
||||
//
|
||||
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
|
||||
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
|
||||
// before that write, which then overwrites the newer texels. (An image the recording only
|
||||
// read keeps the old trade: its earlier reads see the newer texels.)
|
||||
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The next sync would record client texels into the image that is live now: same shape (a
|
||||
// shape change mints a new image instead) and content moved since the last sync.
|
||||
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The live resource, or null.
|
||||
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
|
||||
@@ -2003,6 +2003,9 @@ void main() {
|
||||
outBinding.label, texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
if (!isReadFramebuffer) {
|
||||
textureManager.StampResourceRecordingWrite(*resource);
|
||||
}
|
||||
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
@@ -2119,6 +2122,9 @@ void main() {
|
||||
outBinding.label, texture->GetExternalIndex(), static_cast<Uint32>(requiredAspectMask));
|
||||
return false;
|
||||
}
|
||||
if (!isReadFramebuffer) {
|
||||
textureManager.StampResourceRecordingWrite(*resource);
|
||||
}
|
||||
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
@@ -2159,6 +2165,7 @@ void main() {
|
||||
texture.GetExternalIndex(), mipLevel, resource->mipLevels);
|
||||
return false;
|
||||
}
|
||||
textureManager.StampResourceRecordingWrite(*resource);
|
||||
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
@@ -3175,7 +3182,10 @@ void main() {
|
||||
}
|
||||
|
||||
VulkanRenderer::VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg)
|
||||
: m_window(window), m_config(cfg) {
|
||||
: m_window(window), m_config(cfg), m_presentsToAppWindow(window != NativeWindowType{}) {
|
||||
if (!m_presentsToAppWindow) {
|
||||
m_config.SwapInterval.reset();
|
||||
}
|
||||
// Initialize();
|
||||
}
|
||||
|
||||
@@ -3572,6 +3582,9 @@ void main() {
|
||||
m_device = VK_NULL_HANDLE;
|
||||
}
|
||||
s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
s_vkCmdWireDrawIndirectCount = nullptr;
|
||||
#endif
|
||||
s_vkCmdDrawMultiEXT = nullptr;
|
||||
s_vkCmdDrawMultiIndexedEXT = nullptr;
|
||||
|
||||
@@ -7073,6 +7086,10 @@ void main() {
|
||||
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView) {
|
||||
// First, before anything of this draw is recorded or resolved - on the wire arm too: a
|
||||
// split server records every draw its client sends into the same frame command buffer, so
|
||||
// the same loading frame grows it the same way there.
|
||||
SplitOversizedRecording();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
RewindWireDescriptorSetsIfDue();
|
||||
@@ -7381,6 +7398,15 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
activeRenderPass = nullptr;
|
||||
}
|
||||
// The syncs below upload each sampled texture's pending texels into a batch that runs
|
||||
// ahead of this recording; one whose image the recording already cleared or drew into
|
||||
// would be overwritten by that earlier work. Submit it first, here, before anything below
|
||||
// records into the frame or decides from the active pass.
|
||||
for (auto* sampledTexture : sampledTextures) {
|
||||
if (sampledTexture != nullptr && OrderPendingUploadAfterRecording(frame, *sampledTexture)) {
|
||||
activeRenderPass = nullptr;
|
||||
}
|
||||
}
|
||||
Bool needSampledTextureTransitions = false;
|
||||
auto& sampledResources = m_sampledResourcesScratch;
|
||||
sampledResources.assign(sampledTextures.size(), nullptr);
|
||||
@@ -7699,6 +7725,8 @@ void main() {
|
||||
}
|
||||
|
||||
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
// Before the wire branch, for SetupDraw's reason.
|
||||
SplitOversizedRecording();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
RewindWireDescriptorSetsIfDue();
|
||||
@@ -7760,6 +7788,16 @@ void main() {
|
||||
}
|
||||
|
||||
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
|
||||
SplitOversizedRecording();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-SV: the wire arm's own indirect dispatch, DispatchCompute's shape one call over.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
RewindWireDescriptorSetsIfDue();
|
||||
m_textureManager->CollectGarbage();
|
||||
DispatchWireComputeIndirect(indirect);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
// See DispatchCompute: the dispatch accessor, not the draw one.
|
||||
@@ -8203,6 +8241,7 @@ void main() {
|
||||
m_renderPassManager->QueueRenderbufferClear(
|
||||
{.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment);
|
||||
} else if (attachment.IsTexture()) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, attachment);
|
||||
m_clearManager->QueueClear({.mask = GL_COLOR_BUFFER_BIT, .color = payload.color},
|
||||
attachment);
|
||||
}
|
||||
@@ -8214,6 +8253,19 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((deferredMask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
for (const auto drawBuffer : fbo->GetDrawBuffers()) {
|
||||
if (drawBuffer != FramebufferAttachmentType::None) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(drawBuffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(FramebufferAttachmentType::Depth));
|
||||
}
|
||||
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(FramebufferAttachmentType::Stencil));
|
||||
}
|
||||
m_clearManager->QueueClear(deferredMask, payload, *fbo);
|
||||
m_renderPassManager->QueueRenderbufferClear(deferredMask, payload, *fbo);
|
||||
}
|
||||
@@ -8291,6 +8343,7 @@ void main() {
|
||||
if (!attachment.IsTexture()) {
|
||||
return;
|
||||
}
|
||||
SettleTexelsBeforeQueuedClear(framebuffer, attachment);
|
||||
m_clearManager->QueueClear(payload, attachment);
|
||||
};
|
||||
|
||||
@@ -8657,7 +8710,8 @@ void main() {
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
Bool uploadPendingTexels) {
|
||||
Vector<PendingClearEntry> pendingClears;
|
||||
if (!m_clearManager->GetPendingClears(&texture, pendingClears)) {
|
||||
return true;
|
||||
@@ -8669,8 +8723,9 @@ void main() {
|
||||
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
|
||||
"MaterializePendingClearForTexture requires no active render pass on the target buffer");
|
||||
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr) {
|
||||
auto* resource = uploadPendingTexels ? m_textureManager->SyncTextureAndGetDescriptor(texture)
|
||||
: m_textureManager->FindTextureResource(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
// Declined sync (an incomplete texture, say). Nothing to clear into, and every line
|
||||
// below dereferences this - the assert that used to stand here is compiled out of
|
||||
// every build past DEBUG.
|
||||
@@ -8822,6 +8877,9 @@ void main() {
|
||||
"MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout",
|
||||
texture.GetExternalIndex());
|
||||
resource->layout = sampledLayout;
|
||||
// The clear is now a write in this recording, which is what orders a later upload into
|
||||
// the image after it (OrderPendingUploadAfterRecording).
|
||||
m_textureManager->StampResourceRecordingWrite(*resource);
|
||||
|
||||
m_clearManager->PopPendingClear(&texture);
|
||||
MGLOG_D("MaterializePendingClearForTexture: textureId=%d pending clear materialized",
|
||||
@@ -8829,6 +8887,62 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
if (!m_textureManager->HasPendingTexelUpload(texture)) {
|
||||
return false;
|
||||
}
|
||||
// An open pass drawing into the image stamps that write only when it ends.
|
||||
if (const auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
activeRenderPass != nullptr && ActiveRenderPassUsesTexture(*activeRenderPass, texture)) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
const Bool clearFirst = m_clearManager->HasPendingClear(&texture);
|
||||
if (!clearFirst && !m_textureManager->WasTextureWrittenThisRecording(texture)) {
|
||||
// Nothing recorded writes the image: the upload may run ahead of the recording.
|
||||
return false;
|
||||
}
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
if (clearFirst && !MaterializePendingClearForTexture(frame.commandBuffer, texture,
|
||||
/*uploadPendingTexels=*/false)) {
|
||||
MGLOG_E_ONCE("OrderPendingUploadAfterRecording: textureId=%d kept its queued clear; it may land over "
|
||||
"texels written after it",
|
||||
texture.GetExternalIndex());
|
||||
}
|
||||
if (!FlushPendingCommands()) {
|
||||
MGLOG_E_ONCE("OrderPendingUploadAfterRecording: submit failed; textureId=%d's upload may run ahead of "
|
||||
"earlier work on it",
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
m_frameContext.BeginCommandRecording();
|
||||
// New command buffer: the same reset SetupDraw applies when it begins one.
|
||||
m_lastSampledSetValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::SettleTexelsBeforeQueuedClear(
|
||||
const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
// The default framebuffer's attachments are placeholders: its clears land on the
|
||||
// swapchain image (MaterializePendingClearForDefaultFramebuffer) and it has no texels.
|
||||
if (framebuffer.IsDefaultFramebuffer() || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
|
||||
return;
|
||||
}
|
||||
const auto texture = attachment.GetTexture();
|
||||
if (!texture || (m_textureManager->HasLiveImage(*texture) &&
|
||||
!m_textureManager->HasPendingTexelUpload(*texture))) {
|
||||
return;
|
||||
}
|
||||
OrderPendingUploadAfterRecording(m_frameContext.GetCurrent(), *texture);
|
||||
m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
@@ -10782,6 +10896,7 @@ void main() {
|
||||
const Bool dstClearReady = materializeClear(dstEndpoint);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
|
||||
__func__, CopyImageEndpointName(dstEndpoint));
|
||||
m_textureManager->StampTextureRecordingWrite(dstEndpoint.Texture.get());
|
||||
|
||||
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
|
||||
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
|
||||
@@ -11066,6 +11181,13 @@ void main() {
|
||||
}
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!readFbo->IsDefaultFramebuffer()) {
|
||||
// The read syncs the source texture, whose pending texels must land after work already
|
||||
// recorded against it (see OrderPendingUploadAfterRecording).
|
||||
if (const auto readTexture = readFbo->GetAttachment(readFbo->GetReadBuffer()).GetTexture()) {
|
||||
OrderPendingUploadAfterRecording(frame, *readTexture);
|
||||
}
|
||||
}
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
@@ -11791,6 +11913,9 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The sync below uploads pending texels, which must land after work already recorded
|
||||
// against this image (see OrderPendingUploadAfterRecording).
|
||||
OrderPendingUploadAfterRecording(m_frameContext.GetCurrent(), *textureObject);
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject);
|
||||
// Two shapes end up in the same place, and for the same reason: the GL level being read has
|
||||
// no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy
|
||||
@@ -12136,6 +12261,7 @@ void main() {
|
||||
resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE,
|
||||
"GenerateMipmap failed to resync the backend texture after allocating mip storage.");
|
||||
m_textureManager->StampResourceRecordingWrite(*resource);
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect);
|
||||
Bool transitioned = VkTextureManager::TransitionImageLayout(
|
||||
@@ -14091,6 +14217,54 @@ void main() {
|
||||
return FlushPendingCommands();
|
||||
}
|
||||
|
||||
void VulkanRenderer::SplitOversizedRecording() {
|
||||
const Uint32 maxDraws = MG_Config::Features.MagmaMaxDrawsPerCommandBuffer;
|
||||
if (maxDraws == 0) {
|
||||
return;
|
||||
}
|
||||
// Counted before the check, so a buffer carries at most maxDraws calls before the one
|
||||
// that splits it. OnFrameCommandRecordingBegan resets the count on every new recording.
|
||||
if (++m_drawsInRecording <= maxDraws || !HasPendingRecordedWork()) {
|
||||
return;
|
||||
}
|
||||
// Nothing of the current call is recorded yet, so the split falls between two GL calls,
|
||||
// exactly where a glFlush would put it: the render pass ends with STORE, the next draw
|
||||
// re-begins it with LOAD, and queries and transform feedback are bracketed per draw.
|
||||
if (!FlushPendingCommands()) {
|
||||
return;
|
||||
}
|
||||
// Fresh command buffer: the same reset every other mid-frame flush site applies.
|
||||
m_lastSampledSetValid = false;
|
||||
++m_oversizedRecordingSplits;
|
||||
MGLOG_I_ONCE("DirectVulkan: a frame recorded more than %u draws into one command buffer; submitting it and "
|
||||
"continuing on a fresh one (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded)",
|
||||
maxDraws);
|
||||
MGLOG_D("DirectVulkan: split an oversized recording (%llu so far, submit index %llu)",
|
||||
static_cast<unsigned long long>(m_oversizedRecordingSplits),
|
||||
static_cast<unsigned long long>(m_submitCounter));
|
||||
|
||||
// Splitting only bounds the driver's command memory if the submitted buffers are freed
|
||||
// as fast as they are produced. A GPU that falls behind would park them all on the
|
||||
// retired list, which is the same growth spread over more buffers - so hold the queue
|
||||
// to frames-in-flight + 1 outstanding submissions, the steady state of a pipelined
|
||||
// frame plus this split. Wait on the fence directly rather than through
|
||||
// WaitForSubmitIndex: its opportunistic TryDrainFrameTransients may rewind the transient
|
||||
// arena, and this runs at the start of a draw whose caller may already hold slices of it.
|
||||
const SizeT maxOutstanding = static_cast<SizeT>(m_frameContext.GetFrameCount()) + 1;
|
||||
while (m_inFlightSubmits.size() > maxOutstanding) {
|
||||
const SubmitRecord oldest = m_inFlightSubmits.front();
|
||||
if (oldest.fence == VK_NULL_HANDLE) {
|
||||
break;
|
||||
}
|
||||
const VkResult waitResult = vkWaitForFences(m_device, 1, &oldest.fence, VK_TRUE, UINT64_MAX);
|
||||
if (waitResult != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("SplitOversizedRecording: vkWaitForFences returned %d", waitResult);
|
||||
break;
|
||||
}
|
||||
OnSubmitsCompletedUpTo(oldest.submitIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending) {
|
||||
if (IsSubmitIndexComplete(submitIndex)) {
|
||||
return true;
|
||||
@@ -14136,6 +14310,7 @@ void main() {
|
||||
}
|
||||
|
||||
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
|
||||
m_drawsInRecording = 0;
|
||||
// Dynamic state does not survive a command-buffer boundary.
|
||||
ResetDynamicStateShadow();
|
||||
InvalidateSetupDrawSnapshots();
|
||||
@@ -14406,8 +14581,22 @@ void main() {
|
||||
if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) {
|
||||
m_swapchainResizeRequested = true;
|
||||
}
|
||||
// A new eglSwapInterval lands here too, between frames: this one is on screen and the
|
||||
// next has recorded nothing. Only a different present mode is worth the rebuild (it
|
||||
// drops every pipeline); otherwise the live swapchain already paces as asked.
|
||||
if (m_config.SwapInterval != m_swapchainSwapInterval) {
|
||||
if (SwapchainObject::ChooseSwapchainPresentMode(m_swapchainObject.GetSupportedPresentModes(),
|
||||
m_config.SwapInterval) !=
|
||||
m_swapchainObject.GetPresentMode()) {
|
||||
m_swapchainResizeRequested = true;
|
||||
} else {
|
||||
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
|
||||
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
|
||||
m_swapchainSwapInterval = m_config.SwapInterval;
|
||||
}
|
||||
}
|
||||
if (m_swapchainResizeRequested) {
|
||||
MGLOG_D("Present, processing requested swapchain resize");
|
||||
MGLOG_D("Present, processing requested swapchain rebuild (resize or swap interval)");
|
||||
if (!RecreateSwapchain()) {
|
||||
m_presentSuspended = true;
|
||||
m_swapchainResizeRequested = false;
|
||||
@@ -15156,6 +15345,30 @@ void main() {
|
||||
// P7 gate 5 (g5-msprobe): which multisample depth/stencil resolve arm goes first is measured
|
||||
// once the device exists (ArmWireDepthResolveOrder, below), not keyed on the vendor.
|
||||
m_wirePreferShaderDepthResolve = false;
|
||||
// P11 B2 (T0): a client's AHardwareBuffer imported as a wire buffer store. Wire arms, on
|
||||
// Android, whenever the device advertises the extension - not per session: the device is
|
||||
// created once per server and a later session asking T0 must find it able. Enabling it
|
||||
// changes no T2 behaviour; only an import uses it. The 1.0-era dependencies are core at the
|
||||
// 1.1 this device is created at, and are enabled too when advertised (the spike's set).
|
||||
m_wireAhbImport = false;
|
||||
m_wireGetAhbProperties = nullptr;
|
||||
#if defined(__ANDROID__)
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 &&
|
||||
IsExtensionSupported(availableExtensions, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) &&
|
||||
IsExtensionSupported(availableExtensions, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME)) {
|
||||
m_wireAhbImport = true;
|
||||
for (const char* name : {VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
|
||||
VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
|
||||
VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME,
|
||||
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
|
||||
VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME,
|
||||
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_1_EXTENSION_NAME}) {
|
||||
if (IsExtensionSupported(availableExtensions, name))
|
||||
EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, name);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr &&
|
||||
getPhysicalDeviceProperties2 != nullptr) {
|
||||
@@ -15715,6 +15928,11 @@ void main() {
|
||||
m_wireCreateRenderPass2 = reinterpret_cast<PFN_vkCreateRenderPass2>(
|
||||
vkGetDeviceProcAddr(m_device, wireDepthResolveCore ? "vkCreateRenderPass2" : "vkCreateRenderPass2KHR"));
|
||||
}
|
||||
if (m_wireAhbImport) {
|
||||
m_wireGetAhbProperties =
|
||||
reinterpret_cast<void*>(vkGetDeviceProcAddr(m_device, "vkGetAndroidHardwareBufferPropertiesANDROID"));
|
||||
if (m_wireGetAhbProperties == nullptr) m_wireAhbImport = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_vkCmdDrawIndexedIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
|
||||
@@ -15727,6 +15945,16 @@ void main() {
|
||||
MGLOG_W("VK_KHR_draw_indirect_count enabled but vkCmdDrawIndexedIndirectCount entry point is missing, will continue as if VK_KHR_draw_indirect_count is not supported!");
|
||||
m_drawIndirectCountExtensionEnabled = false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-D: the wire arm's glMultiDrawArraysIndirectCount. Null = that verb reads its count on
|
||||
// the CPU (WireDraw.inc's DrawWireIndirectNative answers false for it).
|
||||
s_vkCmdWireDrawIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
|
||||
vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCountKHR"));
|
||||
if (s_vkCmdWireDrawIndirectCount == nullptr) {
|
||||
s_vkCmdWireDrawIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
|
||||
vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCount"));
|
||||
}
|
||||
#endif
|
||||
|
||||
s_vkCmdDrawMultiEXT = nullptr;
|
||||
s_vkCmdDrawMultiIndexedEXT = nullptr;
|
||||
@@ -16085,7 +16313,12 @@ void main() {
|
||||
m_swapchainObject.Create(m_device, m_physicalDevice.handle, m_surface,
|
||||
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily),
|
||||
static_cast<Uint32>(m_physicalDevice.queueFamilies.presentFamily),
|
||||
m_config.MaxFramesInFlight, desiredExtent);
|
||||
m_config.MaxFramesInFlight, desiredExtent, m_config.SwapInterval);
|
||||
if (m_config.SwapInterval && m_config.SwapInterval != m_swapchainSwapInterval) {
|
||||
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
|
||||
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
|
||||
}
|
||||
m_swapchainSwapInterval = m_config.SwapInterval;
|
||||
// The FragCoordYFlip variants bake this height in; it is the only input to a shader
|
||||
// module that lives outside the GL program, so the factory has to learn it here (and on
|
||||
// every recreation, which is the only way it can change).
|
||||
@@ -16541,10 +16774,29 @@ void main() {
|
||||
m_swapchainResizeRequested = true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::SetSwapInterval(Int interval) {
|
||||
if (m_presentsToAppWindow) {
|
||||
m_config.SwapInterval = interval;
|
||||
}
|
||||
}
|
||||
|
||||
VkInstance VulkanRenderer::GetInstance() const {
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool VulkanRenderer::GetWireAhbImport(WireAhbImport& out) const {
|
||||
out = WireAhbImport{};
|
||||
if (!m_wireAhbImport || m_device == VK_NULL_HANDLE || m_wireGetAhbProperties == nullptr) return false;
|
||||
out.device = m_device;
|
||||
out.queue = m_graphicsQueue;
|
||||
out.queueFamily = static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
||||
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice.handle, &out.memory);
|
||||
out.getAhbProperties = m_wireGetAhbProperties;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
Bool VulkanRenderer::IsDrawIndirectCountExtensionEnabled() const {
|
||||
return m_drawIndirectCountExtensionEnabled;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded -
|
||||
// true for a fence taken at that instant, wrong for work about to be recorded.
|
||||
Uint64 GetWireNextSubmitIndex() const { return m_submitCounter + 1; }
|
||||
// P8-D: the wire arm's indirect draw families, issued as vkCmdDraw[Indexed]Indirect[Count]
|
||||
// from the wire stores MGPipeApplier().VerbIndirectBuffer / VerbIndirectParameterBuffer
|
||||
// name. `offset` / `countOffset` are byte offsets into those stores; `stride` 0 means
|
||||
// tightly packed. FALSE = this device cannot issue the COUNT form natively (the monolith
|
||||
// arm's own condition, see the definition in WireDraw.inc) and the caller must read the
|
||||
// words on the CPU; TRUE = issued, or declined by name.
|
||||
Bool DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride,
|
||||
Bool indexed, Bool counted, Uint64 countOffset);
|
||||
// P11 B2 (T0): what importing a client's AHardwareBuffer as a wire buffer store needs from
|
||||
// this device. False when the device did not take VK_ANDROID_external_memory_android_
|
||||
// hardware_buffer at creation (not Android, not advertised, or a monolith device).
|
||||
struct WireAhbImport {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
Uint32 queueFamily = 0;
|
||||
VkPhysicalDeviceMemoryProperties memory{};
|
||||
void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID
|
||||
};
|
||||
Bool GetWireAhbImport(WireAhbImport& out) const;
|
||||
#endif
|
||||
|
||||
// FrameContext::IRecordingObserver: prepares the frame's timer-query
|
||||
@@ -327,6 +346,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// signal as soon as the GPU actually finishes, which MC 1.21.5's
|
||||
// fence-paced ring buffers rely on to recycle their space.
|
||||
Uint64 GetSyncPointSubmitIndex() const;
|
||||
// For tests: the per-frame descriptor pools (see DescriptorPoolCensus.h).
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus() const {
|
||||
return m_uniformManager ? m_uniformManager->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
|
||||
}
|
||||
// Non-blocking: polls outstanding submission fences and reports
|
||||
// whether every submission up to `submitIndex` has completed.
|
||||
Bool IsSubmitIndexComplete(Uint64 submitIndex);
|
||||
@@ -390,6 +413,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
// eglSwapInterval. Records the request; Present rebuilds the swapchain for it once the
|
||||
// current frame is on screen, and only when it maps to a different present mode. A
|
||||
// renderer created without an app window ignores it: its surface (headless, or an
|
||||
// AImageReader nobody consumes) is never displayed, and FIFO there would block once
|
||||
// the reader's queue fills.
|
||||
void SetSwapInterval(Int interval);
|
||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
||||
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
||||
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
|
||||
@@ -564,6 +593,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// target unwritten while the shader control resolved - the shader arm then resolves
|
||||
// first and the render-pass arm is the fallback (P7 gate 5, g5-msprobe).
|
||||
Bool m_wirePreferShaderDepthResolve = false;
|
||||
// P11 B2 (T0): the device took the AHardwareBuffer import extensions at creation, and the
|
||||
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
|
||||
Bool m_wireAhbImport = false;
|
||||
void* m_wireGetAhbProperties = nullptr;
|
||||
// Runs the probe (memoized per device identity) and sets the member above. Called at the end of
|
||||
// device creation, after ArmPrimGenReroute: it records on m_graphicsQueue.
|
||||
void ArmWireDepthResolveOrder();
|
||||
@@ -655,7 +688,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CollectWireObjects(Uint64 completedSubmit, Bool all = false);
|
||||
Vector<WireRetiredObjects> m_wireRetiredObjects;
|
||||
Uint32 m_wirePreparationDepth = 0;
|
||||
// P8-D: set only while DrawWireIndirectNative is inside SetupDraw, so SetupWireDraw can
|
||||
// tell the wire arm's own indirect draw from a monolith indirect entry point reached under
|
||||
// a transport (still the `buffer-legacy-arm` role violation).
|
||||
Bool m_wireNativeIndirectDraw = false;
|
||||
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
|
||||
// P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the
|
||||
// dispatch-indirect store, as the monolith arm issues it (WireDraw.inc).
|
||||
void DispatchWireComputeIndirect(GLintptr offset);
|
||||
void RewindWireDescriptorSetsIfDue();
|
||||
UniquePtr<RenderPassEntry> m_wireDrawPass;
|
||||
Vector<VkImageView> m_wireDrawViews;
|
||||
@@ -730,6 +770,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only), so the presenting path's frames-in-flight pipelining is
|
||||
// untouched. Returns true when the drain ran.
|
||||
Bool TryDrainFrameTransients();
|
||||
// Bounded recording: called at the top of every GL draw and dispatch, before anything
|
||||
// of that call is recorded. Once the open frame command buffer holds
|
||||
// MagmaMaxDrawsPerCommandBuffer draws/dispatches, submits it (FlushPendingCommands) so
|
||||
// the call continues on a fresh one, then waits until at most frames-in-flight + 1
|
||||
// submissions are outstanding. A driver backs a command buffer with GPU memory it only
|
||||
// returns when the buffer is freed (Adreno: one 16 KiB mapping per chunk), so a frame
|
||||
// that is never split grows it without bound - rd12's loading frame records ~1M draws
|
||||
// into one buffer and runs the process out of vm.max_map_count.
|
||||
void SplitOversizedRecording();
|
||||
|
||||
Vector<SubmitRecord> m_inFlightSubmits;
|
||||
Vector<VkFence> m_freeSubmitFences;
|
||||
@@ -741,6 +790,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// aging clocks, while present-less loops still cross a boundary every few
|
||||
// iterations. Reset in Present.
|
||||
Uint32 m_drainsSinceLastPresent = 0;
|
||||
// GL draws/dispatches recorded into the open frame command buffer; reset whenever a
|
||||
// recording begins (OnFrameCommandRecordingBegan). See SplitOversizedRecording.
|
||||
Uint32 m_drawsInRecording = 0;
|
||||
Uint64 m_oversizedRecordingSplits = 0;
|
||||
|
||||
NativeWindowType m_window = 0;
|
||||
void* m_platformDisplay = nullptr;
|
||||
@@ -760,6 +813,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Shutdown() deletes it.
|
||||
void* m_fallbackImageReader = nullptr;
|
||||
VulkanRendererConfig m_config;
|
||||
// Constructed for the app's window rather than a pbuffer (m_window is later filled in
|
||||
// for the pbuffer fallbacks too, so it cannot tell them apart).
|
||||
Bool m_presentsToAppWindow = false;
|
||||
// The m_config.SwapInterval the live swapchain was built for; Present compares the two.
|
||||
Optional<Int> m_swapchainSwapInterval;
|
||||
Bool m_swapchainResizeRequested = false;
|
||||
// Presentation is suspended while the window is zero-area (minimized): the
|
||||
// swapchain is unusable/out of date, so Present drops frames instead of
|
||||
@@ -878,6 +936,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
||||
Uint32 stride);
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-D: vkCmdDrawIndirectCount (same signature, same extension), for the wire arm's
|
||||
// glMultiDrawArraysIndirectCount. The monolith arm never loads it: it reads that count on
|
||||
// the CPU (DirectVulkan.cpp's MultiDrawArraysIndirectCount).
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
|
||||
#endif
|
||||
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
|
||||
// (and its multiDraw feature) is enabled; null otherwise.
|
||||
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
|
||||
@@ -1861,8 +1925,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue,
|
||||
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
// Records the texture's queued clears. By default the texture is synced first, which
|
||||
// uploads any pending texels BEFORE the clear - right only for texels written before it.
|
||||
// `uploadPendingTexels = false` leaves them pending (OrderPendingUploadAfterRecording).
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
Bool uploadPendingTexels = true);
|
||||
// Texture uploads run in a batch submitted ahead of the frame's recording, so the upload
|
||||
// the next sync of `texture` records would land BEFORE a write already recorded into the
|
||||
// same image (a render, clear, blit or copy), and before a clear still queued for it (such
|
||||
// a clear predates the texels: SettleTexelsBeforeQueuedClear). When either holds, this
|
||||
// records the queued clear without the texels and submits the recording, so the upload
|
||||
// follows both. An image the recording only READ keeps the old trade: no submit, and its
|
||||
// earlier reads see the newer texels. It may end the render pass and replace
|
||||
// frame.commandBuffer (recording again on return), so call it only where the caller holds
|
||||
// no render-pass or command-buffer state across it. Returns whether it submitted.
|
||||
Bool OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
// Called for each texture a glClear/glClearBuffer* is about to queue a clear on: uploads
|
||||
// whatever texels are still pending (and mints the image if there is none), so that any
|
||||
// texel pending when the clear is materialized was written after it.
|
||||
void SettleTexelsBeforeQueuedClear(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
// The multisample arm of the above. Split out rather than branched inline because it
|
||||
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
|
||||
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
|
||||
|
||||
@@ -58,7 +58,7 @@ MGL_WIRE_DECLINE(CurrentAttributePayload)
|
||||
MGL_WIRE_DECLINE(CurrentAttributeUpload)
|
||||
MGL_WIRE_DECLINE(BeginRenderPass)
|
||||
|
||||
// ---- DispatchWireCompute ----------------------------------------------------------------
|
||||
// ---- DispatchWireCompute (and DispatchWireComputeIndirect, P8-SV) -------------------------
|
||||
MGL_WIRE_DECLINE(ComputeProgramLinkOrSpirv)
|
||||
MGL_WIRE_DECLINE(ComputeTextureResourcesUnprepared)
|
||||
MGL_WIRE_DECLINE(ComputePipelineUnavailable)
|
||||
@@ -104,3 +104,25 @@ MGL_WIRE_DECLINE(ShapeCreateImageFailed)
|
||||
// Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in
|
||||
// SyncWireTextureShape still leaving no trace at all.
|
||||
MGL_WIRE_DECLINE(ShapePreserveCopyFailed)
|
||||
|
||||
// ---- VulkanRenderer::GenerateWireMipmap (P8-F) -----------------------------------------
|
||||
// `mipmap-shader-format-or-shape` was a Fatal on this arm and a decline on the monolith arm; it
|
||||
// is the monolith's decline now. Counted once per refused glGenerateMipmap, whose chain is left
|
||||
// as it was.
|
||||
MGL_WIRE_DECLINE(MipmapShaderFormatOrShape)
|
||||
|
||||
// ---- VulkanRenderer::DrawWireIndirectNative (P8-D), in statement order -----------------------
|
||||
MGL_WIRE_DECLINE(IndirectBufferUnbound)
|
||||
MGL_WIRE_DECLINE(IndirectCommandRange)
|
||||
MGL_WIRE_DECLINE(IndirectCountRange)
|
||||
MGL_WIRE_DECLINE(IndirectIndexStore)
|
||||
|
||||
// ---- P8-SV -------------------------------------------------------------------------------
|
||||
// VulkanRenderer::GenerateWireMipmap: a combined depth/stencil (or stencil-only) chain, which no
|
||||
// arm generates (CONTRACT-P7 §3.2). It logged but was never counted, so MGWIRE-DECLINES could
|
||||
// not see it. Counted once per refused glGenerateMipmap, whose chain is left as it was.
|
||||
MGL_WIRE_DECLINE(MipmapDepthStencilAspect)
|
||||
// VulkanRenderer::DispatchWireComputeIndirect, in statement order: the dispatch-indirect store
|
||||
// is missing, or the three group-count words lie outside it or are misaligned.
|
||||
MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound)
|
||||
MGL_WIRE_DECLINE(DispatchIndirectRange)
|
||||
|
||||
@@ -64,6 +64,14 @@
|
||||
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT)
|
||||
return false;
|
||||
if (source.is3D || destination.is3D) return false;
|
||||
// P8-SV: THE PASS MAKES 2D VIEWS (makeView below), which a 1D, 1D-array or 3D image does
|
||||
// not admit. A caller that fills viewType from the image gets a refusal here, and the
|
||||
// wire arm's GenerateWireMipmap asks the same question before it grows the chain.
|
||||
for (const VkImageViewType viewType : {source.viewType, destination.viewType}) {
|
||||
if (viewType == VK_IMAGE_VIEW_TYPE_1D || viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
|
||||
viewType == VK_IMAGE_VIEW_TYPE_3D)
|
||||
return false;
|
||||
}
|
||||
if (source.format != destination.format) return false;
|
||||
// DEPTH ONLY. A combined depth/stencil image can be sampled for its depth and
|
||||
// rendered as a depth attachment, but this pass writes gl_FragDepth and NOTHING
|
||||
|
||||
@@ -207,7 +207,10 @@
|
||||
MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V");
|
||||
return false;
|
||||
}
|
||||
if (aspects & DrawSetupAspect::IndirectDrawBuffer)
|
||||
// P8-D: the wire arm's own indirect draw (DrawWireIndirectNative) is the one caller that
|
||||
// may pass this aspect; a monolith indirect entry point reached here is still the role
|
||||
// violation.
|
||||
if ((aspects & DrawSetupAspect::IndirectDrawBuffer) && !m_wireNativeIndirectDraw)
|
||||
WireBufferLegacyFatal();
|
||||
MakeXfbWritesVisible();
|
||||
const auto* fbo = state.DrawFramebuffer();
|
||||
@@ -294,7 +297,14 @@
|
||||
const Uint32 location = input.bindingAttributeLocations[binding];
|
||||
const auto& attr = vao.Attributes[location];
|
||||
const auto& resource = state.VertexBuffers[location];
|
||||
if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MagmaWireFatal("client-vertex-array@P8");
|
||||
// P8-F: A RECORD INVARIANT, NOT AN UNMIGRATED VERB (it carried a P8 phase tag).
|
||||
// The client turns every enabled client-memory array into an owned buffer before the
|
||||
// draw record is built, or refuses the draw itself (MG_Impl/Pipe/OwnedDrawInputs.h),
|
||||
// so an attribute the program reads with no buffer handle is a foreign peer's record.
|
||||
if (MG_Pipe::MGPipeHandleIsNull(resource.Res))
|
||||
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an "
|
||||
"attribute the program reads carries no buffer handle", location);
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice))
|
||||
MagmaWireFatal("vertex-buffer-resource");
|
||||
@@ -310,8 +320,10 @@
|
||||
const SizeT stride = static_cast<SizeT>(attr.Stride);
|
||||
SizeT count = stride ? 1 + (static_cast<SizeT>(slice.size - offset) - elementSize) / stride : 1;
|
||||
// Direct non-indexed draws bound the conversion; indexed/multi-draw may
|
||||
// name arbitrary vertices, so preserve the entire valid source stream.
|
||||
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor)
|
||||
// name arbitrary vertices, so preserve the entire valid source stream. So may an
|
||||
// indirect draw (P8-D): its vertex range is in GPU words nobody read.
|
||||
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) &&
|
||||
!(aspects & DrawSetupAspect::IndirectDrawBuffer) && !resource.Divisor)
|
||||
count = std::min<SizeT>(count, static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount);
|
||||
if (!count) {
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements");
|
||||
@@ -601,6 +613,136 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P8-D: THE WIRE ARM'S INDIRECT DRAWS READ THEIR WORDS ON THE GPU ----------------------
|
||||
//
|
||||
// The shape the monolith arm records (VulkanRenderer.cpp's MultiDraw{Elements,Arrays}Indirect
|
||||
// and MultiDrawElementsIndirectCount), with the wire store's own VkBuffer in place of the
|
||||
// frontend buffer's resident slice. Before this, every wire indirect draw read its commands
|
||||
// through ReadWireBuffer and expanded them on the CPU, and a store a shader had written made
|
||||
// that read a whole-GPU wait (WaitForWireBufferHostAccess) per draw.
|
||||
//
|
||||
// THE ONE FALSE: a COUNT form this device cannot issue - the monolith arm's own condition for
|
||||
// vkCmdDrawIndexedIndirectCount (MultiDrawElementsIndirectCount), extended to the non-indexed
|
||||
// twin the monolith never loads. Everything else is issued here or declined by name.
|
||||
Bool VulkanRenderer::DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount,
|
||||
GLsizei stride, Bool indexed, Bool counted, Uint64 countOffset) {
|
||||
if (drawcount <= 0) return true;
|
||||
if (counted) {
|
||||
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
|
||||
if (!m_drawIndirectCountExtensionEnabled || countEntry == nullptr ||
|
||||
(!m_multiDrawIndirectFeatureEnabled && drawcount != 1))
|
||||
return false;
|
||||
}
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
// GL's command sizes (GL 4.6 10.3.10), which are also Vulkan's.
|
||||
const Uint64 commandSize = indexed ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32);
|
||||
if (stride == 0) stride = static_cast<GLsizei>(commandSize);
|
||||
BufferSlice commands{};
|
||||
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectBuffer) ||
|
||||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectBuffer, commands)) {
|
||||
MGL_WIRE_DECLINE_AT(IndirectBufferUnbound, "an indirect draw names no live indirect buffer store");
|
||||
return true;
|
||||
}
|
||||
const Uint64 span = static_cast<Uint64>(stride) * static_cast<Uint64>(drawcount - 1) + commandSize;
|
||||
// GL refuses a stride that is not a multiple of four (INVALID_VALUE), and Vulkan requires
|
||||
// it for drawCount > 1; a peer's record that breaks either is declined, not issued.
|
||||
if (stride < static_cast<GLsizei>(commandSize) || (stride & 3) != 0 || (offset & 3u) != 0 ||
|
||||
offset > commands.size || span > commands.size - offset) {
|
||||
MGL_WIRE_DECLINE_AT(IndirectCommandRange, "the indirect commands lie outside their store or are misaligned");
|
||||
return true;
|
||||
}
|
||||
BufferSlice parameters{};
|
||||
if (counted) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectParameterBuffer) ||
|
||||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectParameterBuffer, parameters) ||
|
||||
(countOffset & 3u) != 0 || countOffset > parameters.size || parameters.size - countOffset < sizeof(Uint32)) {
|
||||
MGL_WIRE_DECLINE_AT(IndirectCountRange, "the indirect draw count lies outside its parameter store");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// The index view is the WHOLE bound element store past its binding offset, as the monolith
|
||||
// arm binds it: the commands name their own firstIndex, which only the GPU reads.
|
||||
IndexBufferView indices{};
|
||||
DrawCmdParam vertexRange{};
|
||||
vertexRange.instanceCount = 1;
|
||||
Flags<DrawSetupAspect> aspects = DrawSetupAspect::IndirectDrawBuffer;
|
||||
if (indexed) {
|
||||
const SizeT width = MG_Util::GetGLTypeSize(type);
|
||||
BufferSlice elements{};
|
||||
if (width == 0 || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res) ||
|
||||
!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, elements) ||
|
||||
state.IndexBuffer.Offset > elements.size) {
|
||||
MGL_WIRE_DECLINE_AT(IndirectIndexStore, "an indexed indirect draw names no usable element store");
|
||||
return true;
|
||||
}
|
||||
indices.indexType = type;
|
||||
indices.indexByteOffset = 0;
|
||||
indices.indexByteSize = static_cast<SizeT>(elements.size - state.IndexBuffer.Offset);
|
||||
vertexRange.vertexCount = static_cast<Uint32>(indices.indexByteSize / width);
|
||||
aspects |= DrawSetupAspect::IndexBuffer;
|
||||
}
|
||||
|
||||
// A shader write the store was marked with (MarkWireBufferGpuWritten) is made visible to
|
||||
// the indirect read here, once per write, so a program that omits GL_COMMAND_BARRIER_BIT
|
||||
// is not left racing a read the old CPU path used to take after a whole-GPU wait. The
|
||||
// application's own glMemoryBarrier is BuildMemoryBarrierForGlBarriers, as on the monolith
|
||||
// arm; transfer writes carry their own MEMORY_READ after-barrier (VkBufferManager.cpp).
|
||||
const Bool commandsWritten = m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectBuffer);
|
||||
const Bool countWritten = counted && m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectParameterBuffer);
|
||||
if (commandsWritten || countWritten) {
|
||||
RetireWireDrawPass();
|
||||
auto& barrierFrame = m_frameContext.GetCurrent();
|
||||
if (!barrierFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
|
||||
VkRenderPassManager::EndRenderPass(barrierFrame.commandBuffer);
|
||||
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
|
||||
vkCmdPipelineBarrier(barrierFrame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
|
||||
if (MG_Util::PipeStats::Enabled())
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
|
||||
}
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
m_wireNativeIndirectDraw = true;
|
||||
const Bool ready = SetupDraw(frame, mode, aspects, vertexRange, indexed ? &indices : nullptr);
|
||||
m_wireNativeIndirectDraw = false;
|
||||
if (!ready) return true;
|
||||
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
|
||||
|
||||
const VkCommandBuffer commandBuffer = frame.commandBuffer;
|
||||
const VkDeviceSize first = commands.offset + offset;
|
||||
const Uint32 count = static_cast<Uint32>(drawcount);
|
||||
const Uint32 byteStride = static_cast<Uint32>(stride);
|
||||
if (counted) {
|
||||
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
|
||||
countEntry(commandBuffer, commands.buffer, first, parameters.buffer, parameters.offset + countOffset,
|
||||
count, byteStride);
|
||||
} else if (count == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled)) {
|
||||
// maxDrawIndirectCount bounds one call; GL's drawcount does not.
|
||||
const Uint32 limit = std::max<Uint32>(1u, m_physicalDevice.properties.limits.maxDrawIndirectCount);
|
||||
for (Uint32 base = 0; base < count; base += limit) {
|
||||
const Uint32 chunk = std::min(count - base, limit);
|
||||
const VkDeviceSize at = first + static_cast<VkDeviceSize>(base) * byteStride;
|
||||
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
|
||||
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
|
||||
}
|
||||
} else {
|
||||
// No multiDrawIndirect: one indirect draw per command, still read by the GPU
|
||||
// (the monolith arm's own fallback).
|
||||
for (Uint32 i = 0; i < count; ++i) {
|
||||
const VkDeviceSize at = first + static_cast<VkDeviceSize>(i) * byteStride;
|
||||
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, 1, 0);
|
||||
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, 1, 0);
|
||||
}
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled())
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDraws, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) {
|
||||
RetireWireDrawPass();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
@@ -628,4 +770,80 @@
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
vkCmdDispatch(frame.commandBuffer, x, y, z);
|
||||
}
|
||||
|
||||
// ---- P8-SV: THE WIRE ARM'S INDIRECT DISPATCH READS ITS GROUP COUNTS ON THE GPU ----------
|
||||
//
|
||||
// The monolith arm's shape (VulkanRenderer.cpp's DispatchComputeIndirect) with the wire
|
||||
// store's own VkBuffer in place of the frontend buffer's resident slice. Before this the wire
|
||||
// arm read the three words through ReadWireBuffer and dispatched them from the CPU, and a store
|
||||
// a shader had written made that read a whole-GPU wait (WaitForWireBufferHostAccess) per call.
|
||||
// There is no shape the device cannot issue, so there is no CPU path left: a record whose
|
||||
// store or words are unusable is declined by name.
|
||||
//
|
||||
// The barrier is P8-D's draw rule (DrawWireIndirectNative): a store marked shader-written gets
|
||||
// one MEMORY_WRITE -> INDIRECT_COMMAND_READ barrier before its next indirect read, whether or
|
||||
// not the application issued GL_COMMAND_BARRIER_BIT; DRAW_INDIRECT is the stage that consumes
|
||||
// VkDispatchIndirectCommand too. It is TAKEN before the bindings below, because binding this
|
||||
// dispatch's own storage buffers marks them written, and a store that is both this dispatch's
|
||||
// parameters and one of its outputs must keep that mark for the next reader.
|
||||
void VulkanRenderer::DispatchWireComputeIndirect(GLintptr offset) {
|
||||
RetireWireDrawPass();
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
constexpr Uint64 kCommandSize = 3 * sizeof(Uint32); // GL 4.6 10.3.10 / VkDispatchIndirectCommand
|
||||
BufferSlice parameters{};
|
||||
if (MG_Pipe::MGPipeHandleIsNull(state.VerbDispatchIndirectBuffer) ||
|
||||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbDispatchIndirectBuffer, parameters)) {
|
||||
MGL_WIRE_DECLINE_AT(DispatchIndirectBufferUnbound, "an indirect dispatch names no live dispatch-indirect store");
|
||||
return;
|
||||
}
|
||||
// GL refuses an offset that is negative or not a multiple of four (INVALID_VALUE) and a
|
||||
// command past the end of the buffer (INVALID_OPERATION); a peer's record that breaks
|
||||
// either is declined, not issued.
|
||||
if (offset < 0 || (static_cast<Uint64>(offset) & 3u) != 0 || static_cast<Uint64>(offset) > parameters.size ||
|
||||
parameters.size - static_cast<Uint64>(offset) < kCommandSize) {
|
||||
MGL_WIRE_DECLINE_AT(DispatchIndirectRange, "the dispatch group counts lie outside their store or are misaligned");
|
||||
return;
|
||||
}
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (m_bufferManager.TakeWireIndirectReadBarrier(state.VerbDispatchIndirectBuffer)) {
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
|
||||
if (MG_Util::PipeStats::Enabled())
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
|
||||
}
|
||||
const auto program = WireProgramSource(state.DispatchProgram);
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V");
|
||||
return;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
|
||||
return;
|
||||
}
|
||||
const auto pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (!pipeline) {
|
||||
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
|
||||
return;
|
||||
}
|
||||
// PrepareWireTextureResources may have flushed and begun a new recording.
|
||||
auto& dispatchFrame = m_frameContext.GetCurrent();
|
||||
if (!dispatchFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (!m_uniformManager->BindProgramUniformBuffers(dispatchFrame.commandBuffer, program, programObj,
|
||||
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound");
|
||||
return;
|
||||
}
|
||||
vkCmdBindPipeline(dispatchFrame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
vkCmdDispatchIndirect(dispatchFrame.commandBuffer, parameters.buffer,
|
||||
parameters.offset + static_cast<VkDeviceSize>(offset));
|
||||
if (MG_Util::PipeStats::Enabled())
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDispatches, 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
// missing WHICH feature bit - was the one thing it did not. The census, the retrace
|
||||
// refusal tally and every log grep key on the leading word, so it is unchanged and
|
||||
// unprefixed; everything after the first space is for the reader of the next device run.
|
||||
// P8-F: the refusal is a named decline now (WireDeclines.def MipmapShaderFormatOrShape);
|
||||
// the line it logs keeps this word and this detail.
|
||||
static String MagmaWireShapeDetail(const char* name, VkFormat format, VkFormat storageFormat,
|
||||
const VkFormatProperties& props, VkImageAspectFlags aspect,
|
||||
VkImageViewType viewType, VkImageUsageFlags usage,
|
||||
@@ -1392,16 +1394,22 @@
|
||||
// release build both arms ship as. Nothing in GL promises a stencil mip either - the
|
||||
// pass writes gl_FragDepth and would leave the stencil aspect of the level it produced
|
||||
// undefined, which is worse than leaving the whole level alone and saying so.
|
||||
//
|
||||
// P8-SV: a NAMED decline (WireDeclines.def MipmapDepthStencilAspect), so MGWIRE-DECLINES
|
||||
// counts it like every other refusal on this arm; the line keeps its words.
|
||||
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
MGLOG_E_ONCE("DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or "
|
||||
"stencil-only texture, format %d) has no mip generation on either arm; "
|
||||
"declining and leaving levels %u..%u as they are",
|
||||
static_cast<Uint32>(resource->aspect), static_cast<Int>(resource->format),
|
||||
base + 1, base + count - 1);
|
||||
MGL_WIRE_DECLINE_AT(MipmapDepthStencilAspect,
|
||||
"DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or "
|
||||
"stencil-only texture, format %d) has no mip generation on either arm; "
|
||||
"declining and leaving levels %u..%u as they are",
|
||||
static_cast<Uint32>(resource->aspect), static_cast<Int>(resource->format),
|
||||
base + 1, base + count - 1);
|
||||
return;
|
||||
}
|
||||
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
|
||||
MagmaWireFatal("mipmap-growth");
|
||||
// P8-F: WHICH ARM TAKES THIS CHAIN IS DECIDED BEFORE THE CHAIN IS GROWN, for the
|
||||
// depth-stencil decline's reason above. Nothing here depends on the growth: the format,
|
||||
// its features, the knob tier, the aspect, view type, usage and layer window are the
|
||||
// same before and after it (GrowWireTextureMipChain carries every one of them over).
|
||||
VkFormatProperties props{};
|
||||
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,format,&props);
|
||||
@@ -1411,6 +1419,55 @@
|
||||
(props.optimalTilingFeatures & required) == required;
|
||||
const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ?
|
||||
VK_FILTER_LINEAR : VK_FILTER_NEAREST;
|
||||
const Bool layersInRange = baseLayer < resource->arrayLayers &&
|
||||
layerCount <= resource->arrayLayers - baseLayer;
|
||||
// Both shader arms render a 2D view of one layer of one level, so a 1D, 1D-array or 3D
|
||||
// image is a shape neither takes.
|
||||
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
|
||||
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
|
||||
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
|
||||
// The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses
|
||||
// for an image this function hands it (the view type is passed to it below) - a 1D,
|
||||
// 1D-array or 3D view type, and a format that does not both sample and attach as depth.
|
||||
// Its other refusals, multisample and a default image, are never built here. P8-SV: the
|
||||
// view type half is new; before it a 1D depth chain reached the pass, which made a 2D
|
||||
// view of the 1D image.
|
||||
const auto depthPassFeatures =
|
||||
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
const Bool depthArmTakesShape = planar &&
|
||||
layersInRange && (props.optimalTilingFeatures & depthPassFeatures) == depthPassFeatures;
|
||||
// The colour arm's shape (the two shader shapes are explained at the colour arm below).
|
||||
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
// The shader arm samples through a sampler2D and writes a vec4, which is what
|
||||
// BlitWireColorImage's own float check enforces; naming it here rather than letting the
|
||||
// blit return false is what puts the reason in the decline line.
|
||||
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
|
||||
const Bool renderableFormat =
|
||||
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
||||
const Bool attachable = forcedShaderTier < 2 &&
|
||||
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
|
||||
const Bool viaScratch = !attachable && renderableFormat &&
|
||||
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
|
||||
const Bool colourArmTakesShape =
|
||||
colour && planar && layersInRange && sampledFloat && (attachable || viaScratch);
|
||||
// P8-F: `mipmap-shader-format-or-shape` IS A NAMED DECLINE, NOT A FATAL (rule I (a)).
|
||||
// The monolith arm has always answered this chain with a decline (VulkanRenderer.cpp's
|
||||
// GenerateMipmap: a format without blit support is skipped and the chain left unwritten),
|
||||
// and a shape no shader arm takes is a coverage gap in this backend, not a broken record.
|
||||
// The leading word stays, so every device-log grep keyed on it still finds the line.
|
||||
const Bool shaderArmTakesShape =
|
||||
resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? depthArmTakesShape : colourArmTakesShape;
|
||||
if (!nativeBlit && !shaderArmTakesShape) {
|
||||
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
||||
resource->format, props, resource->aspect, resource->viewType, resource->usageFlags,
|
||||
resource->extent, resource->depth, resource->mipLevels, baseLayer, layerCount,
|
||||
resource->arrayLayers);
|
||||
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; no mip arm takes this shape, levels %u..%u left as they are",
|
||||
detail.c_str(), base + 1, base + count - 1);
|
||||
return;
|
||||
}
|
||||
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
|
||||
MagmaWireFatal("mipmap-growth");
|
||||
if (!nativeBlit && resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): THE DEPTH ARM, and it is first because a
|
||||
// depth format without BLIT_DST is the ordinary case, not the exotic one -
|
||||
@@ -1422,18 +1479,17 @@
|
||||
// The per-level pass is WireDepthMipmap.inc's, which is the monolith's fragment
|
||||
// shader with the monolith's arithmetic on a baked pipeline that owns no frontend
|
||||
// object - §3.2's "observably the monolith's" without the GL program the monolith
|
||||
// needs to get there.
|
||||
if (baseLayer >= resource->arrayLayers || layerCount > resource->arrayLayers - baseLayer)
|
||||
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
||||
resource->format, props, resource->aspect, resource->viewType,
|
||||
resource->usageFlags, resource->extent, resource->depth, resource->mipLevels,
|
||||
baseLayer, layerCount, resource->arrayLayers).c_str());
|
||||
// needs to get there. Its layer window and format were checked before the growth.
|
||||
for (Uint32 level = base + 1; level < base + count; ++level) {
|
||||
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
|
||||
WireImage source{};
|
||||
source.image = resource->image;
|
||||
source.format = format;
|
||||
source.aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
// P8-SV: the image's own view type, so the pass can refuse a shape it cannot
|
||||
// render (it makes 2D views) instead of receiving the struct's 2D default.
|
||||
source.viewType = resource->viewType;
|
||||
source.is3D = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
||||
source.layout = resource->layout;
|
||||
source.trackedLayout = &resource->layout;
|
||||
source.levels = resource->mipLevels;
|
||||
@@ -1445,15 +1501,19 @@
|
||||
destination.level = level;
|
||||
destination.extent = {std::max(resource->extent.width >> level, 1u),
|
||||
std::max(resource->extent.height >> level, 1u)};
|
||||
// The pass reports the SHAPE it cannot take (1D, 3D, multisample, a
|
||||
// format without sampled or depth-attachment support), so the refusal
|
||||
// keeps its name and gains the reason.
|
||||
if (!GenerateWireDepthMipLevel(source, destination))
|
||||
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
||||
// The pass reports the SHAPE it cannot take. depthArmTakesShape above asks
|
||||
// its questions first, so this is reached only if the two lists drift apart
|
||||
// - and then it is the same named decline (P8-F), not a session's end. It
|
||||
// can only refuse the first level: the shape does not change down the chain.
|
||||
if (!GenerateWireDepthMipLevel(source, destination)) {
|
||||
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
||||
resource->format, props, resource->aspect, resource->viewType,
|
||||
resource->usageFlags, resource->extent, resource->depth,
|
||||
resource->mipLevels, baseLayer, layerCount,
|
||||
resource->arrayLayers).c_str());
|
||||
resource->mipLevels, baseLayer, layerCount, resource->arrayLayers);
|
||||
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; the depth pass refused level %u",
|
||||
detail.c_str(), level);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!nativeBlit) {
|
||||
@@ -1470,27 +1530,9 @@
|
||||
// cannot, an owned scratch attachment of the destination level's size renders
|
||||
// the mip and vkCmdCopyImage moves it in: a raw texel move, no conversion, and
|
||||
// the texels still come from GPU contents rather than from a staged shadow.
|
||||
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
|
||||
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
|
||||
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
|
||||
const Bool layersInRange = baseLayer < resource->arrayLayers &&
|
||||
layerCount <= resource->arrayLayers - baseLayer;
|
||||
// The shader arm samples through a sampler2D and writes a vec4, which is what
|
||||
// BlitWireColorImage's own float check enforces; naming it here rather than
|
||||
// letting the blit return false is what puts the reason in the fatal line.
|
||||
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
|
||||
const Bool renderableFormat =
|
||||
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
||||
const Bool attachable = forcedShaderTier < 2 &&
|
||||
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
|
||||
const Bool viaScratch = !attachable && renderableFormat &&
|
||||
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
|
||||
if (!colour || !planar || !layersInRange || !sampledFloat || (!attachable && !viaScratch))
|
||||
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
||||
resource->format, props, resource->aspect, resource->viewType,
|
||||
resource->usageFlags, resource->extent, resource->depth, resource->mipLevels,
|
||||
baseLayer, layerCount, resource->arrayLayers).c_str());
|
||||
//
|
||||
// Which of the two (`attachable` or `viaScratch`), and whether either takes the
|
||||
// shape at all, was decided before the growth (colourArmTakesShape, P8-F).
|
||||
for (Uint32 level = base + 1; level < base + count; ++level) {
|
||||
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
|
||||
WireImage source{};
|
||||
@@ -1574,14 +1616,24 @@
|
||||
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition");
|
||||
// P8-SV: A 3D IMAGE HAS ONE ARRAY LAYER, AND ITS DEPTH IS THE Z EXTENT OF THE BLIT.
|
||||
// ResolveWireTextureStorage answers `layerCount` in slices for Tex3D (Depth >> level,
|
||||
// VkTextureManager.cpp) - the addressing attachments, descriptors and readback use - and
|
||||
// passing that on as the blit's subresource layerCount names layers the image (created
|
||||
// with arrayLayers = 1) does not have (VUID-vkCmdBlitImage-srcImage-00240,
|
||||
// -srcSubresource-01707, -dstSubresource-01708). The monolith arm blits layer 0, count 1,
|
||||
// with the z offsets below halving the depth level by level; so does this one now.
|
||||
const Bool volume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
||||
const Uint32 blitBaseLayer = volume ? 0u : baseLayer;
|
||||
const Uint32 blitLayerCount = volume ? 1u : layerCount;
|
||||
for (Uint32 level=base+1;level<base+count;++level) {
|
||||
VkImageLayout previous = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1);
|
||||
VkImageBlit blit{};
|
||||
blit.srcSubresource = {resource->aspect,level-1,baseLayer,layerCount};
|
||||
blit.dstSubresource = {resource->aspect,level,baseLayer,layerCount};
|
||||
blit.srcSubresource = {resource->aspect,level-1,blitBaseLayer,blitLayerCount};
|
||||
blit.dstSubresource = {resource->aspect,level,blitBaseLayer,blitLayerCount};
|
||||
blit.srcOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>(level-1),1u)),
|
||||
static_cast<Int32>(std::max(resource->extent.height>>(level-1),1u)),
|
||||
static_cast<Int32>(std::max(resource->depth>>(level-1),1u))};
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 CacheVersion = MG_Config::CacheVersion;
|
||||
Uint32 SurfaceWidth = 1;
|
||||
Uint32 SurfaceHeight = 1;
|
||||
// The app's eglSwapInterval; empty until it asks. Window renderers only - see
|
||||
// VulkanRenderer::SetSwapInterval.
|
||||
Optional<Int> SwapInterval;
|
||||
Bool DisablePipelineCache = false;
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
Bool EnableValidationLayers = true;
|
||||
|
||||
@@ -0,0 +1,737 @@
|
||||
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.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 "GbmFrameChannel.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// WHAT THIS FILE IS WRITTEN AGAINST, AND WHAT HAS NOT BEEN RUN.
|
||||
//
|
||||
// The other end exists and is tested on this machine: MobileGL/MG_Gbm/GbmHostChannel.cpp dials
|
||||
// this socket with SOCK_SEQPACKET, reads one message per frame, computes that message's expected
|
||||
// length from the handle's own two counts, and refuses any other length. Every byte below is
|
||||
// written to that parser. There is no phone attached to the machine this was written on, so the
|
||||
// file is compile-verified for aarch64 Android and nothing more: the first run on a device is what
|
||||
// will say whether the relay in RelayHandle() reads back the handle's bytes exactly as
|
||||
// AHardwareBuffer_sendHandleToUnixSocket writes them, which is why that path is written to
|
||||
// survive both layouts the platform's serializer could have (see the note there).
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
namespace {
|
||||
// THE LAYOUT IS A PROTOCOL, SO ITS SIZES ARE ASSERTED AND NOT ASSUMED. The container
|
||||
// derives the length of a frame message from these three sizes plus the handle's counts,
|
||||
// so a field added here, or the packing attribute dropped, would not be a compile error
|
||||
// over there - it would be every frame skipped with a message about its length.
|
||||
static_assert(sizeof(GbmFrameOffer) == 10 * sizeof(Uint32), "the offer is ten 32 bit fields on the wire");
|
||||
static_assert(sizeof(GbmFrameRelease) == 3 * sizeof(Uint32), "the release is three 32 bit fields on the wire");
|
||||
static_assert(sizeof(GbmNativeHandleHeader) == 3 * sizeof(Int32),
|
||||
"a native handle begins with a version and two counts, on every Android ABI");
|
||||
|
||||
// The container reads one message per frame into a 4 KiB buffer and skips a message that
|
||||
// does not fit in it (GbmHostMessageMax, GbmHostChannel.cpp), so a message larger than this
|
||||
// is refused here rather than built and dropped there.
|
||||
constexpr SizeT kMessageMax = 4096;
|
||||
// The descriptors one handle may carry, which is the container's own cap (GbmHostMaxFds).
|
||||
// Every AHardwareBuffer handle this project has seen carries one - the dma-buf - and both
|
||||
// ends refuse a frame whose handle names more than they can take rather than forward a
|
||||
// truncated list into a buffer of unknown memory.
|
||||
constexpr Int kMaxHandleFds = 8;
|
||||
// How many release messages one call reads before it returns to the frame. A cap and not a
|
||||
// loop to exhaustion: the messages left behind stay in the socket, whose buffer the kernel
|
||||
// bounds, and the next frame drains them.
|
||||
constexpr Int kDrainMax = 64;
|
||||
// HOW MANY DIALERS THE KERNEL MAY HOLD FOR THIS LISTENER. One container is the protocol,
|
||||
// but the queue is not the connection: every compositor that was restarted dials once more,
|
||||
// and a listener that only ever has one slot leaves its queue full for the next one. With
|
||||
// listen(fd, 1) that is exactly what was measured - connect() from the container answered
|
||||
// EAGAIN(11) on four attempts out of four after two restarts, and no dialer reached this
|
||||
// side again for as long as the server lived.
|
||||
constexpr Int kDialerBacklog = 16;
|
||||
// The acceptor's poll timeout. It is a wake-up and not a schedule, so it only has to be
|
||||
// short enough that a container dialling into a socket nothing else touches is taken
|
||||
// promptly; a dialer that arrives while the thread is waiting wakes it at once.
|
||||
constexpr Int kAcceptPollMs = 100;
|
||||
// How long the relay read waits for bytes the send one statement earlier has already put in
|
||||
// the socket. It exists only so that an invariant broken somewhere else cannot park the
|
||||
// apply thread; on a local socketpair the wait is zero.
|
||||
constexpr Int kRelayWaitMs = 2;
|
||||
|
||||
// close() may overwrite the errno a caller is in the middle of reporting, and every caller
|
||||
// here is reporting a failure errno describes.
|
||||
void CloseQuietly(Int fd) {
|
||||
if (fd < 0) return;
|
||||
const Int savedErrno = errno;
|
||||
::close(fd);
|
||||
errno = savedErrno;
|
||||
}
|
||||
}
|
||||
|
||||
GbmFrameChannel::~GbmFrameChannel() { Close(); }
|
||||
|
||||
Bool GbmFrameChannel::Open() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_listen >= 0) return true;
|
||||
|
||||
const char* configured = ::getenv(kGbmFrameSocketEnv);
|
||||
const char* path = (configured != nullptr && configured[0] != 0) ? configured : kGbmFrameSocketPath;
|
||||
constexpr SizeT pathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
|
||||
if (std::strlen(path) >= pathMax) {
|
||||
MGLOG_E_ONCE("gbm frame channel: the container socket path %s does not fit in sun_path (%zu bytes); no "
|
||||
"container can be served",
|
||||
path, pathMax);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SOCK_SEQPACKET because that is what the container dials with (GbmHostChannel::EnsureOpen
|
||||
// asks for exactly this), and a stream listener answers it EPROTOTYPE - the container
|
||||
// prints that as "a socket of a different type". SOCK_NONBLOCK because the accept below
|
||||
// runs on the apply thread and may not wait for a container that is not there; SOCK_CLOEXEC
|
||||
// because every process this server starts would otherwise inherit a socket that receives
|
||||
// the phone's frames.
|
||||
const Int fd = ::socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: socket(AF_UNIX, SOCK_SEQPACKET): %s; the frames are drawn and presented "
|
||||
"without a container",
|
||||
std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A node left behind by a server that died is not a reason to refuse a container: bind
|
||||
// would answer EADDRINUSE for a socket nobody is listening on.
|
||||
::unlink(path);
|
||||
|
||||
struct sockaddr_un address = {};
|
||||
address.sun_family = AF_UNIX;
|
||||
// The terminator travels with the name: sun_path is a C string for a pathname socket, and a
|
||||
// name without one is a name the kernel reads past the end of.
|
||||
std::memcpy(address.sun_path, path, std::strlen(path) + 1);
|
||||
if (::bind(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: bind(%s): %s; the frames are drawn and presented without a container",
|
||||
path, std::strerror(errno));
|
||||
CloseQuietly(fd);
|
||||
return false;
|
||||
}
|
||||
// connect() to a pathname socket needs write permission on the node, and the container is
|
||||
// not this process's user, so the umask's usual 0755 would leave it refused for a reason it
|
||||
// could not see. 0666 is what makes the node reachable by the other half of a split run; a
|
||||
// chmod that fails is not fatal, because a peer running as this uid still dials it.
|
||||
if (::chmod(path, 0666) != 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: chmod(%s, 0666): %s; a container running as another user cannot dial "
|
||||
"this socket",
|
||||
path, std::strerror(errno));
|
||||
}
|
||||
if (::listen(fd, kDialerBacklog) != 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: listen(%s): %s; the frames are drawn and presented without a container",
|
||||
path, std::strerror(errno));
|
||||
::unlink(path);
|
||||
CloseQuietly(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_listen = fd;
|
||||
m_path = path;
|
||||
// THE ACCEPTOR IS STARTED WITH THE SOCKET AND NOT WITH THE FIRST FRAME. A compositor dials
|
||||
// at its device creation and keeps the connection, so the dial a frame would have answered
|
||||
// has already happened by the time that frame is offered: an accept on the offer path is an
|
||||
// accept that runs after the thing it exists for. The descriptor is copied into the thread
|
||||
// as an argument - it is the one piece of this object the thread reads without the lock,
|
||||
// which is what lets Close() join it before closing that descriptor.
|
||||
m_accepting.store(true, std::memory_order_release);
|
||||
try {
|
||||
m_acceptor = std::thread(&GbmFrameChannel::AcceptLoop, this, fd);
|
||||
} catch (...) {
|
||||
// A thread that cannot be made is a phone that is out of them, not a reason to refuse
|
||||
// the frames the container can still be given one at a time.
|
||||
m_accepting.store(false, std::memory_order_release);
|
||||
MGLOG_W_ONCE("gbm frame channel: the acceptor thread could not be started; a container is only taken in "
|
||||
"when a frame is offered, and one that dials between frames waits for the next one");
|
||||
}
|
||||
MGLOG_I("gbm frame channel: listening on %s for one container (SOCK_SEQPACKET, one message per frame)", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::EnsureRelay() {
|
||||
if (m_relay[0] >= 0 && m_relay[1] >= 0) return true;
|
||||
// Both ends are dropped before a new pair is made: half a socketpair is a socket with bytes
|
||||
// in it that nothing will ever read, and the next handle read from it would be the tail of
|
||||
// the last one.
|
||||
DropRelay();
|
||||
Int pair[2] = {-1, -1};
|
||||
if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, pair) != 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: socketpair for the handle relay: %s; no frame can be offered until one "
|
||||
"can be made",
|
||||
std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
m_relay[0] = pair[0];
|
||||
m_relay[1] = pair[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::DropRelay() {
|
||||
CloseQuietly(m_relay[0]);
|
||||
CloseQuietly(m_relay[1]);
|
||||
m_relay[0] = -1;
|
||||
m_relay[1] = -1;
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
|
||||
Int* fds, Int& fdCount) {
|
||||
payloadBytes = 0;
|
||||
fdCount = 0;
|
||||
if (!EnsureRelay()) return false;
|
||||
|
||||
// WHY A FRAME'S HANDLE GOES THROUGH A SOCKETPAIR OF THIS CHANNEL'S OWN.
|
||||
//
|
||||
// The container takes ONE message per frame - the offer, then the handle's counts, then the
|
||||
// arrays those counts describe, with the descriptors in that same message's SCM_RIGHTS
|
||||
// control data - and it refuses a message of any other length. Two sends cannot be one
|
||||
// message on a message-oriented socket, so AHardwareBuffer_sendHandleToUnixSocket, which
|
||||
// sends a handle as a message of its own, is pointed at a socketpair this channel owns, and
|
||||
// what comes out of the other end is both halves of what the container wants: the handle's
|
||||
// own bytes, read here, and its descriptors, as SCM_RIGHTS.
|
||||
//
|
||||
// The alternative does not exist: the NDK's <android/hardware_buffer.h> declares no way to
|
||||
// ask a buffer for its descriptors. AHardwareBuffer_getNativeHandle is a platform API and
|
||||
// is absent from the NDK's copy of that header (checked against NDK 27.3.13750724 in this
|
||||
// tree's build-termux toolchain), so this is the one route from an AHardwareBuffer to a
|
||||
// dma-buf descriptor that an app can take.
|
||||
const Int code = AHardwareBuffer_sendHandleToUnixSocket(buffer, m_relay[0]);
|
||||
if (code != 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: the frame's handle did not cross the relay socket (rc %d, errno %d); the "
|
||||
"frame is not offered to the container",
|
||||
code, errno);
|
||||
// The relay is dropped rather than kept: a send that failed part way through would
|
||||
// leave bytes that the next handle's read would take for its own header.
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
|
||||
struct iovec vector = {};
|
||||
vector.iov_base = payload;
|
||||
vector.iov_len = capacity;
|
||||
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &vector;
|
||||
message.msg_iovlen = 1;
|
||||
message.msg_control = control;
|
||||
message.msg_controllen = sizeof(control);
|
||||
|
||||
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC so a descriptor
|
||||
// read out of the relay does not survive into a child of this process, which would then hold
|
||||
// the phone's frame until it died.
|
||||
ssize_t got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||
if (got < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
struct pollfd waiting = {};
|
||||
waiting.fd = m_relay[1];
|
||||
waiting.events = POLLIN;
|
||||
if (::poll(&waiting, 1, kRelayWaitMs) > 0) {
|
||||
// The kernel rewrites these on every call, so a retry starts from the same state as
|
||||
// the first attempt.
|
||||
message.msg_controllen = sizeof(control);
|
||||
message.msg_flags = 0;
|
||||
got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||
}
|
||||
}
|
||||
if (got < 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a relayed handle could not be read back (%d: %s); the frame is not "
|
||||
"offered to the container",
|
||||
errno, std::strerror(errno));
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
if (got == 0) {
|
||||
// Zero bytes on a connected socket is the end of it. Only this process holds either end
|
||||
// of this socketpair, so this is a bug being reported rather than a peer going away.
|
||||
MGLOG_W_ONCE("gbm frame channel: the handle relay is closed; the frame is not offered to the container");
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
|
||||
// The descriptors come out of the control data before anything is decided about the
|
||||
// message, because they belong to this process from the moment they were received: a path
|
||||
// that returns without closing them leaks a descriptor per frame.
|
||||
Int collected = 0;
|
||||
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
|
||||
header = CMSG_NXTHDR(&message, header)) {
|
||||
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS) continue;
|
||||
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
|
||||
const Int count = (Int)(bytes / sizeof(Int));
|
||||
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
|
||||
for (Int i = 0; i < count; ++i) {
|
||||
if (collected < kMaxHandleFds) {
|
||||
fds[collected++] = descriptors[i];
|
||||
} else {
|
||||
// More descriptors than a frame can carry: they are this process's now, so they
|
||||
// are closed here rather than leaked.
|
||||
CloseQuietly(descriptors[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((message.msg_flags & MSG_CTRUNC) != 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a frame's control data did not fit in %zu bytes; the frame is not "
|
||||
"offered because its descriptor list is incomplete",
|
||||
sizeof(control));
|
||||
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
if ((message.msg_flags & MSG_TRUNC) != 0 || (SizeT)got < sizeof(GbmNativeHandleHeader)) {
|
||||
// A message longer than a handle can be, or shorter than the three counts every handle
|
||||
// begins with. Either way it cannot describe a frame, and a relay that has been read
|
||||
// past the end of one message must not carry the next frame's header: it is recreated.
|
||||
MGLOG_W_ONCE("gbm frame channel: a relayed handle is %zd bytes and %s; the frame is not offered", got,
|
||||
((message.msg_flags & MSG_TRUNC) != 0) ? "longer than a handle can be"
|
||||
: "too short to hold its own counts");
|
||||
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
|
||||
// THE BYTES THAT COME BACK ARE THE HANDLE'S INTEGER ARRAY, NOT A handle AND ITS COUNTS.
|
||||
// Measured on the phone: parsing the front of this payload as {version, numFds, numInts}
|
||||
// produced a NumFds taken from the buffer's own integer data, and every frame was refused
|
||||
// with "its handle carries more descriptors than either end takes" - which is what the
|
||||
// container saw as a connection that was accepted and then closed without a frame. The two
|
||||
// counts are known HERE and are not in those bytes: the descriptors came out of the control
|
||||
// data (collected of them) and the payload is their integer array. So the header is BUILT
|
||||
// rather than copied, and what leaves this process is a real native_handle_t layout -
|
||||
// version, numFds, numInts, then the ints - which is exactly what the container's parser
|
||||
// reads (GbmInternal.h's AHBNativeHandleHeader).
|
||||
if (payloadBytes + sizeof(GbmNativeHandleHeader) > capacity) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a handle's integer array does not fit in a frame message with its "
|
||||
"header; the frame is not offered to the container");
|
||||
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
std::memmove(payload + sizeof(GbmNativeHandleHeader), payload, payloadBytes);
|
||||
GbmNativeHandleHeader header = {};
|
||||
header.Version = (Int32)sizeof(GbmNativeHandleHeader);
|
||||
header.NumFds = (Int32)collected;
|
||||
header.NumInts = (Int32)(payloadBytes / sizeof(Int32));
|
||||
std::memcpy(payload, &header, sizeof(header));
|
||||
payloadBytes += sizeof(header);
|
||||
fdCount = collected;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The acceptor. It touches the listener - its own copy of the descriptor, which nothing else
|
||||
// closes while this loop runs - and, once per dialer, the channel under its lock.
|
||||
void GbmFrameChannel::AcceptLoop(Int listen) {
|
||||
while (m_accepting.load(std::memory_order_acquire)) {
|
||||
struct pollfd waiting = {};
|
||||
waiting.fd = listen;
|
||||
waiting.events = POLLIN;
|
||||
const Int ready = ::poll(&waiting, 1, kAcceptPollMs);
|
||||
if (ready < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
MGLOG_W_ONCE("gbm frame channel: poll on the listening socket failed (%d: %s); no container can be "
|
||||
"taken in until the socket is opened again",
|
||||
errno, std::strerror(errno));
|
||||
return;
|
||||
}
|
||||
if (ready == 0) continue;
|
||||
// accept4, so the connection is close-on-exec and non-blocking from the moment it
|
||||
// exists: a container that stops reading must not be able to park a send on either
|
||||
// thread.
|
||||
const Int fd = ::accept4(listen, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED) continue;
|
||||
MGLOG_W_ONCE("gbm frame channel: accept failed (%d: %s); no container can be taken in until the "
|
||||
"socket is opened again",
|
||||
errno, std::strerror(errno));
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
AdoptLocked(fd);
|
||||
}
|
||||
}
|
||||
|
||||
void GbmFrameChannel::AdoptLocked(Int fd) {
|
||||
if (ClientAliveLocked()) {
|
||||
// ONE CLIENT, which is what the protocol is written for, and a dialer that arrives while
|
||||
// a live one is connected is closed rather than left connected - it learns it is not the
|
||||
// one being served instead of holding a connection whose frames nobody reads.
|
||||
CloseQuietly(fd);
|
||||
MGLOG_W_ONCE("gbm frame channel: a second peer dialled %s while a container is connected; it is closed "
|
||||
"and the frames stay with the connected one",
|
||||
m_path.c_str());
|
||||
return;
|
||||
}
|
||||
m_client = fd;
|
||||
MGLOG_I("gbm frame channel: a container is connected on %s; every frame the host offers is sent to it as "
|
||||
"the dma-buf descriptor behind it",
|
||||
m_path.c_str());
|
||||
// AND THE FRAME IT DIALED FOR GOES OUT FROM HERE. A compositor creates its GBM device before
|
||||
// it asks for a surface, so the frame that is the draw target right now was offered before
|
||||
// this connection existed; a container that is not told about it allocates every buffer of
|
||||
// its new swapchain locally, and keeps them for the life of the process.
|
||||
ReofferFrameLocked();
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::ClientAliveLocked() {
|
||||
if (m_client < 0) return false;
|
||||
Uint8 peek = 0;
|
||||
// MSG_PEEK, because a release waiting in the socket belongs to the render path: this test
|
||||
// only asks whether the peer is still there, and a release it consumed would be a frame the
|
||||
// channel believed was still outstanding.
|
||||
const ssize_t got = ::recv(m_client, &peek, sizeof(peek), MSG_DONTWAIT | MSG_PEEK);
|
||||
if (got > 0) return true;
|
||||
if (got == 0) {
|
||||
// Zero bytes on a connected socket is the end of it, whether the container exited or
|
||||
// the kernel gave up on it. A compositor that crashed and is being restarted is
|
||||
// exactly this, and its successor is the dialer standing right here.
|
||||
MarkDown("the container closed the connection", 0);
|
||||
return false;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return true;
|
||||
MarkDown("recv", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::MarkDown(const char* what, Int error) {
|
||||
if (m_client < 0) return;
|
||||
CloseQuietly(m_client);
|
||||
m_client = -1;
|
||||
if (error != 0) {
|
||||
MGLOG_W("gbm frame channel: the container connection is down: %s failed (%d: %s); the frames are still "
|
||||
"drawn and presented, and the container stops receiving them until it dials again",
|
||||
what, error, std::strerror(error));
|
||||
} else {
|
||||
MGLOG_W("gbm frame channel: the container connection is down: %s; the frames are still drawn and "
|
||||
"presented, and the container stops receiving them until it dials again",
|
||||
what);
|
||||
}
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer) {
|
||||
if (buffer == nullptr) return false;
|
||||
// Open() does nothing once the socket exists, so this is an integer test in the common case.
|
||||
Open();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// The releases the container has sent since the last frame, which is also how a container
|
||||
// that died is noticed. Whoever replaced it was taken by the acceptor's own thread the
|
||||
// moment it dialled: that thread is what makes a container's connect() succeed at all.
|
||||
PollLocked();
|
||||
// AND THE FRAME IS REMEMBERED BEFORE IT IS SENT. A container that dials while this call is
|
||||
// running is one the acceptor hands the frame it finds here to, so the two threads must not
|
||||
// be able to disagree about which frame is the one being drawn into.
|
||||
const Uint32 seq = m_nextSeq;
|
||||
RememberFrameLocked(buffer, offer, seq);
|
||||
if (m_client < 0) return false;
|
||||
return SendFrameLocked(buffer, offer, seq, /*counting=*/true);
|
||||
}
|
||||
|
||||
void GbmFrameChannel::RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq) {
|
||||
if (m_frame == buffer) {
|
||||
// The host redrawing into the buffer it already handed over: the same frame under a new
|
||||
// sequence number. Acquiring it again would be a reference this channel takes and never
|
||||
// gives back.
|
||||
m_frameOffer = offer;
|
||||
m_frameSeq = seq;
|
||||
return;
|
||||
}
|
||||
// The reference is this channel's own, and it is what makes the frame a container that dials
|
||||
// late is owed safe to hold: the backend drops its handle the moment the frame is presented,
|
||||
// and a frame owed to a dialer arriving in the same millisecond may not be memory that has
|
||||
// already gone back to the host's pool.
|
||||
AHardwareBuffer_acquire(buffer);
|
||||
ReleaseFrameLocked();
|
||||
m_frame = buffer;
|
||||
m_frameOffer = offer;
|
||||
m_frameSeq = seq;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::ReleaseFrameLocked() {
|
||||
if (m_frame == nullptr) return;
|
||||
AHardwareBuffer_release(m_frame);
|
||||
m_frame = nullptr;
|
||||
m_frameOffer = {};
|
||||
m_frameSeq = 0;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::DropFrame() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
ReleaseFrameLocked();
|
||||
}
|
||||
|
||||
void GbmFrameChannel::ReofferFrameLocked() {
|
||||
if (m_frame == nullptr) return;
|
||||
// The same frame, under the sequence number it was offered with: it IS the frame that is the
|
||||
// draw target now, and a second number for it would be this channel claiming the host had
|
||||
// handed over a frame it never handed over.
|
||||
MGLOG_I("gbm frame channel: a container dialled after frame %u was offered; it is sent that frame, which is "
|
||||
"still the draw target",
|
||||
m_frameSeq);
|
||||
SendFrameLocked(m_frame, m_frameOffer, m_frameSeq, /*counting=*/false);
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq,
|
||||
Bool counting) {
|
||||
if (m_client < 0) return false;
|
||||
|
||||
Uint8 payload[kMessageMax];
|
||||
SizeT payloadBytes = 0;
|
||||
Int fds[kMaxHandleFds];
|
||||
for (Int i = 0; i < kMaxHandleFds; ++i) fds[i] = -1;
|
||||
Int fdCount = 0;
|
||||
if (!RelayHandle(buffer, payload, sizeof(payload), payloadBytes, fds, fdCount)) return false;
|
||||
|
||||
GbmNativeHandleHeader handle = {};
|
||||
std::memcpy(&handle, payload, sizeof(handle));
|
||||
const SizeT tail = ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
|
||||
const SizeT messageBytes = sizeof(GbmFrameOffer) + sizeof(GbmNativeHandleHeader) + tail;
|
||||
|
||||
// EVERY REASON THIS CHANNEL REFUSES A FRAME, IN ONE PLACE. Each of them is a frame the
|
||||
// container would refuse anyway - it checks the same counts and the same geometry - and a
|
||||
// frame that is refused here costs nothing: no message, no sequence number, and no entry in
|
||||
// the count of frames waiting to be released.
|
||||
const char* refusal = nullptr;
|
||||
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0) {
|
||||
refusal = "the host described it with a width, height or stride of zero, and a buffer with no size is "
|
||||
"not one a compositor can render into";
|
||||
} else if (handle.NumFds < 1) {
|
||||
refusal = "its handle carries no descriptor, and a frame without a dma-buf is not one the container "
|
||||
"can render into";
|
||||
} else if (handle.NumFds > kMaxHandleFds) {
|
||||
refusal = "its handle carries more descriptors than either end takes";
|
||||
} else if (handle.NumInts < 0) {
|
||||
refusal = "its handle claims a negative number of integers";
|
||||
} else if ((SizeT)handle.NumInts > kMessageMax / sizeof(Int32)) {
|
||||
// This is what keeps the message's length arithmetic from wrapping on a 32 bit ABI: a
|
||||
// handle whose integer array alone is longer than a whole message cannot be carried by
|
||||
// this protocol whatever the rest of it says, and the check below would otherwise be
|
||||
// reading a number that had already turned into a small and plausible one.
|
||||
refusal = "its handle names more integers than a whole message could carry";
|
||||
} else if (fdCount < handle.NumFds) {
|
||||
refusal = "fewer descriptors arrived with it than its handle names";
|
||||
} else if (messageBytes > kMessageMax) {
|
||||
refusal = "the message it makes is longer than the container reads";
|
||||
}
|
||||
|
||||
Bool offered = false;
|
||||
if (refusal != nullptr) {
|
||||
// ONCE, not per frame: a host that describes every frame the same broken way must not
|
||||
// turn this into a line per frame on the apply thread.
|
||||
MGLOG_W_ONCE("gbm frame channel: frame %u (%ux%u) is not offered to the container: %s", offer.Index,
|
||||
offer.Width, offer.Height, refusal);
|
||||
} else {
|
||||
// One message, built whole: the offer, then the handle's own three counts, then the two
|
||||
// arrays the counts describe. What came back from the relay is copied in as far as it
|
||||
// goes and the remainder is left zero, because the container checks the LENGTH it
|
||||
// derives from those counts and never reads the arrays themselves - it takes the
|
||||
// descriptors from the control data and hands the first one to GBM. So a handle whose
|
||||
// bytes are shorter than its own counts imply is padded rather than refused, and one
|
||||
// longer than they imply is cut to what the counts say; either way the length the
|
||||
// container computes is the length it receives.
|
||||
Uint8 message[kMessageMax] = {};
|
||||
GbmFrameOffer out = {};
|
||||
out.Magic = kGbmFrameOfferMagic;
|
||||
out.Version = kGbmFrameVersion;
|
||||
out.Seq = seq;
|
||||
out.Index = offer.Index;
|
||||
out.Width = offer.Width;
|
||||
out.Height = offer.Height;
|
||||
out.StrideBytes = offer.StrideBytes;
|
||||
out.Format = offer.Format;
|
||||
out.UsageLo = offer.UsageLo;
|
||||
out.UsageHi = offer.UsageHi;
|
||||
std::memcpy(message, &out, sizeof(out));
|
||||
const SizeT handleBytes = (payloadBytes < sizeof(GbmNativeHandleHeader) + tail)
|
||||
? payloadBytes
|
||||
: (sizeof(GbmNativeHandleHeader) + tail);
|
||||
std::memcpy(message + sizeof(out), payload, handleBytes);
|
||||
|
||||
struct iovec vector = {};
|
||||
vector.iov_base = message;
|
||||
vector.iov_len = messageBytes;
|
||||
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
|
||||
struct msghdr outgoing = {};
|
||||
outgoing.msg_iov = &vector;
|
||||
outgoing.msg_iovlen = 1;
|
||||
outgoing.msg_control = control;
|
||||
outgoing.msg_controllen = CMSG_SPACE(sizeof(Int) * (SizeT)handle.NumFds);
|
||||
struct cmsghdr* rights = CMSG_FIRSTHDR(&outgoing);
|
||||
rights->cmsg_level = SOL_SOCKET;
|
||||
rights->cmsg_type = SCM_RIGHTS;
|
||||
rights->cmsg_len = CMSG_LEN(sizeof(Int) * (SizeT)handle.NumFds);
|
||||
std::memcpy(CMSG_DATA(rights), fds, sizeof(Int) * (SizeT)handle.NumFds);
|
||||
|
||||
// MSG_DONTWAIT for the promise at the top of this file, and MSG_NOSIGNAL because a
|
||||
// server killed by SIGPIPE when the container goes away takes the frames it was drawing
|
||||
// with it. A full socket, a reset peer and a broken pipe all say the same thing here:
|
||||
// this connection can no longer be trusted with a frame, so it is closed and the next
|
||||
// container to dial is served.
|
||||
const ssize_t sent = ::sendmsg(m_client, &outgoing, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (sent != (ssize_t)messageBytes) {
|
||||
MarkDown("sendmsg", (sent < 0) ? errno : 0);
|
||||
} else {
|
||||
// A frame offered for the first time is the next sequence number and one more frame
|
||||
// outstanding. The same frame sent again to a container that has just dialled is
|
||||
// neither: the host handed over nothing new, and a count of frames waiting to be
|
||||
// released may not be moved by a message that describes one already in it.
|
||||
if (counting) {
|
||||
m_nextSeq = seq + 1;
|
||||
++m_offered;
|
||||
++m_outstanding;
|
||||
}
|
||||
offered = true;
|
||||
// The first offer is the one worth a line, and it carries the host's own numbers:
|
||||
// the format in particular is the field the two ends could disagree about without
|
||||
// either failing, so the value the host actually sends is put where a run on a
|
||||
// device can be read afterwards.
|
||||
MGLOG_I_ONCE("gbm frame channel: the first frame is offered to the container (seq %u index %u %ux%u "
|
||||
"stride %u format 0x%08x)",
|
||||
out.Seq, out.Index, out.Width, out.Height, out.StrideBytes, out.Format);
|
||||
MGLOG_D("gbm frame channel: frame seq=%u index=%u offered (%zu bytes, %d descriptor(s), %llu "
|
||||
"outstanding)",
|
||||
out.Seq, out.Index, messageBytes, handle.NumFds, (unsigned long long)m_outstanding);
|
||||
}
|
||||
}
|
||||
|
||||
// The kernel took its own copies of these when the message was sent, and one that was not
|
||||
// sent is not the container's to hold: every path closes what the relay handed over.
|
||||
for (Int i = 0; i < fdCount; ++i) CloseQuietly(fds[i]);
|
||||
return offered;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::Poll() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
PollLocked();
|
||||
}
|
||||
|
||||
void GbmFrameChannel::PollLocked() {
|
||||
if (m_client < 0) return;
|
||||
for (Int drained = 0; drained < kDrainMax; ++drained) {
|
||||
GbmFrameRelease release = {};
|
||||
struct iovec vector = {};
|
||||
vector.iov_base = &release;
|
||||
vector.iov_len = sizeof(release);
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &vector;
|
||||
message.msg_iovlen = 1;
|
||||
// recvmsg and not recv, for MSG_TRUNC: a message longer than a release can only be a
|
||||
// peer speaking something else, and a release read out of the first twelve bytes of it
|
||||
// would name a frame nobody released.
|
||||
const ssize_t got = ::recvmsg(m_client, &message, MSG_DONTWAIT);
|
||||
if (got < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
// Nothing said, which is what a container that is between frames looks like.
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) return;
|
||||
MarkDown("recvmsg", errno);
|
||||
return;
|
||||
}
|
||||
if (got == 0) {
|
||||
// Zero bytes on a connected socket is the end of it, whether the container exited or
|
||||
// the kernel gave up on it. Its replacement is taken by the next frame.
|
||||
MarkDown("the container closed the connection", 0);
|
||||
return;
|
||||
}
|
||||
if ((message.msg_flags & MSG_TRUNC) != 0 || got != (ssize_t)sizeof(release)) {
|
||||
// The message is skipped, not the connection: on a message-oriented socket a
|
||||
// message too long for the buffer is consumed whole, so the next one is still a
|
||||
// message rather than the tail of this one.
|
||||
MGLOG_W_ONCE("gbm frame channel: the container sent a message that is not a %zu byte release; it is "
|
||||
"skipped",
|
||||
sizeof(release));
|
||||
continue;
|
||||
}
|
||||
if (release.Magic != kGbmFrameReleaseMagic) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a message from the container begins with 0x%08x rather than 'MGGR'; "
|
||||
"it is skipped",
|
||||
release.Magic);
|
||||
continue;
|
||||
}
|
||||
if (release.Version != kGbmFrameVersion) {
|
||||
// Refused rather than acted on: the version is what says how the rest of the message
|
||||
// is laid out, and a release read with the wrong layout names a frame the container
|
||||
// did not mean.
|
||||
MGLOG_E_ONCE("gbm frame channel: the container speaks protocol version %u where this server speaks "
|
||||
"version %u; its releases are refused until the two agree",
|
||||
release.Version, (Uint32)kGbmFrameVersion);
|
||||
continue;
|
||||
}
|
||||
++m_released;
|
||||
if (m_outstanding > 0) --m_outstanding;
|
||||
// The first release is the only one worth a line: it is the proof that the container
|
||||
// took the descriptor and its compositor retired the frame, where every later one says
|
||||
// the same thing sixty times a second.
|
||||
MGLOG_I_ONCE("gbm frame channel: the container is releasing frames (first: seq %u)", release.Seq);
|
||||
}
|
||||
// Anything past this cap stays in the socket, whose buffer the kernel bounds, and goes out
|
||||
// with the next frame: a peer that says nothing but releases may not spend the apply thread
|
||||
// here.
|
||||
}
|
||||
|
||||
void GbmFrameChannel::Close() {
|
||||
// THE ACCEPTOR IS STOPPED AND JOINED FIRST, and outside the lock. It may be inside that
|
||||
// lock when this is called - a teardown that waited for it while holding it would wait for
|
||||
// ever - and it polls the listening descriptor, which may not be closed under it: the next
|
||||
// open() in this process hands out the descriptor number a moment later, and the thread
|
||||
// would then be polling somebody else's socket.
|
||||
m_accepting.store(false, std::memory_order_release);
|
||||
if (m_acceptor.joinable()) m_acceptor.join();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
// The frame is this channel's reference and is released with it: the backend has already
|
||||
// dropped its own by the time a teardown here is a teardown of the frame as well.
|
||||
ReleaseFrameLocked();
|
||||
// The connection is closed quietly rather than marked down: this is a teardown, not a
|
||||
// failure, and a warning about it would be noise in a log read for the failures.
|
||||
CloseQuietly(m_client);
|
||||
m_client = -1;
|
||||
DropRelay();
|
||||
if (m_listen >= 0) {
|
||||
// The node is this channel's to remove only while it still IS this channel's socket: a
|
||||
// server that was restarted has bound the same path with a socket of its own, and a
|
||||
// teardown that unlinked it would take the successor's node away with it. st_dev and
|
||||
// st_ino against the listening descriptor is the cheapest way to ask, and a check that
|
||||
// fails only leaves a name behind that the next Open() unlinks anyway.
|
||||
struct stat node = {};
|
||||
struct stat socket = {};
|
||||
if (!m_path.empty() && ::stat(m_path.c_str(), &node) == 0 && ::fstat(m_listen, &socket) == 0 &&
|
||||
node.st_dev == socket.st_dev && node.st_ino == socket.st_ino) {
|
||||
::unlink(m_path.c_str());
|
||||
}
|
||||
CloseQuietly(m_listen);
|
||||
m_listen = -1;
|
||||
}
|
||||
m_path.clear();
|
||||
// The counters are history and stay: a caller that logs Outstanding() after a teardown is
|
||||
// asking what happened, and IsUp() is what answers what is connected now.
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// A build that is not the Android render server has no AHardwareBuffer to offer and no
|
||||
// container beside it. The class exists so that callers do not have to be compiled
|
||||
// differently, and every entry point answers the way an absent container does.
|
||||
GbmFrameChannel::~GbmFrameChannel() = default;
|
||||
Bool GbmFrameChannel::Open() { return false; }
|
||||
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer*, const HostFrameOffer&) { return false; }
|
||||
void GbmFrameChannel::Poll() {}
|
||||
void GbmFrameChannel::Close() {}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// End of Source File Header
|
||||
@@ -0,0 +1,281 @@
|
||||
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.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 "HostFrameBridge.h"
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// THE FRAME THE HOST HANDED OVER, HANDED ON TO THE CONTAINER AS A dma-buf.
|
||||
//
|
||||
// The display host owns a pool of AHardwareBuffers: it allocates them, this side imports the
|
||||
// one it offers as a draw target (HostFrameBridge, DirectGLES::HostFrameTarget), and the host
|
||||
// puts it on the glass when this side answers that it is drawn. The Wayland compositor in the
|
||||
// Linux container has to render into THOSE SAME frames - it runs this project's own GBM, and a
|
||||
// gbm_bo_create there answers with the phone's frame instead of a dma-heap allocation of its
|
||||
// own - so every frame this side takes has to reach the container as that frame's dma-buf
|
||||
// descriptor. That, and the release that comes back, is all this channel carries.
|
||||
//
|
||||
// The container's end is MobileGL/MG_Gbm/GbmHostChannel.cpp. It reads exactly the bytes
|
||||
// declared below and answers with a release once its compositor no longer reads the frame.
|
||||
// The three shapes ARE that file's GbmFrameOffer/GbmFrameRelease and GbmInternal.h's
|
||||
// AHBNativeHandleHeader, field for field and packing included. They are restated here rather
|
||||
// than shared because the two ends are built into different libraries from different build
|
||||
// configurations: MG_Backend is the Android render server and never sees libgbm's headers,
|
||||
// while MG_Gbm is built for the container and never sees Android's. A change on either side
|
||||
// is a protocol change, and the magic is what makes a mismatched pair fail loudly instead of
|
||||
// being read as plausible numbers.
|
||||
//
|
||||
// ONE MESSAGE PER FRAME, on SOCK_SEQPACKET, and that is part of the protocol rather than an
|
||||
// implementation detail: an offer and its handle only mean anything together, and the message
|
||||
// boundary is what lets the reader take the two as one thing. The container derives the
|
||||
// length of the message it expects from the handle's own two counts and refuses any other
|
||||
// length, so this end sends the offer immediately followed by the handle's counts and its two
|
||||
// arrays, with the descriptors in the SCM_RIGHTS control data of that same sendmsg.
|
||||
//
|
||||
// The channel is OPTIONAL, and that is the rule the implementation is built around: a
|
||||
// container that is not running, has died, or has stopped reading may cost the render path a
|
||||
// syscall or two, and may never block it, fail it, or grow without bound. Every entry point
|
||||
// below is non-blocking and answers silence with silence.
|
||||
inline constexpr Uint32 kGbmFrameOfferMagic = 0x4647474Du; // 'M','G','G','F' in memory order
|
||||
inline constexpr Uint32 kGbmFrameReleaseMagic = 0x5247474Du; // 'M','G','G','R' in memory order
|
||||
inline constexpr Uint32 kGbmFrameVersion = 1u;
|
||||
// WHERE THE CONTAINER DIALS. /data/local/tmp is the one directory both halves of a split run
|
||||
// can be pointed at on a device, and the default lives on this end because this end is the
|
||||
// server. The container's compiled-in default is /run/mobilegl-gbm.sock - the same protocol
|
||||
// with the path each side can reach unaided - so a deployment that puts the two in different
|
||||
// mount namespaces sets MOBILEGL_GBM_SOCKET on both; it is read here and in
|
||||
// GbmHostChannelCreate, from the same variable, for that reason.
|
||||
inline constexpr const char* kGbmFrameSocketPath = "/data/local/tmp/mobilegl-gbm.sock";
|
||||
inline constexpr const char* kGbmFrameSocketEnv = "MOBILEGL_GBM_SOCKET";
|
||||
|
||||
// One frame the host offered, described for the container. Width, Height, StrideBytes and
|
||||
// Format are the host's own numbers for the frame (HostFrameOffer), carried through
|
||||
// unchanged: they describe the memory the descriptor points at, and this end is not the place
|
||||
// where a number the other two ends agreed on gets rewritten.
|
||||
struct GbmFrameOffer {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Seq; // this channel's, one per frame offered
|
||||
Uint32 Index; // the host's own buffer index, echoed so the phone can match a frame
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 StrideBytes;
|
||||
Uint32 Format;
|
||||
Uint32 UsageLo;
|
||||
Uint32 UsageHi;
|
||||
} __attribute__((packed));
|
||||
|
||||
// What the container says once it is done with a frame. It may arrive late, it may never
|
||||
// arrive, and both are cheap here: the channel keeps a count and no per-frame record (see
|
||||
// Outstanding below), so nothing is leaked and nothing grows when one is missed.
|
||||
struct GbmFrameRelease {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Seq;
|
||||
} __attribute__((packed));
|
||||
|
||||
// THE OTHER DIRECTION: A dma-buf THE CONTAINER WANTS THIS SIDE TO BACK A TEXTURE WITH.
|
||||
//
|
||||
// The compositor's scene reaches the display daemon's dma-bufs through glEGLImageTargetTexture2DOES
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp, MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp both
|
||||
// name this half as the next change). eglCreateImageKHR has already held a dup() of every plane's
|
||||
// descriptor since the image was made; what was missing is the render server's half - telling THIS
|
||||
// side which texture that dma-buf backs, over the one channel that can carry a descriptor across the
|
||||
// container boundary. This message is that half: the client names the texture target and the
|
||||
// texture's own name, describes the buffer exactly as drmModeAddFB2 would, and the plane descriptors
|
||||
// ride in the SCM_RIGHTS control data of the same sendmsg. One message per import, as SEQPACKET
|
||||
// requires, so a descriptor and its description are never read apart.
|
||||
//
|
||||
// The reply is what the client waits for before a framebuffer is allowed to be complete over the
|
||||
// texture: Ok=0 means this side could not make the import real, and the client keeps treating the
|
||||
// texture as having no storage rather than letting it render into nothing.
|
||||
inline constexpr Uint32 kGbmImportRequestMagic = 0x4947474Du; // 'M','G','G','I' in memory order
|
||||
inline constexpr Uint32 kGbmImportReplyMagic = 0x52474749u; // 'I','G','G','R' in memory order
|
||||
inline constexpr Uint32 kGbmImportVersion = 1u;
|
||||
inline constexpr Uint32 kGbmImportMaxPlanes = 4u;
|
||||
|
||||
struct GbmImportRequest {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
|
||||
Uint32 TexName; // the CLIENT's texture name; this side resolves its twin
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 FourCC;
|
||||
Uint32 PlaneCount; // 1..kGbmImportMaxPlanes; the descriptors are the message's control data
|
||||
Int32 Stride[kGbmImportMaxPlanes];
|
||||
Int32 Offset[kGbmImportMaxPlanes];
|
||||
Uint64 Modifier[kGbmImportMaxPlanes];
|
||||
Uint32 HasModifier[kGbmImportMaxPlanes];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct GbmImportReply {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Ok;
|
||||
Uint32 Reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
// An AHardwareBuffer native handle as it appears on the wire: the platform's native_handle_t
|
||||
// (version, then the two counts, then the descriptor array followed by the integer array),
|
||||
// written by AHardwareBuffer_sendHandleToUnixSocket and read by the container's
|
||||
// AHBNativeHandleHeader. The counts are 32 bit on every Android ABI, which is why they are
|
||||
// spelled out here rather than taken from a platform header this library cannot include.
|
||||
struct GbmNativeHandleHeader {
|
||||
Int32 Version;
|
||||
Int32 NumFds;
|
||||
Int32 NumInts;
|
||||
} __attribute__((packed));
|
||||
|
||||
// ONE CONTAINER, ONE LISTENING SOCKET, TWO THREADS: the render path's, and an acceptor's.
|
||||
//
|
||||
// A compositor dials ONCE - when it creates its GBM device - and keeps that connection for the
|
||||
// life of the process, while this side offers a frame per drawn frame. An accept that only ran
|
||||
// inside OfferFrame therefore ran after the dial it was supposed to answer, and a dialer that
|
||||
// nobody took kept the listener's queue full for every later one. That is measured on the
|
||||
// phone rather than argued: with accept() only on the offer path, connect() from the container
|
||||
// answered EAGAIN(11) on four attempts out of four while the node sat there listening.
|
||||
//
|
||||
// So the accept has a thread of its own, started by Open() and stopped by Close(), and it
|
||||
// adopts what it takes on the spot - including sending it the frame the host last handed over,
|
||||
// because a compositor's first allocation is the one its swapchain is built from. The
|
||||
// connected socket, the frame that is remembered, the relay and the counters are shared between
|
||||
// the two threads and guarded by the one mutex below; both entry points are non-blocking, so
|
||||
// neither thread can park the other for longer than a syscall.
|
||||
class GbmFrameChannel {
|
||||
public:
|
||||
GbmFrameChannel() = default;
|
||||
GbmFrameChannel(const GbmFrameChannel&) = delete;
|
||||
GbmFrameChannel& operator=(const GbmFrameChannel&) = delete;
|
||||
~GbmFrameChannel();
|
||||
|
||||
// Creates the listening socket if it is not there yet and starts the acceptor, and does
|
||||
// nothing otherwise. It never waits for a container: the socket is bound, a backlog is
|
||||
// given to listen(), and the acceptor takes whoever dials from then on - whether or not a
|
||||
// frame is being drawn. A caller that wants the container to find the socket before the
|
||||
// first frame - the container dials once at its own device creation and retries about once
|
||||
// a second - can call this when the host-framed surface is initialized, beside
|
||||
// HostFrameBridge::Open.
|
||||
Bool Open();
|
||||
|
||||
// Offers one frame the host just handed over, as the dma-buf descriptor behind it.
|
||||
// False means the frame did not go out, for any of the reasons this channel treats as
|
||||
// ordinary: no container is connected, the container just died, or the frame could not be
|
||||
// described. None of them is the caller's problem and none of them is fatal - the frame
|
||||
// is drawn and presented exactly as it would have been without a channel at all.
|
||||
Bool OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer);
|
||||
|
||||
// Reads whatever the container has said since the last call and marks the channel down if
|
||||
// the connection has ended. OfferFrame already does this once per frame, so a caller only
|
||||
// needs it if it wants releases drained on a path that has no new frame in it.
|
||||
void Poll();
|
||||
|
||||
// The frame the host handed over is gone - it was presented, or it was replaced - and this
|
||||
// channel may no longer hold it or offer it to anybody. THE CHANNEL ITSELF STAYS OPEN, and
|
||||
// that is the point of the call: the listening socket, its node and the container's
|
||||
// connection outlive every frame, and a channel that was closed and reopened per frame took
|
||||
// the node away sixty times a second - a container dialling at the wrong moment got ENOENT
|
||||
// for a socket that was there a millisecond earlier and would be there a millisecond later.
|
||||
void DropFrame();
|
||||
|
||||
// True while a container is connected and the frames are being offered to it.
|
||||
Bool IsUp() const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_client >= 0;
|
||||
}
|
||||
|
||||
// How many frames have been offered and not released, as a count of messages and not a
|
||||
// record of them. It is here to be logged, and it is deliberately the only per-frame state
|
||||
// the channel keeps: a release that never arrives costs a number that is too high, and
|
||||
// nothing else, which is what makes a container that stops reading harmless.
|
||||
Uint64 Outstanding() const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_outstanding;
|
||||
}
|
||||
|
||||
// Closes the connection, the listening socket and the node it created. Idempotent, safe
|
||||
// from a destructor, and safe to call while a container is connected: the container sees
|
||||
// the connection end and goes back to allocating its frames locally, which is the state it
|
||||
// starts in.
|
||||
void Close();
|
||||
|
||||
private:
|
||||
// The socketpair a frame's handle is handed to and read back from. Recreated on demand,
|
||||
// because the one call the NDK offers for a buffer's descriptors sends them as a message
|
||||
// of its own and the container takes one message per frame - see the note in the .cpp.
|
||||
Bool EnsureRelay();
|
||||
void DropRelay();
|
||||
Bool RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
|
||||
Int* fds, Int& fdCount);
|
||||
|
||||
// Takes the one container the protocol allows, and closes anyone else who dials while a
|
||||
// LIVE one is connected rather than leaving them to believe they are being served. A
|
||||
// connection whose peer has exited is not a live one: it is closed here and its dialer's
|
||||
// replacement is taken, which is what a compositor that crashed and was restarted is.
|
||||
void AdoptLocked(Int fd);
|
||||
|
||||
// True while the connected container is still there. recvmsg with MSG_PEEK is what asks:
|
||||
// zero bytes is the peer's close, EAGAIN is a peer that is between frames, and a release
|
||||
// waiting in the socket is a peer that is alive - peeked at rather than read, because the
|
||||
// render path is the one that spends releases.
|
||||
Bool ClientAliveLocked();
|
||||
|
||||
// Takes the one container the protocol allows out of whoever dialled.
|
||||
void AcceptLoop(Int listen);
|
||||
|
||||
// Sends the frame that is the draw target to a container that has just connected.
|
||||
void ReofferFrameLocked();
|
||||
|
||||
// Remembers the frame the host last handed over, holding a reference to it so that a
|
||||
// container which dials after the offer can still be given it. The previous one is
|
||||
// released here; nothing else in the channel holds a frame.
|
||||
void RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq);
|
||||
void ReleaseFrameLocked();
|
||||
|
||||
// The offer itself: the handle relayed out of the buffer, the message built, one sendmsg.
|
||||
// A frame that has already been offered once is sent again with the sequence number it was
|
||||
// offered under and does not move the counters, because it is the same frame and not a new
|
||||
// one.
|
||||
Bool SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq, Bool counting);
|
||||
|
||||
// The body of Poll(), for a caller that is already holding the lock.
|
||||
void PollLocked();
|
||||
|
||||
// Closes the connection and says why, once. The channel stays open for the next container.
|
||||
void MarkDown(const char* what, Int error);
|
||||
|
||||
Int m_listen = -1;
|
||||
Int m_client = -1;
|
||||
Int m_relay[2] = {-1, -1};
|
||||
String m_path;
|
||||
// Bumped only for a frame that actually went out, so a sequence number is never reused and
|
||||
// a release can never be read as belonging to a frame of an earlier connection.
|
||||
Uint32 m_nextSeq = 0;
|
||||
Uint64 m_offered = 0;
|
||||
Uint64 m_released = 0;
|
||||
Uint64 m_outstanding = 0;
|
||||
// The frame the host handed over most recently, held by this channel's own reference for as
|
||||
// long as it is the draw target: it is what a container that dials late is sent.
|
||||
struct AHardwareBuffer* m_frame = nullptr;
|
||||
HostFrameOffer m_frameOffer = {};
|
||||
Uint32 m_frameSeq = 0;
|
||||
// The acceptor, and the flag that ends it. The listening descriptor is handed to the thread
|
||||
// as its own copy, so it is the only thing in here that thread reads without the lock - and
|
||||
// Close() joins the thread before it closes that descriptor.
|
||||
std::thread m_acceptor;
|
||||
std::atomic<Bool> m_accepting{false};
|
||||
// Guards every member above, from both threads. Mutable because the two const questions a
|
||||
// caller asks about the channel - is a container connected, how many frames are outstanding
|
||||
// - are answers that have to be read under it.
|
||||
mutable std::mutex m_mutex;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.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 "HostFrameBridge.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
namespace {
|
||||
Bool ReadExactly(Int fd, void* out, SizeT n) {
|
||||
Uint8* at = static_cast<Uint8*>(out);
|
||||
while (n > 0) {
|
||||
const ssize_t got = ::read(fd, at, n);
|
||||
if (got <= 0) {
|
||||
if (got < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
at += got;
|
||||
n -= static_cast<SizeT>(got);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool WriteExactly(Int fd, const void* in, SizeT n) {
|
||||
const Uint8* at = static_cast<const Uint8*>(in);
|
||||
while (n > 0) {
|
||||
const ssize_t put = ::write(fd, at, n);
|
||||
if (put <= 0) {
|
||||
if (put < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
at += put;
|
||||
n -= static_cast<SizeT>(put);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
HostFrameBridge::~HostFrameBridge() { Close(); }
|
||||
|
||||
Bool HostFrameBridge::Open(const char* path, Int acceptTimeoutMs, String& why) {
|
||||
Close();
|
||||
const char* name = (path != nullptr && path[0] != 0) ? path : kHostFrameSocket;
|
||||
|
||||
m_listen = ::socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (m_listen < 0) {
|
||||
why = std::format("socket(AF_UNIX): {}", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
if (std::strlen(name) >= sizeof(address.sun_path)) {
|
||||
why = std::format("the socket name is longer than the kernel takes: {}", name);
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
// "@name" is the abstract namespace: sun_path[0] is NUL and the name follows it.
|
||||
// Nothing is created on disk, so nothing is left behind and no file permission stands
|
||||
// between two apps that may not write each other's directories.
|
||||
if (name[0] == '@') {
|
||||
address.sun_path[0] = 0;
|
||||
std::strncpy(address.sun_path + 1, name + 1, sizeof(address.sun_path) - 2);
|
||||
} else {
|
||||
std::strncpy(address.sun_path, name, sizeof(address.sun_path) - 1);
|
||||
// A leftover socket file from a server that died is not a reason to refuse the host.
|
||||
::unlink(name);
|
||||
}
|
||||
if (::bind(m_listen, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) {
|
||||
why = std::format("bind({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (::listen(m_listen, 1) != 0) {
|
||||
why = std::format("listen({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
pollfd waiting{};
|
||||
waiting.fd = m_listen;
|
||||
waiting.events = POLLIN;
|
||||
const Int ready = ::poll(&waiting, 1, acceptTimeoutMs);
|
||||
if (ready <= 0) {
|
||||
why = (ready == 0) ? std::format("no display host dialled {} within {} ms", name, acceptTimeoutMs)
|
||||
: std::format("poll({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
m_fd = ::accept(m_listen, nullptr, nullptr);
|
||||
if (m_fd < 0) {
|
||||
why = std::format("accept({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
MGLOG_I("host frame bridge: a display host is on %s", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool HostFrameBridge::Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why) {
|
||||
*buffer = nullptr;
|
||||
offer = HostFrameOffer{};
|
||||
if (m_fd < 0) {
|
||||
why = "the bridge is not open";
|
||||
return false;
|
||||
}
|
||||
AHardwareBuffer* received = nullptr;
|
||||
if (AHardwareBuffer_recvHandleFromUnixSocket(m_fd, &received) != 0 || received == nullptr) {
|
||||
why = "the host stopped offering frames, or the handle did not cross";
|
||||
return false;
|
||||
}
|
||||
if (!ReadExactly(m_fd, &offer, sizeof(offer)) || offer.Magic != kHostFrameMagic) {
|
||||
// The handle is already this side's by the time this fails, so it goes back before
|
||||
// the failure does: a reference held past the connection starves the host's pool.
|
||||
AHardwareBuffer_release(received);
|
||||
why = std::format("the frame's description did not follow its handle (magic 0x{:08x})", offer.Magic);
|
||||
return false;
|
||||
}
|
||||
*buffer = received;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool HostFrameBridge::Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why) {
|
||||
if (m_fd < 0) {
|
||||
why = "the bridge is not open";
|
||||
return false;
|
||||
}
|
||||
HostFrameAck ack{};
|
||||
ack.Magic = kHostFrameMagic;
|
||||
ack.Version = kHostFrameVersion;
|
||||
ack.Index = offer.Index;
|
||||
ack.Imported = imported ? 1u : 0u;
|
||||
ack.Drawn = drawn ? 1u : 0u;
|
||||
ack.FenceOk = fenceOk ? 1u : 0u;
|
||||
if (!WriteExactly(m_fd, &ack, sizeof(ack))) {
|
||||
why = std::format("the answer to frame {} did not go out: {}", offer.Index, std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
// The host answers with its own view of the pixels. It is what closes the round trip, so
|
||||
// it is read here and kept rather than left in the socket for the next frame to trip over.
|
||||
m_lastSeen = HostFrameSeen{};
|
||||
if (!ReadExactly(m_fd, &m_lastSeen, sizeof(m_lastSeen)) || m_lastSeen.Magic != kHostFrameMagic) {
|
||||
why = std::format("frame {} was answered but the host's readback did not arrive", offer.Index);
|
||||
m_lastSeen = HostFrameSeen{};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostFrameBridge::Close() {
|
||||
if (m_fd >= 0) ::close(m_fd);
|
||||
if (m_listen >= 0) ::close(m_listen);
|
||||
m_fd = -1;
|
||||
m_listen = -1;
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
HostFrameBridge::~HostFrameBridge() = default;
|
||||
Bool HostFrameBridge::Open(const char*, Int, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameBridge::Acquire(struct AHardwareBuffer**, HostFrameOffer&, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameBridge::Complete(const HostFrameOffer&, Bool, Bool, Bool, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
void HostFrameBridge::Close() {}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,103 @@
|
||||
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.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>
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// THE DISPLAY HOST'S FRAMES, AS THEY ARRIVE.
|
||||
//
|
||||
// The Android side that owns the screen allocates its frames itself and offers each one over
|
||||
// an AF_UNIX socket: the handle travels ahead of a fixed-size description on the same
|
||||
// connection, and the host then waits for this side to say the frame is drawn before it puts
|
||||
// it on the glass. Why the host allocates rather than this side asking for the display's own
|
||||
// buffers is measured, not chosen: AHardwareBuffer_createFromHandle accepts an
|
||||
// AHardwareBuffer this process allocated and refuses one the display queue handed over
|
||||
// (gralloc BAD_VALUE), so the only frames a render server can draw into are the ones the host
|
||||
// made for it.
|
||||
//
|
||||
// The three message shapes below ARE the host's (its ahb_bridge.h), field for field and
|
||||
// packing included. They are restated here rather than shared because the two ends are built
|
||||
// by different projects; a change on either side is a protocol change, and the magic is what
|
||||
// makes a mismatched pair fail loudly instead of being read as plausible numbers.
|
||||
inline constexpr Uint32 kHostFrameMagic = 0x4D474C41u; // the host spells it "ALGM" in memory
|
||||
inline constexpr Uint32 kHostFrameVersion = 1u;
|
||||
// AN ABSTRACT NAME, not a path: the two ends are separate apps, and neither may create a
|
||||
// socket file in /data/local/tmp (its directory grants others traverse only). An abstract
|
||||
// name lives in the kernel namespace instead of the filesystem, so there is no file
|
||||
// permission to get wrong - the same reason this project names its own endpoints @mgl-*.
|
||||
inline constexpr const char* kHostFrameSocket = "@mobilegl-host-frame";
|
||||
|
||||
struct HostFrameOffer {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Index;
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 StrideBytes;
|
||||
Uint32 Format;
|
||||
Uint32 UsageLo;
|
||||
Uint32 UsageHi;
|
||||
char Note[64];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HostFrameAck {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Index;
|
||||
Uint32 Imported;
|
||||
Uint32 Drawn;
|
||||
Uint32 FenceOk;
|
||||
Uint8 Color[4];
|
||||
char Note[64];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HostFrameSeen {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Index;
|
||||
Uint32 Locked;
|
||||
Uint8 Observed[4];
|
||||
Uint8 ObservedCorner[4];
|
||||
char Note[64];
|
||||
} __attribute__((packed));
|
||||
|
||||
// One host connection at a time, which is what the host offers: it dials, hands over a frame,
|
||||
// waits for the answer, and hands over the next.
|
||||
class HostFrameBridge {
|
||||
public:
|
||||
HostFrameBridge() = default;
|
||||
HostFrameBridge(const HostFrameBridge&) = delete;
|
||||
HostFrameBridge& operator=(const HostFrameBridge&) = delete;
|
||||
~HostFrameBridge();
|
||||
|
||||
// Listens on `path` (the well-known name by default) and waits for the host. A host that
|
||||
// never dials is answered with false and a reason, never an abort.
|
||||
Bool Open(const char* path, Int acceptTimeoutMs, String& why);
|
||||
|
||||
// Takes the next frame. Ownership of the returned AHardwareBuffer is the caller's, who
|
||||
// must hand it back through ReleaseOwned() once the frame is off the glass.
|
||||
Bool Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why);
|
||||
|
||||
// Tells the host what became of the frame it offered, and reads back what the host's own
|
||||
// view of the pixels holds. `drawn` is what the host waits for before it presents.
|
||||
Bool Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why);
|
||||
|
||||
// The readback the host returned for the last completed frame, as the host reported it.
|
||||
const HostFrameSeen& LastSeen() const { return m_lastSeen; }
|
||||
|
||||
void Close();
|
||||
|
||||
private:
|
||||
Int m_fd = -1;
|
||||
Int m_listen = -1;
|
||||
HostFrameSeen m_lastSeen{};
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h> // MGPipeForgetAllPublications, at the end of ShutdownSplitRoles
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/FatalFunnel.h> // InstallPipeSessionFailHook, installed at step 0 below
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
@@ -81,9 +82,10 @@ namespace MobileGL::MG_Backend {
|
||||
// DirectGLES consumes all thirteen migrated families (P2's 0..6, P3a's 7..8, P4a's
|
||||
// 9..12): it registers the resource op table in Initialize()
|
||||
// (BackendObject_DirectGLES.cpp:849) and reads every other family out of gPipeInputs.
|
||||
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
|
||||
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
|
||||
// the same fact ObjectSubsystemControlScenario already pins from the client side.
|
||||
// DirectVulkan registers its wire resource op table off monolith only
|
||||
// (VkBufferManager::RegisterWireResourceOps, VkBufferManager.cpp): under a transport it
|
||||
// owns buffer stores and claims bit 7 (the P4a mask, see ConsumedSubsystemsFor below);
|
||||
// under monolith it registers none and its buffers keep the legacy BufferBackendOps path.
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
|
||||
//
|
||||
// kCapRunAheadApply says "this server applies an unbarriered record without reading
|
||||
@@ -185,9 +187,9 @@ namespace MobileGL::MG_Backend {
|
||||
// ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block):
|
||||
// every optional capability bit belongs to the package that owns its question, and
|
||||
// withholding one leaves the legacy path running, which is the safe direction.
|
||||
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
|
||||
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
|
||||
// what keeps every one of them out of the first IPC frame (contract table 0).
|
||||
// kCapNeedsHostIndexBytes, kCapNeedsHostUboBytes and kCapViewportArray are RESERVED
|
||||
// (P8-F) and never set here: the first two are the only things that ask for an
|
||||
// MGHostSpan, and 0 keeps every one of them off the wire (contract table 0).
|
||||
//
|
||||
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
|
||||
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
|
||||
@@ -397,6 +399,9 @@ namespace MobileGL::MG_Backend {
|
||||
// and every later split bring-up in the process fails at CreateBackend's m_backend!=null
|
||||
// guard.
|
||||
MG_Remote::Server::ServerLoopInstance().Stop();
|
||||
// The server's twins died with the session; the frontend objects that are destroyed after
|
||||
// this (the GL context's) have nothing left on the other side to delete.
|
||||
MG_Pipe::MGPipeForgetAllPublications();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
// MobileGL - MobileGL/MG_Gbm/GbmApi.cpp
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
//
|
||||
// The extern "C" surface: every entry point gbm.h declares, and nothing else.
|
||||
//
|
||||
// This file is a translation layer, and it is kept that way on purpose. Each entry
|
||||
// point does three things - check the pointers it was handed, take the device's one
|
||||
// mutex for the whole call, and hand the work to GbmDevice.cpp - so the concurrency
|
||||
// rule ("one mutex per device, taken once at the API boundary, never inside the
|
||||
// allocation code") is visible in one place instead of being spread over the
|
||||
// backends. A missing lock is a bug that only shows up in the compositor, so the
|
||||
// boilerplate is deliberately identical in every function.
|
||||
//
|
||||
// Two conventions are observable from outside and are stated here because they are
|
||||
// the ones a caller can get wrong:
|
||||
//
|
||||
// * gbm_bo_get_fd and gbm_bo_get_fd_for_plane return a DUP the caller owns. The
|
||||
// caller closes what it was given; the buffer's own descriptor is closed once,
|
||||
// when the buffer is destroyed, and never by a caller's close.
|
||||
// * Nothing here throws and nothing here aborts. A failure is reported the way
|
||||
// GBM reports failures: null or -1 with errno set, so that the strerror(errno)
|
||||
// a caller prints describes the failure that actually happened.
|
||||
|
||||
#include "GbmInternal.h"
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Gbm;
|
||||
|
||||
namespace
|
||||
{
|
||||
// The one mutex that guards a record, reached through the device that owns it.
|
||||
// Null means the pointer was not one this library handed out, and every entry
|
||||
// point turns that into its own failure answer rather than dereferencing it.
|
||||
//
|
||||
// A buffer or surface with no owner cannot exist while the library is used
|
||||
// correctly - the device destroys everything it still holds before it goes away
|
||||
// - so null here is a defence against a stale pointer rather than a state the
|
||||
// code produces.
|
||||
std::mutex* MutexOf(struct gbm_device* device) noexcept
|
||||
{
|
||||
return device != nullptr ? &device->Impl.Mutex : nullptr;
|
||||
}
|
||||
|
||||
std::mutex* MutexOf(struct gbm_bo* bo) noexcept
|
||||
{
|
||||
return bo != nullptr ? MutexOf(bo->Impl.Owner) : nullptr;
|
||||
}
|
||||
|
||||
std::mutex* MutexOf(struct gbm_surface* surface) noexcept
|
||||
{
|
||||
return surface != nullptr ? MutexOf(surface->Impl.Owner) : nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" struct gbm_device* gbm_create_device(int fd)
|
||||
{
|
||||
// No lock to take: the device being built is not reachable by any other thread
|
||||
// until this call returns it.
|
||||
return GbmDeviceCreate(fd);
|
||||
}
|
||||
|
||||
extern "C" void gbm_device_destroy(struct gbm_device* gbm)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// The lock is taken and deliberately never released: the mutex lives inside the
|
||||
// record this call deletes, and unlocking a destroyed mutex is worse than
|
||||
// leaving a dead one locked. Any other thread touching this device after its
|
||||
// owner declared it dead is already outside what GBM can be asked to survive.
|
||||
mutex->lock();
|
||||
GbmDeviceDestroy(*gbm);
|
||||
}
|
||||
|
||||
extern "C" int gbm_device_get_fd(struct gbm_device* gbm)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// The device's own dup of the fd it was created with, or -1 when it was created
|
||||
// without one and no render node could be opened. That is not an error state:
|
||||
// the self-allocated backend never talks to the node, so a device can be fully
|
||||
// usable and still have no descriptor to report.
|
||||
return gbm->Impl.DeviceFd;
|
||||
}
|
||||
|
||||
extern "C" const char* gbm_device_get_backend_name(struct gbm_device* gbm)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// The name says which memory this device hands out ("mobilegl-dma-heap" or
|
||||
// "mobilegl-memfd"), because the callers that read it - a compositor logging its
|
||||
// backend, a peer test recording what it got - are asking exactly that. The
|
||||
// string lives in the device and outlives every call that can read it.
|
||||
return gbm->Impl.BackendName.c_str();
|
||||
}
|
||||
|
||||
extern "C" int gbm_device_is_format_supported(struct gbm_device* gbm, uint32_t format, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Every buffer this device produces is linear, mappable and shareable, so the
|
||||
// usage flags do not narrow the answer to any of the usual combinations -
|
||||
// including GBM_BO_USE_SCANOUT, which a linear dma-buf from the system heap
|
||||
// satisfies. The one request the backend cannot honour is protected memory.
|
||||
if ((flags & GBM_BO_USE_PROTECTED) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return GbmFormatIsSupported(format) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int gbm_device_get_format_modifier_plane_count(struct gbm_device* gbm, uint32_t format, uint64_t modifier)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Only linear memory exists here, so any other modifier names a buffer this
|
||||
// device will never produce and the answer is zero planes. INVALID is answered
|
||||
// as well, because it means "no modifier was declared", which is the same
|
||||
// layout question asked without an answer.
|
||||
if (modifier != DrmFormatModLinear && modifier != DrmFormatModInvalid)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
GbmPlaneLayout planes[GbmMaxPlanes] = {};
|
||||
Int planeCount = 0;
|
||||
SizeT totalBytes = 0;
|
||||
if (!GbmDescribeFormat(format, 1, 1, planes, planeCount, totalBytes))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return planeCount;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Buffer objects
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_create(struct gbm_device* gbm, uint32_t width, uint32_t height, uint32_t format,
|
||||
uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// No modifier list: the buffer is linear and gbm_bo_get_modifier says so.
|
||||
return GbmBufferCreate(*gbm, width, height, format, flags, nullptr, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_create_with_modifiers(struct gbm_device* gbm, uint32_t width, uint32_t height,
|
||||
uint32_t format, const uint64_t* modifiers,
|
||||
const unsigned int count)
|
||||
{
|
||||
// The modifier-less variant is the one with no flags parameter, so it is the
|
||||
// flags-0 case of the other, and saying that here keeps one implementation.
|
||||
return gbm_bo_create_with_modifiers2(gbm, width, height, format, modifiers, count, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_create_with_modifiers2(struct gbm_device* gbm, uint32_t width, uint32_t height,
|
||||
uint32_t format, const uint64_t* modifiers,
|
||||
const unsigned int count, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmBufferCreate(*gbm, width, height, format, flags, modifiers, (Uint32)count);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_import(struct gbm_device* gbm, uint32_t type, void* buffer, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmBufferImport(*gbm, type, buffer, flags);
|
||||
}
|
||||
|
||||
extern "C" void* gbm_bo_map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width, uint32_t height,
|
||||
uint32_t flags, uint32_t* stride, void** map_data)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
// The rectangle's size is not used to size the mapping: the whole buffer is
|
||||
// mapped once, at its own length, and the rectangle is applied as an offset into
|
||||
// it. That is what lets two overlapping maps share one address, and it is why a
|
||||
// buffer's mapping stays valid for a caller that only ever asks for a corner.
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)flags;
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
Uint32 mapStride = 0;
|
||||
void* address = GbmBufferMap(*bo, x, y, mapStride);
|
||||
if (address == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (stride != nullptr)
|
||||
{
|
||||
*stride = mapStride;
|
||||
}
|
||||
if (map_data != nullptr)
|
||||
{
|
||||
// The same address the function returns. Callers pass back either one to
|
||||
// gbm_bo_unmap, and both have to name this mapping.
|
||||
*map_data = address;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
extern "C" void gbm_bo_unmap(struct gbm_bo* bo, void* map_data)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
GbmBufferUnmap(*bo, map_data);
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_width(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Width;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_height(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Height;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_stride(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Strides[0];
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_stride_for_plane(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// A plane this buffer does not have is answered with zero, which is the only
|
||||
// value a caller can tell apart from a real stride.
|
||||
if (plane < 0 || plane >= bo->Impl.PlaneCount)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
return bo->Impl.Strides[plane];
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_format(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Format;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_bpp(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Bits per pixel: a 32 bit format answers 32, which is what a caller computing a
|
||||
// row length in bits or a row length in bytes from the same number expects.
|
||||
return bo->Impl.BytesPerPixel * 8;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_offset(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
if (plane < 0 || plane >= bo->Impl.PlaneCount)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
return bo->Impl.Offsets[plane];
|
||||
}
|
||||
|
||||
extern "C" struct gbm_device* gbm_bo_get_device(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
return bo->Impl.Owner;
|
||||
}
|
||||
|
||||
extern "C" union gbm_bo_handle gbm_bo_get_handle(struct gbm_bo* bo)
|
||||
{
|
||||
union gbm_bo_handle handle;
|
||||
handle.ptr = nullptr;
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
handle.s32 = -1;
|
||||
return handle;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// The kernel-level name of a dma-buf in this process is its file descriptor. A
|
||||
// GEM handle would be a name that exists only relative to a DRM node, and this
|
||||
// library deliberately does not require one, so the fd is the only answer here
|
||||
// that is true for every buffer it hands out. Callers that need a name for a
|
||||
// device should ask for the fd and pass that.
|
||||
handle.s32 = bo->Impl.Fds[0];
|
||||
return handle;
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_get_fd(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// A dup, close-on-exec, owned by the caller. Closing it cannot affect the buffer
|
||||
// or any other caller's copy.
|
||||
return GbmBufferDupFd(*bo, 0);
|
||||
}
|
||||
|
||||
extern "C" uint64_t gbm_bo_get_modifier(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return DrmFormatModInvalid;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Modifier;
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_get_plane_count(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.PlaneCount;
|
||||
}
|
||||
|
||||
extern "C" union gbm_bo_handle gbm_bo_get_handle_for_plane(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
union gbm_bo_handle handle;
|
||||
handle.ptr = nullptr;
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
handle.s32 = -1;
|
||||
return handle;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
if (plane < 0 || plane >= bo->Impl.PlaneCount)
|
||||
{
|
||||
errno = EINVAL;
|
||||
handle.s32 = -1;
|
||||
return handle;
|
||||
}
|
||||
// Same reasoning as gbm_bo_get_handle: the fd is the buffer's kernel name. Every
|
||||
// plane of a self-allocated buffer lives in the one dma-buf, so all of them
|
||||
// answer with that descriptor; the plane's position inside it is
|
||||
// gbm_bo_get_offset.
|
||||
const Int fd = bo->Impl.Fds[plane] >= 0 ? bo->Impl.Fds[plane] : bo->Impl.Fds[0];
|
||||
handle.s32 = fd;
|
||||
return handle;
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_get_fd_for_plane(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmBufferDupFd(*bo, plane);
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_write(struct gbm_bo* bo, const void* buf, size_t count)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr || buf == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (count > bo->Impl.AllocationSize)
|
||||
{
|
||||
// Refused rather than truncated: a caller that writes 4 MiB into a 1 MiB
|
||||
// buffer and is told "0 bytes written, success" would read back memory it
|
||||
// never wrote and call the result correct.
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
// The write goes through the same counted mapping the rest of the API uses, so a
|
||||
// caller that already holds the buffer mapped does not get a second mapping (or
|
||||
// lose its own) behind its back.
|
||||
Uint32 stride = 0;
|
||||
void* address = GbmBufferMap(*bo, 0, 0, stride);
|
||||
if (address == nullptr)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
memcpy(address, buf, count);
|
||||
GbmBufferUnmap(*bo, address);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void gbm_bo_set_user_data(struct gbm_bo* bo, void* data,
|
||||
void (*destroy_user_data)(struct gbm_bo*, void*))
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Assigning over existing data does not call the old callback: the record calls
|
||||
// it exactly once, when the buffer goes away, which is the one moment a caller
|
||||
// can still recognise the pointer it registered.
|
||||
bo->Impl.UserData = data;
|
||||
bo->Impl.UserDataDestroy = destroy_user_data;
|
||||
}
|
||||
|
||||
extern "C" void* gbm_bo_get_user_data(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.UserData;
|
||||
}
|
||||
|
||||
extern "C" void gbm_bo_destroy(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Drops this caller's reference. A buffer a surface still holds stays alive -
|
||||
// that is what the surface's own reference is for - so destroying a buffer that
|
||||
// is currently presented is not a use-after-free waiting to happen.
|
||||
GbmBufferRelease(*bo->Impl.Owner, *bo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Surfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" struct gbm_surface* gbm_surface_create(struct gbm_device* gbm, uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmSurfaceCreate(*gbm, width, height, format, flags, nullptr, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_surface* gbm_surface_create_with_modifiers(struct gbm_device* gbm, uint32_t width,
|
||||
uint32_t height, uint32_t format,
|
||||
const uint64_t* modifiers,
|
||||
const unsigned int count)
|
||||
{
|
||||
// As with the buffer pair: the modifier-less form is the flags-0 case.
|
||||
return gbm_surface_create_with_modifiers2(gbm, width, height, format, modifiers, count, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_surface* gbm_surface_create_with_modifiers2(struct gbm_device* gbm, uint32_t width,
|
||||
uint32_t height, uint32_t format,
|
||||
const uint64_t* modifiers,
|
||||
const unsigned int count, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmSurfaceCreate(*gbm, width, height, format, flags, modifiers, (Uint32)count);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_surface_lock_front_buffer(struct gbm_surface* surface)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Null with errno = EBUSY when the compositor still holds every buffer: that is a
|
||||
// state the caller has to handle, and pretending otherwise by handing out a
|
||||
// buffer that is still being scanned out would be a torn frame rather than an
|
||||
// error.
|
||||
return GbmSurfaceLockFront(*surface);
|
||||
}
|
||||
|
||||
extern "C" void gbm_surface_release_buffer(struct gbm_surface* surface, struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr || bo == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
GbmSurfaceReleaseBuffer(*surface, *bo);
|
||||
}
|
||||
|
||||
extern "C" int gbm_surface_has_free_buffers(struct gbm_surface* surface)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Truthful by construction: the swapchain exists from the moment the surface
|
||||
// does, so this answers about buffers that were really allocated rather than
|
||||
// about how many could still be.
|
||||
return GbmSurfaceHasFreeBuffers(*surface) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" void gbm_surface_destroy(struct gbm_surface* surface)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
GbmSurfaceDestroy(*surface);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Format names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" char* gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc* desc)
|
||||
{
|
||||
if (desc == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
// The name of a fourcc is its four characters, most significant byte last, and
|
||||
// that is all there is to it: a caller compares these strings against "AR24" and
|
||||
// friends, and the big-endian bit is part of the value rather than something to
|
||||
// spell out here.
|
||||
desc->name[0] = (char)(gbm_format & 0xffu);
|
||||
desc->name[1] = (char)((gbm_format >> 8) & 0xffu);
|
||||
desc->name[2] = (char)((gbm_format >> 16) & 0xffu);
|
||||
desc->name[3] = (char)((gbm_format >> 24) & 0xffu);
|
||||
desc->name[4] = '\0';
|
||||
return desc->name;
|
||||
}
|
||||
|
||||
// End of Source File Header
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,835 @@
|
||||
// MobileGL - MobileGL/MG_Gbm/GbmHostChannel.cpp
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
//
|
||||
// The host-frame backend: the socket the container receives the phone's frames on.
|
||||
//
|
||||
// The container half of MobileGL runs a Wayland compositor, and the frames that
|
||||
// compositor renders into have to be the frames the phone already owns - the phone
|
||||
// allocates them, its GPU draws into them, and its display scans them out. A buffer
|
||||
// allocated from the container's own dma-heap would be a copy that has to travel back
|
||||
// to the phone before anything can be shown, so when the display host is reachable this
|
||||
// backend hands the compositor the phone's own buffers instead, and the fd the
|
||||
// compositor passes on is the fd of a frame the display path already knows.
|
||||
//
|
||||
// What that costs is the one thing GBM normally promises: the geometry. A frame's
|
||||
// width, height, stride and format are the display's and not the caller's, so a
|
||||
// gbm_bo_create that takes an offered frame answers with the offer's geometry, and the
|
||||
// caller reads it back with gbm_bo_get_width and its neighbours. That is this backend's
|
||||
// documented behaviour rather than a surprise: the host decides what a frame is.
|
||||
//
|
||||
// Three rules keep this backend from ever being worse than not having it.
|
||||
//
|
||||
// * Nothing on the allocation path blocks. The socket is non-blocking and connect()
|
||||
// to a pathname socket either succeeds or fails immediately, so a call that cannot
|
||||
// be served from the host falls through to a local allocation inside the same call
|
||||
// instead of waiting for a phone that may never answer. The one wait in this file is
|
||||
// in device creation, after a display host has already accepted the connection, and
|
||||
// it exists because of a measured race: the compositor builds its swapchain within
|
||||
// microseconds of creating the device, while the offer for the first frame needs the
|
||||
// round trip of the connection plus whatever the phone does before it sends. Without
|
||||
// that wait the swapchain - the buffers that are shown for the life of the process -
|
||||
// is allocated locally and the host's frames arrive for nobody to take.
|
||||
// * A channel that dies is closed and marked down, and the next frame is allocated
|
||||
// locally again. This library is loaded by a compositor that has to keep painting,
|
||||
// so losing the display host may cost the frames their path to the screen but it
|
||||
// may not cost the session.
|
||||
// * A frame is released exactly once, when the record holding it is destroyed, and
|
||||
// only into the connection that offered it. Releasing on gbm_bo_destroy would be a
|
||||
// lie - the surface may still be scanning that frame out - and releasing an old
|
||||
// frame into a new connection would free a slot belonging to another session.
|
||||
//
|
||||
// The device mutex is the only lock in here. Both callers of this file - the allocation
|
||||
// path and the buffer record's destructor - already run under it, so there is no second
|
||||
// lock to order against and no state of this file's own that another thread can see.
|
||||
|
||||
#include "GbmInternal.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
|
||||
namespace MobileGL::MG_Gbm
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// One frame per message, and a message is the offer followed by the handle the
|
||||
// offer describes. 4 KiB is far more than an AHardwareBuffer handle needs (a
|
||||
// handle is a version, two counts and their two arrays) while still being small
|
||||
// enough that a message which does not fit is a peer speaking something else -
|
||||
// which is worth refusing rather than growing a buffer for.
|
||||
constexpr SizeT GbmHostMessageMax = 4096;
|
||||
|
||||
// The descriptors one handle may carry. An AHardwareBuffer handle carries one
|
||||
// (the dma-buf) in every layout this library has seen and the protocol reads only
|
||||
// the first, so this sizes the control buffer rather than claiming anything about
|
||||
// real handles.
|
||||
constexpr Int GbmHostMaxFds = 8;
|
||||
|
||||
// How many messages one gbm_bo_create may look at before it gives up and
|
||||
// allocates locally. An unusable message is skipped rather than fatal, so this
|
||||
// bounds the work a peer that sends nothing but unusable messages can cause
|
||||
// inside a single allocation.
|
||||
constexpr Int GbmHostTakeAttempts = 16;
|
||||
|
||||
// The floor between two connection attempts. A container with no display host is
|
||||
// the normal case, so a retry may not turn every allocation into a connect plus a
|
||||
// log line; one attempt per second is often enough that a phone which comes up is
|
||||
// picked up within a frame or two of the compositor's next allocation.
|
||||
constexpr std::chrono::milliseconds GbmHostRetryInterval{1000};
|
||||
|
||||
// How long device creation waits for the display host's first frame once the
|
||||
// connection is up. A frame the phone offers immediately needs one round trip on
|
||||
// a unix socket, so this is orders of magnitude more than that and it is only
|
||||
// ever spent by a device that has a host which then says nothing; a compile or a
|
||||
// test run without a display host never reaches it, because the connect fails
|
||||
// first.
|
||||
constexpr Int GbmHostFirstOfferWaitMs = 250;
|
||||
|
||||
// Frame releases the socket would not take. A release is twelve bytes, so a full
|
||||
// socket buffer is hundreds of frames' worth of them and this queue is only
|
||||
// reached once the phone has stopped reading; the cap is what stops a peer that
|
||||
// never reads from growing it without bound.
|
||||
constexpr SizeT GbmHostMaxPendingReleases = 256;
|
||||
|
||||
// sun_path is where a pathname socket's name lives, and a name that does not fit
|
||||
// is one the kernel cannot be told about at all.
|
||||
constexpr SizeT GbmHostPathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
|
||||
|
||||
// close() may overwrite errno, and every caller here is in the middle of
|
||||
// reporting a failure that errno describes. Saving it around the close keeps the
|
||||
// reported failure the one that happened.
|
||||
void GbmHostCloseQuietly(Int fd) noexcept
|
||||
{
|
||||
if (fd < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const Int savedErrno = errno;
|
||||
close(fd);
|
||||
errno = savedErrno;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// One connection to the display host, for the life of one device.
|
||||
class GbmHostChannel
|
||||
{
|
||||
public:
|
||||
explicit GbmHostChannel(const String& path) noexcept : Path(path) {}
|
||||
~GbmHostChannel() noexcept;
|
||||
|
||||
GbmHostChannel(const GbmHostChannel&) = delete;
|
||||
GbmHostChannel& operator=(const GbmHostChannel&) = delete;
|
||||
|
||||
// Connects when there is no connection and the retry interval has passed. A
|
||||
// failure is never the caller's problem: the channel stays closed and the
|
||||
// allocation that asked for a frame falls back to local memory.
|
||||
void EnsureOpen(::gbm_device& device) noexcept;
|
||||
|
||||
// Waits, once, for the connection's first frame to be readable. Called from
|
||||
// device creation and nowhere else: see the banner for why that one wait exists.
|
||||
void AwaitFirstOffer(::gbm_device& device) noexcept;
|
||||
|
||||
// The next frame the host offered, or false when none is waiting.
|
||||
Bool TakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept;
|
||||
|
||||
// Queues the release for one frame and tries to send what is queued.
|
||||
void ReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept;
|
||||
|
||||
private:
|
||||
// Turns one message into a frame. Everything it refuses is reported with the
|
||||
// reason, because a peer this library cannot understand is the one thing that
|
||||
// would otherwise be invisible from the outside.
|
||||
Bool ParseOffer(const Uint8* payload, SizeT length, const Int* fds, Int fdCount,
|
||||
GbmHostFrame& frameOut) noexcept;
|
||||
|
||||
// Sends what is queued. False means the connection can no longer be trusted to
|
||||
// carry a twelve byte message, with errno describing what happened.
|
||||
Bool FlushReleases() noexcept;
|
||||
|
||||
// Closes the socket and drops whatever was queued for it.
|
||||
void CloseSocket() noexcept;
|
||||
|
||||
// Closes the socket and tells the device what that means for its backend name.
|
||||
void MarkDown(::gbm_device& device, const char* what, Int error) noexcept;
|
||||
|
||||
String Path;
|
||||
Int SocketFd = -1;
|
||||
// Set by a connection this process has just made, and read once by the allocation that
|
||||
// follows it: the accept and the frame that answers the dial happen on the other side of
|
||||
// this socket, so an allocation that reads immediately after connecting reads before the
|
||||
// answer to its own connection has been written. See TakeFrame.
|
||||
Bool FreshConnection = false;
|
||||
// Bumped by every connection, so a frame taken from a connection which has since
|
||||
// died can be recognised as one that must not be released into its replacement.
|
||||
Uint64 Generation = 0;
|
||||
std::chrono::steady_clock::time_point NextRetry{};
|
||||
// The errno of the last failed attempt, so that a failure which repeats once a
|
||||
// second is not logged once a second.
|
||||
Int LastFailureErrno = 0;
|
||||
std::vector<GbmFrameRelease> PendingReleases;
|
||||
Bool ReleaseOverflowLogged = false;
|
||||
};
|
||||
|
||||
// ---- the offered-frame record ------------------------------------------
|
||||
|
||||
GbmHostFrame::~GbmHostFrame()
|
||||
{
|
||||
// The record owns the dup it was built on, which is why it is move-only: a copy
|
||||
// would close the same descriptor twice.
|
||||
if (Fd >= 0)
|
||||
{
|
||||
close(Fd);
|
||||
Fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
GbmHostFrame::GbmHostFrame(GbmHostFrame&& other) noexcept
|
||||
{
|
||||
*this = static_cast<GbmHostFrame&&>(other);
|
||||
}
|
||||
|
||||
GbmHostFrame& GbmHostFrame::operator=(GbmHostFrame&& other) noexcept
|
||||
{
|
||||
if (this == &other)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
if (Fd >= 0)
|
||||
{
|
||||
close(Fd);
|
||||
}
|
||||
Seq = other.Seq;
|
||||
Index = other.Index;
|
||||
Width = other.Width;
|
||||
Height = other.Height;
|
||||
StrideBytes = other.StrideBytes;
|
||||
Format = other.Format;
|
||||
BytesPerPixel = other.BytesPerPixel;
|
||||
Usage = other.Usage;
|
||||
Generation = other.Generation;
|
||||
Fd = other.Fd;
|
||||
Size = other.Size;
|
||||
// The source must not close what this record now owns.
|
||||
other.Fd = -1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ---- the connection ----------------------------------------------------
|
||||
|
||||
GbmHostChannel::~GbmHostChannel() noexcept
|
||||
{
|
||||
// One last try for whatever is queued - the phone may still be reading - and
|
||||
// then the socket goes away with the process.
|
||||
if (SocketFd >= 0 && !PendingReleases.empty())
|
||||
{
|
||||
FlushReleases();
|
||||
}
|
||||
if (!PendingReleases.empty())
|
||||
{
|
||||
MGLOG_W("gbm: %zu frame release(s) could not be sent to the display host at %s; the phone reclaims "
|
||||
"those slots when the session ends",
|
||||
PendingReleases.size(), Path.c_str());
|
||||
}
|
||||
CloseSocket();
|
||||
}
|
||||
|
||||
void GbmHostChannel::EnsureOpen(::gbm_device& device) noexcept
|
||||
{
|
||||
if (SocketFd >= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
if (now < NextRetry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
NextRetry = now + GbmHostRetryInterval;
|
||||
|
||||
// SOCK_SEQPACKET because a frame and its descriptors are one message and the
|
||||
// reader may not reassemble either half; SOCK_NONBLOCK because the caller is a
|
||||
// compositor's frame loop and may not be parked on a phone; SOCK_CLOEXEC because
|
||||
// every process this compositor starts would otherwise inherit a socket it can
|
||||
// read frames from.
|
||||
const Int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
{
|
||||
MGLOG_E_ONCE("gbm: cannot create the socket to the display host (%d: %s); every frame is allocated "
|
||||
"locally",
|
||||
errno, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Path.empty() || Path.size() >= GbmHostPathMax)
|
||||
{
|
||||
MGLOG_E_ONCE("gbm: the display host socket path \"%s\" does not fit in sun_path (%zu bytes); every "
|
||||
"frame is allocated locally",
|
||||
Path.c_str(), GbmHostPathMax);
|
||||
GbmHostCloseQuietly(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
struct sockaddr_un address = {};
|
||||
address.sun_family = AF_UNIX;
|
||||
// The terminator travels with the name: sun_path is a C string for a pathname
|
||||
// socket, and a name without one is a name the kernel reads past the end of.
|
||||
memcpy(address.sun_path, Path.c_str(), Path.size() + 1);
|
||||
|
||||
if (connect(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0)
|
||||
{
|
||||
const Int connectErrno = errno;
|
||||
GbmHostCloseQuietly(fd);
|
||||
// A failure that repeats once a second is not news: a container with no
|
||||
// display host fails here on every retry, and a line per second saying so
|
||||
// would bury the rest of the log. A failure that changed is news, because it
|
||||
// says the path or the peer changed.
|
||||
if (connectErrno != LastFailureErrno)
|
||||
{
|
||||
LastFailureErrno = connectErrno;
|
||||
if (connectErrno == EPROTOTYPE)
|
||||
{
|
||||
MGLOG_E("gbm: %s is a socket of a different type (%d: %s). The frame protocol is "
|
||||
"SOCK_SEQPACKET - one message per frame, carrying that frame's descriptors - "
|
||||
"because a reader cannot tell from a byte stream which frame a descriptor "
|
||||
"belongs to; frames are allocated locally instead",
|
||||
Path.c_str(), connectErrno, strerror(connectErrno));
|
||||
}
|
||||
else
|
||||
{
|
||||
MGLOG_I("gbm: no display host at %s (%d: %s); every frame is allocated locally until one "
|
||||
"answers",
|
||||
Path.c_str(), connectErrno, strerror(connectErrno));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SocketFd = fd;
|
||||
FreshConnection = true;
|
||||
// A new connection is a new session with its own frames, which is what makes the
|
||||
// generation a correct answer to "may this release be sent here".
|
||||
Generation++;
|
||||
LastFailureErrno = 0;
|
||||
device.Impl.HostAvailable = true;
|
||||
device.Impl.BackendName = "mobilegl-host-frame";
|
||||
MGLOG_I("gbm: connected to the display host at %s (one SOCK_SEQPACKET message per frame, up to %zu "
|
||||
"bytes); frames offered over it become gbm_bos whose geometry is the offer's",
|
||||
Path.c_str(), GbmHostMessageMax);
|
||||
}
|
||||
|
||||
void GbmHostChannel::AwaitFirstOffer(::gbm_device& device) noexcept
|
||||
{
|
||||
if (SocketFd < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::chrono::steady_clock::time_point deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(GbmHostFirstOfferWaitMs);
|
||||
for (;;)
|
||||
{
|
||||
const std::chrono::milliseconds left =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now());
|
||||
if (left.count() <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// poll() and not recvmsg(): the frame has to stay in the socket for the
|
||||
// allocation that is about to ask for it, and a peek at a message carrying
|
||||
// SCM_RIGHTS is a question with no safe answer.
|
||||
struct pollfd waiting = {};
|
||||
waiting.fd = SocketFd;
|
||||
waiting.events = POLLIN;
|
||||
const Int ready = poll(&waiting, 1, (Int)left.count());
|
||||
if (ready > 0)
|
||||
{
|
||||
MGLOG_I("gbm: the display host at %s has a frame waiting; the buffers the compositor is about "
|
||||
"to allocate come from it",
|
||||
Path.c_str());
|
||||
return;
|
||||
}
|
||||
if (ready == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (errno != EINTR)
|
||||
{
|
||||
MarkDown(device, "poll", errno);
|
||||
return;
|
||||
}
|
||||
}
|
||||
MGLOG_W("gbm: the display host at %s accepted the connection but offered no frame within %d ms; the "
|
||||
"buffers allocated before its first frame are local ones",
|
||||
Path.c_str(), GbmHostFirstOfferWaitMs);
|
||||
}
|
||||
|
||||
Bool GbmHostChannel::TakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept
|
||||
{
|
||||
EnsureOpen(device);
|
||||
if (SocketFd < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// THE FIRST ALLOCATION AFTER A DIAL WAITS FOR THE ANSWER TO IT. The accept and the offer that
|
||||
// answers the connection this call has just made are written by the phone, a round trip
|
||||
// behind the connect() that returns here — and what is allocated before that offer arrives
|
||||
// is a local buffer, which is what a compositor builds its whole swapchain out of and then
|
||||
// keeps for the life of the process. So a connection made on this path is given the same
|
||||
// first-offer window device creation gives one, once, and only while it is new.
|
||||
if (FreshConnection)
|
||||
{
|
||||
FreshConnection = false;
|
||||
AwaitFirstOffer(device);
|
||||
}
|
||||
// Releases the socket would not take last time are retried here, because this is
|
||||
// the one call in the library that happens once per frame and therefore the
|
||||
// natural place for a queue waiting for room to get its next chance.
|
||||
if (!PendingReleases.empty() && !FlushReleases())
|
||||
{
|
||||
MarkDown(device, "sendmsg", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint8 payload[GbmHostMessageMax];
|
||||
for (Int attempt = 0; attempt < GbmHostTakeAttempts; ++attempt)
|
||||
{
|
||||
Int receivedFds[GbmHostMaxFds];
|
||||
for (Int i = 0; i < GbmHostMaxFds; ++i)
|
||||
{
|
||||
receivedFds[i] = -1;
|
||||
}
|
||||
|
||||
struct iovec payloadVector = {};
|
||||
payloadVector.iov_base = payload;
|
||||
payloadVector.iov_len = sizeof(payload);
|
||||
|
||||
Uint8 control[CMSG_SPACE(sizeof(Int) * GbmHostMaxFds)] = {};
|
||||
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &payloadVector;
|
||||
message.msg_iovlen = 1;
|
||||
message.msg_control = control;
|
||||
message.msg_controllen = sizeof(control);
|
||||
|
||||
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC
|
||||
// so a descriptor the phone sent does not survive into a child of this
|
||||
// compositor, which would then hold a frame nothing will ever release.
|
||||
const ssize_t received = recvmsg(SocketFd, &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||
if (received < 0)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
{
|
||||
// Nothing offered yet, which is what an idle display looks like. The
|
||||
// caller allocates locally and asks again on its next frame.
|
||||
return false;
|
||||
}
|
||||
if (errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MarkDown(device, "recvmsg", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The descriptors come out of the control data before anything is decided
|
||||
// about the message, because they belong to this process from the moment it
|
||||
// was received: a path that returns without closing them leaks a descriptor
|
||||
// per unusable message.
|
||||
Int fdCount = 0;
|
||||
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
|
||||
header = CMSG_NXTHDR(&message, header))
|
||||
{
|
||||
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
|
||||
const Int count = (Int)(bytes / sizeof(Int));
|
||||
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
|
||||
for (Int i = 0; i < count; ++i)
|
||||
{
|
||||
if (fdCount < GbmHostMaxFds)
|
||||
{
|
||||
receivedFds[fdCount++] = descriptors[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
// More descriptors than a frame can carry: they are this
|
||||
// process's now, so they are closed here rather than leaked.
|
||||
GbmHostCloseQuietly(descriptors[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((message.msg_flags & MSG_CTRUNC) != 0)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's control data did not fit in %zu bytes; the frame is "
|
||||
"skipped because its descriptor list is incomplete",
|
||||
sizeof(control));
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (received == 0)
|
||||
{
|
||||
// Zero bytes on a connected socket is the end of it, whether the phone
|
||||
// closed cleanly or the kernel gave up on it. Either way the frames have
|
||||
// to come from somewhere else from here on.
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
MarkDown(device, "the display host closed the connection", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((message.msg_flags & MSG_TRUNC) != 0)
|
||||
{
|
||||
// Longer than a frame message can be, so it is not one - and on a
|
||||
// message-oriented socket the rest is already gone while the next message
|
||||
// is still a whole message, which is why this skips one message instead
|
||||
// of ending the channel.
|
||||
MGLOG_W_ONCE("gbm: the display host sent a message longer than %zu bytes; it is skipped",
|
||||
GbmHostMessageMax);
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const Bool parsed = ParseOffer(payload, (SizeT)received, receivedFds, fdCount, frameOut);
|
||||
// Whatever the frame did not keep is closed here: the copy of the dma-buf
|
||||
// descriptor the phone sent (the frame owns its own dup) and the rest of the
|
||||
// handle, which is a layout this library does not interpret.
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
if (parsed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool GbmHostChannel::ParseOffer(const Uint8* payload, SizeT length, const Int* fds, Int fdCount,
|
||||
GbmHostFrame& frameOut) noexcept
|
||||
{
|
||||
constexpr SizeT HeaderBytes = sizeof(GbmFrameOffer) + sizeof(AHBNativeHandleHeader);
|
||||
if (length < HeaderBytes)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: the display host sent a %zu byte message where a frame message is at least %zu "
|
||||
"bytes (the offer followed by the handle header); it is skipped",
|
||||
length, HeaderBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
GbmFrameOffer offer = {};
|
||||
memcpy(&offer, payload, sizeof(offer));
|
||||
if (offer.Magic != GbmFrameOfferMagic)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: a message from the display host begins with 0x%08x rather than 'MGGF'; it is "
|
||||
"skipped",
|
||||
offer.Magic);
|
||||
return false;
|
||||
}
|
||||
if (offer.Version != GbmHostProtocolVersion)
|
||||
{
|
||||
// Refused rather than read as if the versions agreed: the version is what
|
||||
// says how the rest of the message is laid out, and a frame read with the
|
||||
// wrong layout is memory the compositor would render into.
|
||||
MGLOG_E_ONCE("gbm: the display host offers frames in protocol version %u where this library speaks "
|
||||
"version %u; its frames are refused until the two agree",
|
||||
offer.Version, (Uint32)GbmHostProtocolVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
AHBNativeHandleHeader handle = {};
|
||||
memcpy(&handle, payload + sizeof(offer), sizeof(handle));
|
||||
if (handle.NumFds < 1 || handle.NumInts < 0)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's handle carries %d descriptor(s) and %d integer(s), where the "
|
||||
"dma-buf is the first descriptor and there has to be one; the frame is skipped",
|
||||
handle.NumFds, handle.NumInts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The handle describes itself with its two counts, so the message has to be
|
||||
// exactly as long as those counts make it. Any other length means the two ends
|
||||
// disagree about the layout, and a frame built from a message whose layout was
|
||||
// guessed is a buffer of unknown memory.
|
||||
const SizeT expected = HeaderBytes + ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
|
||||
if (length != expected)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's message is %zu bytes where its handle (%d descriptors, %d "
|
||||
"integers) makes it %zu; the frame is skipped",
|
||||
length, handle.NumFds, handle.NumInts, expected);
|
||||
return false;
|
||||
}
|
||||
if (fdCount < handle.NumFds)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's handle names %d descriptor(s) and %d arrived with the "
|
||||
"message; the frame is skipped",
|
||||
handle.NumFds, fdCount);
|
||||
return false;
|
||||
}
|
||||
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame is %ux%u with a stride of %u bytes; a buffer with no size is "
|
||||
"not one this library can hand out",
|
||||
offer.Width, offer.Height, offer.StrideBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The frame is built on a dup of the first descriptor rather than on the
|
||||
// descriptor itself: the copy that arrived belongs to the message, and the frame
|
||||
// outlives it. Close-on-exec, like every other descriptor this library hands out.
|
||||
const Int frameFd = fcntl(fds[0], F_DUPFD_CLOEXEC, 0);
|
||||
if (frameFd < 0)
|
||||
{
|
||||
MGLOG_E_ONCE("gbm: cannot dup the descriptor of an offered frame (%d: %s)", errno, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// How long the frame really is, asked of the descriptor instead of computed from
|
||||
// the geometry: a gralloc dma-buf is usually larger than stride x height, and a
|
||||
// mapping has to cover what exists rather than what the arithmetic expects.
|
||||
SizeT size = 0;
|
||||
const off_t end = lseek(frameFd, 0, SEEK_END);
|
||||
if (end > 0)
|
||||
{
|
||||
size = (SizeT)end;
|
||||
}
|
||||
const SizeT described = (SizeT)offer.StrideBytes * (SizeT)offer.Height;
|
||||
if (size == 0)
|
||||
{
|
||||
// A descriptor that does not answer for its own length - an exporter that
|
||||
// does not implement llseek - leaves the offer's arithmetic as the only
|
||||
// length there is.
|
||||
size = described;
|
||||
}
|
||||
if (size < described)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's descriptor holds %zu bytes where %u rows of %u bytes need %zu; "
|
||||
"the frame is skipped rather than mapped over memory that is not there",
|
||||
size, offer.Height, offer.StrideBytes, described);
|
||||
GbmHostCloseQuietly(frameFd);
|
||||
return false;
|
||||
}
|
||||
|
||||
frameOut.Seq = offer.Seq;
|
||||
frameOut.Index = offer.Index;
|
||||
frameOut.Width = offer.Width;
|
||||
frameOut.Height = offer.Height;
|
||||
frameOut.StrideBytes = offer.StrideBytes;
|
||||
frameOut.Format = offer.Format;
|
||||
frameOut.Usage = ((Uint64)offer.UsageHi << 32) | (Uint64)offer.UsageLo;
|
||||
frameOut.Generation = Generation;
|
||||
frameOut.Fd = frameFd;
|
||||
frameOut.Size = size;
|
||||
frameOut.BytesPerPixel = GbmFormatBytesPerPixel(offer.Format);
|
||||
if (frameOut.BytesPerPixel == 0)
|
||||
{
|
||||
// A format this library has no table entry for is still a frame the phone
|
||||
// drew. The offer's stride is then the only description of a pixel that
|
||||
// exists: a row of Width pixels is StrideBytes long.
|
||||
frameOut.BytesPerPixel = offer.StrideBytes / offer.Width;
|
||||
}
|
||||
|
||||
MGLOG_I("gbm: frame seq=%u index=%u %ux%u stride=%u format=0x%08x (%u bytes per pixel, %zu bytes "
|
||||
"measured) taken from the display host",
|
||||
offer.Seq, offer.Index, offer.Width, offer.Height, offer.StrideBytes, offer.Format,
|
||||
frameOut.BytesPerPixel, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GbmHostChannel::ReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept
|
||||
{
|
||||
if (SocketFd < 0)
|
||||
{
|
||||
// A release only means anything on the connection that offered the frame,
|
||||
// and there is none.
|
||||
return;
|
||||
}
|
||||
if (generation != Generation)
|
||||
{
|
||||
// The frame was offered by a connection that has since died and been
|
||||
// replaced. Sending this into the replacement would name a frame of another
|
||||
// session, and the phone would recycle a slot this buffer was not made of.
|
||||
MGLOG_W_ONCE("gbm: a frame offered by a display host connection that has since died is not "
|
||||
"released into its replacement");
|
||||
return;
|
||||
}
|
||||
if (PendingReleases.size() >= GbmHostMaxPendingReleases)
|
||||
{
|
||||
if (!ReleaseOverflowLogged)
|
||||
{
|
||||
ReleaseOverflowLogged = true;
|
||||
MGLOG_W("gbm: %zu frame releases to the display host at %s are already waiting for room in the "
|
||||
"socket; this one is dropped rather than queued without bound, and the phone reclaims "
|
||||
"the slot when the session ends",
|
||||
PendingReleases.size(), Path.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GbmFrameRelease release = {};
|
||||
release.Magic = GbmFrameReleaseMagic;
|
||||
release.Version = GbmHostProtocolVersion;
|
||||
release.Seq = seq;
|
||||
PendingReleases.push_back(release);
|
||||
|
||||
if (!FlushReleases())
|
||||
{
|
||||
MarkDown(device, "sendmsg", errno);
|
||||
}
|
||||
}
|
||||
|
||||
Bool GbmHostChannel::FlushReleases() noexcept
|
||||
{
|
||||
while (!PendingReleases.empty())
|
||||
{
|
||||
struct iovec releaseVector = {};
|
||||
releaseVector.iov_base = &PendingReleases.front();
|
||||
releaseVector.iov_len = sizeof(GbmFrameRelease);
|
||||
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &releaseVector;
|
||||
message.msg_iovlen = 1;
|
||||
|
||||
// MSG_DONTWAIT for the same reason every other call here is non-blocking, and
|
||||
// MSG_NOSIGNAL because a compositor killed by SIGPIPE when the phone goes
|
||||
// away is a desktop that disappears with it.
|
||||
const ssize_t sent = sendmsg(SocketFd, &message, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (sent == (ssize_t)sizeof(GbmFrameRelease))
|
||||
{
|
||||
PendingReleases.erase(PendingReleases.begin());
|
||||
continue;
|
||||
}
|
||||
if (sent < 0 && errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
{
|
||||
// A full socket is a phone that has stopped reading rather than a dead
|
||||
// one: the release stays queued and goes out with a later frame.
|
||||
return true;
|
||||
}
|
||||
// A short send, a reset and a broken pipe all say the same thing: this
|
||||
// connection can no longer be trusted to have carried a twelve byte message,
|
||||
// and acting on a maybe is worse than handing the slot back when the session
|
||||
// ends. errno describes which of them it was.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GbmHostChannel::CloseSocket() noexcept
|
||||
{
|
||||
if (SocketFd >= 0)
|
||||
{
|
||||
GbmHostCloseQuietly(SocketFd);
|
||||
SocketFd = -1;
|
||||
}
|
||||
PendingReleases.clear();
|
||||
}
|
||||
|
||||
void GbmHostChannel::MarkDown(::gbm_device& device, const char* what, Int error) noexcept
|
||||
{
|
||||
if (!PendingReleases.empty())
|
||||
{
|
||||
MGLOG_W("gbm: %zu frame release(s) were still queued for the display host and cannot be sent over a "
|
||||
"connection that is gone; the phone reclaims those slots when the session ends",
|
||||
PendingReleases.size());
|
||||
}
|
||||
CloseSocket();
|
||||
if (error != 0)
|
||||
{
|
||||
MGLOG_W("gbm: the display host connection at %s is down: %s failed (%d: %s); frames are allocated "
|
||||
"locally from here on, and a new connection is attempted on a later allocation",
|
||||
Path.c_str(), what, error, strerror(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
MGLOG_W("gbm: the display host connection at %s is down: %s; frames are allocated locally from here "
|
||||
"on, and a new connection is attempted on a later allocation",
|
||||
Path.c_str(), what);
|
||||
}
|
||||
device.Impl.HostAvailable = false;
|
||||
device.Impl.BackendName = device.Impl.SelfAllocatedBackendName;
|
||||
}
|
||||
|
||||
// ---- the device-facing entry points ------------------------------------
|
||||
|
||||
void GbmHostChannelCreate(::gbm_device& device) noexcept
|
||||
{
|
||||
const char* configured = getenv("MOBILEGL_GBM_SOCKET");
|
||||
const String path =
|
||||
(configured != nullptr && configured[0] != '\0') ? String(configured) : String("/run/mobilegl-gbm.sock");
|
||||
device.Impl.SocketPath = path;
|
||||
try
|
||||
{
|
||||
GbmHostChannel* channel = new GbmHostChannel(path);
|
||||
device.Impl.Host = channel;
|
||||
// The one attempt made at creation. It is allowed to fail: a machine that is
|
||||
// not the container half of a split run has no display host, and that is a
|
||||
// device which allocates locally rather than a device that could not exist.
|
||||
channel->EnsureOpen(device);
|
||||
// A connection that came up is waited on once, and only here, so that the
|
||||
// compositor's swapchain is built out of the host's frames rather than out of
|
||||
// local ones it will keep for the life of the process.
|
||||
channel->AwaitFirstOffer(device);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
device.Impl.Host = nullptr;
|
||||
device.Impl.HostAvailable = false;
|
||||
MGLOG_W("gbm: the display host channel could not be created; every frame is allocated locally");
|
||||
}
|
||||
}
|
||||
|
||||
void GbmHostChannelDestroy(::gbm_device& device) noexcept
|
||||
{
|
||||
GbmHostChannel* channel = device.Impl.Host;
|
||||
if (channel == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
device.Impl.Host = nullptr;
|
||||
device.Impl.HostAvailable = false;
|
||||
delete channel;
|
||||
}
|
||||
|
||||
Bool GbmHostChannelTakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept
|
||||
{
|
||||
GbmHostChannel* channel = device.Impl.Host;
|
||||
if (channel == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return channel->TakeFrame(device, frameOut);
|
||||
}
|
||||
|
||||
void GbmHostChannelReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept
|
||||
{
|
||||
GbmHostChannel* channel = device.Impl.Host;
|
||||
if (channel == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
channel->ReleaseFrame(device, seq, generation);
|
||||
}
|
||||
} // namespace MobileGL::MG_Gbm
|
||||
|
||||
// End of Source File Header
|
||||
@@ -0,0 +1,476 @@
|
||||
// MobileGL - MobileGL/MG_Gbm/GbmInternal.h
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
//
|
||||
// This header carries the internal model shared by the GBM front-end files: the
|
||||
// device, buffer-object and surface records, the two allocation backends, and the
|
||||
// wire protocol used to talk to a phone that owns the real display hardware.
|
||||
//
|
||||
// The reason a GBM implementation has to exist in this tree at all is that the
|
||||
// container half of MobileGL runs a Wayland compositor (kwin). That compositor
|
||||
// needs libgbm to allocate the buffers it renders into, and the fd of such a
|
||||
// buffer is handed to the Anland display producer, which ships the frame to the
|
||||
// phone. If Mesa's libgbm is loaded, the compositor renders into Mesa buffers and
|
||||
// the phone ends up receiving a copy through a path that was never designed to
|
||||
// carry them. When this library is loaded instead, the buffers are either frames
|
||||
// the phone itself offered to us, or buffers allocated from the kernel's dma-heap
|
||||
// so that they are dma-bufs the display host can consume directly.
|
||||
//
|
||||
// The ownership rules below are the whole contract, because every one of them is
|
||||
// observable from outside this library:
|
||||
//
|
||||
// * A buffer record owns its descriptors. gbm_bo_get_fd and
|
||||
// gbm_bo_get_fd_for_plane hand out a dup, so a caller that closes what it was
|
||||
// given cannot invalidate the buffer, and the matching close happens exactly
|
||||
// once, in ~GbmBufferObject.
|
||||
// * A mapping is one mmap of the buffer's fd, counted rather than duplicated, so
|
||||
// two overlapping gbm_bo_map calls share one address and the last matching
|
||||
// gbm_bo_unmap releases it.
|
||||
// * A surface owns one reference per swapchain buffer, and every
|
||||
// gbm_surface_lock_front_buffer hands the caller a reference of its own. That
|
||||
// is what makes gbm_bo_destroy on a locked buffer harmless: it drops the
|
||||
// caller's reference and leaves the surface's bookkeeping intact.
|
||||
// * One mutex per device. GBM calls arrive from the compositor's render thread
|
||||
// and from its main thread, and the state they touch (the buffer table, the
|
||||
// swapchains, the backends) is small enough that one lock is cheaper than
|
||||
// several - and, more importantly, one lock cannot be taken in the wrong order.
|
||||
//
|
||||
// The helpers declared at the bottom of this file are the entire internal interface
|
||||
// between GbmDevice.cpp (the records and the self-allocated backend) and GbmApi.cpp
|
||||
// (the extern "C" surface). Every one of them assumes the device mutex is already
|
||||
// held: the lock is taken once, in the API layer, so that a compositor call can
|
||||
// never interleave with another half way through a swapchain update.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#include <gbm.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Gbm
|
||||
{
|
||||
// DRM_FORMAT_MOD_LINEAR and DRM_FORMAT_MOD_INVALID. drm_fourcc.h is not part of
|
||||
// libgbm's interface, and these two values are frozen kernel UAPI, so they are
|
||||
// spelled out here rather than made a build dependency on libdrm's headers.
|
||||
static constexpr Uint64 DrmFormatModLinear = 0ULL;
|
||||
static constexpr Uint64 DrmFormatModInvalid = 0x00ffffffffffffffULL;
|
||||
|
||||
// Every buffer this library hands out is described by one of these. HostFrame
|
||||
// buffers are owned by the phone and only borrowed by us, so they must be
|
||||
// released back over the channel instead of being freed locally.
|
||||
enum class GbmBackend
|
||||
{
|
||||
None = 0,
|
||||
HostFrame,
|
||||
DmaHeap,
|
||||
MemFd,
|
||||
};
|
||||
|
||||
// The phone offers frames with this packed struct immediately followed by an
|
||||
// AHardwareBuffer native handle in the payload of one message, with the handle's
|
||||
// descriptors in the SCM_RIGHTS control data of that same message. The layout is
|
||||
// fixed by the phone side, so the packing attribute is load bearing and the field
|
||||
// order may not be rearranged.
|
||||
//
|
||||
// The transport below the two structs is SOCK_SEQPACKET, and that is part of the
|
||||
// protocol rather than an implementation detail: a frame and its descriptors only
|
||||
// mean anything together, and the message boundary is what lets the reader take
|
||||
// the two as one thing. A stream socket would let a reader receive half an offer,
|
||||
// or a descriptor whose position in the byte stream it had to reconstruct, and the
|
||||
// failure mode of getting that arithmetic wrong is a frame silently made of
|
||||
// another frame's memory.
|
||||
//
|
||||
// Format is a DRM fourcc and not an AHardwareBuffer format, because it is what
|
||||
// gbm_bo_get_format has to answer with and a compositor compares it against
|
||||
// DRM_FORMAT_* values. StrideBytes is the byte distance between two rows of plane
|
||||
// 0, which is the same single-stride description GBM_BO_IMPORT_FD carries - and
|
||||
// the reason a frame read from this struct is reported as linear.
|
||||
struct GbmFrameOffer
|
||||
{
|
||||
uint32_t Magic; // 'M','G','G','F' little endian on both ends of the wire
|
||||
uint32_t Version;
|
||||
uint32_t Seq;
|
||||
uint32_t Index;
|
||||
uint32_t Width;
|
||||
uint32_t Height;
|
||||
uint32_t StrideBytes;
|
||||
uint32_t Format;
|
||||
uint32_t UsageLo;
|
||||
uint32_t UsageHi;
|
||||
} __attribute__((packed));
|
||||
|
||||
// The container replies with this once it no longer reads the frame, which is
|
||||
// what lets the phone recycle the slot. Without it the display host would run
|
||||
// out of buffers after a handful of frames.
|
||||
struct GbmFrameRelease
|
||||
{
|
||||
uint32_t Magic; // 'M','G','G','R'
|
||||
uint32_t Version;
|
||||
uint32_t Seq;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t GbmFrameOfferMagic = 0x4647474Du; // "MGGF"
|
||||
static constexpr uint32_t GbmFrameReleaseMagic = 0x5247474Du; // "MGGR"
|
||||
static constexpr uint32_t GbmHostProtocolVersion = 1;
|
||||
|
||||
// THE REVERSE DIRECTION: this end asking the render server to back one of its textures with a
|
||||
// dma-buf. The compositor's scene reaches the display daemon's buffers through
|
||||
// glEGLImageTargetTexture2DOES, and the server has to be told which texture that dma-buf backs -
|
||||
// over this same socket, because it is the one channel that can carry a descriptor across the
|
||||
// container boundary. Restated here rather than shared with MG_Backend/GbmFrameChannel.h for the
|
||||
// same reason the two frame shapes are: the ends are built into different libraries from
|
||||
// different build configurations (that file is the Android render server and never sees libgbm).
|
||||
// Field for field, packing included; the plane descriptors ride in the SCM_RIGHTS control data of
|
||||
// the same sendmsg, one message per import.
|
||||
struct GbmImportRequest
|
||||
{
|
||||
uint32_t Magic; // 'M','G','G','I'
|
||||
uint32_t Version;
|
||||
uint32_t Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
|
||||
uint32_t TexName; // THIS side's texture name; the server resolves its twin
|
||||
uint32_t Width;
|
||||
uint32_t Height;
|
||||
uint32_t FourCC;
|
||||
uint32_t PlaneCount;
|
||||
int32_t Stride[4];
|
||||
int32_t Offset[4];
|
||||
uint64_t Modifier[4];
|
||||
uint32_t HasModifier[4];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct GbmImportReply
|
||||
{
|
||||
uint32_t Magic; // 'I','G','G','R'
|
||||
uint32_t Version;
|
||||
uint32_t Ok;
|
||||
uint32_t Reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t GbmImportRequestMagic = 0x4947474Du; // "MGGI"
|
||||
static constexpr uint32_t GbmImportReplyMagic = 0x52474749u; // "IGGR"
|
||||
static constexpr uint32_t GbmImportMaxPlanes = 4;
|
||||
|
||||
// An AHardwareBuffer native handle as it appears on the wire: three counts
|
||||
// followed by numFds file descriptors and numInts integers. The counts are
|
||||
// 32 bit even on 64 bit Android, which is why they are spelled out here
|
||||
// rather than using the platform's native_handle_t.
|
||||
struct AHBNativeHandleHeader
|
||||
{
|
||||
int32_t Version;
|
||||
int32_t NumFds;
|
||||
int32_t NumInts;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t GbmMaxPlanes = 4;
|
||||
|
||||
// How one plane of a buffer is laid out inside the single allocation that backs
|
||||
// it. Multi-plane formats keep every plane in one dma-buf and describe the
|
||||
// separation with Offsets, which is the layout a modifier of
|
||||
// DRM_FORMAT_MOD_LINEAR can express and the only one this library produces.
|
||||
struct GbmPlaneLayout
|
||||
{
|
||||
Uint32 Stride = 0;
|
||||
Uint32 Offset = 0;
|
||||
SizeT Size = 0;
|
||||
};
|
||||
|
||||
// One frame the phone offered, as this process holds it: the offer's own
|
||||
// description of the memory, plus the descriptor this library duped out of the
|
||||
// handle the phone sent. It owns that descriptor for as long as it lives, so a
|
||||
// caller that takes a frame and then fails still closes it, and the descriptor the
|
||||
// phone sent is closed with the rest of the message's descriptors rather than kept.
|
||||
struct GbmHostFrame
|
||||
{
|
||||
Uint32 Seq = 0;
|
||||
Uint32 Index = 0;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 StrideBytes = 0;
|
||||
Uint32 Format = 0;
|
||||
// Bytes per pixel of plane 0. The offer carries a fourcc, so it comes from the
|
||||
// format table when there is an entry for it and from the stride otherwise.
|
||||
Uint32 BytesPerPixel = 0;
|
||||
Uint64 Usage = 0;
|
||||
// Which connection offered the frame. A release names a frame, and a frame only
|
||||
// means anything to the connection that offered it.
|
||||
Uint64 Generation = 0;
|
||||
// The duped dma-buf descriptor, and how many bytes it actually backs as asked
|
||||
// of the descriptor itself rather than computed from the offered geometry.
|
||||
Int Fd = -1;
|
||||
SizeT Size = 0;
|
||||
|
||||
// Move-only, because two records naming one descriptor would close it twice.
|
||||
GbmHostFrame() = default;
|
||||
GbmHostFrame(const GbmHostFrame&) = delete;
|
||||
GbmHostFrame& operator=(const GbmHostFrame&) = delete;
|
||||
GbmHostFrame(GbmHostFrame&& other) noexcept;
|
||||
GbmHostFrame& operator=(GbmHostFrame&& other) noexcept;
|
||||
~GbmHostFrame();
|
||||
};
|
||||
|
||||
// One imported or locally allocated buffer. The struct is private to this
|
||||
// library; gbm_bo is the public opaque name and is defined in terms of it.
|
||||
struct GbmBufferObject
|
||||
{
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Format = 0;
|
||||
Uint32 Stride = 0;
|
||||
Uint64 Modifier = DrmFormatModLinear;
|
||||
Uint64 Usage = 0;
|
||||
Int PlaneCount = 1;
|
||||
// Bytes per pixel of plane 0. Kept beside the layout because gbm_bo_map
|
||||
// needs it to place a sub-rectangle, and because a format this library can
|
||||
// allocate but cannot describe in bytes would be a format it must refuse.
|
||||
Uint32 BytesPerPixel = 0;
|
||||
|
||||
// Fds owned by this record. get_fd and get_fd_for_plane hand out dups so
|
||||
// that a caller closing its fd can never invalidate the buffer. Index 0 is
|
||||
// the only one a self-allocated buffer has: every plane of a linear image
|
||||
// lives in that one dma-buf, and Fds[1..] stay -1 for it. An import with
|
||||
// several descriptors fills several entries, and the destructor closes
|
||||
// every entry that is not -1.
|
||||
Int Fds[GbmMaxPlanes] = {-1, -1, -1, -1};
|
||||
Uint32 Strides[GbmMaxPlanes] = {0, 0, 0, 0};
|
||||
Uint32 Offsets[GbmMaxPlanes] = {0, 0, 0, 0};
|
||||
Uint64 Modifiers[GbmMaxPlanes] = {0, 0, 0, 0};
|
||||
|
||||
// The number of bytes the descriptors actually back, rounded up to the page
|
||||
// size because that is what both backends allocate. It is the only length
|
||||
// that is safe to mmap: a mapping longer than the object faults on the last
|
||||
// page, and a shorter one would hide the caller's own stride arithmetic.
|
||||
SizeT AllocationSize = 0;
|
||||
|
||||
GbmBackend Backend = GbmBackend::None;
|
||||
|
||||
// Set only for host frames: the slot index and sequence number that the
|
||||
// release message has to mention so the phone can recycle the right slot, and
|
||||
// the connection the frame arrived on. A frame left over from a connection
|
||||
// that has since died is never released into the one that replaced it, because
|
||||
// that would free a slot belonging to another session.
|
||||
Uint32 HostIndex = 0;
|
||||
Uint32 HostSeq = 0;
|
||||
Uint64 HostGeneration = 0;
|
||||
Bool HostOwned = false;
|
||||
|
||||
// How many maps are outstanding. The mapping itself is kept here so that
|
||||
// unmap can find and tear down the right one.
|
||||
Int MapRefCount = 0;
|
||||
void* MapAddress = nullptr;
|
||||
SizeT MapSize = 0;
|
||||
|
||||
// Surfaces keep their buffers alive by count rather than by ownership, so
|
||||
// the device knows when a bo is truly unreferenced.
|
||||
Int RefCount = 1;
|
||||
|
||||
// gbm_bo_set_user_data's pair. The destroy callback is invoked with the
|
||||
// gbm_bo that is going away, once, from the destructor below.
|
||||
void* UserData = nullptr;
|
||||
void (*UserDataDestroy)(::gbm_bo*, void*) = nullptr;
|
||||
|
||||
::gbm_device* Owner = nullptr;
|
||||
|
||||
// The record releases its own descriptors and its own mapping, so that no
|
||||
// failure path anywhere in this library can leak either one - an allocation
|
||||
// that fails half way through construction still cleans up when the record
|
||||
// it was building is destroyed.
|
||||
~GbmBufferObject();
|
||||
};
|
||||
|
||||
// A real swapchain. The compositor asks for a buffer to render into, presents
|
||||
// it, and later returns it; the three states below are exactly the states the
|
||||
// GBM API can express.
|
||||
struct GbmSurface
|
||||
{
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Format = 0;
|
||||
Uint32 Flags = 0;
|
||||
|
||||
std::vector<::gbm_bo*> Buffers;
|
||||
// Index of the buffer currently handed to the compositor as the front
|
||||
// buffer, or -1 when nothing has been locked.
|
||||
Int LockedIndex = -1;
|
||||
// Indices of buffers the compositor has released and that may be handed
|
||||
// out again, plus the one that is locked.
|
||||
std::vector<Bool> Free;
|
||||
|
||||
::gbm_device* Owner = nullptr;
|
||||
|
||||
// Dropping the surface's references here is what keeps the creation path
|
||||
// free of a second failure path: if the third swapchain buffer cannot be
|
||||
// allocated, unwinding the half-built surface returns the two that exist.
|
||||
~GbmSurface();
|
||||
};
|
||||
|
||||
// The device ties one backend choice, one host channel and one mutex together.
|
||||
// The single mutex is deliberate: GBM calls arrive from the compositor's
|
||||
// render thread and from its main thread, and the state they touch (the bo
|
||||
// table, the backends) is small enough that one lock is cheaper than several.
|
||||
struct GbmDevice
|
||||
{
|
||||
// The fd this device was created with. It is a private dup taken at
|
||||
// creation, never the caller's descriptor, so gbm_device_destroy can close
|
||||
// it without ever closing an fd the caller still has open. -1 when the
|
||||
// caller passed none and no render node could be opened: the self-allocated
|
||||
// backend does not need a DRM node, so that is a working device and not an
|
||||
// error.
|
||||
Int DeviceFd = -1;
|
||||
// The dma-heap node this device allocates from, or -1 when none could be
|
||||
// opened, in which case every allocation falls back to a memfd.
|
||||
Int HeapFd = -1;
|
||||
String HeapPath;
|
||||
|
||||
// Where the display host is reached, as configured when the device was created.
|
||||
String SocketPath;
|
||||
GbmBackend DefaultBackend = GbmBackend::DmaHeap;
|
||||
// True while the channel below is connected and frames are arriving over it.
|
||||
// False means every allocation this device answers is answered locally.
|
||||
Bool HostAvailable = false;
|
||||
// The name gbm_device_get_backend_name reports. It names what this device
|
||||
// actually does, because a compositor that logs it is asking exactly that - so
|
||||
// it changes when the host channel connects and when that connection dies, and
|
||||
// SelfAllocatedBackendName is what it falls back to when it does.
|
||||
String BackendName = "mobilegl";
|
||||
String SelfAllocatedBackendName = "mobilegl";
|
||||
|
||||
std::mutex Mutex;
|
||||
std::unordered_map<::gbm_bo*, GbmBufferObject*> Buffers;
|
||||
std::unordered_map<::gbm_surface*, GbmSurface*> Surfaces;
|
||||
|
||||
// The host channel is a separate translation unit so that the socket protocol
|
||||
// never leaks into the allocation paths. It lives for as long as the device
|
||||
// does and is reached only under this device's mutex, which is what lets the
|
||||
// allocation path ask it for a frame without a second lock to order against.
|
||||
class GbmHostChannel* Host = nullptr;
|
||||
|
||||
// Closes the heap node and the private device dup, so that a device which
|
||||
// fails to finish constructing still releases the descriptors it opened.
|
||||
~GbmDevice();
|
||||
};
|
||||
|
||||
// ---- format description ------------------------------------------------
|
||||
|
||||
// Fills planes[]/planeCount/totalBytes with the linear layout this library
|
||||
// would allocate for format at width x height, and returns false for a format
|
||||
// it cannot lay out. gbm_bo_create refuses a format this refuses, because a
|
||||
// buffer whose plane layout was guessed is worse than a buffer that was not
|
||||
// handed out at all.
|
||||
Bool GbmDescribeFormat(Uint32 format, Uint32 width, Uint32 height, GbmPlaneLayout planes[GbmMaxPlanes],
|
||||
Int& planeCount, SizeT& totalBytes) noexcept;
|
||||
|
||||
// Whether this library can allocate the format at all.
|
||||
Bool GbmFormatIsSupported(Uint32 format) noexcept;
|
||||
|
||||
// Bytes per pixel of plane 0: what gbm_bo_map needs to place a sub-rectangle,
|
||||
// and half of what gbm_bo_get_bpp reports.
|
||||
Uint32 GbmFormatBytesPerPixel(Uint32 format) noexcept;
|
||||
|
||||
// ---- records and the self-allocated backend ----------------------------
|
||||
|
||||
// Builds a device. The only helper here that is called without the mutex,
|
||||
// because no other thread can see the device it is building.
|
||||
::gbm_device* GbmDeviceCreate(Int fd) noexcept;
|
||||
|
||||
// Tears the device down: every remaining surface, every remaining buffer, the
|
||||
// heap node, the private device fd, and the record itself.
|
||||
void GbmDeviceDestroy(::gbm_device& device) noexcept;
|
||||
|
||||
// Allocates a buffer from the device's heap node, or from a memfd when there
|
||||
// is none, and records it in the device's table. Returns null with errno set.
|
||||
::gbm_bo* GbmBufferCreate(::gbm_device& device, Uint32 width, Uint32 height, Uint32 format, Uint64 usage,
|
||||
const Uint64* modifiers, Uint32 modifierCount) noexcept;
|
||||
|
||||
// Wraps descriptors the caller already owns. The descriptors are duped, so the
|
||||
// caller keeps ownership of its own; type is GBM_BO_IMPORT_FD or
|
||||
// GBM_BO_IMPORT_FD_MODIFIER.
|
||||
::gbm_bo* GbmBufferImport(::gbm_device& device, Uint32 type, const void* buffer, Uint64 usage) noexcept;
|
||||
|
||||
// Drops one reference, destroying the buffer when it was the last one.
|
||||
void GbmBufferRelease(::gbm_device& device, ::gbm_bo& bo) noexcept;
|
||||
|
||||
// Destroys a buffer outright, whatever its reference count says. Only the
|
||||
// device's own teardown uses this: by then no caller's reference is meaningful.
|
||||
void GbmBufferForceDestroy(::gbm_device& device, ::gbm_bo& bo) noexcept;
|
||||
|
||||
// Hands out a dup of the descriptor backing a plane, which the caller then owns.
|
||||
// A plane this buffer does not have is refused with errno = EINVAL.
|
||||
Int GbmBufferDupFd(::gbm_bo& bo, Int plane) noexcept;
|
||||
|
||||
// Maps the whole buffer once and counts the call; strideOut receives the
|
||||
// stride of plane 0. The rect (x, y) is applied to the returned address.
|
||||
void* GbmBufferMap(::gbm_bo& bo, Uint32 x, Uint32 y, Uint32& strideOut) noexcept;
|
||||
|
||||
// Drops one mapping; the last one unmaps.
|
||||
void GbmBufferUnmap(::gbm_bo& bo, void* mapData) noexcept;
|
||||
|
||||
// ---- host frames -------------------------------------------------------
|
||||
|
||||
// Builds the channel to the display host and makes one attempt to connect it, then
|
||||
// stores it in the device. A machine that is not the container half of a split run
|
||||
// has nothing at that path, and that is a device which allocates locally from then
|
||||
// on - not a device that failed to be created.
|
||||
void GbmHostChannelCreate(::gbm_device& device) noexcept;
|
||||
|
||||
// Closes the channel and releases what it holds. Called from the device's
|
||||
// destructor, after the buffers that still name offered frames are gone, so that
|
||||
// their releases can still be sent.
|
||||
void GbmHostChannelDestroy(::gbm_device& device) noexcept;
|
||||
|
||||
// Takes the next frame the host offered, or returns false when none is waiting or
|
||||
// the channel is down. Never blocks: the socket is non-blocking, so a caller that
|
||||
// finds nothing allocates locally in the same call, which is what makes this usable
|
||||
// from a compositor's frame loop.
|
||||
Bool GbmHostChannelTakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept;
|
||||
|
||||
// Tells the host that a frame may be recycled. Called from the buffer record's
|
||||
// destructor, so a frame goes back exactly when nothing in this process can still
|
||||
// read it, and only into the connection that offered it.
|
||||
void GbmHostChannelReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept;
|
||||
|
||||
// ---- swapchains --------------------------------------------------------
|
||||
|
||||
// Builds a surface and its swapchain up front, so that
|
||||
// gbm_surface_has_free_buffers can answer truthfully from the first call.
|
||||
::gbm_surface* GbmSurfaceCreate(::gbm_device& device, Uint32 width, Uint32 height, Uint32 format, Uint64 usage,
|
||||
const Uint64* modifiers, Uint32 modifierCount) noexcept;
|
||||
|
||||
// Unregisters the surface and destroys it, returning the swapchain buffers it
|
||||
// still owns to the device.
|
||||
void GbmSurfaceDestroy(::gbm_surface& surface) noexcept;
|
||||
|
||||
// Hands out the next free swapchain buffer with a reference of its own, or
|
||||
// null with errno = EBUSY when the compositor still holds every one of them.
|
||||
::gbm_bo* GbmSurfaceLockFront(::gbm_surface& surface) noexcept;
|
||||
|
||||
// Takes a locked buffer back, dropping the reference the lock handed out.
|
||||
void GbmSurfaceReleaseBuffer(::gbm_surface& surface, ::gbm_bo& bo) noexcept;
|
||||
|
||||
Bool GbmSurfaceHasFreeBuffers(::gbm_surface& surface) noexcept;
|
||||
} // namespace MobileGL::MG_Gbm
|
||||
|
||||
// The public opaque types are the internal records. Callers only ever see the
|
||||
// pointers, so this costs nothing and removes a layer of casting.
|
||||
struct gbm_device
|
||||
{
|
||||
MobileGL::MG_Gbm::GbmDevice Impl;
|
||||
};
|
||||
|
||||
struct gbm_bo
|
||||
{
|
||||
MobileGL::MG_Gbm::GbmBufferObject Impl;
|
||||
};
|
||||
|
||||
struct gbm_surface
|
||||
{
|
||||
MobileGL::MG_Gbm::GbmSurface Impl;
|
||||
};
|
||||
|
||||
// End of Source File Header
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "EGLImpl.h"
|
||||
#include "EGLPlatformExtensions.h"
|
||||
#include "WaylandWindow.h"
|
||||
#include "../GetProcAddress.h"
|
||||
#include <Init.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
@@ -20,6 +22,36 @@
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
// The KHR spellings of the image entry points, exported by Exporting/Definitions.cpp. Declared
|
||||
// here because the bundled EGL headers declare them only under EGL_EGLEXT_PROTOTYPES, which no
|
||||
// build of this library defines, and because eglGetProcAddress below answers them by name.
|
||||
extern "C" EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image);
|
||||
|
||||
// The swap-and-sync family the compositor resolves by name while bringing its EGL backend up; defined
|
||||
// in Exporting/Definitions.cpp and answered from GetProcAddress below (see the definitions).
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
|
||||
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
|
||||
extern "C" EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
|
||||
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
|
||||
// THE DEVICE-QUERY FAMILY, exported by Exporting/Definitions.cpp and answered by name below: kwin
|
||||
// reaches eglQueryDisplayAttribEXT through libepoxy's pointer (that is how finding a render device
|
||||
// starts), and libepoxy asks for it by name. See the definitions for why these had to exist.
|
||||
extern "C" EGLBoolean eglQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices);
|
||||
extern "C" EGLBoolean eglQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value);
|
||||
extern "C" const char* eglQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name);
|
||||
extern "C" EGLBoolean eglQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value);
|
||||
extern "C" EGLSurface eglCreatePlatformWindowSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_window,
|
||||
const EGLAttrib* attrib_list);
|
||||
extern "C" EGLSurface eglCreatePlatformPixmapSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
|
||||
const EGLAttrib* attrib_list);
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
namespace {
|
||||
using EGLStateContext = MG_State::EGLState::EGLContext;
|
||||
@@ -116,6 +148,73 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return reinterpret_cast<void*>(static_cast<SizeT>(nativeHandle));
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
// Every Wayland window surface's presentation, by surface. Guarded by EGLOperationMutex,
|
||||
// which every entry point that reaches it holds.
|
||||
UnorderedMap<EGLSurface, UniquePtr<Wayland::WindowSurface>>& WaylandSurfaces() {
|
||||
static auto* surfaces = new UnorderedMap<EGLSurface, UniquePtr<Wayland::WindowSurface>>();
|
||||
return *surfaces;
|
||||
}
|
||||
|
||||
// A WINDOW ON A WAYLAND DISPLAY. Whatever MOBILEGL_IPC_SURFACE says, the window is this
|
||||
// application's own wl_surface, so neither the server's window nor the display host's frame
|
||||
// is what it shows: the backend draws into a pbuffer of the window's size, and each swap
|
||||
// reads the frame back and attaches it to the wl_surface (WaylandWindow.h). Answers false
|
||||
// when the display is not a Wayland one; otherwise *out is the surface, or EGL_NO_SURFACE
|
||||
// with the error set.
|
||||
template <typename State>
|
||||
Bool CreateWaylandWindowSurface(State* state, EGLDisplay dpy, EGLConfig config, void* window,
|
||||
Bool platformWindow, EGLSurface* out) {
|
||||
Uint64 nativeDisplay = 0;
|
||||
EGLenum platform = EGL_NONE;
|
||||
if (!state->GetDisplayNative(dpy, &nativeDisplay, &platform) ||
|
||||
!Wayland::IsWaylandDisplay(nativeDisplay, platform)) {
|
||||
return false;
|
||||
}
|
||||
*out = EGL_NO_SURFACE;
|
||||
if (!state->IsDisplayInitialized(dpy)) {
|
||||
state->SetError(EGL_NOT_INITIALIZED);
|
||||
return true;
|
||||
}
|
||||
if (!state->ValidateConfigOnDisplay(dpy, config)) {
|
||||
state->SetError(EGL_BAD_CONFIG);
|
||||
return true;
|
||||
}
|
||||
EGLint width = 0;
|
||||
EGLint height = 0;
|
||||
if (!Wayland::WindowSize(window, &width, &height)) {
|
||||
state->SetError(EGL_BAD_NATIVE_WINDOW);
|
||||
return true;
|
||||
}
|
||||
EGLint alpha = 0;
|
||||
(void)state->GetConfigAttrib(dpy, config, EGL_ALPHA_SIZE, &alpha);
|
||||
auto presentation =
|
||||
Wayland::WindowSurface::Create(reinterpret_cast<void*>(static_cast<uintptr_t>(nativeDisplay)), window,
|
||||
width, height, alpha > 0);
|
||||
if (!presentation) {
|
||||
state->SetError(EGL_BAD_NATIVE_WINDOW);
|
||||
return true;
|
||||
}
|
||||
const EGLSurface surface =
|
||||
platformWindow ? state->CreatePlatformWindowSurface(dpy, config, window, nullptr)
|
||||
: state->CreateWindowSurface(
|
||||
dpy, config, (NativeWindowType)(reinterpret_cast<uintptr_t>(window)), nullptr);
|
||||
if (surface == EGL_NO_SURFACE) return true;
|
||||
(void)state->ResizeSurface(dpy, surface, width, height);
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject || !backendObject->CreateEGLPbufferSurface(surface, width, height)) {
|
||||
MGLOG_E("eglCreateWindowSurface: the backend refused the %dx%d drawable behind a Wayland window",
|
||||
width, height);
|
||||
state->DestroySurface(dpy, surface);
|
||||
state->SetError(EGL_BAD_ALLOC);
|
||||
return true;
|
||||
}
|
||||
WaylandSurfaces()[surface] = std::move(presentation);
|
||||
*out = surface;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
|
||||
@@ -124,6 +223,16 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
{
|
||||
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
|
||||
EGLSurface waylandSurface = EGL_NO_SURFACE;
|
||||
if (CreateWaylandWindowSurface(state, dpy, config, ToVoidHandle(window), /*platformWindow=*/false,
|
||||
&waylandSurface)) {
|
||||
return waylandSurface;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!state->IsDisplayInitialized(dpy)) {
|
||||
state->SetError(EGL_NOT_INITIALIZED);
|
||||
return EGL_NO_SURFACE;
|
||||
@@ -137,8 +246,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// the window is the SERVER's: a headless client passes none, so NULL is not
|
||||
// EGL_BAD_NATIVE_WINDOW here. Whatever the client passed never reaches the wire - the
|
||||
// remote backend sends WindowKind::ServerOwned with token 0 (BackendObject_Remote).
|
||||
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
|
||||
if (IsNullNativeHandle(window) && !serverOwnedWindow) {
|
||||
// Either on-screen shape leaves this side without a window of its own: =server asks the
|
||||
// server for its own window, =host draws into the frames the display host offers.
|
||||
const Bool windowlessOnScreen =
|
||||
MG_Config::ServerOwnedWindowSurfaces() || MG_Config::HostFrameWindowSurfaces();
|
||||
if (IsNullNativeHandle(window) && !windowlessOnScreen) {
|
||||
#else
|
||||
if (IsNullNativeHandle(window)) {
|
||||
#endif
|
||||
@@ -146,19 +258,51 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
|
||||
Uint32 requestedWidth = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_WIDTH, 0), 0));
|
||||
Uint32 requestedHeight = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
// P12: A GAME'S OWN WINDOW SIZE IS THE SIZE OF THE FRAMES IT RENDERS, and a server-owned
|
||||
// window has to take that size or clip. SDL (Minecraft's Android backend) creates the EGL
|
||||
// window surface with no EGL_WIDTH / EGL_HEIGHT, so the request went out as 0x0 - "use the
|
||||
// server window's own size" - and a landscape 2620x1280 game drawn into a portrait 1280x2620
|
||||
// server window came out clipped to its bottom-left 1280x1280 corner (Espryt and Magma
|
||||
// alike; the same game in a landscape window was right). The native window it handed us
|
||||
// knows the size the game believes it has: ask for THAT, and the server fixes its window
|
||||
// buffer to it and aspect-fits the view (MobileGLDisplayActivity), letterboxed.
|
||||
if (windowlessOnScreen && requestedWidth == 0 && requestedHeight == 0 && !IsNullNativeHandle(window)) {
|
||||
auto* native = reinterpret_cast<ANativeWindow*>(ToVoidHandle(window));
|
||||
const int32_t nativeWidth = ANativeWindow_getWidth(native);
|
||||
const int32_t nativeHeight = ANativeWindow_getHeight(native);
|
||||
if (nativeWidth > 0 && nativeHeight > 0) {
|
||||
requestedWidth = static_cast<Uint32>(nativeWidth);
|
||||
requestedHeight = static_cast<Uint32>(nativeHeight);
|
||||
MGLOG_I("eglCreateWindowSurface: no EGL_WIDTH/EGL_HEIGHT; the on-screen surface is asked for "
|
||||
"the native window's %dx%d", nativeWidth, nativeHeight);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const MG_Backend::WindowHandle windowHandle = {
|
||||
.Backend = DetectWindowBackend(),
|
||||
.Handle = ToVoidHandle(window),
|
||||
.Width = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_WIDTH, 0), 0)),
|
||||
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0)),
|
||||
.Width = requestedWidth,
|
||||
.Height = requestedHeight,
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EGLSurface surface = serverOwnedWindow
|
||||
? state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false)
|
||||
: state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
if (MG_Config::ServerOwnedWindowSurfaces()) {
|
||||
// The server's own window: it makes one and draws into it.
|
||||
surface = state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false);
|
||||
} else if (MG_Config::HostFrameWindowSurfaces()) {
|
||||
// The display host's frames, as above: no window of the client's either way.
|
||||
surface = state->CreateHostFrameWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false);
|
||||
} else {
|
||||
surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
}
|
||||
#else
|
||||
EGLSurface surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
#endif
|
||||
@@ -188,6 +332,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!state->ValidateSurfaceOnDisplay(dpy, draw)) {
|
||||
MGLOG_E_ONCE("eglSwapBuffers: surface %p is not a surface of display %p (EGL_BAD_SURFACE)", draw, dpy);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -197,6 +342,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
// A Wayland window shows what is attached to it: the frame is read back and attached
|
||||
// BEFORE the swap, while it is still the drawable's content.
|
||||
if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end()) (void)it->second->Present();
|
||||
#endif
|
||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
||||
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
@@ -207,7 +357,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
EGLBoolean ChooseConfig(EGLDisplay dpy, const EGLint* attrib_list, EGLConfig* configs, EGLint config_size,
|
||||
EGLint* num_config) {
|
||||
auto* state = GetState();
|
||||
// eglChooseConfig can be an application's first EGL call after eglGetPlatformDisplay + eglInitialize,
|
||||
// and for a display built from an EGLDeviceEXT nothing has brought MobileGL up yet: GetState()
|
||||
// then answers NULL and the call lands in EGLContext::ChooseConfig with a null this - measured as
|
||||
// a crash inside that function (pc/lr both resolved to it) with kwin's own "pEGLContext is null.
|
||||
// MG_State may not be initialized." as the only warning, and reported one layer up as
|
||||
// "Could not initialize rendering context".
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -215,11 +371,26 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
|
||||
EGLContext CreateContext(EGLDisplay dpy, EGLConfig config, EGLContext shareCtx, const EGLint* attrib_list) {
|
||||
auto* state = GetState();
|
||||
// WHICH CONTEXT IS BEING ASKED FOR, and what came back. kwin's OpenGL compositing fails with
|
||||
// "Could not initialize rendering context" while the same path succeeds from a probe against
|
||||
// this same library (probe10: a 3.3 core context on a GBM surface, makeCurrent answered 1),
|
||||
// so the difference has to be in this argument list - and an EGL vendor cannot see it from the
|
||||
// outside. The list is dumped in EGL_NONE-terminated pairs; EGL_NONE is 0x3038, not 0.
|
||||
MGLOG_I("eglCreateContext(dpy=%p, config=%p, share=%p)", dpy, config, shareCtx);
|
||||
if (attrib_list != nullptr) {
|
||||
for (int index = 0; attrib_list[index] != EGL_NONE; index += 2) {
|
||||
MGLOG_I(" attrib 0x%04x = %d", attrib_list[index], attrib_list[index + 1]);
|
||||
}
|
||||
} else {
|
||||
MGLOG_I(" (no attribute list)");
|
||||
}
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_NO_CONTEXT;
|
||||
}
|
||||
return state->CreateContext(dpy, config, shareCtx, attrib_list);
|
||||
EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
|
||||
MGLOG_I("eglCreateContext -> %p", static_cast<void*>(context));
|
||||
return context;
|
||||
}
|
||||
|
||||
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||
@@ -259,6 +430,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return state->ConsumeError();
|
||||
}
|
||||
|
||||
// THE DISPLAY HOST'S FRAME, AS THE COMPOSITOR PRESENTS IT. Set by the surfaceless-bind hook in
|
||||
// MakeCurrent when this client (the compositor) takes the host frame; read by glFlush, which is
|
||||
// that compositor's only frame boundary - it never calls eglSwapBuffers, and without a present
|
||||
// the display host is never told a frame is done, so nothing ever reaches the glass.
|
||||
EGLDisplay g_hostFramePresentDisplay = nullptr;
|
||||
EGLSurface g_hostFramePresentSurface = nullptr;
|
||||
|
||||
EGLBoolean MakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
|
||||
auto* state = GetState();
|
||||
@@ -275,6 +453,81 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
|
||||
threadId.c_str(), dpy, draw, read, ctx, oldDisplay, oldDraw, oldRead, oldContext);
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// THE DISPLAY HOST'S FRAMES, TO A CLIENT THAT ASKS FOR NO SURFACE.
|
||||
//
|
||||
// A compositor on this route never calls eglCreateWindowSurface: it imports the display host's
|
||||
// dma-bufs and draws into them through framebuffers, binding its contexts with EGL_NO_SURFACE.
|
||||
// The host frame bridge, though, is opened by exactly one thing - a surface being CREATED and
|
||||
// activated (ServerLoop's CreateWindowSurface with the host-frame window backend ->
|
||||
// InitHostFrameSurface) - so with no surface ever created the bridge stayed shut, the display
|
||||
// host kept offering frames into a name nobody served ("offer buffer 0: bridge fd=-1"), and no
|
||||
// frame could reach the glass no matter how much was drawn. So: when the session asked for
|
||||
// host-framed surfaces and a real context is being bound with no surface, make it the surface
|
||||
// eglCreatePlatformWindowSurface would have made, once, and bind that. The size is the display
|
||||
// host's own frame size; the client's window value is not on this path at all.
|
||||
// ONLY THE COMPOSITOR TAKES THE HOST FRAME. A desktop is full of processes that bind a
|
||||
// context with no surface - konsole, plasmashell, kactivitymanagerd - and measured with
|
||||
// probe15 as the second client: such a bind from a NON-compositor process made the host-frame
|
||||
// activation fail (makeCurrent=0) and the process aborted. The compositor is marked by its
|
||||
// launcher with MOBILEGL_HOST_FRAME_CLIENT=1; everyone else keeps the plain surfaceless bind,
|
||||
// which is all they ever wanted.
|
||||
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT &&
|
||||
MG_Config::HostFrameWindowSurfaces() &&
|
||||
(std::getenv("MOBILEGL_HOST_FRAME_CLIENT") != nullptr &&
|
||||
std::string("1") == std::getenv("MOBILEGL_HOST_FRAME_CLIENT"))) {
|
||||
static Bool hostFrameSurfaceTaken = false;
|
||||
if (!hostFrameSurfaceTaken) {
|
||||
hostFrameSurfaceTaken = true;
|
||||
EGLConfig hostFrameConfig = nullptr;
|
||||
EGLint hostFrameConfigCount = 0;
|
||||
const EGLint hostFrameChoose[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE};
|
||||
if (state->ChooseConfig(dpy, hostFrameChoose, &hostFrameConfig, 1,
|
||||
&hostFrameConfigCount) &&
|
||||
hostFrameConfigCount > 0 && hostFrameConfig != nullptr) {
|
||||
// BOTH HALVES, exactly as eglCreateWindowSurface makes one: the EGL state's
|
||||
// registration (without it every later bind and swap of this surface was refused
|
||||
// as EGL_BAD_SURFACE - measured: "eglMakeCurrent rejected by EGLState
|
||||
// error=0x300d", and glFlush's swaps returned before reaching the backend), then
|
||||
// the backend's own window-surface entry, which the remote backend routes to the
|
||||
// host frame (BackendObject_Remote::CreateHostFrameWindowSurface) - the call that
|
||||
// puts a CreateWindowSurface frame with the host-frame window backend on the wire.
|
||||
const EGLSurface hostFrameSurface = state->CreateHostFrameWindowSurface(
|
||||
dpy, hostFrameConfig, nullptr, 1440, 3200, /*platformWindow=*/false);
|
||||
MG_Backend::WindowHandle hostFrameHandle{};
|
||||
hostFrameHandle.Width = 1440;
|
||||
hostFrameHandle.Height = 3200;
|
||||
if (hostFrameSurface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("host frame: the EGL state refused the display host's frame surface");
|
||||
} else if (!MG_Backend::pActiveBackendObject->CreateEGLWindowSurface(hostFrameSurface,
|
||||
hostFrameHandle)) {
|
||||
MGLOG_E("host frame: the backend refused the display host's frame surface");
|
||||
state->DestroySurface(dpy, hostFrameSurface);
|
||||
} else {
|
||||
// The frame boundary of a compositor that never swaps is its glFlush. glFlush
|
||||
// has no EGL context of its own, so remember HERE which surface presents the
|
||||
// display host's frame; glFlush presents it (see the host-frame present in
|
||||
// glFlush's implementation).
|
||||
g_hostFramePresentDisplay = dpy;
|
||||
g_hostFramePresentSurface = hostFrameSurface;
|
||||
MGLOG_I("host frame: the first surfaceless eglMakeCurrent made the display host's "
|
||||
"frame surface %p (%dx%d)",
|
||||
hostFrameSurface, 1440, 3200);
|
||||
}
|
||||
} else {
|
||||
MGLOG_E("host frame: no config for the display host's frame surface");
|
||||
}
|
||||
}
|
||||
// EVERY surfaceless bind, not only the first: the compositor rebinds with EGL_NO_SURFACE
|
||||
// all the time, and a bind without the surface leaves nothing current for glFlush's swap
|
||||
// to present (BackendObject::SwapEGLBuffers: "draw surface is not current on this thread").
|
||||
if (g_hostFramePresentSurface != nullptr && dpy == g_hostFramePresentDisplay) {
|
||||
draw = g_hostFramePresentSurface;
|
||||
read = g_hostFramePresentSurface;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!state->MakeCurrent(dpy, draw, read, ctx)) {
|
||||
const EGLint error = state->ConsumeError();
|
||||
MGLOG_D("eglMakeCurrent rejected by EGLState thread=%s error=0x%04x", threadId.c_str(), error);
|
||||
@@ -332,6 +585,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state->DestroySurface(dpy, surface)) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
WaylandSurfaces().erase(surface);
|
||||
#endif
|
||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||
backendObject->ReleaseEGLSurface(surface);
|
||||
}
|
||||
@@ -440,14 +696,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
case EGL_CLIENT_APIS:
|
||||
return "OpenGL OpenGL_ES";
|
||||
case EGL_EXTENSIONS:
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
return "EGL_EXT_client_extensions "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
}
|
||||
return "EGL_KHR_create_context "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
// One list, in EGLPlatformExtensions.h, because the vendor string glvnd reads and
|
||||
// this query an application reads have to agree: an application that does not see the
|
||||
// platform extension here never asks for a platform display at all.
|
||||
return display == EGL_NO_DISPLAY ? kClientExtensionString : kDisplayExtensionString;
|
||||
default:
|
||||
state->SetError(EGL_BAD_PARAMETER);
|
||||
return nullptr;
|
||||
@@ -682,6 +934,23 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return state->DestroyImage(dpy, image) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
EGLImageKHR CreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list) {
|
||||
// EGL_KHR_image_base spells its attribute list in EGLint pairs and the core eglCreateImage
|
||||
// in EGLAttrib pairs, and the two are not the same memory: on a 64-bit build one entry is
|
||||
// twice the width of the other, so the caller's array cannot be handed over as it stands.
|
||||
// Re-spelling it is therefore part of the entry point rather than a detail of the state.
|
||||
Vector<EGLAttrib> attribs;
|
||||
if (attrib_list != nullptr) {
|
||||
for (SizeT i = 0; attrib_list[i] != EGL_NONE; i += 2) {
|
||||
attribs.push_back(static_cast<EGLAttrib>(attrib_list[i]));
|
||||
attribs.push_back(static_cast<EGLAttrib>(attrib_list[i + 1]));
|
||||
}
|
||||
attribs.push_back(static_cast<EGLAttrib>(EGL_NONE));
|
||||
}
|
||||
return CreateImage(dpy, ctx, target, buffer, attribs.empty() ? nullptr : attribs.data());
|
||||
}
|
||||
|
||||
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
|
||||
(void)attrib_list;
|
||||
|
||||
@@ -698,11 +967,24 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
{
|
||||
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
|
||||
EGLSurface waylandSurface = EGL_NO_SURFACE;
|
||||
if (CreateWaylandWindowSurface(state, dpy, config, native_window, /*platformWindow=*/true,
|
||||
&waylandSurface)) {
|
||||
return waylandSurface;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 (on-screen server window), D1: as CreateWindowSurface - with MOBILEGL_IPC_SURFACE=server
|
||||
// the window is the server's, and a headless client's NULL is accepted.
|
||||
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
|
||||
if (native_window == nullptr && !serverOwnedWindow) {
|
||||
// Either on-screen shape leaves this side without a window of its own: =server asks the
|
||||
// server for its own window, =host draws into the frames the display host offers.
|
||||
const Bool windowlessOnScreen =
|
||||
MG_Config::ServerOwnedWindowSurfaces() || MG_Config::HostFrameWindowSurfaces();
|
||||
if (native_window == nullptr && !windowlessOnScreen) {
|
||||
#else
|
||||
if (native_window == nullptr) {
|
||||
#endif
|
||||
@@ -726,11 +1008,21 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EGLSurface surface = serverOwnedWindow
|
||||
? state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true)
|
||||
: state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
if (MG_Config::ServerOwnedWindowSurfaces()) {
|
||||
// The server's own window: it makes one and draws into it.
|
||||
surface = state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true);
|
||||
} else if (MG_Config::HostFrameWindowSurfaces()) {
|
||||
// The display host's frames: it allocates them, the server draws into them,
|
||||
// and the host puts each one on the glass when the server says it is drawn.
|
||||
surface = state->CreateHostFrameWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true);
|
||||
} else {
|
||||
surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
}
|
||||
#else
|
||||
EGLSurface surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
#endif
|
||||
@@ -799,6 +1091,72 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MobileGL::EnsureInitialized();
|
||||
|
||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||
|
||||
// THE KHR SPELLINGS ARE ENTRY POINTS OF THEIR OWN, and by name is the only way anything
|
||||
// asks for them. Measured in the Fedora container that runs this library as the glvnd EGL
|
||||
// vendor, before they were answered here: eglGetProcAddress("eglCreateImageKHR") -> NULL and
|
||||
// eglGetProcAddress("eglDestroyImageKHR") -> NULL. kwin_wayland's anland backend imports
|
||||
// the display daemon's dma-bufs through exactly these two (AnlandEglLayer::importBuffers ->
|
||||
// EglBackend::importDmaBufAsTexture -> EglDisplay::importBufferAsImage ->
|
||||
// EglDisplay::createImage, which calls the pointer without a null check), so a NULL here is
|
||||
// a compositor that cannot draw a single pixel into the frames the daemon hands it. The
|
||||
// core spellings were already in the table below; these are the same functions under the
|
||||
// names EGL_KHR_image_base gives them, which is also why they answer in the glvnd vendor
|
||||
// table beside them (Exporting/GlvndVendor.cpp).
|
||||
if (std::strcmp(name, "eglCreateImageKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreateImageKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglDestroyImageKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglDestroyImageKHR);
|
||||
}
|
||||
// The device-query family and the EXT platform-surface spellings. Measured in the container
|
||||
// before they were answered here: eglGetProcAddress returned NULL for eglQueryDisplayAttribEXT,
|
||||
// eglQueryDevicesEXT, eglQueryDeviceAttribEXT, eglQueryDeviceStringEXT and
|
||||
// eglCreatePlatformWindowSurfaceEXT while the extension strings announced EGL_EXT_device_query -
|
||||
// a NULL libepoxy then called (kwin crashed at pc=0), and withdrawing the announcement instead
|
||||
// left kwin without a render device and its compositing inactive.
|
||||
if (std::strcmp(name, "eglQueryDevicesEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDevicesEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglQueryDeviceAttribEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDeviceAttribEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglQueryDeviceStringEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDeviceStringEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglQueryDisplayAttribEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDisplayAttribEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglCreatePlatformWindowSurfaceEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreatePlatformWindowSurfaceEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglCreatePlatformPixmapSurfaceEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreatePlatformPixmapSurfaceEXT);
|
||||
}
|
||||
// The swap-and-sync spellings kwin resolves by name while it is still bringing its EGL backend
|
||||
// up (see the definitions: EGL_KHR_fence_sync, the damage variants, EGL_EXT_buffer_age).
|
||||
if (std::strcmp(name, "eglSwapBuffersWithDamageKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglSwapBuffersWithDamageKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglSwapBuffersWithDamageEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglSwapBuffersWithDamageEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglCreateSyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreateSyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglDestroySyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglDestroySyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglClientWaitSyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglClientWaitSyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglWaitSyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglWaitSyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglGetSyncAttribKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglGetSyncAttribKHR);
|
||||
}
|
||||
|
||||
void* proc = MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_D("Failed to get function: %s", name);
|
||||
|
||||
@@ -8,8 +8,18 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
// EGLImageKHR: the return type of the KHR spelling of eglCreateImage below. Includes.h brings in
|
||||
// the core egl.h, which has no KHR types in it.
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// The display host's frame surface, as taken by the surfaceless-bind hook in MakeCurrent, and the
|
||||
// display it belongs to. glFlush reads these: a compositor's frame boundary is glFlush, and the
|
||||
// host frame's "frame is done" signal IS a swap of this surface. Null until a client marked
|
||||
// MOBILEGL_HOST_FRAME_CLIENT=1 actually took the frame.
|
||||
extern EGLDisplay g_hostFramePresentDisplay;
|
||||
extern EGLSurface g_hostFramePresentSurface;
|
||||
|
||||
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
|
||||
const EGLint* attrib_list);
|
||||
EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
@@ -53,6 +63,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
EGLBoolean GetSyncAttrib(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib* value);
|
||||
EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLAttrib* attrib_list);
|
||||
// The same image under EGL_KHR_image_base's name and its EGLint attribute list: the two lists
|
||||
// are pairs of different widths, so the caller's array is re-spelled rather than cast.
|
||||
EGLImageKHR CreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list);
|
||||
EGLBoolean DestroyImage(EGLDisplay dpy, EGLImage image);
|
||||
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list);
|
||||
EGLSurface CreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.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
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// THE PLATFORMS THIS IMPLEMENTATION SERVES, in the words EGL spells them.
|
||||
//
|
||||
// Two callers need exactly the same list, and they disagree if it is written twice:
|
||||
// eglQueryString(EGL_EXTENSIONS) is what an application reads BEFORE it decides to call
|
||||
// eglGetPlatformDisplay at all - libepoxy refuses to use eglGetPlatformDisplayEXT unless the
|
||||
// string names EGL_EXT_platform_base - and the glvnd vendor string is what the system's
|
||||
// libEGL reads to decide which vendor library a platform request should be routed to. A
|
||||
// vendor that does not claim the platform is never asked for it, and the caller is told there
|
||||
// is no provider: the library can be loaded, initialized, and still never called.
|
||||
//
|
||||
// GBM is on the list because a compositor asks for its display on the GBM device it owns.
|
||||
// This side treats that device the way it treats every other native display - the frames come
|
||||
// from the display host, not from the device - so the claim is exact rather than optimistic.
|
||||
inline constexpr const char* kDeviceExtensionString =
|
||||
"EGL_EXT_device_base "
|
||||
"EGL_EXT_device_enumeration "
|
||||
"EGL_EXT_device_query "
|
||||
"EGL_EXT_device_drm "
|
||||
"EGL_EXT_device_drm_render_node";
|
||||
|
||||
// EGL_EXT_client_extensions IS ON THE PLATFORM LIST FOR A MEASURED REASON. libglvnd only folds
|
||||
// a vendor client-extension string (the answer to eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS))
|
||||
// into its own no-display answer when the vendor PLATFORM string - the one __egl_Main publishes
|
||||
// for the platform-extensions query - names EGL_EXT_client_extensions. Without it the client
|
||||
// string never reached libepoxy, so libepoxy concluded EGL_EXT_device_query was absent and left
|
||||
// epoxy_eglQueryDisplayAttribEXT as a NULL pointer, while kwin (which reads the display string,
|
||||
// where the extension IS present) called it: measured as signal 11, fault_addr=(nil), pc=0x0,
|
||||
// from KWin::EglDisplay::determineRenderNode() through the GOT slot whose relocation is
|
||||
// R_AARCH64_GLOB_DAT epoxy_eglQueryDisplayAttribEXT.
|
||||
// Wayland where a Wayland client can exist (WaylandWindow.h: window surfaces presented through
|
||||
// wl_shm). Without it glvnd never hands this vendor a Wayland display at all.
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS " EGL_KHR_platform_wayland EGL_EXT_platform_wayland"
|
||||
#else
|
||||
#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS ""
|
||||
#endif
|
||||
inline constexpr const char* kPlatformExtensionString =
|
||||
"EGL_EXT_client_extensions "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_KHR_platform_gbm "
|
||||
"EGL_EXT_platform_gbm "
|
||||
"EGL_MESA_platform_gbm "
|
||||
"EGL_EXT_platform_device "
|
||||
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
|
||||
|
||||
// What a display answers for EGL_EXTENSIONS: the extensions this implementation provides that
|
||||
// are not platform ones, then the platform list above.
|
||||
// EGL_KHR_no_config_context and EGL_KHR_surfaceless_context are on the display and client lists
|
||||
// because both are answered by this implementation rather than announced by a driver: a context
|
||||
// is created with EGL_NO_CONFIG_KHR and made current with EGL_NO_SURFACE, which is the pair a
|
||||
// compositor asks for when it renders into framebuffers of its own. Measured on kwin_wayland
|
||||
// against this vendor before they were here: createContext() refused with
|
||||
// "EGL_KHR_no_config_context extension is unsupported" and the session ended at
|
||||
// "no usable DRM render device; cannot bring up OpenGL compositing".
|
||||
//
|
||||
// EGL_KHR_image_base AND EGL_EXT_image_dma_buf_import ARE ONE FEATURE HERE, and they are on the
|
||||
// list for a measured reason rather than a plausible one. In the Fedora container that runs
|
||||
// this library as the glvnd EGL vendor, eglGetProcAddress("eglCreateImageKHR") and
|
||||
// eglGetProcAddress("eglDestroyImageKHR") both returned NULL, and kwin_wayland 6.7.3's anland
|
||||
// backend imports the display daemon's dma-bufs through exactly those two
|
||||
// (AnlandEglLayer::importBuffers -> EglBackend::importDmaBufAsTexture ->
|
||||
// EglDisplay::importBufferAsImage -> EglDisplay::createImage) before it can draw a pixel into a
|
||||
// frame the daemon produced - so with a NULL the whole composited scene has no image to be
|
||||
// drawn into. Both names are backed by entry points that exist: EGL_KHR_image_base by
|
||||
// eglCreateImage/eglDestroyImage and their KHR spellings (Exporting/Definitions.cpp, and the
|
||||
// same names in the glvnd vendor table), EGL_EXT_image_dma_buf_import by EGL_LINUX_DMA_BUF_EXT
|
||||
// images, which carry the caller's EGL_WIDTH, EGL_HEIGHT and EGL_LINUX_DRM_FOURCC_EXT and own a
|
||||
// dup() of every plane's fd, offset and pitch (EGLState/Core.cpp, CreateImage).
|
||||
//
|
||||
// EGL_EXT_image_dma_buf_import_modifiers IS DELIBERATELY NOT NAMED. Its own two entry points -
|
||||
// eglQueryDmaBufFormatsEXT and eglQueryDmaBufModifiersEXT - do not exist in this library, and a
|
||||
// name in this string means the entry points beside it are there. The modifier attributes
|
||||
// (EGL_DMA_BUF_PLANEn_MODIFIER_LO/HI_EXT) are nevertheless ACCEPTED and carried on the image:
|
||||
// a caller does not need the name to send them, and kwin's does not look at any extension
|
||||
// string before it does - EglDisplay::importDmaBufAsImage appends the pair whenever the
|
||||
// buffer's modifier is not DRM_FORMAT_MOD_INVALID.
|
||||
inline constexpr const char* kDisplayExtensionString =
|
||||
"EGL_KHR_create_context "
|
||||
// WHAT THE COMPOSITOR CHECKS BETWEEN ITS CONTEXT AND ITS FIRST SURFACE. EglBackend::init
|
||||
// asks for these three by name right after initRenderingContext() and before createSurfaces();
|
||||
// the entry points behind them are published beside this string (Exporting/Definitions.cpp),
|
||||
// so the announcement and the answers are one thing rather than two.
|
||||
"EGL_KHR_fence_sync "
|
||||
"EGL_KHR_wait_sync "
|
||||
"EGL_KHR_swap_buffers_with_damage "
|
||||
"EGL_EXT_swap_buffers_with_damage "
|
||||
"EGL_EXT_buffer_age "
|
||||
"EGL_KHR_image_base "
|
||||
"EGL_EXT_image_dma_buf_import "
|
||||
"EGL_KHR_no_config_context "
|
||||
"EGL_KHR_surfaceless_context "
|
||||
// ANNOUNCED BECAUSE IT IS NOW ANSWERED: the four names below are published as real entry
|
||||
// points and eglGetProcAddress answers them (see Exporting/Definitions.cpp and
|
||||
// EGLImpl::GetProcAddress). Both earlier states were measured failures: announcing the
|
||||
// extension while the names resolved to NULL made kwin jump to that NULL (signal 11,
|
||||
// pc=0x0, from KWin::EglDisplay::determineRenderNode()), and withdrawing the announcement
|
||||
// left kwin without a render device so its compositing never became active and nothing was
|
||||
// ever painted.
|
||||
"EGL_EXT_device_base "
|
||||
"EGL_EXT_device_enumeration "
|
||||
"EGL_EXT_device_query "
|
||||
"EGL_EXT_device_drm "
|
||||
"EGL_EXT_device_drm_render_node "
|
||||
// KWin::EglDisplay::determineRenderNode() at libkwin.so.6+0x308b9c. A compositor that is
|
||||
// never told the extension exists cannot take that path; announcing it again is the right
|
||||
// move once these four names are really published and resolvable.
|
||||
"EGL_EXT_platform_device "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_KHR_platform_gbm "
|
||||
"EGL_EXT_platform_gbm "
|
||||
"EGL_MESA_platform_gbm "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
|
||||
// ... and what EGL_NO_DISPLAY answers: the same, plus the client-extension marker that tells a
|
||||
// caller the no-display query is meaningful here.
|
||||
inline constexpr const char* kClientExtensionString =
|
||||
"EGL_EXT_client_extensions "
|
||||
"EGL_KHR_create_context "
|
||||
"EGL_KHR_image_base "
|
||||
"EGL_EXT_image_dma_buf_import "
|
||||
"EGL_KHR_no_config_context "
|
||||
"EGL_KHR_surfaceless_context "
|
||||
// ANNOUNCED BECAUSE IT IS NOW ANSWERED: the four names below are published as real entry
|
||||
// points and eglGetProcAddress answers them (see Exporting/Definitions.cpp and
|
||||
// EGLImpl::GetProcAddress). Both earlier states were measured failures: announcing the
|
||||
// extension while the names resolved to NULL made kwin jump to that NULL (signal 11,
|
||||
// pc=0x0, from KWin::EglDisplay::determineRenderNode()), and withdrawing the announcement
|
||||
// left kwin without a render device so its compositing never became active and nothing was
|
||||
// ever painted.
|
||||
"EGL_EXT_device_base "
|
||||
"EGL_EXT_device_enumeration "
|
||||
"EGL_EXT_device_query "
|
||||
"EGL_EXT_device_drm "
|
||||
"EGL_EXT_device_drm_render_node "
|
||||
// KWin::EglDisplay::determineRenderNode() at libkwin.so.6+0x308b9c. A compositor that is
|
||||
// never told the extension exists cannot take that path; announcing it again is the right
|
||||
// move once these four names are really published and resolvable.
|
||||
"EGL_EXT_platform_device "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_KHR_platform_gbm "
|
||||
"EGL_EXT_platform_gbm "
|
||||
"EGL_MESA_platform_gbm "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include <Includes.h>
|
||||
// EGLImageKHR, for the KHR spellings of the image entry points defined at the bottom of this file:
|
||||
// Includes.h brings in the core egl.h only.
|
||||
#include <EGL/eglext.h>
|
||||
#include "../EGLImpl.h"
|
||||
|
||||
MOBILEGL_EGL_API EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
|
||||
@@ -228,6 +231,25 @@ MOBILEGL_EGL_API EGLBoolean eglDestroyImage(EGLDisplay dpy, EGLImage image) {
|
||||
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
|
||||
}
|
||||
|
||||
// EGL_KHR_image_base's spellings of the same two entry points, exported under their own names
|
||||
// because that is the only thing that answers for them: measured in the Fedora container that runs
|
||||
// this library as the glvnd EGL vendor, eglGetProcAddress("eglCreateImageKHR") and
|
||||
// eglGetProcAddress("eglDestroyImageKHR") both returned NULL, and kwin_wayland's anland backend
|
||||
// imports the display daemon's dma-bufs through exactly these two.
|
||||
MOBILEGL_EGL_API EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list) {
|
||||
MGLOG_D("eglCreateImageKHR(dpy=%p, ctx=%p, target=%u, buffer=%p, attrib_list=%p)", dpy, ctx, target, buffer,
|
||||
attrib_list);
|
||||
// Not an alias of eglCreateImage: this list is EGLint pairs and the core one EGLAttrib pairs.
|
||||
return MobileGL::MG_Impl::EGLImpl::CreateImageKHR(dpy, ctx, target, buffer, attrib_list);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image) {
|
||||
MGLOG_D("eglDestroyImageKHR(dpy=%p, image=%p)", dpy, image);
|
||||
// One handle under two spellings: EGLImageKHR and EGLImage are the same pointer.
|
||||
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLDisplay eglGetPlatformDisplay(EGLenum platform, void* native_display,
|
||||
const EGLAttrib* attrib_list) {
|
||||
MGLOG_D("eglGetPlatformDisplay(platform=%u, native_display=%p, attrib_list=%p)", platform, native_display,
|
||||
@@ -250,6 +272,138 @@ MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurface(EGLDisplay dpy, EGLCo
|
||||
return MobileGL::MG_Impl::EGLImpl::CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
}
|
||||
|
||||
// THE SWAP-AND-SYNC FAMILY, which kwin asks for in EglBackend::init right after the rendering
|
||||
// context is up and BEFORE it creates any surface: EGL_KHR_fence_sync, EGL_KHR_swap_buffers_with_damage
|
||||
// and EGL_EXT_buffer_age are checked there, and a compositing mode whose backend init returns false
|
||||
// is exactly the "Could not fulfill the requested compositing mode in KWIN_COMPOSE: 1" this run
|
||||
// reports. Each of these names is a spelling of something this library already implements (the core
|
||||
// eglSwapBuffers and the state layer's sync objects), so they are published as real exports rather
|
||||
// than promised and not delivered.
|
||||
MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects) {
|
||||
// The damage rectangles are a hint about what changed; this side swaps the whole surface either
|
||||
// way, so the honest answer is the core swap.
|
||||
(void)rects;
|
||||
(void)n_rects;
|
||||
return MobileGL::MG_Impl::EGLImpl::SwapBuffers(dpy, surface) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects) {
|
||||
return eglSwapBuffersWithDamageKHR(dpy, surface, rects, n_rects);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list) {
|
||||
// EGL_KHR_fence_sync's list is EGLint pairs; the state layer takes EGLAttrib pairs.
|
||||
EGLAttrib attributes[16];
|
||||
int count = 0;
|
||||
if (attrib_list != nullptr) {
|
||||
for (int index = 0; attrib_list[index] != EGL_NONE && count + 1 < 16; index += 2) {
|
||||
attributes[count++] = static_cast<EGLAttrib>(attrib_list[index]);
|
||||
attributes[count++] = static_cast<EGLAttrib>(attrib_list[index + 1]);
|
||||
}
|
||||
}
|
||||
attributes[count] = EGL_NONE;
|
||||
return reinterpret_cast<EGLSyncKHR>(MobileGL::MG_Impl::EGLImpl::CreateSync(dpy, type, attributes));
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync) {
|
||||
return MobileGL::MG_Impl::EGLImpl::DestroySync(dpy, reinterpret_cast<EGLSync>(sync)) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout) {
|
||||
return MobileGL::MG_Impl::EGLImpl::ClientWaitSync(dpy, reinterpret_cast<EGLSync>(sync), flags,
|
||||
static_cast<EGLTime>(timeout));
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags) {
|
||||
return MobileGL::MG_Impl::EGLImpl::WaitSync(dpy, reinterpret_cast<EGLSync>(sync), flags) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value) {
|
||||
EGLAttrib answered = EGL_NONE;
|
||||
if (!MobileGL::MG_Impl::EGLImpl::GetSyncAttrib(dpy, reinterpret_cast<EGLSync>(sync), attribute, &answered)) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (value != nullptr) {
|
||||
*value = static_cast<EGLint>(answered);
|
||||
}
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
// THE DEVICE-QUERY FAMILY, PUBLISHED AS REAL ENTRY POINTS. It is not academic: with the extension
|
||||
// announced but these names unresolvable, libepoxy leaves epoxy_eglQueryDisplayAttribEXT NULL and
|
||||
// kwin, trusting the announcement, jumps to it (measured: signal 11, fault_addr=(nil), pc=0x0 from
|
||||
// KWin::EglDisplay::determineRenderNode()); with the announcement withdrawn instead, kwin stops
|
||||
// asking and its compositing never becomes active (measured: org.kde.kwin.Compositing active =
|
||||
// false), so nothing is ever painted and the display daemon frames are never imported. Publishing
|
||||
// them is the only answer that leaves kwin both safe and able to find its render device.
|
||||
namespace {
|
||||
// One device, one identity - the same shape the vendor side uses (Exporting/GlvndVendor.cpp): a
|
||||
// pointer to a private object, so a device from somewhere else cannot compare equal by accident.
|
||||
struct DeviceToken { int unused; };
|
||||
DeviceToken g_deviceToken;
|
||||
} // namespace
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) {
|
||||
if (num_devices == nullptr) return EGL_FALSE;
|
||||
*num_devices = 1;
|
||||
if (devices != nullptr && max_devices >= 1) devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) {
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken) || value == nullptr) return EGL_FALSE;
|
||||
switch (attribute) {
|
||||
case EGL_DRM_DEVICE_FILE_EXT:
|
||||
case EGL_DRM_RENDER_NODE_FILE_EXT:
|
||||
*value = reinterpret_cast<EGLAttrib>("/dev/dri/renderD128");
|
||||
return EGL_TRUE;
|
||||
default:
|
||||
return EGL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API const char* eglQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) {
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken)) return nullptr;
|
||||
// THE NODE IS NAMED, AND THAT CHANGED BECAUSE THE MEASUREMENT CHANGED. An empty answer was the
|
||||
// right shape while this query was unreachable: nothing asked, so nothing depended on it. Now
|
||||
// that the name resolves, it is exactly what kwin asks on the way into its EGL backend, and the
|
||||
// fork says out loud what an empty answer costs it - "Error during init of AnlandEglBackend",
|
||||
// after which it falls back to the plain AnlandBackend, whose compositing never becomes active
|
||||
// (supportInformation: "LogicalOutput backend: KWin::AnlandBackend" + "Compositing is not
|
||||
// active"), so nothing is ever painted and the display daemon frames are never imported. The
|
||||
// path answered is the one the environment configures the compositor with (ANLAND_DRM_DEVICE),
|
||||
// so the node named here and the node in use are the same node.
|
||||
if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) {
|
||||
return "/dev/dri/renderD128";
|
||||
}
|
||||
if (name == EGL_EXTENSIONS) return "EGL_EXT_device_drm_render_node";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) {
|
||||
(void)dpy;
|
||||
if (value == nullptr) return EGL_FALSE;
|
||||
if (attribute == EGL_DEVICE_EXT) {
|
||||
*value = reinterpret_cast<EGLAttrib>(&g_deviceToken);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
// The EXT spellings of the platform surface entry points: EGL_EXT_platform_base defines them by those
|
||||
// names, and a dispatcher-based client resolves them by those names. Identical ABI (EGLAttrib list).
|
||||
MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_window,
|
||||
const EGLAttrib* attrib_list) {
|
||||
return eglCreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
|
||||
const EGLAttrib* attrib_list) {
|
||||
return eglCreatePlatformPixmapSurface(dpy, config, native_pixmap, attrib_list);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurface(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
|
||||
const EGLAttrib* attrib_list) {
|
||||
MGLOG_D("eglCreatePlatformPixmapSurface(dpy=%p, config=%p, native_pixmap=%p, attrib_list=%p)", dpy, config,
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.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
|
||||
|
||||
// MOBILEGL AS A GLVND EGL VENDOR.
|
||||
//
|
||||
// On a system with libglvnd, the libEGL.so.1 an application links is not an implementation at all:
|
||||
// it is a dispatcher, and the implementations are vendor libraries it loads from
|
||||
// /usr/share/glvnd/egl_vendor.d (or from whatever __EGL_VENDOR_LIBRARY_FILENAMES names) and routes
|
||||
// every EGL call to. That is the supported way to be the implementation an application actually
|
||||
// talks to, and on this machine it is the ONLY way that reaches a caller which does not link EGL
|
||||
// directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its entry points at
|
||||
// startup, so a library injected with LD_PRELOAD is invisible to it however loudly it is named.
|
||||
//
|
||||
// What glvnd asks a vendor for is the other half of that ABI, and this file is it: glvnd calls
|
||||
// __egl_Main once, the library answers with the functions it wants dispatched to it, and - the part
|
||||
// that decides whether a compositor can use it at all - with the string naming the PLATFORMS it
|
||||
// serves. A vendor that does not claim EGL_PLATFORM_GBM_KHR is never asked for a GBM display, and
|
||||
// the caller that asked for one is told there is no usable device: the client sitting in its maps,
|
||||
// loaded and never called, is exactly what that looks like from the outside.
|
||||
//
|
||||
// The entry points below are this library's own (MG_Impl::EGLImpl, the same functions
|
||||
// Exporting/Definitions.cpp exports), so a vendor call and a direct call cannot drift apart.
|
||||
|
||||
#include <Includes.h>
|
||||
#include <glvnd/libeglabi.h>
|
||||
// EGLImageKHR, for the KHR spellings the entry-point table below answers: Includes.h brings in the
|
||||
// core egl.h only.
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#include "../EGLImpl.h"
|
||||
#include "../EGLPlatformExtensions.h"
|
||||
|
||||
// The KHR spellings of the image entry points, declared here because the bundled EGL headers
|
||||
// declare them only under EGL_EGLEXT_PROTOTYPES and because glvnd asks for entry points BY NAME -
|
||||
// the table below is where it finds this one.
|
||||
extern "C" EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image);
|
||||
|
||||
// THE SWAP-AND-SYNC EXPORTS, declared here for the same reason the image pair is: they are defined in
|
||||
// Exporting/Definitions.cpp and this file only names them in the vendor table below.
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
|
||||
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
|
||||
extern "C" EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
|
||||
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
|
||||
|
||||
namespace {
|
||||
using namespace MobileGL;
|
||||
|
||||
const __EGLapiExports* g_eglExports = nullptr;
|
||||
|
||||
// The platforms this vendor serves. GBM is the one that matters here: the compositor asks for
|
||||
// a display on the GBM device it opened, and a vendor that does not name the platform is not
|
||||
// asked at all. This side treats a GBM device as it treats every other native display - the
|
||||
// frames come from the display host, not from the device - so claiming the platform is honest
|
||||
// rather than optimistic.
|
||||
constexpr const char* kPlatformExtensions = MG_Impl::EGLImpl::kPlatformExtensionString;
|
||||
|
||||
// glvnd asks for the client-only extension list separately from the platform one; the split is
|
||||
// its own invention, and this side answers both from the same query.
|
||||
const char* EGLAPIENTRY VendorQueryString(EGLDisplay dpy, EGLint name) {
|
||||
return MG_Impl::EGLImpl::QueryString(dpy, name);
|
||||
}
|
||||
|
||||
EGLDisplay EGLAPIENTRY VendorGetPlatformDisplay(EGLenum platform, void* native_display,
|
||||
const EGLAttrib* attrib_list) {
|
||||
// EGL_NONE means the application called eglGetDisplay(EGL_DEFAULT_DISPLAY): the vendor is
|
||||
// being asked to name its own default display.
|
||||
if (platform == EGL_NONE) {
|
||||
return MG_Impl::EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
}
|
||||
return MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list);
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorGetSupportsAPI(EGLenum api) {
|
||||
// What this library answers for EGL_CLIENT_APIS, said as a question.
|
||||
return (api == EGL_OPENGL_ES_API || api == EGL_OPENGL_API) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
const char* EGLAPIENTRY VendorGetVendorString(int name) {
|
||||
if (name == __EGL_VENDOR_STRING_PLATFORM_EXTENSIONS) {
|
||||
return kPlatformExtensions;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// GLVND ASKS FOR EVERY EGL ENTRY POINT BY NAME, AND DROPS THE VENDOR IF ONE IS MISSING: the
|
||||
// first eight functions the table below names are looked up before this library is ever asked
|
||||
// for a display, and a single null answer means glvnd quietly stops considering it - the
|
||||
// library is loaded, __egl_Main has run, and nothing is ever dispatched to it.
|
||||
struct EntryPoint {
|
||||
const char* name;
|
||||
void* address;
|
||||
};
|
||||
|
||||
// ---- the device family -------------------------------------------------------------------
|
||||
//
|
||||
// A compositor does not ask for a display and hope. It asks which devices exist, reads the DRM
|
||||
// node off the one it wants, and only then asks for a display ON that device - so a vendor that
|
||||
// answers "no devices" is one it cannot use, however well the vendor draws. The message it
|
||||
// gives is "no usable DRM render device", which names where it stopped rather than why.
|
||||
//
|
||||
// This implementation therefore names one device: the render node of the machine it runs on.
|
||||
// That is an honest answer even though the frames do not come from that node - they come from
|
||||
// the display host on the other end of the socket, which is why every surface here is a host
|
||||
// frame - because the question is which device this display is associated with, and the answer
|
||||
// is what lets the compositor match its own node to the display it is about to draw into.
|
||||
const char* DrmRenderNode() {
|
||||
// MOBILEGL_DEVICE_DRM_NODE names another node: a machine with more than one GPU, or a test
|
||||
// that wants the mismatch refused rather than papered over.
|
||||
const char* configured = std::getenv("MOBILEGL_DEVICE_DRM_NODE");
|
||||
return (configured != nullptr && configured[0] != '\0') ? configured : "/dev/dri/renderD128";
|
||||
}
|
||||
|
||||
// One device, one identity. A pointer to a private object rather than a number, so that a
|
||||
// device from somewhere else cannot compare equal to this one by accident.
|
||||
struct DeviceToken { int unused; };
|
||||
DeviceToken g_deviceToken;
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) {
|
||||
if (num_devices == nullptr) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
*num_devices = 1;
|
||||
if (devices != nullptr && max_devices >= 1) {
|
||||
devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
|
||||
}
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) {
|
||||
MGLOG_I("vendor eglQueryDeviceAttribEXT(device=%p, attribute=0x%04x)", static_cast<void*>(device),
|
||||
static_cast<unsigned>(attribute));
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken) || value == nullptr) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
switch (attribute) {
|
||||
case EGL_DRM_DEVICE_FILE_EXT:
|
||||
case EGL_DRM_RENDER_NODE_FILE_EXT:
|
||||
// THE ATTRIBUTE FORM OF THE SAME QUERY, and the last place the empty answer lived. kwin
|
||||
// asks for the node as an ATTRIBUTE as well as a string; with this one empty its log said
|
||||
// "couldn't find dev node for drm device" even after the string form was answered, and
|
||||
// the compositing mode it had just successfully started through this library was then
|
||||
// declared unfulfillable. The path is the environment's own (ANLAND_DRM_DEVICE).
|
||||
*value = reinterpret_cast<EGLAttrib>("/dev/dri/renderD128");
|
||||
return EGL_TRUE;
|
||||
default:
|
||||
return EGL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
const char* EGLAPIENTRY VendorQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) {
|
||||
// WHICH DEVICE QUERY THE COMPOSITOR MAKES, and by which name: the string form and the attribute
|
||||
// form are different entry points with different answers (glvnd answers the string form itself
|
||||
// and only forwards the attribute one), so the caller decides which of the two it gets.
|
||||
MGLOG_I("vendor eglQueryDeviceStringEXT(device=%p, name=0x%04x)", static_cast<void*>(device),
|
||||
static_cast<unsigned>(name));
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken)) {
|
||||
return nullptr;
|
||||
}
|
||||
// THE NODE IS NAMED HERE TOO, AND THAT IS WHAT THE VENDOR-SIDE ANSWER WAS MISSING. An empty
|
||||
// answer was measured to be harmless only while this query was unreachable; through glvnd it
|
||||
// IS the query a compositor makes before it opens anything, and an empty string is what its
|
||||
// next line reports - "Failed to open drm node : No such file or directory" (the name is the
|
||||
// blank between "node" and ":"), followed by "couldn't find dev node for drm device" and a
|
||||
// dead OpenGL compositing mode. The path is the one the environment configures the
|
||||
// compositor with (ANLAND_DRM_DEVICE), so the node named here and the node in use are one.
|
||||
if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) {
|
||||
return "/dev/dri/renderD128";
|
||||
}
|
||||
if (name == EGL_EXTENSIONS) {
|
||||
return "EGL_EXT_device_drm_render_node";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) {
|
||||
(void)dpy;
|
||||
if (value == nullptr) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (attribute == EGL_DEVICE_EXT) {
|
||||
*value = reinterpret_cast<EGLAttrib>(&g_deviceToken);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
// Three of the names glvnd insists on are debug extensions this implementation does not
|
||||
// provide, and glvnd insists on them all the same - a vendor that answers null for any of them
|
||||
// is dropped. They are answered honestly rather than pretended: KHR_debug is not in the
|
||||
// extension strings above, so a caller that reads them will not call these at all.
|
||||
EGLint EGLAPIENTRY VendorDebugMessageControlKHR(EGLDEBUGPROCKHR callback, const EGLAttrib* attrib_list) {
|
||||
(void)callback;
|
||||
(void)attrib_list;
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDebugKHR(EGLint attribute, EGLAttrib* value) {
|
||||
(void)attribute;
|
||||
(void)value;
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
EGLint EGLAPIENTRY VendorLabelObjectKHR(EGLDisplay dpy, EGLenum objectType, EGLObjectKHR object, EGLLabelKHR label) {
|
||||
(void)dpy;
|
||||
(void)objectType;
|
||||
(void)object;
|
||||
(void)label;
|
||||
return EGL_BAD_PARAMETER;
|
||||
}
|
||||
|
||||
const EntryPoint kEglEntryPoints[] = {
|
||||
// The core of EGL 1.5, every one of which glvnd looks up by name before it considers this
|
||||
// library usable.
|
||||
{"eglInitialize", reinterpret_cast<void*>(eglInitialize)},
|
||||
{"eglChooseConfig", reinterpret_cast<void*>(eglChooseConfig)},
|
||||
{"eglCopyBuffers", reinterpret_cast<void*>(eglCopyBuffers)},
|
||||
{"eglCreateContext", reinterpret_cast<void*>(eglCreateContext)},
|
||||
{"eglCreatePbufferSurface", reinterpret_cast<void*>(eglCreatePbufferSurface)},
|
||||
{"eglCreatePixmapSurface", reinterpret_cast<void*>(eglCreatePixmapSurface)},
|
||||
{"eglCreateWindowSurface", reinterpret_cast<void*>(eglCreateWindowSurface)},
|
||||
{"eglDestroyContext", reinterpret_cast<void*>(eglDestroyContext)},
|
||||
{"eglDestroySurface", reinterpret_cast<void*>(eglDestroySurface)},
|
||||
{"eglGetConfigAttrib", reinterpret_cast<void*>(eglGetConfigAttrib)},
|
||||
{"eglGetConfigs", reinterpret_cast<void*>(eglGetConfigs)},
|
||||
{"eglMakeCurrent", reinterpret_cast<void*>(eglMakeCurrent)},
|
||||
{"eglQueryContext", reinterpret_cast<void*>(eglQueryContext)},
|
||||
{"eglQuerySurface", reinterpret_cast<void*>(eglQuerySurface)},
|
||||
{"eglSwapBuffers", reinterpret_cast<void*>(eglSwapBuffers)},
|
||||
{"eglTerminate", reinterpret_cast<void*>(eglTerminate)},
|
||||
{"eglWaitGL", reinterpret_cast<void*>(eglWaitGL)},
|
||||
{"eglWaitNative", reinterpret_cast<void*>(eglWaitNative)},
|
||||
{"eglBindTexImage", reinterpret_cast<void*>(eglBindTexImage)},
|
||||
{"eglReleaseTexImage", reinterpret_cast<void*>(eglReleaseTexImage)},
|
||||
{"eglSurfaceAttrib", reinterpret_cast<void*>(eglSurfaceAttrib)},
|
||||
{"eglSwapInterval", reinterpret_cast<void*>(eglSwapInterval)},
|
||||
{"eglBindAPI", reinterpret_cast<void*>(eglBindAPI)},
|
||||
{"eglCreatePbufferFromClientBuffer", reinterpret_cast<void*>(eglCreatePbufferFromClientBuffer)},
|
||||
{"eglReleaseThread", reinterpret_cast<void*>(eglReleaseThread)},
|
||||
{"eglWaitClient", reinterpret_cast<void*>(eglWaitClient)},
|
||||
{"eglGetError", reinterpret_cast<void*>(eglGetError)},
|
||||
{"eglCreateSync", reinterpret_cast<void*>(eglCreateSync)},
|
||||
{"eglDestroySync", reinterpret_cast<void*>(eglDestroySync)},
|
||||
{"eglClientWaitSync", reinterpret_cast<void*>(eglClientWaitSync)},
|
||||
{"eglGetSyncAttrib", reinterpret_cast<void*>(eglGetSyncAttrib)},
|
||||
{"eglWaitSync", reinterpret_cast<void*>(eglWaitSync)},
|
||||
{"eglCreateImage", reinterpret_cast<void*>(eglCreateImage)},
|
||||
{"eglDestroyImage", reinterpret_cast<void*>(eglDestroyImage)},
|
||||
// THE KHR SPELLINGS OF THOSE TWO, and they are entries rather than aliases because glvnd
|
||||
// resolves a name it does not know through this table alone. Measured in the Fedora
|
||||
// container that runs this library as the EGL vendor: with only the core pair here,
|
||||
// eglGetProcAddress("eglCreateImageKHR") and eglGetProcAddress("eglDestroyImageKHR") both
|
||||
// answered NULL, and kwin_wayland's anland backend reaches the display daemon's dma-bufs
|
||||
// through them (AnlandEglLayer::importBuffers -> EglBackend::importDmaBufAsTexture ->
|
||||
// EglDisplay::importBufferAsImage) - so the compositor had no entry point to import a frame
|
||||
// with. eglCreateImageKHR is not the same ABI as eglCreateImage: EGL_KHR_image_base's
|
||||
// attribute list is EGLint pairs, which is why Exporting/Definitions.cpp exports it as its
|
||||
// own function rather than letting the core spelling stand in for it.
|
||||
{"eglCreateImageKHR", reinterpret_cast<void*>(eglCreateImageKHR)},
|
||||
{"eglDestroyImageKHR", reinterpret_cast<void*>(eglDestroyImageKHR)},
|
||||
{"eglCreatePlatformWindowSurface", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
|
||||
{"eglCreatePlatformPixmapSurface", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
|
||||
// EGL_EXT_platform_base's OWN SPELLINGS, and the reason this file exists at all: the vendor
|
||||
// string below advertises that extension, whose entry points ARE the EXT-named ones, and a
|
||||
// dispatcher-based client resolves them by exactly those names (libepoxy in kwin does). With
|
||||
// only the core spellings in this table the lookup answered NULL and the caller - which had
|
||||
// already checked the extension string and so had every reason to trust it - jumped to it:
|
||||
// measured as signal 11 with fault_addr=(nil) and pc=0x0 immediately after "MobileGL
|
||||
// initialized", in both the systemd and the direct run, and it is what kept the compositor
|
||||
// from ever reaching its output. The ABI is identical (an EGLAttrib list, not EGLint pairs),
|
||||
// so the core implementation is what serves them.
|
||||
{"eglCreatePlatformWindowSurfaceEXT", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
|
||||
{"eglCreatePlatformPixmapSurfaceEXT", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
|
||||
// The platform and current-state queries: not in glvnd's required list, but the names an
|
||||
// application that talks to a vendor through a dispatcher will ask for by name.
|
||||
{"eglGetDisplay", reinterpret_cast<void*>(eglGetDisplay)},
|
||||
{"eglGetCurrentDisplay", reinterpret_cast<void*>(eglGetCurrentDisplay)},
|
||||
{"eglGetCurrentSurface", reinterpret_cast<void*>(eglGetCurrentSurface)},
|
||||
{"eglGetCurrentContext", reinterpret_cast<void*>(eglGetCurrentContext)},
|
||||
{"eglQueryAPI", reinterpret_cast<void*>(eglQueryAPI)},
|
||||
{"eglGetProcAddress", reinterpret_cast<void*>(eglGetProcAddress)},
|
||||
// The two queries whose answer depends on which side of glvnd is asking.
|
||||
{"eglQueryString", reinterpret_cast<void*>(VendorQueryString)},
|
||||
{"eglGetPlatformDisplay", reinterpret_cast<void*>(VendorGetPlatformDisplay)},
|
||||
{"eglGetPlatformDisplayEXT", reinterpret_cast<void*>(VendorGetPlatformDisplay)},
|
||||
// What glvnd requires and this implementation does not otherwise have.
|
||||
{"eglQueryDevicesEXT", reinterpret_cast<void*>(VendorQueryDevicesEXT)},
|
||||
{"eglQueryDeviceAttribEXT", reinterpret_cast<void*>(VendorQueryDeviceAttribEXT)},
|
||||
{"eglQueryDeviceStringEXT", reinterpret_cast<void*>(VendorQueryDeviceStringEXT)},
|
||||
{"eglQueryDisplayAttribEXT", reinterpret_cast<void*>(VendorQueryDisplayAttribEXT)},
|
||||
// The swap-and-sync family, published through the same table the names are answered from.
|
||||
// Declared above this table because they are exports of Definitions.cpp, like the image pair.
|
||||
{"eglSwapBuffersWithDamageKHR", reinterpret_cast<void*>(eglSwapBuffersWithDamageKHR)},
|
||||
{"eglSwapBuffersWithDamageEXT", reinterpret_cast<void*>(eglSwapBuffersWithDamageEXT)},
|
||||
{"eglCreateSyncKHR", reinterpret_cast<void*>(eglCreateSyncKHR)},
|
||||
{"eglDestroySyncKHR", reinterpret_cast<void*>(eglDestroySyncKHR)},
|
||||
{"eglClientWaitSyncKHR", reinterpret_cast<void*>(eglClientWaitSyncKHR)},
|
||||
{"eglWaitSyncKHR", reinterpret_cast<void*>(eglWaitSyncKHR)},
|
||||
{"eglGetSyncAttribKHR", reinterpret_cast<void*>(eglGetSyncAttribKHR)},
|
||||
{"eglDebugMessageControlKHR", reinterpret_cast<void*>(VendorDebugMessageControlKHR)},
|
||||
{"eglQueryDebugKHR", reinterpret_cast<void*>(VendorQueryDebugKHR)},
|
||||
{"eglLabelObjectKHR", reinterpret_cast<void*>(VendorLabelObjectKHR)},
|
||||
};
|
||||
|
||||
// THE SAME ANSWER TO BOTH OF GLVND'S QUESTIONS, and that is not a shortcut - glvnd asks them
|
||||
// from two places and a library that answers one of them has a hole the size of the other:
|
||||
//
|
||||
// getProcAddress is asked for every entry point glvnd builds its own static EGL table
|
||||
// from, so a null here drops the vendor outright.
|
||||
// getDispatchAddress is what glvnd falls back to for eglGetProcAddress on any name it does
|
||||
// not know itself - which is every extension entry point that is not in
|
||||
// its list, the device queries among them. A vendor that answers these
|
||||
// from a GL-only table hands the application a null for a function the
|
||||
// vendor implements, and the caller reports the extension as missing.
|
||||
void* LookupEntryPoint(const char* procName) {
|
||||
if (procName == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const EntryPoint& entry : kEglEntryPoints) {
|
||||
if (std::strcmp(entry.name, procName) == 0) {
|
||||
return entry.address;
|
||||
}
|
||||
}
|
||||
// Anything that is not EGL is a GL or GLES entry point, and the tree's own resolver is the
|
||||
// table for those.
|
||||
return reinterpret_cast<void*>(MG_Impl::EGLImpl::GetProcAddress(procName));
|
||||
}
|
||||
|
||||
void* EGLAPIENTRY VendorGetProcAddress(const char* procName) { return LookupEntryPoint(procName); }
|
||||
|
||||
// glvnd hands a vendor the index each EGL entry point occupies in its dispatch table, so that a
|
||||
// vendor whose entry points are thin wrappers around glvnd can call back into the right slot.
|
||||
// This library's entry points ARE the implementation - they never re-enter glvnd - so there is
|
||||
// nothing to record; the callback exists because glvnd refuses a vendor that does not provide
|
||||
// one, and refusing it means the library is loaded and never dispatched to.
|
||||
void EGLAPIENTRY VendorSetDispatchIndex(const char* procName, int index) {
|
||||
(void)procName;
|
||||
(void)index;
|
||||
}
|
||||
|
||||
void* EGLAPIENTRY VendorGetDispatchAddress(const char* procName) {
|
||||
// glvnd asks THIS vendor for GL entry points as well: after eglMakeCurrent, the dispatch
|
||||
// table of the now-current context is filled from here, one name at a time. LookupEntryPoint
|
||||
// knows EGL names only, so every GL name (glGetString, glCreateProgram, ...) answered NULL,
|
||||
// glvnd wrote NULL into its dispatch slots, and the first glGetString of the compositor's
|
||||
// context came back NULL - kwin's "Could not initialize rendering context", while the very
|
||||
// same context answered fine through a direct eglGetProcAddress (measured: probe6 vs probe12).
|
||||
// GL names go through the full GL/EGL lookup, which is the same table eglGetProcAddress uses.
|
||||
if (void* address = LookupEntryPoint(procName)) {
|
||||
return address;
|
||||
}
|
||||
return reinterpret_cast<void*>(MG_Impl::EGLImpl::GetProcAddress(procName));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" MOBILEGL_EGL_API EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports,
|
||||
__EGLvendorInfo* vendor, __EGLapiImports* imports) {
|
||||
(void)vendor;
|
||||
if (EGL_VENDOR_ABI_GET_MAJOR_VERSION(version) != EGL_VENDOR_ABI_MAJOR_VERSION) {
|
||||
// A vendor that does not know the ABI it is being handed must say so, not guess: the
|
||||
// structures on either side are the ABI.
|
||||
MGLOG_E("__egl_Main: unsupported glvnd vendor ABI version 0x%08x (this library implements %u.%u)", version,
|
||||
static_cast<unsigned>(EGL_VENDOR_ABI_MAJOR_VERSION),
|
||||
static_cast<unsigned>(EGL_VENDOR_ABI_MINOR_VERSION));
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (exports == nullptr || imports == nullptr) {
|
||||
MGLOG_E("__egl_Main: glvnd handed no exports/imports table");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
g_eglExports = exports;
|
||||
|
||||
imports->getPlatformDisplay = VendorGetPlatformDisplay;
|
||||
imports->getSupportsAPI = VendorGetSupportsAPI;
|
||||
imports->getVendorString = VendorGetVendorString;
|
||||
imports->getProcAddress = VendorGetProcAddress;
|
||||
imports->getDispatchAddress = VendorGetDispatchAddress;
|
||||
imports->setDispatchIndex = VendorSetDispatchIndex;
|
||||
|
||||
MGLOG_I("glvnd vendor: MobileGL is serving EGL through libglvnd (ABI %u.%u, platforms: %s)",
|
||||
static_cast<unsigned>(EGL_VENDOR_ABI_MAJOR_VERSION), static_cast<unsigned>(EGL_VENDOR_ABI_MINOR_VERSION),
|
||||
kPlatformExtensions);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.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 "WaylandWindow.h"
|
||||
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <cerrno>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
namespace {
|
||||
// libwayland-client's types are opaque here: everything goes through wl_proxy_* and the
|
||||
// exported interface descriptions, and nothing reads a field of either.
|
||||
struct wl_proxy;
|
||||
struct wl_interface;
|
||||
struct wl_event_queue;
|
||||
|
||||
// wayland-egl-backend.h, WL_EGL_WINDOW_VERSION 3: the struct libwayland-egl allocates for
|
||||
// wl_egl_window_create and the driver reads. The ABI is versioned by its first word.
|
||||
struct EglWindow {
|
||||
const intptr_t version;
|
||||
int width;
|
||||
int height;
|
||||
int dx;
|
||||
int dy;
|
||||
int attached_width;
|
||||
int attached_height;
|
||||
void* driver_private;
|
||||
void (*resize_callback)(EglWindow*, void*);
|
||||
void (*destroy_window_callback)(void*);
|
||||
wl_proxy* surface;
|
||||
};
|
||||
constexpr intptr_t kEglWindowVersion = 3;
|
||||
|
||||
// Request opcodes (wayland.xml, the order the requests are declared in).
|
||||
constexpr uint32_t kDisplayGetRegistry = 1;
|
||||
constexpr uint32_t kRegistryBind = 0;
|
||||
constexpr uint32_t kShmCreatePool = 0;
|
||||
constexpr uint32_t kShmPoolCreateBuffer = 0;
|
||||
constexpr uint32_t kShmPoolDestroy = 1;
|
||||
constexpr uint32_t kBufferDestroy = 0;
|
||||
constexpr uint32_t kSurfaceAttach = 1;
|
||||
constexpr uint32_t kSurfaceDamage = 2;
|
||||
constexpr uint32_t kSurfaceCommit = 6;
|
||||
constexpr uint32_t kSurfaceDamageBuffer = 9; // since wl_surface version 4
|
||||
constexpr uint32_t kMarshalFlagDestroy = 1u << 0;
|
||||
// wl_shm formats. The two every compositor must support; byte order in memory is
|
||||
// B, G, R, A for both (little-endian 0xAARRGGBB).
|
||||
constexpr uint32_t kShmFormatArgb8888 = 0;
|
||||
constexpr uint32_t kShmFormatXrgb8888 = 1;
|
||||
|
||||
struct Api {
|
||||
Bool loaded = false;
|
||||
const wl_interface* displayInterface = nullptr;
|
||||
const wl_interface* registryInterface = nullptr;
|
||||
const wl_interface* shmInterface = nullptr;
|
||||
const wl_interface* shmPoolInterface = nullptr;
|
||||
const wl_interface* bufferInterface = nullptr;
|
||||
wl_proxy* (*marshalFlags)(wl_proxy*, uint32_t, const wl_interface*, uint32_t, uint32_t, ...) = nullptr;
|
||||
int (*addListener)(wl_proxy*, void (**)(void), void*) = nullptr;
|
||||
void (*destroy)(wl_proxy*) = nullptr;
|
||||
uint32_t (*getVersion)(wl_proxy*) = nullptr;
|
||||
void* (*createWrapper)(void*) = nullptr;
|
||||
void (*wrapperDestroy)(void*) = nullptr;
|
||||
void (*setQueue)(wl_proxy*, wl_event_queue*) = nullptr;
|
||||
wl_event_queue* (*createQueue)(void*) = nullptr;
|
||||
void (*queueDestroy)(wl_event_queue*) = nullptr;
|
||||
int (*roundtripQueue)(void*, wl_event_queue*) = nullptr;
|
||||
int (*dispatchQueue)(void*, wl_event_queue*) = nullptr;
|
||||
int (*dispatchQueuePending)(void*, wl_event_queue*) = nullptr;
|
||||
int (*flush)(void*) = nullptr;
|
||||
};
|
||||
|
||||
// Loaded once. RTLD_NOLOAD first: a Wayland client has the library already, and the copy
|
||||
// it is using is the one whose wl_display this must talk to.
|
||||
const Api& WaylandApi() {
|
||||
static Api api;
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [] {
|
||||
void* lib = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_NOLOAD);
|
||||
if (lib == nullptr) lib = dlopen("libwayland-client.so.0", RTLD_NOW);
|
||||
if (lib == nullptr) return;
|
||||
const auto sym = [lib](const char* name) { return dlsym(lib, name); };
|
||||
api.displayInterface = static_cast<const wl_interface*>(sym("wl_display_interface"));
|
||||
api.registryInterface = static_cast<const wl_interface*>(sym("wl_registry_interface"));
|
||||
api.shmInterface = static_cast<const wl_interface*>(sym("wl_shm_interface"));
|
||||
api.shmPoolInterface = static_cast<const wl_interface*>(sym("wl_shm_pool_interface"));
|
||||
api.bufferInterface = static_cast<const wl_interface*>(sym("wl_buffer_interface"));
|
||||
api.marshalFlags = reinterpret_cast<decltype(api.marshalFlags)>(sym("wl_proxy_marshal_flags"));
|
||||
api.addListener = reinterpret_cast<decltype(api.addListener)>(sym("wl_proxy_add_listener"));
|
||||
api.destroy = reinterpret_cast<decltype(api.destroy)>(sym("wl_proxy_destroy"));
|
||||
api.getVersion = reinterpret_cast<decltype(api.getVersion)>(sym("wl_proxy_get_version"));
|
||||
api.createWrapper = reinterpret_cast<decltype(api.createWrapper)>(sym("wl_proxy_create_wrapper"));
|
||||
api.wrapperDestroy = reinterpret_cast<decltype(api.wrapperDestroy)>(sym("wl_proxy_wrapper_destroy"));
|
||||
api.setQueue = reinterpret_cast<decltype(api.setQueue)>(sym("wl_proxy_set_queue"));
|
||||
api.createQueue = reinterpret_cast<decltype(api.createQueue)>(sym("wl_display_create_queue"));
|
||||
api.queueDestroy = reinterpret_cast<decltype(api.queueDestroy)>(sym("wl_event_queue_destroy"));
|
||||
api.roundtripQueue = reinterpret_cast<decltype(api.roundtripQueue)>(sym("wl_display_roundtrip_queue"));
|
||||
api.dispatchQueue = reinterpret_cast<decltype(api.dispatchQueue)>(sym("wl_display_dispatch_queue"));
|
||||
api.dispatchQueuePending =
|
||||
reinterpret_cast<decltype(api.dispatchQueuePending)>(sym("wl_display_dispatch_queue_pending"));
|
||||
api.flush = reinterpret_cast<decltype(api.flush)>(sym("wl_display_flush"));
|
||||
api.loaded = api.displayInterface && api.registryInterface && api.shmInterface &&
|
||||
api.shmPoolInterface && api.bufferInterface && api.marshalFlags && api.addListener && api.destroy &&
|
||||
api.getVersion && api.createWrapper && api.wrapperDestroy && api.setQueue &&
|
||||
api.createQueue && api.queueDestroy && api.roundtripQueue && api.dispatchQueue &&
|
||||
api.dispatchQueuePending && api.flush;
|
||||
if (!api.loaded) MGLOG_E("Wayland: libwayland-client.so.0 lacks an entry point this library uses");
|
||||
});
|
||||
return api;
|
||||
}
|
||||
|
||||
struct ShmBuffer {
|
||||
wl_proxy* buffer = nullptr;
|
||||
void* map = nullptr;
|
||||
SizeT size = 0;
|
||||
EGLint width = 0;
|
||||
EGLint height = 0;
|
||||
EGLint stride = 0;
|
||||
Bool busy = false;
|
||||
};
|
||||
|
||||
// The compositor is done with a buffer: it may be drawn into again.
|
||||
void OnBufferRelease(void* data, wl_proxy*) { static_cast<ShmBuffer*>(data)->busy = false; }
|
||||
void (*const kBufferListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnBufferRelease)};
|
||||
} // namespace
|
||||
|
||||
struct WindowSurface::Impl {
|
||||
void* display = nullptr;
|
||||
EglWindow* window = nullptr;
|
||||
wl_event_queue* queue = nullptr;
|
||||
void* displayWrapper = nullptr;
|
||||
wl_proxy* registry = nullptr;
|
||||
wl_proxy* shm = nullptr;
|
||||
uint32_t format = kShmFormatXrgb8888;
|
||||
EGLint width = 0;
|
||||
EGLint height = 0;
|
||||
// Three: one on the glass, one the compositor may still be reading, one to draw into.
|
||||
ShmBuffer buffers[3];
|
||||
Vector<Uint8> scratch;
|
||||
|
||||
~Impl() {
|
||||
const Api& api = WaylandApi();
|
||||
for (auto& buffer : buffers) Release(buffer);
|
||||
// Neither wl_shm (version 1) nor wl_registry has a destroy request: the proxies go
|
||||
// on this side only.
|
||||
if (shm) api.destroy(shm);
|
||||
if (registry) api.destroy(registry);
|
||||
if (displayWrapper) api.wrapperDestroy(displayWrapper);
|
||||
if (queue) api.queueDestroy(queue);
|
||||
if (window && window->driver_private == this) {
|
||||
window->driver_private = nullptr;
|
||||
window->destroy_window_callback = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Release(ShmBuffer& buffer) {
|
||||
const Api& api = WaylandApi();
|
||||
if (buffer.buffer) api.marshalFlags(buffer.buffer, kBufferDestroy, nullptr, api.getVersion(buffer.buffer),
|
||||
kMarshalFlagDestroy);
|
||||
if (buffer.map) munmap(buffer.map, buffer.size);
|
||||
buffer = ShmBuffer{};
|
||||
}
|
||||
|
||||
Bool Allocate(ShmBuffer& buffer) {
|
||||
const Api& api = WaylandApi();
|
||||
Release(buffer);
|
||||
const EGLint stride = width * 4;
|
||||
const SizeT size = static_cast<SizeT>(stride) * static_cast<SizeT>(height);
|
||||
const int fd = memfd_create("mobilegl-wayland", MFD_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
MGLOG_E("Wayland: memfd_create for a %dx%d buffer failed: %s", width, height, std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (ftruncate(fd, static_cast<off_t>(size)) != 0) {
|
||||
MGLOG_E("Wayland: sizing a %dx%d buffer failed: %s", width, height, std::strerror(errno));
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
void* map = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (map == MAP_FAILED) {
|
||||
MGLOG_E("Wayland: mapping a %dx%d buffer failed: %s", width, height, std::strerror(errno));
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
// One pool per buffer, destroyed at once: the buffer keeps the memory alive.
|
||||
wl_proxy* pool = api.marshalFlags(shm, kShmCreatePool, api.shmPoolInterface, api.getVersion(shm), 0, nullptr,
|
||||
fd, static_cast<int32_t>(size));
|
||||
close(fd);
|
||||
if (pool == nullptr) {
|
||||
munmap(map, size);
|
||||
return false;
|
||||
}
|
||||
wl_proxy* wlBuffer = api.marshalFlags(pool, kShmPoolCreateBuffer, api.bufferInterface, api.getVersion(pool),
|
||||
0, nullptr, 0, width, height, stride, format);
|
||||
api.marshalFlags(pool, kShmPoolDestroy, nullptr, api.getVersion(pool), kMarshalFlagDestroy);
|
||||
if (wlBuffer == nullptr) {
|
||||
munmap(map, size);
|
||||
return false;
|
||||
}
|
||||
buffer.buffer = wlBuffer;
|
||||
buffer.map = map;
|
||||
buffer.size = size;
|
||||
buffer.width = width;
|
||||
buffer.height = height;
|
||||
buffer.stride = stride;
|
||||
buffer.busy = false;
|
||||
api.addListener(wlBuffer, const_cast<void (**)(void)>(kBufferListener), &buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A buffer the compositor is not reading: the events already received first, then (all
|
||||
// three still held) a blocking wait for the next release.
|
||||
ShmBuffer* NextFree() {
|
||||
const Api& api = WaylandApi();
|
||||
for (;;) {
|
||||
if (api.dispatchQueuePending(display, queue) < 0) return nullptr;
|
||||
for (auto& buffer : buffers) {
|
||||
if (buffer.buffer == nullptr || buffer.busy) continue;
|
||||
if (buffer.width != width || buffer.height != height) {
|
||||
if (!Allocate(buffer)) return nullptr;
|
||||
}
|
||||
return &buffer;
|
||||
}
|
||||
for (auto& buffer : buffers) {
|
||||
if (buffer.buffer == nullptr) return Allocate(buffer) ? &buffer : nullptr;
|
||||
}
|
||||
if (api.dispatchQueue(display, queue) < 0) return nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
void OnRegistryGlobal(void* data, wl_proxy* registry, uint32_t name, const char* interface, uint32_t) {
|
||||
auto* impl = static_cast<WindowSurface::Impl*>(data);
|
||||
if (impl->shm != nullptr || std::strcmp(interface, "wl_shm") != 0) return;
|
||||
const Api& api = WaylandApi();
|
||||
impl->shm = api.marshalFlags(registry, kRegistryBind, api.shmInterface, 1, 0, name, "wl_shm", 1u, nullptr);
|
||||
}
|
||||
void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {}
|
||||
void (*const kRegistryListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnRegistryGlobal),
|
||||
reinterpret_cast<void (*)(void)>(&OnRegistryGlobalRemove)};
|
||||
|
||||
// The application destroyed its wl_egl_window first: nothing may be read from it after this.
|
||||
void OnWindowDestroyed(void* data) { static_cast<WindowSurface::Impl*>(data)->window = nullptr; }
|
||||
} // namespace
|
||||
|
||||
Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform) {
|
||||
if (platform == kPlatformWayland) return true;
|
||||
if (nativeDisplay == 0 || (platform != EGL_NONE && platform != 0)) return false;
|
||||
const Api& api = WaylandApi();
|
||||
if (!api.loaded) return false;
|
||||
return *reinterpret_cast<const void* const*>(static_cast<uintptr_t>(nativeDisplay)) == api.displayInterface;
|
||||
}
|
||||
|
||||
Bool IsWaylandWindow(const void* window) {
|
||||
if (window == nullptr) return false;
|
||||
const auto* eglWindow = static_cast<const EglWindow*>(window);
|
||||
return eglWindow->version >= kEglWindowVersion && eglWindow->surface != nullptr;
|
||||
}
|
||||
|
||||
Bool WindowSize(const void* window, EGLint* width, EGLint* height) {
|
||||
if (!IsWaylandWindow(window)) return false;
|
||||
const auto* eglWindow = static_cast<const EglWindow*>(window);
|
||||
*width = eglWindow->width;
|
||||
*height = eglWindow->height;
|
||||
return eglWindow->width > 0 && eglWindow->height > 0;
|
||||
}
|
||||
|
||||
WindowSurface::WindowSurface(UniquePtr<Impl> impl) : m_impl(std::move(impl)) {}
|
||||
WindowSurface::~WindowSurface() = default;
|
||||
|
||||
UniquePtr<WindowSurface> WindowSurface::Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height,
|
||||
Bool hasAlpha) {
|
||||
const Api& api = WaylandApi();
|
||||
if (!api.loaded) {
|
||||
MGLOG_E("Wayland: a window surface needs libwayland-client.so.0, and it could not be used");
|
||||
return nullptr;
|
||||
}
|
||||
if (wlDisplay == nullptr || !IsWaylandWindow(wlEglWindow) || width <= 0 || height <= 0) return nullptr;
|
||||
auto impl = MakeUnique<Impl>();
|
||||
impl->display = wlDisplay;
|
||||
impl->window = static_cast<EglWindow*>(wlEglWindow);
|
||||
impl->width = width;
|
||||
impl->height = height;
|
||||
impl->format = hasAlpha ? kShmFormatArgb8888 : kShmFormatXrgb8888;
|
||||
// A queue of this surface's own, so the application's dispatching never sees these events
|
||||
// and this never dispatches the application's.
|
||||
impl->queue = api.createQueue(wlDisplay);
|
||||
impl->displayWrapper = api.createWrapper(wlDisplay);
|
||||
if (impl->queue == nullptr || impl->displayWrapper == nullptr) return nullptr;
|
||||
api.setQueue(static_cast<wl_proxy*>(impl->displayWrapper), impl->queue);
|
||||
auto* wrapper = static_cast<wl_proxy*>(impl->displayWrapper);
|
||||
impl->registry = api.marshalFlags(wrapper, kDisplayGetRegistry, api.registryInterface, api.getVersion(wrapper),
|
||||
0, nullptr);
|
||||
if (impl->registry == nullptr) return nullptr;
|
||||
api.addListener(impl->registry, const_cast<void (**)(void)>(kRegistryListener), impl.get());
|
||||
if (api.roundtripQueue(wlDisplay, impl->queue) < 0 || impl->shm == nullptr) {
|
||||
MGLOG_E("Wayland: the compositor offers no wl_shm; a window surface cannot be presented");
|
||||
return nullptr;
|
||||
}
|
||||
impl->window->driver_private = impl.get();
|
||||
impl->window->destroy_window_callback = &OnWindowDestroyed;
|
||||
MGLOG_I("Wayland: window surface %dx%d presented through wl_shm (%s)", width, height,
|
||||
hasAlpha ? "ARGB8888" : "XRGB8888");
|
||||
return UniquePtr<WindowSurface>(new WindowSurface(std::move(impl)));
|
||||
}
|
||||
|
||||
Bool WindowSurface::Present() {
|
||||
Impl& impl = *m_impl;
|
||||
if (impl.window == nullptr) return false;
|
||||
const Api& api = WaylandApi();
|
||||
ShmBuffer* buffer = impl.NextFree();
|
||||
if (buffer == nullptr) {
|
||||
MGLOG_E_ONCE("Wayland: no wl_shm buffer could be taken for the frame; it is not shown");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The frame, out of the default framebuffer, through this library's own entry points: the
|
||||
// application's read framebuffer, pack buffer and pack state are saved and put back, so
|
||||
// the readback is invisible to it.
|
||||
namespace GL = MG_Impl::GLImpl;
|
||||
GLint readFramebuffer = 0, packBuffer = 0, alignment = 4, rowLength = 0, skipRows = 0, skipPixels = 0;
|
||||
GL::GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer);
|
||||
GL::GetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer);
|
||||
GL::GetIntegerv(GL_PACK_ALIGNMENT, &alignment);
|
||||
GL::GetIntegerv(GL_PACK_ROW_LENGTH, &rowLength);
|
||||
GL::GetIntegerv(GL_PACK_SKIP_ROWS, &skipRows);
|
||||
GL::GetIntegerv(GL_PACK_SKIP_PIXELS, &skipPixels);
|
||||
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
GL::PixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
GL::PixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
GL::PixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
GL::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
const SizeT rowBytes = static_cast<SizeT>(impl.width) * 4;
|
||||
impl.scratch.resize(rowBytes * static_cast<SizeT>(impl.height));
|
||||
GL::ReadPixels(0, 0, impl.width, impl.height, GL_RGBA, GL_UNSIGNED_BYTE, impl.scratch.data());
|
||||
GL::PixelStorei(GL_PACK_ALIGNMENT, alignment);
|
||||
GL::PixelStorei(GL_PACK_ROW_LENGTH, rowLength);
|
||||
GL::PixelStorei(GL_PACK_SKIP_ROWS, skipRows);
|
||||
GL::PixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
|
||||
if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(packBuffer));
|
||||
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readFramebuffer));
|
||||
|
||||
// GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A.
|
||||
auto* dst = static_cast<Uint8*>(buffer->map);
|
||||
for (EGLint y = 0; y < impl.height; ++y) {
|
||||
const Uint8* src = impl.scratch.data() + static_cast<SizeT>(impl.height - 1 - y) * rowBytes;
|
||||
Uint8* row = dst + static_cast<SizeT>(y) * static_cast<SizeT>(buffer->stride);
|
||||
for (EGLint x = 0; x < impl.width; ++x) {
|
||||
row[x * 4 + 0] = src[x * 4 + 2];
|
||||
row[x * 4 + 1] = src[x * 4 + 1];
|
||||
row[x * 4 + 2] = src[x * 4 + 0];
|
||||
row[x * 4 + 3] = src[x * 4 + 3];
|
||||
}
|
||||
}
|
||||
|
||||
wl_proxy* surface = impl.window->surface;
|
||||
const uint32_t surfaceVersion = api.getVersion(surface);
|
||||
api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, buffer->buffer, impl.window->dx,
|
||||
impl.window->dy);
|
||||
if (surfaceVersion >= 4) {
|
||||
api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, impl.width,
|
||||
impl.height);
|
||||
} else {
|
||||
api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, impl.width, impl.height);
|
||||
}
|
||||
api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0);
|
||||
buffer->busy = true;
|
||||
impl.window->attached_width = impl.width;
|
||||
impl.window->attached_height = impl.height;
|
||||
impl.window->dx = 0;
|
||||
impl.window->dy = 0;
|
||||
api.flush(impl.display);
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.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
|
||||
|
||||
// WAYLAND WINDOW SURFACES. A Wayland client hands eglCreateWindowSurface a wl_egl_window, and what
|
||||
// the compositor shows is whatever wl_buffer the client attaches to that window's wl_surface. The
|
||||
// pixels of a MobileGL frame are drawn where the backend is - on a split build, in another process
|
||||
// on another OS - so a window surface here is a pbuffer the backend draws into, plus this: at every
|
||||
// swap the frame is read back and attached to the wl_surface as a wl_shm buffer.
|
||||
//
|
||||
// libwayland-client is loaded at run time (it is already in every Wayland client's process), so the
|
||||
// library neither links it nor needs its headers to build.
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MOBILEGL_WAYLAND_WINDOWS 1
|
||||
#else
|
||||
#define MOBILEGL_WAYLAND_WINDOWS 0
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
// EGL_PLATFORM_WAYLAND_KHR and EGL_PLATFORM_WAYLAND_EXT are the same enum.
|
||||
inline constexpr EGLenum kPlatformWayland = 0x31D8;
|
||||
|
||||
// Whether a display is a Wayland one: asked for as EGL_PLATFORM_WAYLAND, or (eglGetDisplay with
|
||||
// no platform) a native display that is a wl_display - recognised the way Mesa does, by its
|
||||
// first word being libwayland-client's wl_display_interface.
|
||||
Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform);
|
||||
|
||||
// Whether `window` looks like a wl_egl_window this library can present to.
|
||||
Bool IsWaylandWindow(const void* window);
|
||||
|
||||
// The size the application last gave its wl_egl_window.
|
||||
Bool WindowSize(const void* window, EGLint* width, EGLint* height);
|
||||
|
||||
// One window surface's presentation: the shm buffers, the event queue they are dispatched on,
|
||||
// and the readback scratch. Created for a wl_egl_window on a wl_display; null with the reason
|
||||
// logged when libwayland-client or the compositor's wl_shm is not there.
|
||||
class WindowSurface {
|
||||
public:
|
||||
static UniquePtr<WindowSurface> Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height,
|
||||
Bool hasAlpha);
|
||||
~WindowSurface();
|
||||
WindowSurface(const WindowSurface&) = delete;
|
||||
WindowSurface& operator=(const WindowSurface&) = delete;
|
||||
|
||||
// Reads the frame back out of the current context's default framebuffer and puts it on
|
||||
// the window: attach, damage, commit. The application's read-framebuffer and pack state
|
||||
// are put back exactly as they were. False (logged) when the frame did not reach the
|
||||
// window; the swap itself goes on regardless.
|
||||
Bool Present();
|
||||
|
||||
struct Impl;
|
||||
|
||||
private:
|
||||
explicit WindowSurface(UniquePtr<Impl> impl);
|
||||
UniquePtr<Impl> m_impl;
|
||||
};
|
||||
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,9 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The dma-buf import's client half: eglCreateImageKHR keeps a dup() of every plane's descriptor,
|
||||
// and this is what tells the render server which texture that buffer backs (see the header).
|
||||
#include <MG_Remote/Client/DmaBufImport.h>
|
||||
#include <Includes.h>
|
||||
#include "../Buffer/GL_Buffer.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
@@ -22,6 +25,13 @@
|
||||
#include "../Sync/GL_Sync.h"
|
||||
#include "../Debug/GL_Debug.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
// The EGL side of the state, for the two GL_OES_EGL_image entry points: what an EGLImage handle
|
||||
// names is an object eglCreateImageKHR built (its geometry, its fourcc and the descriptors it owns),
|
||||
// and that object lives in the EGL state. Same include the other GLImpl files that touch it use.
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
// The compositor's frame boundary: glFlush presents the display host's frame when the surfaceless
|
||||
// bind hook took one (see EGLImpl.cpp). The EGL side declares the pair and the swap entry.
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
|
||||
@@ -125,6 +135,17 @@ MOBILEGL_GL_API void glFlush() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MobileGL::MG_Pipe::MGPipeClientFlush();
|
||||
#endif
|
||||
// A COMPOSITOR'S FRAME BOUNDARY. kwin binds the display host's frame surface and draws the
|
||||
// desktop into it, then calls glFlush - and glFlush alone. It never calls eglSwapBuffers, so
|
||||
// without this the stream reached the server and the server drew, but the display host was
|
||||
// never told a frame was done: the host's own present loop (AHardwareBuffer_acquire ->
|
||||
// draw -> notify) is what puts pixels on the glass, and its notify is exactly this swap.
|
||||
// Guarded by whether the surfaceless-bind hook actually took the host frame.
|
||||
if (MobileGL::MG_Impl::EGLImpl::g_hostFramePresentSurface != nullptr &&
|
||||
MobileGL::MG_Impl::EGLImpl::g_hostFramePresentDisplay != nullptr) {
|
||||
MobileGL::MG_Impl::EGLImpl::SwapBuffers(MobileGL::MG_Impl::EGLImpl::g_hostFramePresentDisplay,
|
||||
MobileGL::MG_Impl::EGLImpl::g_hostFramePresentSurface);
|
||||
}
|
||||
}
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferRenderbuffer, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferRenderbuffer, target, attachment, renderbuffertarget, renderbuffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture2D, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture2D, target, attachment, textarget, texture, level)
|
||||
@@ -1297,17 +1318,325 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmaxARB, GLenum target, GLboolean rese
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferSampleLocationsfvARB, GLenum target, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferSampleLocationsfvARB, target, start, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferSampleLocationsfvARB, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferSampleLocationsfvARB, framebuffer, start, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EvaluateDepthValuesARB, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EvaluateDepthValuesARB, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteObjectARB, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteObjectARB, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, GetHandleARB, GLenum pname) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, GetHandleARB, pname)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DetachObjectARB, GLhandleARB containerObj, GLhandleARB attachedObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DetachObjectARB, containerObj, attachedObj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateShaderObjectARB, GLenum shaderType) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateShaderObjectARB, shaderType)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateProgramObjectARB, void) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateProgramObjectARB, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, AttachObjectARB, GLhandleARB containerObj, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, AttachObjectARB, containerObj, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramObjectARB, GLhandleARB programObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramObjectARB, programObj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterfvARB, GLhandleARB obj, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterfvARB, obj, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterivARB, GLhandleARB obj, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterivARB, obj, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInfoLogARB, GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInfoLogARB, obj, maxLength, length, infoLog)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetAttachedObjectsARB, GLhandleARB containerObj, GLsizei maxCount, GLsizei* count, GLhandleARB* obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetAttachedObjectsARB, containerObj, maxCount, count, obj)
|
||||
// GL_ARB_shader_objects, GL_ARB_vertex_shader, GL_ARB_fragment_shader.
|
||||
//
|
||||
// kwin_wayland 6.7.3 refuses a MobileGL context over three of the names these extensions add.
|
||||
// EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the DESKTOP-OpenGL branch for
|
||||
// every context whose GL_VERSION is not an "OpenGL ES" string:
|
||||
// const bool supportsGLSL = m_isOpenglES || (hasOpenglExtension("GL_ARB_shader_objects") &&
|
||||
// hasOpenglExtension("GL_ARB_fragment_shader") && hasOpenglExtension("GL_ARB_vertex_shader"));
|
||||
// GL_VERSION here is "4.6.0 MobileGL ...", so all three names were REQUIRED of a context that did
|
||||
// not carry them: checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize rendering
|
||||
// context" and quit without drawing a frame. Both backends now advertise the three, and this block
|
||||
// is what makes that advertisement true instead of a test unlock - every entry point the three
|
||||
// extensions define is implemented here, over the shader and program objects the core entry points
|
||||
// already own:
|
||||
//
|
||||
// ARB_shader_objects the 39 functions of the block include/GL/glext.h:4095-4173 declares.
|
||||
// ARB_vertex_shader the three attrib-location entry points of glext.h:4856-4862.
|
||||
// ARB_fragment_shader none. Its "New Procedures and Functions" section reads "None" - it is the
|
||||
// capability half of the pair, and the reason kwin names it at all.
|
||||
//
|
||||
// HANDLES. The ARB family names objects with GLhandleARB. This repository's own glext.h types that
|
||||
// as void* on Apple and as unsigned int on every other platform (glext.h:4055-4059, the same split
|
||||
// the system headers carry), so on Linux, Windows and Android a handle IS the GLuint name this
|
||||
// implementation hands out and both conversions below are the identity there. The pointer arm is
|
||||
// not decoration: a caller on Apple passes a pointer, and an id written straight into its void*
|
||||
// slot would be a size error rather than a cast error. H is a template parameter, and both arms
|
||||
// cast through it, so the arm a platform does not use is never instantiated - and never has to be
|
||||
// well-formed for the other typedef: reinterpret_cast between two integers, which is what the
|
||||
// pointer arm becomes on Linux, is not a cast clang accepts. The shape is the one
|
||||
// MG_State/EGLState/Core.h already uses for EGL's handles.
|
||||
template <typename H = GLhandleARB>
|
||||
static GLhandleARB ToGLHandleARB(GLuint name) {
|
||||
if constexpr (std::is_pointer_v<H>) {
|
||||
return reinterpret_cast<H>(static_cast<std::uintptr_t>(name));
|
||||
} else {
|
||||
return static_cast<H>(name);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename H = GLhandleARB>
|
||||
static GLuint FromGLHandleARB(GLhandleARB handle) {
|
||||
if constexpr (std::is_pointer_v<H>) {
|
||||
return static_cast<GLuint>(reinterpret_cast<std::uintptr_t>(static_cast<H>(handle)));
|
||||
} else {
|
||||
return static_cast<GLuint>(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// The answer behind GetObjectParameter{if}vARB, and the reason neither of them carries a pname
|
||||
// translation table: the GL_OBJECT_*_ARB tokens ARE the core tokens, numerically. Core GL took the
|
||||
// object pnames over unchanged when ARB_shader_objects was folded in -
|
||||
// GL_OBJECT_DELETE_STATUS_ARB through GL_OBJECT_SHADER_SOURCE_LENGTH_ARB are 0x8B80-0x8B88, the
|
||||
// same numbers glcorearb.h gives GL_DELETE_STATUS through GL_SHADER_SOURCE_LENGTH
|
||||
// (glext.h:4086-4094) - so the mapping is a RENAME and the same pname goes to the core getter of
|
||||
// the same object kind. Handing it to that getter is also the right ERROR behaviour, and the reason
|
||||
// this does not whitelist pnames: GL_OBJECT_LINK_STATUS_ARB asked of a SHADER has to come back
|
||||
// INVALID_ENUM, and glGetShaderiv is where the set of pnames a shader accepts is defined.
|
||||
//
|
||||
// The one token core never took is GL_OBJECT_TYPE_ARB (0x8B4E; core's GL_OBJECT_TYPE, 0x9112, is
|
||||
// ARB_program_interface_query's interface class, a different question with a different value
|
||||
// space), so the object kind is the single thing answered here without a core getter. It is also
|
||||
// the only way an ARB-era caller can ask whether a handle names a program or a shader: there is no
|
||||
// glIsProgramARB/glIsShaderARB in ARB_shader_objects (include/GL/glext.h:4095-4173 declares
|
||||
// neither, and the glIsProgramARB the registry does know, glext.h:3280, belongs to
|
||||
// ARB_vertex_program/ARB_fragment_program - those ASM program objects are not these).
|
||||
static MobileGL::Bool AnswerObjectParameterARB(GLuint name, GLenum pname, GLint* params) {
|
||||
if (pname == GL_OBJECT_TYPE_ARB) {
|
||||
if (glIsProgram(name)) {
|
||||
*params = GL_PROGRAM_OBJECT_ARB;
|
||||
return true;
|
||||
}
|
||||
if (glIsShader(name)) {
|
||||
*params = GL_SHADER_OBJECT_ARB;
|
||||
return true;
|
||||
}
|
||||
// The error ARB_vertex_shader's own text specifies: "The error INVALID_VALUE is generated
|
||||
// by any command that takes one or more handles as input, and one or more handles are not
|
||||
// an object handle generated by OpenGL."
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidValue,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(name) + " is not an object handle generated by OpenGL."));
|
||||
return false;
|
||||
}
|
||||
if (glIsProgram(name)) {
|
||||
glGetProgramiv(name, pname, params);
|
||||
return true;
|
||||
}
|
||||
// A shader, or a name that is neither. glGetShaderiv owns both the shader pname set and the
|
||||
// name validation, which is the one place that splits a program name handed to a shader query
|
||||
// (INVALID_OPERATION) from a name GL never generated (INVALID_VALUE) - the split GL 3.3 core
|
||||
// 2.11 requires. It writes nothing in either error case, which is what the caller reads as
|
||||
// "no answer".
|
||||
const MobileGL::Bool isShader = glIsShader(name) == GL_TRUE;
|
||||
glGetShaderiv(name, pname, params);
|
||||
return isShader;
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glDeleteObjectARB(GLhandleARB obj) {
|
||||
// ARB_shader_objects has ONE object namespace - a handle names a program or a shader and
|
||||
// nothing else - and DeleteObjectARB is the deletion entry point for both kinds, so the kind
|
||||
// is decided here. IsProgram/IsShader answer over the same single namespace this
|
||||
// implementation hands names out of, so a program name cannot be taken for a shader name.
|
||||
// Zero, and a handle that names neither object, are ignored: there is no object to delete.
|
||||
const GLuint name = FromGLHandleARB(obj);
|
||||
if (glIsProgram(name)) {
|
||||
glDeleteProgram(name);
|
||||
} else if (glIsShader(name)) {
|
||||
glDeleteShader(name);
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glGetHandleARB(GLenum pname) {
|
||||
if (pname != GL_PROGRAM_OBJECT_ARB) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidEnum,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"pname " + std::to_string(pname) + " is not GL_PROGRAM_OBJECT_ARB."));
|
||||
return ToGLHandleARB(0u);
|
||||
}
|
||||
// "The program object currently in use", read from the state GL_CURRENT_PROGRAM is answered
|
||||
// from - so a program bound through glUseProgram is found here too, and a
|
||||
// UseProgramObjectARB(0) reads back as the zero handle.
|
||||
const auto& currentProgram = MobileGL::MG_State::pGLContext->GetCurrentProgram();
|
||||
return ToGLHandleARB(currentProgram ? static_cast<GLuint>(currentProgram->GetExternalIndex()) : 0u);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glDetachObjectARB(GLhandleARB containerObj, GLhandleARB attachedObj) {
|
||||
glDetachShader(FromGLHandleARB(containerObj), FromGLHandleARB(attachedObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glCreateShaderObjectARB(GLenum shaderType) {
|
||||
return ToGLHandleARB(glCreateShader(shaderType));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glShaderSourceARB(GLhandleARB shaderObj, GLsizei count, const GLcharARB** string,
|
||||
const GLint* length) {
|
||||
glShaderSource(FromGLHandleARB(shaderObj), count, string, length);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glCompileShaderARB(GLhandleARB shaderObj) {
|
||||
glCompileShader(FromGLHandleARB(shaderObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glCreateProgramObjectARB(void) {
|
||||
return ToGLHandleARB(glCreateProgram());
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glAttachObjectARB(GLhandleARB containerObj, GLhandleARB obj) {
|
||||
glAttachShader(FromGLHandleARB(containerObj), FromGLHandleARB(obj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glLinkProgramARB(GLhandleARB programObj) {
|
||||
glLinkProgram(FromGLHandleARB(programObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUseProgramObjectARB(GLhandleARB programObj) {
|
||||
glUseProgram(FromGLHandleARB(programObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glValidateProgramARB(GLhandleARB programObj) {
|
||||
glValidateProgram(FromGLHandleARB(programObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1fARB(GLint location, GLfloat v0) {
|
||||
glUniform1f(location, v0);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2fARB(GLint location, GLfloat v0, GLfloat v1) {
|
||||
glUniform2f(location, v0, v1);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) {
|
||||
glUniform3f(location, v0, v1, v2);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) {
|
||||
glUniform4f(location, v0, v1, v2, v3);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1iARB(GLint location, GLint v0) {
|
||||
glUniform1i(location, v0);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2iARB(GLint location, GLint v0, GLint v1) {
|
||||
glUniform2i(location, v0, v1);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3iARB(GLint location, GLint v0, GLint v1, GLint v2) {
|
||||
glUniform3i(location, v0, v1, v2);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4iARB(GLint location, GLint v0, GLint v1, GLint v2, GLint v3) {
|
||||
glUniform4i(location, v0, v1, v2, v3);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform1fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform2fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform3fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform4fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform1iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform2iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform3iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform4iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniformMatrix2fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix2fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniformMatrix3fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix3fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniformMatrix4fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix4fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetObjectParameterfvARB(GLhandleARB obj, GLenum pname, GLfloat* params) {
|
||||
// The fv form asks the same questions - the ARB object model holds no float-valued object
|
||||
// state - so the answer is the iv answer widened. A query the iv path refuses leaves the
|
||||
// output untouched here as well, rather than putting a plausible-looking 0.0 where there is
|
||||
// no answer.
|
||||
GLint value = 0;
|
||||
if (!AnswerObjectParameterARB(FromGLHandleARB(obj), pname, &value)) return;
|
||||
*params = static_cast<GLfloat>(value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetObjectParameterivARB(GLhandleARB obj, GLenum pname, GLint* params) {
|
||||
AnswerObjectParameterARB(FromGLHandleARB(obj), pname, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetInfoLogARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) {
|
||||
// The same handle-kind dispatch as AnswerObjectParameterARB, and the same fallback: the shader
|
||||
// getter is also where the name validation and its error code live.
|
||||
const GLuint name = FromGLHandleARB(obj);
|
||||
if (glIsProgram(name)) {
|
||||
glGetProgramInfoLog(name, maxLength, length, infoLog);
|
||||
} else {
|
||||
glGetShaderInfoLog(name, maxLength, length, infoLog);
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetAttachedObjectsARB(GLhandleARB containerObj, GLsizei maxCount, GLsizei* count,
|
||||
GLhandleARB* obj) {
|
||||
// The caller's array holds GLhandleARB while glGetAttachedShaders writes GLuint names, and on
|
||||
// Apple those are different sizes, so the ids are gathered first and converted one at a time.
|
||||
// A negative maxCount is passed through: glGetAttachedShaders is where its INVALID_VALUE is.
|
||||
MobileGL::Vector<GLuint> shaders(maxCount > 0 ? static_cast<MobileGL::SizeT>(maxCount) : 0, 0);
|
||||
GLsizei attached = 0;
|
||||
glGetAttachedShaders(FromGLHandleARB(containerObj), maxCount, &attached, shaders.data());
|
||||
for (GLsizei i = 0; i < attached; ++i) {
|
||||
obj[i] = ToGLHandleARB(shaders[i]);
|
||||
}
|
||||
if (count) *count = attached;
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLint glGetUniformLocationARB(GLhandleARB programObj, const GLcharARB* name) {
|
||||
return glGetUniformLocation(FromGLHandleARB(programObj), name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetActiveUniformARB(GLhandleARB programObj, GLuint index, GLsizei maxLength,
|
||||
GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
|
||||
glGetActiveUniform(FromGLHandleARB(programObj), index, maxLength, length, size, type, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetUniformfvARB(GLhandleARB programObj, GLint location, GLfloat* params) {
|
||||
glGetUniformfv(FromGLHandleARB(programObj), location, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetUniformivARB(GLhandleARB programObj, GLint location, GLint* params) {
|
||||
glGetUniformiv(FromGLHandleARB(programObj), location, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetShaderSourceARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length,
|
||||
GLcharARB* source) {
|
||||
glGetShaderSource(FromGLHandleARB(obj), maxLength, length, source);
|
||||
}
|
||||
|
||||
// ARB_vertex_shader's three entry points. The generic-attribute family that extension also
|
||||
// describes is spelled with the ARB_vertex_program names (glVertexAttribPointerARB and the rest),
|
||||
// which glext.h declares in that other block and which MobileGL does not advertise; nothing here
|
||||
// claims them.
|
||||
MOBILEGL_GL_API void glBindAttribLocationARB(GLhandleARB programObj, GLuint index, const GLcharARB* name) {
|
||||
glBindAttribLocation(FromGLHandleARB(programObj), index, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetActiveAttribARB(GLhandleARB programObj, GLuint index, GLsizei maxLength,
|
||||
GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
|
||||
glGetActiveAttrib(FromGLHandleARB(programObj), index, maxLength, length, size, type, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLint glGetAttribLocationARB(GLhandleARB programObj, const GLcharARB* name) {
|
||||
return glGetAttribLocation(FromGLHandleARB(programObj), name);
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedStringARB, GLenum type, GLint namelen, const GLchar* name, GLint strinen, const GLchar* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedStringARB, type, namelen, name, strinen, string)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteNamedStringARB, GLint namelen, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteNamedStringARB, namelen, name)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompileShaderIncludeARB, GLuint shader, GLsizei count, const GLchar* const* path, const GLint* length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompileShaderIncludeARB, shader, count, path, length)
|
||||
@@ -1713,6 +2042,141 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendEnviATI, GLenum pname, GLint para
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendEnvfATI, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexBlendEnvfATI, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EGLImageTargetTexStorageEXT, GLenum target, GLeglImageOES image, const GLint* attrib_list) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EGLImageTargetTexStorageEXT, target, image, attrib_list)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EGLImageTargetTextureStorageEXT, GLuint texture, GLeglImageOES image, const GLint* attrib_list) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EGLImageTargetTextureStorageEXT, texture, image, attrib_list)
|
||||
|
||||
// GL_OES_EGL_image.
|
||||
//
|
||||
// MEASURED, and the whole reason this block exists: libepoxy in kwin_wayland prints
|
||||
// No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image
|
||||
// and refuses the import path there - and that path is how the compositor's scene reaches the
|
||||
// display daemon's dma-bufs, i.e. how a Linux desktop gets onto the phone's screen. The EGL half is
|
||||
// real: eglCreateImageKHR builds an EGL_LINUX_DMA_BUF_EXT image that owns a dup() of every plane's
|
||||
// descriptor and carries that buffer's geometry and fourcc (MG_State/EGLState/Core.h). So a handle
|
||||
// that arrives here is resolvable, and what these two entry points do is exactly that: resolve it,
|
||||
// refuse what names nothing, and say ONCE what is not wired yet. What is not wired yet is the
|
||||
// render-server half - telling the server which texture a dma-buf backs, over the one channel that
|
||||
// can carry a descriptor across the container boundary - so a framebuffer built over an imported
|
||||
// texture is not complete until that lands. Pretending otherwise (attaching the image to a texture
|
||||
// object that has no storage behind it) would turn "not implemented" into "mysteriously black",
|
||||
// which is what the log line below is here to prevent.
|
||||
MOBILEGL_GL_API void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image) {
|
||||
// 0x8D65 is GL_TEXTURE_EXTERNAL_OES, which the headers this file compiles against do not spell
|
||||
// out. It matters here MORE than GL_TEXTURE_2D does: a compositor that could not map an
|
||||
// imported dma-buf's format to a non-external one binds its image to THIS target, so refusing it
|
||||
// would turn "no format map yet" into an INVALID_ENUM the caller cannot act on (kwin 6.7.3 does
|
||||
// exactly that when EglDisplay::queryImportFormats() answers nothing).
|
||||
constexpr GLenum kTextureExternalOes = 0x8D65;
|
||||
if (target != GL_TEXTURE_2D && target != kTextureExternalOes) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidEnum,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"target is not a texture target this entry point takes."));
|
||||
return;
|
||||
}
|
||||
EGLint imageWidth = 0;
|
||||
EGLint imageHeight = 0;
|
||||
EGLint imageFourCC = 0;
|
||||
EGLint imagePlanes = 0;
|
||||
const MobileGL::Bool imageKnown =
|
||||
MobileGL::MG_State::pEGLContext != nullptr &&
|
||||
MobileGL::MG_State::pEGLContext->DescribeImage(reinterpret_cast<::EGLImage>(image), &imageWidth, &imageHeight,
|
||||
&imageFourCC, &imagePlanes);
|
||||
if (!imageKnown) {
|
||||
// A handle this side did not hand out: ARB/OES text puts that on INVALID_VALUE, and it is
|
||||
// the answer a caller can act on rather than a texture that renders nowhere.
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidValue,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"image is not an EGLImage this display created."));
|
||||
return;
|
||||
}
|
||||
MGLOG_W_ONCE("glEGLImageTargetTexture2DOES: taking target 0x%04x over an EGLImage of %dx%d fourcc 0x%08x "
|
||||
"(%d plane(s)) that this side holds; the render server has NOT been told which texture that "
|
||||
"dma-buf backs yet, so a framebuffer over this texture is not complete (the fd transport over "
|
||||
"the GBM socket and the server-side import are the next change)",
|
||||
target, imageWidth, imageHeight, imageFourCC, imagePlanes);
|
||||
|
||||
// AND NOW WIRE IT: a texture bound to an imported EGLImage has no storage on the render server,
|
||||
// and a framebuffer built over it there is not complete - kwin 6.7.3 aborts on its own
|
||||
// RenderTarget assertion right after this log line (measured twice). So the server is told which
|
||||
// buffer backs the texture: one message per plane, each carrying that plane's descriptor over the
|
||||
// session transport's SCM_RIGHTS channel. No session, or a transport without that channel, is a
|
||||
// refusal by name from ShareImportedDmaBuf - the texture simply keeps having no storage, which is
|
||||
// the honest state rather than a silent render into nothing.
|
||||
MGLOG_I("dma-buf import: entry (target=0x%04x, image=%p)", static_cast<unsigned>(target), static_cast<void*>(image));
|
||||
if (MobileGL::MG_State::pEGLContext != nullptr) {
|
||||
constexpr MobileGL::Int kDmaBufImportPlanes = 4;
|
||||
EGLint planeCount = 0;
|
||||
EGLint planeFds[kDmaBufImportPlanes] = {-1, -1, -1, -1};
|
||||
EGLint planeOffsets[kDmaBufImportPlanes] = {0, 0, 0, 0};
|
||||
EGLint planePitches[kDmaBufImportPlanes] = {0, 0, 0, 0};
|
||||
MobileGL::Uint64 planeModifiers[kDmaBufImportPlanes] = {0, 0, 0, 0};
|
||||
MobileGL::Bool planeHasModifier[kDmaBufImportPlanes] = {false, false, false, false};
|
||||
if (MobileGL::MG_State::pEGLContext->DescribeImagePlanes(
|
||||
reinterpret_cast<::EGLImage>(image), &planeCount, planeFds, planeOffsets, planePitches,
|
||||
planeModifiers, planeHasModifier, kDmaBufImportPlanes)) {
|
||||
// The texture the image was bound to. 0x8D65 has no TextureTarget member in this state
|
||||
// layer (nothing else here binds it), so the name is looked up through Texture2D and may
|
||||
// come back 0 - the server logs what it was told rather than guessing.
|
||||
MobileGL::Uint32 texName = 0;
|
||||
if (MobileGL::MG_State::pGLContext != nullptr) {
|
||||
const MobileGL::Int unit = MobileGL::MG_State::pGLContext->GetActiveTextureUnit();
|
||||
// THE EXTERNAL SLOT, which is where a compositor's imported dma-buf texture lives.
|
||||
const auto& bound = MobileGL::MG_State::pGLContext->GetTextureUnitObject(unit)
|
||||
.GetBindingSlot(MobileGL::TextureTarget::TextureExternalOES)
|
||||
.GetBoundObject();
|
||||
if (bound != nullptr) {
|
||||
texName = static_cast<MobileGL::Uint32>(bound->GetExternalIndex());
|
||||
// AN IMPORTED TEXTURE IS BACKED, SO IT IS COMPLETE. Without this the state layer
|
||||
// answers incompletely (TextureObjectBase::IsComplete only looks at the internal
|
||||
// format) and the compositor, having attached it to a framebuffer, reports
|
||||
// "framebuffer for dmabuf 0 is not complete" and stops before drawing anything.
|
||||
bound->SetInternalFormat(MobileGL::TextureInternalFormat::RGBA8);
|
||||
}
|
||||
}
|
||||
MGLOG_I("dma-buf import: textures resolved (texName=%u planes=%d), sharing", texName,
|
||||
static_cast<int>(planeCount));
|
||||
for (MobileGL::Int plane = 0; plane < planeCount; plane++) {
|
||||
if (planeFds[plane] < 0) continue;
|
||||
MGLOG_I("dma-buf import: sharing plane %d fd=%d", static_cast<int>(plane), planeFds[plane]);
|
||||
(void)MobileGL::MG_Remote::Client::ShareImportedDmaBuf(
|
||||
static_cast<MobileGL::Uint32>(target), texName, static_cast<MobileGL::Uint32>(imageWidth),
|
||||
static_cast<MobileGL::Uint32>(imageHeight), static_cast<MobileGL::Uint32>(imageFourCC),
|
||||
static_cast<MobileGL::Uint32>(plane), static_cast<MobileGL::Uint32>(planeCount), planeFds[plane],
|
||||
planePitches[plane], planeOffsets[plane], planeModifiers[plane], planeHasModifier[plane]);
|
||||
MGLOG_I("dma-buf import: plane %d shared", static_cast<int>(plane));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glEGLImageTargetRenderbufferStorageOES(GLenum target, GLeglImageOES image) {
|
||||
if (target != GL_RENDERBUFFER) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidEnum,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"target is not GL_RENDERBUFFER."));
|
||||
return;
|
||||
}
|
||||
EGLint imageWidth = 0;
|
||||
EGLint imageHeight = 0;
|
||||
EGLint imageFourCC = 0;
|
||||
EGLint imagePlanes = 0;
|
||||
const MobileGL::Bool imageKnown =
|
||||
MobileGL::MG_State::pEGLContext != nullptr &&
|
||||
MobileGL::MG_State::pEGLContext->DescribeImage(reinterpret_cast<::EGLImage>(image), &imageWidth, &imageHeight,
|
||||
&imageFourCC, &imagePlanes);
|
||||
if (!imageKnown) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidValue,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"image is not an EGLImage this display created."));
|
||||
return;
|
||||
}
|
||||
MGLOG_W_ONCE("glEGLImageTargetRenderbufferStorageOES: the renderbuffer takes an EGLImage of %dx%d fourcc "
|
||||
"0x%08x; the render-server half of the import is the next change, exactly as for the texture "
|
||||
"entry point beside this one",
|
||||
imageWidth, imageHeight, imageFourCC);
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformBufferEXT, GLuint program, GLint location, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformBufferEXT, program, location, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetUniformBufferSizeEXT, GLuint program, GLint location) DECLARE_GL_FUNCTION_STUB_END(GLint, GetUniformBufferSizeEXT, program, location)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLintptr, GetUniformOffsetEXT, GLuint program, GLint location) DECLARE_GL_FUNCTION_STUB_END(GLintptr, GetUniformOffsetEXT, program, location)
|
||||
|
||||
@@ -100,6 +100,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
// NOT gated on the host frame, and that is deliberate: this only decides the ERROR CODE
|
||||
// once completeness has already failed (CheckFramebufferStatus_State asks
|
||||
// CheckCompleteness first), and the imported dma-buf's attachment is counted as valid
|
||||
// THERE (MG_State/GLState/FramebufferState/FramebufferObject.cpp, whose gate lands before
|
||||
// its own validity test for exactly this reason). Touching it here would add a second
|
||||
// place that has to know the backend's frame - and the frontend knowing that fact is the
|
||||
// compromise the completeness comment already names the end of.
|
||||
for (const auto& attachment : framebufferObject.GetAllAttachmentObjects()) {
|
||||
if (attachment.IsValid() && !attachment.IsEmpty()) {
|
||||
return true;
|
||||
@@ -611,7 +618,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
textureObject->GetTarget() != expectedTextureTarget) {
|
||||
(textureObject->GetTarget() != expectedTextureTarget &&
|
||||
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
|
||||
expectedTextureTarget == TextureTarget::Texture2D))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1382,7 +1391,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
textureObject->GetTarget() != expectedTextureTarget) {
|
||||
(textureObject->GetTarget() != expectedTextureTarget &&
|
||||
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
|
||||
expectedTextureTarget == TextureTarget::Texture2D))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1532,7 +1543,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
textureObject->GetTarget() != expectedTextureTarget) {
|
||||
(textureObject->GetTarget() != expectedTextureTarget &&
|
||||
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
|
||||
expectedTextureTarget == TextureTarget::Texture2D))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
|
||||
@@ -41,6 +41,18 @@
|
||||
// call sites below are declarations only, exactly as the frontend's mutators are.
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
|
||||
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): AN UNPACK SOURCE IS READ FROM THE BUFFER'S CLIENT SHADOW,
|
||||
// and under a transport a GPU write into that buffer - a read_pixels_to_buffer, which is how a
|
||||
// glReadPixels into a pack buffer now crosses, or a shader store - reaches the shadow only through
|
||||
// SyncGpuWrites, exactly as glMapBufferRange and glGetBufferSubData already do. Monolith keeps its
|
||||
// read as it was: its ReadPixels maps the driver PBO back into the shadow inside the call, and the
|
||||
// pull build compiles this to nothing (G1).
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { if (MG_Config::Transport != MG_Config::TransportMode::Monolith) (buffer)->SyncGpuWrites(); } while (0)
|
||||
#else
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) ((void)0)
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
static UnorderedMap<Uint, Bool> g_autoGenerateMipmapByTextureId;
|
||||
@@ -1773,6 +1785,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& unpackBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (!unpackBuffer) return data;
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(unpackBuffer);
|
||||
return reinterpret_cast<const char*>(unpackBuffer->MappedData()) + reinterpret_cast<SizeT>(data);
|
||||
}
|
||||
|
||||
@@ -1819,6 +1832,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
@@ -1941,6 +1955,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGLOG_D("TexSubImage2D_State: Using Pixel Unpack Buffer Object ID: %u",
|
||||
pixelUnpackBufferObject->GetExternalIndex());
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
@@ -2030,6 +2045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
@@ -2535,6 +2551,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__,
|
||||
pixelUnpackBufferObject->GetExternalIndex());
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
@@ -2682,6 +2699,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__,
|
||||
pixelUnpackBufferObject->GetExternalIndex());
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
@@ -2800,6 +2818,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
@@ -6262,6 +6281,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
|
||||
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
|
||||
reinterpret_cast<SizeT>(pixels);
|
||||
}
|
||||
|
||||
@@ -869,6 +869,55 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glNamedBufferPageCommitmentARB, name);
|
||||
GETPROC(glTexPageCommitmentARB, name);
|
||||
GETPROC(glTexBufferARB, name);
|
||||
// ARB_shader_objects and ARB_vertex_shader, by name, because a name is the only way an
|
||||
// application ever asks for an extension entry point. Measured path: kwin_wayland resolves GL
|
||||
// through libepoxy, and libepoxy asks eglGetProcAddress -> EGLImpl::GetProcAddress -> this
|
||||
// table. Before these lines the callers that need the extension whose STRINGS make
|
||||
// checkSupported() pass got a nullptr for every function of it - dlsym on the library found
|
||||
// them, this table did not. The strings and the implementations are advertised from
|
||||
// MG_Impl/GLImpl/Exporting/Definitions.cpp and the two BackendObject_* extension lists.
|
||||
GETPROC(glDeleteObjectARB, name);
|
||||
GETPROC(glGetHandleARB, name);
|
||||
GETPROC(glDetachObjectARB, name);
|
||||
GETPROC(glCreateShaderObjectARB, name);
|
||||
GETPROC(glShaderSourceARB, name);
|
||||
GETPROC(glCompileShaderARB, name);
|
||||
GETPROC(glCreateProgramObjectARB, name);
|
||||
GETPROC(glAttachObjectARB, name);
|
||||
GETPROC(glLinkProgramARB, name);
|
||||
GETPROC(glUseProgramObjectARB, name);
|
||||
GETPROC(glValidateProgramARB, name);
|
||||
GETPROC(glUniform1fARB, name);
|
||||
GETPROC(glUniform2fARB, name);
|
||||
GETPROC(glUniform3fARB, name);
|
||||
GETPROC(glUniform4fARB, name);
|
||||
GETPROC(glUniform1iARB, name);
|
||||
GETPROC(glUniform2iARB, name);
|
||||
GETPROC(glUniform3iARB, name);
|
||||
GETPROC(glUniform4iARB, name);
|
||||
GETPROC(glUniform1fvARB, name);
|
||||
GETPROC(glUniform2fvARB, name);
|
||||
GETPROC(glUniform3fvARB, name);
|
||||
GETPROC(glUniform4fvARB, name);
|
||||
GETPROC(glUniform1ivARB, name);
|
||||
GETPROC(glUniform2ivARB, name);
|
||||
GETPROC(glUniform3ivARB, name);
|
||||
GETPROC(glUniform4ivARB, name);
|
||||
GETPROC(glUniformMatrix2fvARB, name);
|
||||
GETPROC(glUniformMatrix3fvARB, name);
|
||||
GETPROC(glUniformMatrix4fvARB, name);
|
||||
GETPROC(glGetObjectParameterfvARB, name);
|
||||
GETPROC(glGetObjectParameterivARB, name);
|
||||
GETPROC(glGetInfoLogARB, name);
|
||||
GETPROC(glGetAttachedObjectsARB, name);
|
||||
GETPROC(glGetUniformLocationARB, name);
|
||||
GETPROC(glGetActiveUniformARB, name);
|
||||
GETPROC(glGetUniformfvARB, name);
|
||||
GETPROC(glGetUniformivARB, name);
|
||||
GETPROC(glGetShaderSourceARB, name);
|
||||
GETPROC(glBindAttribLocationARB, name);
|
||||
GETPROC(glGetActiveAttribARB, name);
|
||||
GETPROC(glGetAttribLocationARB, name);
|
||||
GETPROC(glDepthRangeArraydvNV, name);
|
||||
GETPROC(glDepthRangeIndexeddNV, name);
|
||||
GETPROC(glBlendBarrierKHR, name);
|
||||
@@ -886,6 +935,8 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glBeginPerfMonitorAMD, name);
|
||||
GETPROC(glEndPerfMonitorAMD, name);
|
||||
GETPROC(glGetPerfMonitorCounterDataAMD, name);
|
||||
GETPROC(glEGLImageTargetTexture2DOES, name);
|
||||
GETPROC(glEGLImageTargetRenderbufferStorageOES, name);
|
||||
GETPROC(glEGLImageTargetTexStorageEXT, name);
|
||||
GETPROC(glEGLImageTargetTextureStorageEXT, name);
|
||||
GETPROC(glLabelObjectEXT, name);
|
||||
|
||||
@@ -1828,6 +1828,11 @@ namespace MobileGL::MG_Pipe {
|
||||
*entry = Entry{};
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
for (auto& table : m_kinds) table.clear();
|
||||
m_band.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Uint32 Gen = 0;
|
||||
@@ -1892,6 +1897,15 @@ namespace MobileGL::MG_Pipe {
|
||||
PublicationLatch().NoteUnpublished(kind, handle);
|
||||
}
|
||||
|
||||
void MGPipeForgetAllPublications() {
|
||||
PublicationLatch().Clear();
|
||||
// The two families whose death asks their own record rather than the latch: vertex
|
||||
// elements (MGPipeEmitVertexElementsDestroyAndFree's RecordIsPublished) and buffers
|
||||
// (MGPipeEmitResourceDestroyAndFree's WasPublished).
|
||||
MGPipeVertexInputEmitterInstance().ForgetAllRecords();
|
||||
MGPipeResourceTrackerInstance().ForgetAllPublished();
|
||||
}
|
||||
|
||||
// THE GATE ITSELF, AS AN OBSERVABLE (ID-39, widened by S-3 / ID-41). Every P4a birth hook
|
||||
// below and every `wants()` row in the walk resolve through FamilyIsLive /
|
||||
// P4aFamilyHasItsConsumer / P4aFamilyDependenciesAreSet, and none of the three is reachable
|
||||
|
||||
@@ -340,6 +340,10 @@ namespace MobileGL::MG_Pipe {
|
||||
const SizeT slot = handle.Slot;
|
||||
return slot < m_bySlot.size() && m_bySlot[slot].Published;
|
||||
}
|
||||
// The session that held every buffer's record is gone (MGPipeForgetAllPublications).
|
||||
void ForgetAllPublished() {
|
||||
for (Entry& entry : m_bySlot) entry.Published = false;
|
||||
}
|
||||
|
||||
// The sticky everBoundAs mask. Sticky exactly as MGPResourceDesc::ImageBindableHint's
|
||||
// everImageBound is: ORed, never cleared, so a buffer that was an element array once
|
||||
|
||||
@@ -247,10 +247,10 @@ namespace MobileGL::MG_Pipe {
|
||||
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
|
||||
header.WritableMask[w] = writableMask[w];
|
||||
}
|
||||
// HostSpanCount IS 0 ALWAYS on Espryt, and that is a ruling rather than an omission:
|
||||
// the second var-tail exists for kCapNeedsHostUboBytes (Magma's named-UBO ring) and
|
||||
// that bit is 0 for the whole of P5 (CONTRACT-P5.md table 0). The codec's host-span
|
||||
// honesty pass is the guard that says so out loud if a backend ever publishes it.
|
||||
// HostSpanCount IS 0 ALWAYS, and that is a ruling rather than an omission: the
|
||||
// second var-tail exists for kCapNeedsHostUboBytes (a Magma named-UBO ring that was
|
||||
// never built) and that bit is RESERVED (P8-F): no server publishes it. The codec's
|
||||
// host-span honesty pass is the guard that says so out loud if a peer sends one.
|
||||
header.HostSpanCount = 0;
|
||||
header.ContentHash = hash;
|
||||
MGPipeRouteSetShaderBuffers(header, m_entries.data());
|
||||
|
||||
@@ -233,9 +233,9 @@ namespace MobileGL::MG_Pipe {
|
||||
desc.StorageKind = MGPipeTextureStorageKindForTarget(texture.GetTarget());
|
||||
desc.BindMask = bindMask;
|
||||
// STICKY AND FOREVER: everImageBound, the client-side answer MGPipeTypes.h asks for.
|
||||
// It is the PREVENTION half of the texture-remint stall class - a texture the server
|
||||
// knows may be image-bound is allocated image-bindable up front, so the re-mint that
|
||||
// would have to pull its texels back never happens.
|
||||
// It PREVENTS the texture re-mint - a texture the server knows may be image-bound is
|
||||
// allocated image-bindable up front, so the server never has to read its levels back
|
||||
// and re-upload them into a new carrier (P9 W2).
|
||||
desc.ImageBindableHint = (bindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
|
||||
if (storageDefined) {
|
||||
const MGPipeTextureExtent extent = MGPipeTextureExtentOf(texture);
|
||||
@@ -702,8 +702,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
|
||||
// no further respecify, that being what immutable means - so for the canonical order
|
||||
// `glTexStorage2D(...); glBindImageTexture(...)` the applier's record kept
|
||||
// ImageBindableHint = 0 for ever and the PREVENTION half of the texture-remint stall
|
||||
// class never fired. So a mask that actually MOVES re-emits the stored descriptor with
|
||||
// ImageBindableHint = 0 for ever and the re-mint PREVENTION never fired. So a mask that actually MOVES re-emits the stored descriptor with
|
||||
// the new mask: every storage-defining field is byte-identical to what the applier
|
||||
// holds, which is exactly the shape wire applies as a METADATA UPDATE - the descriptor
|
||||
// is replaced, no reallocation is acked, and NO pending upload is dropped, so a mask
|
||||
@@ -1470,21 +1469,53 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPBox unionBox = MGPipeBoxOfDirtyRegion(mipmap->GetStorageDirtyRegion(uploadTarget, level));
|
||||
if (unionBox.W == 0 || unionBox.H == 0 || unionBox.D == 0) return false;
|
||||
|
||||
// THE REGION LIST BEHIND THE BOX. 0 is legal and means "the union box is the whole
|
||||
// story" - it covers a single rect (identical to the box by construction), more
|
||||
// rects than the cap, and a summed area so close to the box's that one big upload
|
||||
// beats many small ones. The invariant that makes the server's choice safe is that
|
||||
// the two describe the SAME texels: every rect lies inside the box, and their union
|
||||
// is the box.
|
||||
// THE REGION LIST BEHIND THE BOX IS THE LEVEL'S EXACT DIRTY FOOTPRINT (P11 M2, dev
|
||||
// 9524c688): disjoint boxes holding every texel written since the level was last
|
||||
// clean and no other. The storage's rect list this used to carry only COVERS the
|
||||
// writes - it merges touching rects into their bounding box, folds when full and is
|
||||
// withheld (0) past 3/4 of the union box - and once the far side's image may hold GPU
|
||||
// writes, whatever it uploads beyond the writes goes up over them. Both servers read
|
||||
// the list as the texels to upload: Magma always, Espryt once its driver texture may
|
||||
// hold GPU writes (and otherwise it re-applies the storage's box-or-rects choice
|
||||
// itself, so the Mali job count is the server's call as before).
|
||||
//
|
||||
// 0 IS NOW THE WHOLE-LEVEL SPELLING - a footprint that is the whole level - with one
|
||||
// exception (below). Every other write carries its boxes, a single one included, so the
|
||||
// applier's accumulation (boxes union, lists concatenate, "0 regions" makes the entry
|
||||
// box only) never widens two exact contributions into their union box. THE EXCEPTION:
|
||||
// a footprint larger than one record may carry (kMGPipeMaxPendingUploadRegions) falls
|
||||
// back to the storage's rect list - a COVER, stated once in the log - and when that list
|
||||
// is withheld (0: its rects fill 3/4 of the union box) this 0 is a PARTIAL, INEXACT
|
||||
// box. A reader that takes "box only" as "the client owns every texel of the box"
|
||||
// (AdoptDriverLevelIntoShadow's covering-box rule) is exact everywhere but here.
|
||||
//
|
||||
// READ AS BOXES, ONCE, because both shapes below want them in that form: the
|
||||
// whole-level record converts each one straight back through MGPipeBuildSubRegion,
|
||||
// and a piece clips each one to its own slab.
|
||||
MG_State::GLState::MipmapDirtyRegion rects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
const SizeT rectCount = mipmap->GetStorageDirtyRects(
|
||||
uploadTarget, level, rects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||
MGPBox rectBoxes[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
for (SizeT i = 0; i < rectCount; ++i) rectBoxes[i] = MGPipeBoxOfDirtyRegion(rects[i]);
|
||||
mipmap->GetStorageDirtyFootprint(uploadTarget, level, m_footprint);
|
||||
m_rectBoxes.clear();
|
||||
const Bool wholeLevelWrite =
|
||||
m_footprint.size() == 1 && m_footprint.front().lo.x() <= 0 && m_footprint.front().lo.y() <= 0 &&
|
||||
m_footprint.front().lo.z() <= 0 && m_footprint.front().hi.x() >= levelSize.x() &&
|
||||
m_footprint.front().hi.y() >= levelSize.y() &&
|
||||
m_footprint.front().hi.z() >= std::max<Int>(levelSize.z(), 1);
|
||||
if (wholeLevelWrite) {
|
||||
// The box is the story.
|
||||
} else if (m_footprint.size() <= static_cast<SizeT>(kMGPipeMaxPendingUploadRegions)) {
|
||||
for (const auto& box : m_footprint) m_rectBoxes.push_back(MGPipeBoxOfDirtyRegion(box));
|
||||
} else {
|
||||
MGLOG_W_ONCE("MGPipe: texture {slot=%u, gen=%u} level %u has %zu disjoint dirty boxes, more than one "
|
||||
"record carries (%u); it goes out as the storage's rect list, which covers the writes "
|
||||
"but not exactly",
|
||||
pending.Handle.Slot, pending.Handle.Gen, static_cast<Uint>(level), m_footprint.size(),
|
||||
kMGPipeMaxPendingUploadRegions);
|
||||
MG_State::GLState::MipmapDirtyRegion rects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
const SizeT coverCount = mipmap->GetStorageDirtyRects(
|
||||
uploadTarget, level, rects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||
for (SizeT i = 0; i < coverCount; ++i) m_rectBoxes.push_back(MGPipeBoxOfDirtyRegion(rects[i]));
|
||||
}
|
||||
const SizeT rectCount = m_rectBoxes.size();
|
||||
const MGPBox* rectBoxes = m_rectBoxes.data();
|
||||
|
||||
Bool dispatched = false;
|
||||
Bool accepted = true;
|
||||
@@ -1624,6 +1655,10 @@ namespace MobileGL::MG_Pipe {
|
||||
Vector<Entry> m_renderbuffers;
|
||||
Vector<DrainEntry> m_drain;
|
||||
Vector<MGPSubRegion> m_regions;
|
||||
// EmitOneLevel's scratch, kept for its capacity: the level's exact footprint and the
|
||||
// boxes the record's regions are built from.
|
||||
Vector<MG_State::GLState::MipmapDirtyRegion> m_footprint;
|
||||
Vector<MGPBox> m_rectBoxes;
|
||||
Bool m_draining = false;
|
||||
|
||||
MGPResourceDesc m_lastDesc{};
|
||||
|
||||
@@ -263,8 +263,8 @@ namespace MobileGL::MG_Pipe {
|
||||
if (vao) {
|
||||
if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) {
|
||||
m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId());
|
||||
// The ELEMENT_ARRAY bit, and it is the one the split path keys on
|
||||
// (kCapNeedsHostIndexBytes -> restart rewriting, multi-draw flattening).
|
||||
// The INDEX|ELEMENT_ARRAY bind bits. ELEMENT_ARRAY's D-B7 reader
|
||||
// (kCapNeedsHostIndexBytes) is RESERVED (P8-F) and was never built.
|
||||
// Noted at every draw for RefreshBindMask's reason: an EBO defined through
|
||||
// DSA and unbound before its last respecify would otherwise never publish
|
||||
// it, and getting that bit wrong is invisible in monolith.
|
||||
@@ -351,6 +351,14 @@ namespace MobileGL::MG_Pipe {
|
||||
m_boundLifetimeId = 0;
|
||||
}
|
||||
|
||||
// The session that held the records is gone (MGPipeForgetAllPublications): both halves go,
|
||||
// the record half included, because no applier holds any of them any more.
|
||||
void ForgetAllRecords() {
|
||||
for (Latch& latch : m_latch) latch = Latch{};
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
}
|
||||
|
||||
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,11 @@ namespace MGITest {
|
||||
};
|
||||
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out);
|
||||
|
||||
// The process-wide texture-remint pull count (PipeStats "tex-remint-pulls", `trp=` on the
|
||||
// The process-wide texture re-mint count (PipeStats "tex-remint-pulls", `trp=` on the
|
||||
// summary line; ROADMAP open question 2). Arms the PipeStats counters for this process on
|
||||
// the first call, which is what lets a case read the number without a stats-enabled lane.
|
||||
// Process-wide means it sees the SERVER's count only where the server is this process:
|
||||
// monolith and inproc, never spawn or tcp.
|
||||
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out);
|
||||
// Espryt's count of texture uploads it actually issued (PipeStats "tex-upload-emissions"):
|
||||
// what tells a CONSUMED pending upload apart from a DROPPED one, since the record's set is
|
||||
|
||||
@@ -25,6 +25,9 @@ foreach(listName IN ITEMS @MGL_SPLIT_ARM_TEST_LISTS@)
|
||||
elseif(entry MATCHES "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares$")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||
"MGITEST_PMAP_LANE=1;MGITEST_PERSISTENT_MAP_ARM=emulated;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1")
|
||||
elseif(entry MATCHES "LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares$")
|
||||
# P11 A2: every split arm runs T2, so the arena is declared emulated.
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MGITEST_PERSISTENT_MAP_ARM=emulated")
|
||||
elseif(entry MATCHES "CtWireScenario\\.")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_TEST_SERVER_COUNTERS=1")
|
||||
endif()
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <MG_Remote/Client/CapsMirror.h>
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/Client/EmitTables.h>
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#include <chrono>
|
||||
#include <atomic>
|
||||
@@ -164,6 +165,14 @@ namespace MGITest {
|
||||
state.cmdWrapPads = encoder.CmdWrapPads();
|
||||
state.cmdBytesWritten = encoder.CmdBytesWritten();
|
||||
state.stageReclaimWaits = encoder.StageReclaimWaits();
|
||||
state.replyPostings = session->ReplyPostings();
|
||||
state.packBufferReadbackMarks = MobileGL::MG_Remote::Client::ProducerMarkCount(
|
||||
MobileGL::MG_Remote::Client::GpuWriteProducer::ReadPixelsPackBuffer);
|
||||
const auto fences = MobileGL::MG_Remote::Client::ReadFencePollCounters();
|
||||
state.fenceLocalAnswers = fences.LocalAnswers;
|
||||
state.fenceEscalations = fences.Escalations;
|
||||
state.fenceRoundTrips = fences.RoundTrips;
|
||||
state.fenceServerReports = fences.ServerReports;
|
||||
}
|
||||
#endif
|
||||
return state;
|
||||
|
||||
@@ -119,6 +119,26 @@ namespace MGITest {
|
||||
unsigned long long cmdWrapPads = 0;
|
||||
unsigned long long cmdBytesWritten = 0;
|
||||
unsigned long long stageReclaimWaits = 0;
|
||||
|
||||
// ---- P9 (W1, CONTRACT-P9.md §1): the pack-buffer readback's two proofs -------------
|
||||
//
|
||||
// replyPostings: records this client published on a row that carries a reply slot
|
||||
// (ClientSession::ReplyPostings, always counted - it is the create window's clock, not a
|
||||
// stats counter). A read into a pack buffer that moves it went through the reply form.
|
||||
// packBufferReadbackMarks: GpuWritePending's row-4 producer count - the buffers a pack
|
||||
// read marked GPU-written. The reply form marks nothing, so the pair tells the two forms
|
||||
// apart from both sides.
|
||||
unsigned long long replyPostings = 0;
|
||||
unsigned long long packBufferReadbackMarks = 0;
|
||||
|
||||
// ---- P10 (CONTRACT-P10.md §1): what the fence polls cost ----------------------------
|
||||
// EmitTables.h FencePollCounters, verbatim: polls answered with no record, zero-timeout
|
||||
// polls escalated to a round trip, every fence answer that crossed, and the server's
|
||||
// kEventFenceSignaled reports consumed.
|
||||
unsigned long long fenceLocalAnswers = 0;
|
||||
unsigned long long fenceEscalations = 0;
|
||||
unsigned long long fenceRoundTrips = 0;
|
||||
unsigned long long fenceServerReports = 0;
|
||||
};
|
||||
|
||||
SplitRuntimeState PeekSplitRuntime();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "WireDeclinePeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h>
|
||||
#include <cstring>
|
||||
#define MGITEST_WIRE_DECLINE_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_WIRE_DECLINE_PEEK_LIVE)
|
||||
bool PeekWireDeclineCount(const char* row, unsigned long long* out) {
|
||||
if (row == nullptr || out == nullptr) return false;
|
||||
namespace Magma = MobileGL::MG_Backend::DirectVulkan;
|
||||
for (MobileGL::SizeT i = 0; i < Magma::WireDeclineTally::kSiteCount; ++i) {
|
||||
const auto site = static_cast<Magma::WireDeclineSite>(i);
|
||||
if (std::strcmp(Magma::WireDeclineSiteName(site), row) != 0) continue;
|
||||
*out = Magma::WireDeclineTally::Get(site);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
bool PeekWireDeclineCount(const char*, unsigned long long*) { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,20 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P8-SV: one row of the Magma wire arm's decline tally (WireDeclines.def, the counts behind the
|
||||
// server's MGWIRE-DECLINES lines), read as a run total out of THIS process - WireIndirectPeek.h's
|
||||
// shape. Process-wide means the SERVER's counts only where the server is this process: the inproc
|
||||
// arm, never spawn or tcp, and never the monolith arm, which takes no wire path. False where the
|
||||
// reading cannot be taken: a pull build, Android, or a name that is not a row.
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
bool PeekWireDeclineCount(const char* row, unsigned long long* out);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,38 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "WireIndirectPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#define MGITEST_WIRE_INDIRECT_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_WIRE_INDIRECT_PEEK_LIVE)
|
||||
bool PeekWireIndirectCounters(WireIndirectCounters* out) {
|
||||
if (out == nullptr) return false;
|
||||
namespace Stats = MobileGL::MG_Util::PipeStats;
|
||||
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
|
||||
out->nativeDraws = Stats::TotalCalls(Stats::CallClass::WireIndirectNativeDraws);
|
||||
out->cpuExpansions = Stats::TotalCalls(Stats::CallClass::WireIndirectCpuExpansions);
|
||||
out->barriers = Stats::TotalCalls(Stats::CallClass::WireIndirectBarriers);
|
||||
out->hostWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaits);
|
||||
out->indirectWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaitsIndirect);
|
||||
out->nativeDispatches = Stats::TotalCalls(Stats::CallClass::WireIndirectNativeDispatches);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool PeekWireIndirectCounters(WireIndirectCounters*) { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,29 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P8-D: the Magma wire arm's indirect-draw counters (PipeStats `windr[...]`), read as run totals
|
||||
// out of THIS process - P4aFinalFixPeek.cpp's shape. Process-wide means the SERVER's counts only
|
||||
// where the server is this process: monolith and inproc, never spawn or tcp. The first call arms
|
||||
// the counters (a case reads before and after its workload, so it must peek before it draws).
|
||||
// False where the reading cannot be taken: a pull build, Android.
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
struct WireIndirectCounters {
|
||||
unsigned long long nativeDraws = 0; // wire-indirect-native-draws (`wind`)
|
||||
unsigned long long cpuExpansions = 0; // wire-indirect-cpu-expansions (`wixp`)
|
||||
unsigned long long barriers = 0; // wire-indirect-barriers (`wibar`)
|
||||
unsigned long long hostWaits = 0; // wire-host-waits (`whw`)
|
||||
unsigned long long indirectWaits = 0; // wire-host-waits-indirect (`whwi`)
|
||||
unsigned long long nativeDispatches = 0; // P8-SV: wire-indirect-native-dispatches (`wdsp`)
|
||||
};
|
||||
|
||||
bool PeekWireIndirectCounters(WireIndirectCounters* out);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,244 @@
|
||||
# P8-A (ID-P8-2): the monolith cases' split, spawn and tcp registrations.
|
||||
#
|
||||
# Included ONCE from CMakeLists.txt, inside `if (MOBILEGL_BUILD_DISAGGREGATED)`, and the place is
|
||||
# load-bearing twice over: it must come BEFORE the Magma tier-2 replay (every
|
||||
# mgl_itest_register_split_arms call here is replayed onto DirectVulkan's informational
|
||||
# integration-magma-all-{split,spawn,tcp} arms from MGL_SPLIT_ARM_CALLS) and BEFORE the
|
||||
# SplitLogPaths configure_file at the end of that file (every entry here gets its private
|
||||
# MOBILEGL_LOG_FILE_PATH from MGL_SPLIT_ARM_TEST_LISTS, or SplitLogPaths.PrivateAndDistinct reds).
|
||||
#
|
||||
# THE GATE IS scripts/ci/split_coverage.py: after the arm and tail segments come off, every
|
||||
# monolith DirectGLES. / DirectVulkan. case is on a gated split arm or is named, with a reason
|
||||
# class, in scripts/data/split_coverage_exemptions.txt. Deleting a line below reds it BY NAME.
|
||||
# The per-case census this list was cut from (inproc / spawn / tcp, junit per arm) is
|
||||
# docs/Disaggregated/notes/p8/A.md.
|
||||
|
||||
# ---- (a) THE AMBIENT CASES -----------------------------------------------------------------
|
||||
#
|
||||
# Every case here was registered on monolith only, and on all three arms under the armed split
|
||||
# environment it either PASSED (records crossed: ScenarioFixture's EmitSeq assertion is armed on
|
||||
# these arms) or SKIPPED for the same reason its monolith entry skips on this host (ClipDistance,
|
||||
# DualSourceBlend, IterationRP*, SsboDeclarationForm's two known gaps, and the knob-pinned cases
|
||||
# the (a') block below runs with their knob). Cases that belong to monolith by nature are in the
|
||||
# exemption table instead, never here: no-records (limits / shader-binary queries answered from
|
||||
# the caps mirror), single-backend (Magma-only lanes and declines), mechanism, pending-fix.
|
||||
#
|
||||
# ONE FILTER, not one call per scenario: each call is three discovery runs plus three more for
|
||||
# the Magma replay. Explicit names where a scenario is split between registered and exempt
|
||||
# cases; `Scenario.*` only where every case of the scenario is here.
|
||||
set(MGL_P8A_AMBIENT_CASES
|
||||
AdvertisedLimitsScenario.ImageUnitBindingsAreReportedFromTheFrontendState
|
||||
AdvertisedLimitsScenario.IndexedBufferBindingsAreReportedVerbatimOnBothWidths
|
||||
BufferTextureScenario.*
|
||||
ClearTexImageUndefinedLevelZeroScenario.*
|
||||
ClipDistanceScenario.*
|
||||
CopyImageLevelRangeScenario.*
|
||||
CopyImagePacked16Scenario.*
|
||||
CrossFrameBufferScenario.*
|
||||
DefaultFramebufferAcrossSwapScenario.*
|
||||
DepthStencilReadbackAttachmentShapeScenario.DepthOfALayeredCubeAttachmentReadsBack
|
||||
DepthStencilReadbackAttachmentShapeScenario.DepthOfAPlainTexture2DAttachmentReadsBack
|
||||
DepthStencilReadbackAttachmentShapeScenario.DepthOfAnArrayLayerAttachmentReadsBack
|
||||
DepthStencilReadbackAttachmentShapeScenario.PackedArrayLayerAttachmentReadsBackBothAspects
|
||||
DepthStencilReadbackScenario.*
|
||||
DoublePrecisionScenario.*
|
||||
DualSourceBlendScenario.*
|
||||
EmptyScissorScenario.*
|
||||
F1WireScenario.ColorBlitToDefaultFromANonSampleableSourceDegrades
|
||||
F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect
|
||||
F1WireScenario.ColorBlitToDefaultIgnoresViewportAndPreservesOrientation
|
||||
F1WireScenario.ComputeImageStoreFramebufferPixels
|
||||
F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer
|
||||
F1WireScenario.ComputeWrittenVertexAndIndexBuffersDrawAndReadBack
|
||||
F1WireScenario.CopyImageInPlaceAcrossLevelsAndLayers
|
||||
F1WireScenario.EnabledVertexBufferDrawKeepsNamedP7Fatal
|
||||
F1WireScenario.GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels
|
||||
F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow
|
||||
F1WireScenario.MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch
|
||||
F1WireScenario.PartialTextureUploadPreservesGpuClearPixels
|
||||
F1WireScenario.SrgbDrawTracksFramebufferConversion
|
||||
F1WireScenario.StreamedBufferSubDataBeforeEachDrawIsOrdered
|
||||
F1WireScenario.Texture1DFramebufferClearPixels
|
||||
F1WireScenario.TextureReadbackLargerThanAReplySlotContainsGpuWrites
|
||||
F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer
|
||||
F1WireScenario.UniformBufferRangeAndRebindPixels
|
||||
F1WireScenario.VertexIdSamplerAndScalarUniformPixels
|
||||
FormatlessImageBakeScenario.*
|
||||
FragCoordOriginScenario.*
|
||||
FragmentOutputArrayIndexScenario.*
|
||||
GeometryDrawModeScenario.*
|
||||
GuiBatchScenario.*
|
||||
# dev f973008c merge: the empty-binding uniform block (853c5f12 + P11 7784d6bb)
|
||||
UnboundUniformBlockScenario.*
|
||||
ImageFormatQualifierScenario.*
|
||||
ImageLoadStoreSsoScenario.PerElementImageUnitsReachAPipelineDraw
|
||||
ImageLoadStoreSsoScenario.ReassigningAnImageUnitBetweenDrawsReachesTheNextDraw
|
||||
IterationRPFirstReductionScenario.*
|
||||
IterationRPProgram203Scenario.*
|
||||
IterationRPScratchFixScenario.*
|
||||
LayeredAttachmentBarrierScenario.*
|
||||
LayeredAttachmentShapeScenario.*
|
||||
NonCoreImageFormatScenario.*
|
||||
OrientationScenario.*
|
||||
P4aFinalFixScenario.*
|
||||
P4aSeamAuditScenario.*
|
||||
PipelineFailureScenario.*
|
||||
PointSizeDemotionScenario.*
|
||||
ProgramPipelineScenario.ASamplerUnitRewrittenBetweenDrawsKeepsPaintingTheRightTexture
|
||||
ProgramPipelineScenario.AStageProgramsUniformBlockBindingReachesThePipelineDraw
|
||||
ProgramPipelineScenario.ATwoStagePipelinePaintsWhatItsStagesDescribe
|
||||
ProgramPipelineScenario.AUniformDeclaredInTwoStagesKeepsTheValueTheWrittenStageHolds
|
||||
ProgramPipelineScenario.ComputeAndGraphicsStagesShareOnePipeline
|
||||
ProgramPipelineScenario.RebindingAUniformBlockBetweenDrawsReachesTheNextDraw
|
||||
ProgramPipelineScenario.SwitchingBetweenAPipelineAndAMonolithicProgramLeavesNoError
|
||||
ProgramPipelineScenario.UniformsGoToTheActiveShaderProgram
|
||||
RenderbufferBlendFormatScenario.*
|
||||
ResidentIndexScenario.*
|
||||
SampleMaskScopeScenario.*
|
||||
SampleVariablesScenario.*
|
||||
SnormAttachmentScenario.*
|
||||
SpirvShaderBinaryScenario.SpecializedModulesLinkAndRenderWithTheirConstantsApplied
|
||||
SsboArrayDynamicIndexScenario.*
|
||||
SsboArrayLengthScenario.*
|
||||
SsboDeclarationFormScenario.*
|
||||
StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding
|
||||
StreamedArenaScenario.*
|
||||
TessellationDrawModeScenario.*
|
||||
ThreeChannelAttachmentScenario.*
|
||||
UnboundCounterBlockScenario.*
|
||||
UnlocatedIoBlockScenario.*
|
||||
UnwrittenPositionOutputScenario.ARedeclaredButUnwrittenPositionStillDraws
|
||||
UnwrittenPositionOutputScenario.AShaderWithNoPositionBlockStillDraws
|
||||
UnwrittenPositionOutputScenario.AWrittenRedeclaredPositionStillDraws
|
||||
UnwrittenPositionOutputScenario.CapturingAWrittenPositionStillDraws
|
||||
VertexArrayEnableDisableScenario.*
|
||||
VertexAttribBindingScenario.*
|
||||
ViewportArrayScenario.*
|
||||
)
|
||||
list(JOIN MGL_P8A_AMBIENT_CASES ":" mglP8aAmbientFilter)
|
||||
mgl_itest_register_split_arms("${mglP8aAmbientFilter}" "")
|
||||
|
||||
# ---- (a') THE SERVER-READ KNOB LANES, EACH WITH A TCP SUPERVISOR OF ITS OWN ------------------
|
||||
#
|
||||
# The monolith file pins five whole-process knobs in lanes of their own (PointSizeDemotion.,
|
||||
# UnlocatedIoBlocks., NoViewportArrayEmulation., WidenedPacked16., ForcedDepthStencilEmulation.),
|
||||
# because without them the cases are unfalsifiable on llvmpipe or skip. Every one of the five is
|
||||
# read through ConfigLoader IN THE PROCESS THAT RUNS THE BACKEND - the server. Under inproc that
|
||||
# is this process and under spawn the child inherits ::environ, so the entry's ENVIRONMENT
|
||||
# reaches it; under tcp the server is a supervisor started by a fixture, and the lane-wide
|
||||
# TcpServer.Start supervisor never sees a per-entry variable. MEASURED on this tree before this
|
||||
# block existed: with the knob in the client's environment only, the Tcp copies of
|
||||
# PointSizeDemotion's and UnlocatedIoBlocks' arming cases read no arming line and
|
||||
# NoViewportArrayEmulation's negative control saw the emulation still on - red, where the Split
|
||||
# and Spawn copies were green.
|
||||
#
|
||||
# THE KNOB CANNOT GO ON THE SHARED FIXTURE (the IC TcpServer.Start precedent, which works for
|
||||
# MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW only because every arm sets that one): it would retarget
|
||||
# every other tcp case - viewport emulation off breaks ViewportArrayScenario's positive cases,
|
||||
# the IO-block strip rewrites every interface block. So each lane gets a supervisor of its own,
|
||||
# on its own port, started by its own fixture with the knob in the fixture's ENVIRONMENT.
|
||||
#
|
||||
# THE PORTS: the lane endpoint's port + MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET + the lane's index.
|
||||
# The default offset (1000) keeps the five clear of every per-tree lane endpoint in use (40613
|
||||
# CI, 407xx / 409xx split and verify builds, 484x0 package trees). One overlap remains: a tree
|
||||
# whose split and verify builds sit one port apart (409x3 / 409x4) gets knob ports one apart as
|
||||
# well, so the two builds must not run integration-tcp at the same moment - set the cache
|
||||
# variable differently in one of them if they ever do.
|
||||
#
|
||||
# THE LOCK IS STILL mobilegl-tcp: spawn_lane_parity.py / junit_tally.py require it of every
|
||||
# integration-tcp entry, and one tcp session at a time is what every other tcp entry assumes.
|
||||
#
|
||||
# NOT FOR DirectVulkan: all five knobs are Espryt's (Magma routes viewports natively, has no
|
||||
# IO-block strip, no packed-16 widening, no DS readback emulation; point-size demotion's Magma
|
||||
# lane is monolith-only and stays so here).
|
||||
set(MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET 1000 CACHE STRING
|
||||
"Port offset (from MOBILEGL_ITEST_TCP_ENDPOINT's) of the per-knob loopback supervisors")
|
||||
if(NOT MOBILEGL_ITEST_TCP_ENDPOINT MATCHES "^(tcp://.+):([0-9]+)$")
|
||||
message(FATAL_ERROR "P8-A: MOBILEGL_ITEST_TCP_ENDPOINT '${MOBILEGL_ITEST_TCP_ENDPOINT}' is not "
|
||||
"tcp://<host>:<port>; the per-knob tcp supervisors derive their ports from it")
|
||||
endif()
|
||||
set(mglP8aTcpHost "${CMAKE_MATCH_1}")
|
||||
set(mglP8aTcpPort "${CMAKE_MATCH_2}")
|
||||
|
||||
set(MGL_P8A_KNOB_LANES PointSizeDemotion UnlocatedIoBlocks NoViewportArrayEmulation WidenedPacked16
|
||||
ForcedDs)
|
||||
set(MGL_P8A_KNOB_FILTER_PointSizeDemotion "PointSizeDemotionScenario.*")
|
||||
set(MGL_P8A_KNOB_ENV_PointSizeDemotion "MOBILEGL_POINT_SIZE_DEMOTION=1")
|
||||
set(MGL_P8A_KNOB_FILTER_UnlocatedIoBlocks "UnlocatedIoBlockScenario.*")
|
||||
set(MGL_P8A_KNOB_ENV_UnlocatedIoBlocks "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1")
|
||||
# One case, as on monolith: the three positive cases describe behaviour the backend does not
|
||||
# have with the emulation off.
|
||||
set(MGL_P8A_KNOB_FILTER_NoViewportArrayEmulation
|
||||
"ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero")
|
||||
set(MGL_P8A_KNOB_ENV_NoViewportArrayEmulation "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0")
|
||||
set(MGL_P8A_KNOB_FILTER_WidenedPacked16 "CopyImagePacked16Scenario.*")
|
||||
set(MGL_P8A_KNOB_ENV_WidenedPacked16 "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1")
|
||||
# ForcedDs keeps the spelling the existing Split/Spawn DepthStencilReadbackMatrix block uses. The
|
||||
# default-framebuffer case was left out here while it was red on spawn and tcp; P8-SE fixed it
|
||||
# (notes/p8/SE.md) and it runs under the knob on every arm like the rest.
|
||||
set(MGL_P8A_KNOB_FILTER_ForcedDs
|
||||
"DepthStencilReadbackScenario.*:DepthStencilReadbackAttachmentShapeScenario.*")
|
||||
set(MGL_P8A_KNOB_ENV_ForcedDs "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1")
|
||||
# The Matrix scenario's Tcp copy moves HERE from the ForcedDs block (same names): that block
|
||||
# put the knob in the client's environment only, so its Tcp arm ran the native readback path.
|
||||
set(MGL_P8A_KNOB_TCP_FILTER_ForcedDs
|
||||
"DepthStencilReadbackMatrixScenario.*:${MGL_P8A_KNOB_FILTER_ForcedDs}")
|
||||
|
||||
set(mglP8aKnobIndex 0)
|
||||
foreach(mglP8aLane IN LISTS MGL_P8A_KNOB_LANES)
|
||||
string(TOLOWER "${mglP8aLane}" mglP8aLaneLower)
|
||||
math(EXPR mglP8aKnobPort "${mglP8aTcpPort} + ${MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET} + ${mglP8aKnobIndex}")
|
||||
math(EXPR mglP8aKnobIndex "${mglP8aKnobIndex} + 1")
|
||||
set(mglP8aKnobEndpoint "${mglP8aTcpHost}:${mglP8aKnobPort}")
|
||||
set(mglP8aKnobFixture "mobilegl-tcp-p8a-${mglP8aLaneLower}")
|
||||
set(mglP8aKnobState "${CMAKE_CURRENT_BINARY_DIR}/tcp-server-p8a-${mglP8aLaneLower}.json")
|
||||
set(mglP8aKnob "${MGL_P8A_KNOB_ENV_${mglP8aLane}}")
|
||||
add_test(NAME TcpServer.Start.P8a${mglP8aLane} COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
|
||||
start --server "${MGL_ITEST_SPLIT_SERVER_PATH}" --endpoint "${mglP8aKnobEndpoint}"
|
||||
--state "${mglP8aKnobState}")
|
||||
add_test(NAME TcpServer.Stop.P8a${mglP8aLane} COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
|
||||
stop --state "${mglP8aKnobState}")
|
||||
# The texture-view opt-in as on the shared fixture, so this supervisor differs from it in
|
||||
# the knob alone.
|
||||
set_tests_properties(TcpServer.Start.P8a${mglP8aLane} PROPERTIES
|
||||
FIXTURES_SETUP ${mglP8aKnobFixture} TIMEOUT 20
|
||||
ENVIRONMENT "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1;${mglP8aKnob}")
|
||||
set_tests_properties(TcpServer.Stop.P8a${mglP8aLane} PROPERTIES
|
||||
FIXTURES_CLEANUP ${mglP8aKnobFixture} TIMEOUT 20)
|
||||
|
||||
foreach(mglP8aArm Split Spawn Tcp)
|
||||
string(TOLOWER "${mglP8aArm}" mglP8aArmLower)
|
||||
string(TOUPPER "${mglP8aArm}" mglP8aArmUpper)
|
||||
set(mglP8aEnvironment "${MGL_ITEST_GLES_${mglP8aArmUpper}_ENVIRONMENT}")
|
||||
set(mglP8aFilter "${MGL_P8A_KNOB_FILTER_${mglP8aLane}}")
|
||||
set(mglP8aProperties)
|
||||
if(mglP8aArm STREQUAL "Tcp")
|
||||
string(REPLACE "MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT}"
|
||||
"MOBILEGL_IPC_CONTROL=${mglP8aKnobEndpoint}"
|
||||
mglP8aEnvironment "${mglP8aEnvironment}")
|
||||
if(mglP8aEnvironment STREQUAL MGL_ITEST_GLES_TCP_ENVIRONMENT)
|
||||
message(FATAL_ERROR "P8-A: the Tcp arm environment names no "
|
||||
"MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT} to retarget")
|
||||
endif()
|
||||
set(mglP8aProperties FIXTURES_REQUIRED ${mglP8aKnobFixture} RESOURCE_LOCK mobilegl-tcp)
|
||||
if(DEFINED MGL_P8A_KNOB_TCP_FILTER_${mglP8aLane})
|
||||
set(mglP8aFilter "${MGL_P8A_KNOB_TCP_FILTER_${mglP8aLane}}")
|
||||
endif()
|
||||
endif()
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.${mglP8aArm}.${mglP8aLane}."
|
||||
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
|
||||
TEST_FILTER "${mglP8aFilter}"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-${mglP8aArmLower}"
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${mglP8aEnvironment}\;${mglP8aKnob}"
|
||||
${mglP8aProperties}
|
||||
)
|
||||
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
|
||||
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
|
||||
endforeach()
|
||||
endforeach()
|
||||
message(STATUS "Integration tests: P8-A registers ${mglP8aKnobIndex} server-knob lane(s) on "
|
||||
"split/spawn/tcp, each tcp arm on its own supervisor")
|
||||
@@ -0,0 +1,373 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageStoreReadbackScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glCopyImageSubData INTO A LEVEL THE DRIVER WILL NOT READ BACK (P8-E,
|
||||
// docs/Disaggregated/notes/p8/E.md).
|
||||
//
|
||||
// The formats are the ones an ES driver keeps as texture-only storage: GL_RGB16 (EXT_texture_norm16
|
||||
// makes the three-channel spelling sampleable, never colour-renderable) and the 16-bit SNORM family
|
||||
// (renderable only with EXT_render_snorm, which Adreno does not have). Their glGetTexImage cannot go
|
||||
// through a framebuffer on such a driver, so something other than the GPU image has to answer it:
|
||||
// the CPU shadow on the monolith arm, the server's staged store on a split one. A copy writes the
|
||||
// GPU image and nothing else, which is exactly the write such a byte store never sees.
|
||||
//
|
||||
// GL_RGB9_E5 is the positive control: a 32-bit packed format, read through the scratch GL_R32UI
|
||||
// image whoever wrote the level, so it must stay green under every configuration below.
|
||||
//
|
||||
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp, every case, with no knob:
|
||||
// on llvmpipe every one of these formats is attachable, so these runs pin the GPU route and the
|
||||
// copy itself. Magma is the control backend - it reads the VkImage back whatever the format, so it
|
||||
// has no store route to exercise.
|
||||
//
|
||||
// THE KNOB. MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h> is read BY THE ESPRYT SERVER
|
||||
// (WireTextureReadback.inc, ReadTextureImageWire): a colour level of exactly that extent is
|
||||
// treated as a level the driver refused to attach, which is what an Adreno does for every one of
|
||||
// these formats and what llvmpipe can be made to do no other way. The `DirectGLES.<arm>.StoreRead.`
|
||||
// entries set it, and this scenario then builds its textures at THAT extent instead of the default
|
||||
// one - so the knob is scoped by the extent, and the tcp lane's shared supervisor can carry it
|
||||
// (the TcpServer.Start fixture's ENVIRONMENT) without touching any other entry, this scenario's
|
||||
// own knob-free tcp entries included. Under the knob every read must still be right, and the
|
||||
// server log must name the store route - otherwise the entry would be pinning the GPU route a
|
||||
// second time under a name that claims otherwise.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kRefuseExtentKnob = "MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT";
|
||||
// What the Espryt server logs (once per process) when it answers a read from its store.
|
||||
constexpr const char* kStoreReadMarker = "texture-readback-from-store";
|
||||
// The knob-free extent. Deliberately not the extent the knob entries use, so the tcp
|
||||
// supervisor - which carries the knob for the whole lane - leaves these textures alone.
|
||||
constexpr int kDefaultWidth = 12;
|
||||
constexpr int kDefaultHeight = 6;
|
||||
|
||||
struct CopyFormat {
|
||||
GLenum internalFormat;
|
||||
GLenum format;
|
||||
GLenum type;
|
||||
int components; // 16-bit components per texel; 0 for the one 32-bit packed format
|
||||
bool isSigned;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
const CopyFormat kFormats[] = {
|
||||
{GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT, 3, false, "rgb16"},
|
||||
{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, 4, true, "rgba16_snorm"},
|
||||
{GL_RGB16_SNORM, GL_RGB, GL_SHORT, 3, true, "rgb16_snorm"},
|
||||
{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT, 4, false, "rgba16"},
|
||||
{GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, 0, false, "rgb9_e5"},
|
||||
};
|
||||
|
||||
int TexelBytes(const CopyFormat& format) { return format.components == 0 ? 4 : format.components * 2; }
|
||||
|
||||
// Every word differs from its neighbours and from the other patterns, so a mis-addressed
|
||||
// row, a swapped endpoint or a copy that never happened cannot cancel out. SNORM never
|
||||
// gets -32768: it and -32767 are the same value (-1.0), and a read that goes through a
|
||||
// normalized float is entitled to answer either.
|
||||
std::vector<std::uint8_t> MakeTexels(const CopyFormat& format, int width, int height, std::uint32_t seed) {
|
||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
std::vector<std::uint8_t> bytes(texels * static_cast<std::size_t>(TexelBytes(format)));
|
||||
if (format.components == 0) {
|
||||
for (std::size_t i = 0; i < texels; ++i) {
|
||||
std::uint32_t word = (static_cast<std::uint32_t>(i) + 1u) * 2654435761u ^ seed * 40503u;
|
||||
word ^= word >> 13;
|
||||
std::memcpy(bytes.data() + i * 4, &word, 4);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
const std::size_t words = texels * static_cast<std::size_t>(format.components);
|
||||
for (std::size_t i = 0; i < words; ++i) {
|
||||
std::uint32_t mix = (static_cast<std::uint32_t>(i) + 1u) * 2654435761u ^ seed * 40503u;
|
||||
mix ^= mix >> 15;
|
||||
std::uint16_t value = static_cast<std::uint16_t>(mix >> 8);
|
||||
if (format.isSigned && value == 0x8000u) value = 0x8001u;
|
||||
std::memcpy(bytes.data() + i * 2, &value, 2);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// `image` with the w x h block at (dx, dy) replaced by the block of `source` at (sx, sy).
|
||||
std::vector<std::uint8_t> WithBlock(std::vector<std::uint8_t> image, int width, const std::vector<std::uint8_t>& source,
|
||||
int sourceWidth, int sx, int sy, int dx, int dy, int w, int h,
|
||||
int texelBytes) {
|
||||
for (int row = 0; row < h; ++row) {
|
||||
std::memcpy(image.data() + (static_cast<std::size_t>(dy + row) * width + dx) * texelBytes,
|
||||
source.data() + (static_cast<std::size_t>(sy + row) * sourceWidth + sx) * texelBytes,
|
||||
static_cast<std::size_t>(w) * texelBytes);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
class CopyImageStoreReadbackScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_wire = SplitRuntimeSkipReason().empty();
|
||||
m_espryt = Gl().BackendName() == "DirectGLES";
|
||||
if (const char* knob = std::getenv(kRefuseExtentKnob)) {
|
||||
int width = 0, height = 0;
|
||||
ASSERT_EQ(std::sscanf(knob, "%dx%d", &width, &height), 2)
|
||||
<< kRefuseExtentKnob << "=" << knob << " is not <w>x<h>";
|
||||
ASSERT_GE(width, 12) << "the copy windows below need at least a 12x6 level";
|
||||
ASSERT_GE(height, 6) << "the copy windows below need at least a 12x6 level";
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
m_forced = true;
|
||||
}
|
||||
// 16-bit RGB rows are 6 bytes a texel: the default 4-byte alignment would pad every
|
||||
// row of an odd-width level and shear both the upload and the read.
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (!m_textures.empty()) {
|
||||
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
}
|
||||
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
if (m_fbo != 0) {
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
}
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
GLuint MakeTexture(const CopyFormat& format, const std::vector<std::uint8_t>& texels, bool immutable) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
if (immutable) {
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, format.internalFormat, m_width, m_height);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_width, m_height, format.format, format.type,
|
||||
texels.data());
|
||||
} else {
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), m_width, m_height, 0,
|
||||
format.format, format.type, texels.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> ReadTexImage(GLuint texture, const CopyFormat& format) {
|
||||
std::vector<std::uint8_t> bytes(
|
||||
static_cast<std::size_t>(m_width) * m_height * static_cast<std::size_t>(TexelBytes(format)), 0xA5);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, format.format, format.type, bytes.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// The first texel that differs names the defect (which half of the copy, which word);
|
||||
// the rest would only bury it.
|
||||
void ExpectImage(const std::vector<std::uint8_t>& got, const std::vector<std::uint8_t>& want,
|
||||
const CopyFormat& format, const std::string& what) {
|
||||
ASSERT_EQ(got.size(), want.size());
|
||||
const int texelBytes = TexelBytes(format);
|
||||
for (std::size_t at = 0; at < got.size(); ++at) {
|
||||
if (got[at] == want[at]) continue;
|
||||
const std::size_t texel = at / static_cast<std::size_t>(texelBytes);
|
||||
const int x = static_cast<int>(texel % static_cast<std::size_t>(m_width));
|
||||
const int y = static_cast<int>(texel / static_cast<std::size_t>(m_width));
|
||||
std::string gotHex, wantHex;
|
||||
char buffer[4];
|
||||
for (int b = 0; b < texelBytes; ++b) {
|
||||
std::snprintf(buffer, sizeof(buffer), "%02x", got[texel * texelBytes + b]);
|
||||
gotHex += buffer;
|
||||
std::snprintf(buffer, sizeof(buffer), "%02x", want[texel * texelBytes + b]);
|
||||
wantHex += buffer;
|
||||
}
|
||||
ADD_FAILURE() << what << " (" << format.name << ", " << Gl().BackendName() << ", "
|
||||
<< (m_wire ? "split" : "monolith") << (m_forced ? ", store read forced" : "")
|
||||
<< "): texel (" << x << ", " << y << ") reads " << gotHex << ", want " << wantHex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Under the knob the Espryt server must have answered from its store at least once;
|
||||
// otherwise the entry proves nothing about that route.
|
||||
void ExpectStoreReadNamedSince(const PipeStatsWindow::LogMark& mark) {
|
||||
if (!m_forced || !m_espryt || !m_wire) return;
|
||||
const std::string log = PipeStatsWindow::ReadServerLogSince(mark);
|
||||
EXPECT_NE(log.find(kStoreReadMarker), std::string::npos)
|
||||
<< kRefuseExtentKnob << " is set but the server log carries no '" << kStoreReadMarker
|
||||
<< "' line: the knob did not reach the server, or no read took the store route";
|
||||
}
|
||||
|
||||
// The copy window: 5x3 texels from (1, 1) of the source to (6, 2) of the destination,
|
||||
// inside a 12x6 level and away from the partial upload's (0, 0) 3x2 corner.
|
||||
static constexpr int kSrcX = 1, kSrcY = 1, kDstX = 6, kDstY = 2, kCopyW = 5, kCopyH = 3;
|
||||
|
||||
void CopyWindow(GLuint source, GLuint destination) {
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D, 0, kSrcX, kSrcY, 0, destination, GL_TEXTURE_2D, 0, kDstX,
|
||||
kDstY, 0, kCopyW, kCopyH, 1);
|
||||
}
|
||||
|
||||
bool m_wire = false;
|
||||
bool m_espryt = false;
|
||||
bool m_forced = false;
|
||||
int m_width = kDefaultWidth;
|
||||
int m_height = kDefaultHeight;
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// The control: an uploaded level reads back as uploaded.
|
||||
TEST_F(CopyImageStoreReadbackScenario, AnUploadedLevelReadsBackExactly) {
|
||||
if (!Ready()) return;
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
for (const CopyFormat& format : kFormats) {
|
||||
const auto texels = MakeTexels(format, m_width, m_height, 11);
|
||||
const GLuint texture = MakeTexture(format, texels, /*immutable=*/false);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
|
||||
ExpectImage(ReadTexImage(texture, format), texels, format, "the uploaded level");
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
|
||||
}
|
||||
ExpectStoreReadNamedSince(mark);
|
||||
}
|
||||
|
||||
// THE CASE THE PACKAGE IS ABOUT: the copy lands in the destination's GPU image only, and the
|
||||
// read has to see it - whatever answers the read.
|
||||
TEST_F(CopyImageStoreReadbackScenario, ACopiedWindowReadsBackOverTheUpload) {
|
||||
if (!Ready()) return;
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
for (const CopyFormat& format : kFormats) {
|
||||
const auto sourceTexels = MakeTexels(format, m_width, m_height, 23);
|
||||
const auto destinationTexels = MakeTexels(format, m_width, m_height, 37);
|
||||
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/false);
|
||||
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
|
||||
CopyWindow(source, destination);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glCopyImageSubData raised a GL error";
|
||||
const auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX,
|
||||
kDstY, kCopyW, kCopyH, TexelBytes(format));
|
||||
ExpectImage(ReadTexImage(destination, format), want, format, "the copy destination");
|
||||
ExpectImage(ReadTexImage(source, format), sourceTexels, format, "the copy source");
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
|
||||
}
|
||||
ExpectStoreReadNamedSince(mark);
|
||||
}
|
||||
|
||||
// The same into immutable storage (glTexStorage2D, then the whole level uploaded).
|
||||
TEST_F(CopyImageStoreReadbackScenario, ACopiedWindowReadsBackFromAnImmutableLevel) {
|
||||
if (!Ready()) return;
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
for (const CopyFormat& format : kFormats) {
|
||||
const auto sourceTexels = MakeTexels(format, m_width, m_height, 41);
|
||||
const auto destinationTexels = MakeTexels(format, m_width, m_height, 43);
|
||||
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/true);
|
||||
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/true);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
|
||||
CopyWindow(source, destination);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glCopyImageSubData raised a GL error";
|
||||
const auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX,
|
||||
kDstY, kCopyW, kCopyH, TexelBytes(format));
|
||||
ExpectImage(ReadTexImage(destination, format), want, format, "the immutable copy destination");
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
|
||||
}
|
||||
ExpectStoreReadNamedSince(mark);
|
||||
}
|
||||
|
||||
// A copy, then an ordinary partial upload into the same level elsewhere. The upload is the
|
||||
// client's; the copied window is not, and a byte store that took the upload as "the level"
|
||||
// would lose it again.
|
||||
TEST_F(CopyImageStoreReadbackScenario, APartialUploadAfterTheCopyKeepsTheCopiedWindow) {
|
||||
if (!Ready()) return;
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
for (const CopyFormat& format : kFormats) {
|
||||
const auto sourceTexels = MakeTexels(format, m_width, m_height, 53);
|
||||
const auto destinationTexels = MakeTexels(format, m_width, m_height, 59);
|
||||
const auto patch = MakeTexels(format, 3, 2, 61);
|
||||
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/false);
|
||||
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
|
||||
CopyWindow(source, destination);
|
||||
glBindTexture(GL_TEXTURE_2D, destination);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 3, 2, format.format, format.type, patch.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": the copy or the upload raised a GL error";
|
||||
auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX, kDstY,
|
||||
kCopyW, kCopyH, TexelBytes(format));
|
||||
want = WithBlock(want, m_width, patch, 3, 0, 0, 0, 0, 3, 2, TexelBytes(format));
|
||||
ExpectImage(ReadTexImage(destination, format), want, format, "the copy destination after the upload");
|
||||
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
|
||||
}
|
||||
ExpectStoreReadNamedSince(mark);
|
||||
}
|
||||
|
||||
// A RENDERBUFFER endpoint. The split arm resolves it by handle (DirectGLES.cpp,
|
||||
// MakeGLESCopyImageEndpoint), which is what retired the "CopyImageSubData+RENDERBUFFER"
|
||||
// latch; this is the case that says so on every arm. It has no store to answer from - a
|
||||
// renderbuffer's texels were never uploaded - so the knob entries do not run it.
|
||||
TEST_F(CopyImageStoreReadbackScenario, ARenderbufferSourceLandsInTheTexture) {
|
||||
if (!Ready()) return;
|
||||
const CopyFormat format{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT, 4, false, "rgba16"};
|
||||
const auto destinationTexels = MakeTexels(format, m_width, m_height, 71);
|
||||
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA16, m_width, m_height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "the RGBA16 renderbuffer is not attachable";
|
||||
// k / 65535 for four 16-bit words: exact through a float clear and a UNORM16 store.
|
||||
const std::uint16_t words[4] = {0x1234, 0x5678, 0x9abc, 0xffff};
|
||||
const GLfloat color[4] = {words[0] / 65535.0f, words[1] / 65535.0f, words[2] / 65535.0f,
|
||||
words[3] / 65535.0f};
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearBufferfv(GL_COLOR, 0, color);
|
||||
BindDefaultFramebuffer();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
|
||||
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, kSrcX, kSrcY, 0, destination, GL_TEXTURE_2D, 0,
|
||||
kDstX, kDstY, 0, kCopyW, kCopyH, 1);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glCopyImageSubData from the renderbuffer raised a GL error";
|
||||
std::vector<std::uint8_t> cleared(static_cast<std::size_t>(m_width) * m_height * 8);
|
||||
for (std::size_t texel = 0; texel < cleared.size() / 8; ++texel) {
|
||||
std::memcpy(cleared.data() + texel * 8, words, 8);
|
||||
}
|
||||
const auto want = WithBlock(destinationTexels, m_width, cleared, m_width, kSrcX, kSrcY, kDstX, kDstY,
|
||||
kCopyW, kCopyH, 8);
|
||||
ExpectImage(ReadTexImage(destination, format), want, format, "the texture a renderbuffer was copied into");
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glGetTexImage raised a GL error";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -257,7 +257,7 @@ namespace {
|
||||
// RESIDENT, residency comes only from AdoptPersistentMap, and every persistent-map
|
||||
// acquisition declines under split by R-6 (PipeApply.cpp's MGPipeApplyMapPersistent:
|
||||
// "A SPLIT BUILD RUNS AT TIER T2 AND DECLINES EVERY ACQUISITION, ALWAYS"; T0/T1 are
|
||||
// P11's and AdoptTierIsEmulate is a named Fatal for them). So the capability is wired
|
||||
// unimplemented, refused by name at the handshake since P11 A1). So the capability is wired
|
||||
// AHEAD of its consumer on purpose, and `rsd=` on the PipeStats line is the counter that
|
||||
// will show the records the day P11 lands a tier that adopts. See notes/p7/magma-c.md.
|
||||
TEST_F(CtWireScenario, TheServerPublishesTheResidentSubDataCapabilityFromItsOwnTable) {
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DeletedBoundBufferScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A BUFFER DELETED WHILE IT IS STILL BOUND TO AN INDEXED BINDING POINT.
|
||||
//
|
||||
// glDeleteBuffers unbinds the buffer from every binding point of the current context (GL 4.6
|
||||
// core 5.1.2, ES 3.2 6.1.1), indexed ones included, and the frontend does (BufferState::
|
||||
// MarkBufferObjectForDeletion). Under a transport that is not enough: the SERVER holds its own
|
||||
// copy of each indexed window (set_shader_buffers / the uniform window), re-sent only when the
|
||||
// target's bind-point GENERATION moves (Tracker.h's bits 15/16/17). A delete that cleared the
|
||||
// points without moving the generation left the server naming the dead buffer's handle; once
|
||||
// the client recycled that handle's slot at a newer generation, the next draw or dispatch that
|
||||
// walked the window asked the backend for the old one:
|
||||
// Espryt Fatal{ProtocolCorruption, "BackendSlotTable.Generation"} (the whole session dies);
|
||||
// Magma the handle resolves to nothing and the draw / dispatch that declares the point is
|
||||
// dropped.
|
||||
// (P11 B2 addendum; first seen as LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack
|
||||
// followed by TheArenaLandsInTheTierItsLaneDeclares in one process.)
|
||||
//
|
||||
// Each case, for one indexed target: bind a buffer at point 0 and use it (so the window naming it
|
||||
// reaches the server), delete it WHILE BOUND, create buffers until its slot has been reused at a
|
||||
// newer generation, then draw and dispatch with a program that DECLARES point 0 but never
|
||||
// touches it (a uniform-guarded branch: reading an empty point would be undefined, declaring it
|
||||
// is not). Both must run - no Fatal, a marker written, the frame drawn - and a fresh buffer bound
|
||||
// at the same point afterwards must be read correctly.
|
||||
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// More than enough for any free-list order: the dead buffer's slot is reused well before.
|
||||
constexpr int kRecycledBuffers = 16;
|
||||
constexpr unsigned int kMarker = 0xC0FFEEu;
|
||||
constexpr unsigned int kFreshWord = 0x1234u;
|
||||
constexpr GLuint kResultBinding = 7;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||||
)";
|
||||
constexpr const char* kQuadFragmentSource = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// The target's declaration and the one read of it; the read is only ever taken with
|
||||
// u_read = 1 (a fresh buffer bound). Result: the marker, unconditional, and what was read.
|
||||
std::string ComputeSource(const char* declaration, const char* read) {
|
||||
std::ostringstream os;
|
||||
os << "#version 430 core\n"
|
||||
"layout(local_size_x = 1) in;\n"
|
||||
"layout(std430, binding = 7) buffer Result { uint marker; uint value; };\n"
|
||||
"uniform uint u_read;\n"
|
||||
<< declaration << "\n"
|
||||
<< "void main() {\n"
|
||||
" uint v = 0u;\n"
|
||||
" if (u_read != 0u) { v = " << read << "; }\n"
|
||||
" marker = 0xC0FFEEu;\n"
|
||||
" value = v;\n"
|
||||
"}\n";
|
||||
return os.str();
|
||||
}
|
||||
|
||||
constexpr const char* kCaptureVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
out float vs_value;
|
||||
void main() {
|
||||
vs_value = float(gl_VertexID) * 2.0 + 1.0;
|
||||
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||
gl_PointSize = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
class DeletedBoundBufferScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_quad = Link(kQuadVertexSource, kQuadFragmentSource);
|
||||
ASSERT_NE(m_quad, 0u) << m_log;
|
||||
const float quad[] = {-1.f, -1.f, 1.f, -1.f, 1.f, 1.f, -1.f, -1.f, 1.f, 1.f, -1.f, 1.f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
// The result block lives at its own point, bound ONCE here: nothing later rebinds
|
||||
// a storage point, so the storage window the server holds is only ever re-sent
|
||||
// by what the case itself does.
|
||||
const unsigned int zero[2] = {0u, 0u};
|
||||
glGenBuffers(1, &m_result);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(zero), zero, GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kResultBinding, m_result);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (!m_owned.empty()) glDeleteBuffers(static_cast<GLsizei>(m_owned.size()), m_owned.data());
|
||||
if (m_result != 0) glDeleteBuffers(1, &m_result);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
for (GLuint p : {m_quad, m_compute, m_capture}) {
|
||||
if (p != 0) glDeleteProgram(p);
|
||||
}
|
||||
m_owned.clear();
|
||||
m_result = m_vbo = m_vao = m_quad = m_compute = m_capture = 0;
|
||||
}
|
||||
|
||||
GLuint Compile(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_log = std::string("shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint LinkShaders(std::initializer_list<GLuint> shaders, const char* varying = nullptr) {
|
||||
const GLuint program = glCreateProgram();
|
||||
for (GLuint s : shaders) glAttachShader(program, s);
|
||||
if (varying != nullptr) glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
for (GLuint s : shaders) glDeleteShader(s);
|
||||
GLint ok = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_log = std::string("program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint Link(const char* vs, const char* fs, const char* varying = nullptr) {
|
||||
const GLuint v = Compile(GL_VERTEX_SHADER, vs);
|
||||
if (v == 0) return 0;
|
||||
const GLuint f = Compile(GL_FRAGMENT_SHADER, fs);
|
||||
if (f == 0) {
|
||||
glDeleteShader(v);
|
||||
return 0;
|
||||
}
|
||||
return LinkShaders({v, f}, varying);
|
||||
}
|
||||
|
||||
GLuint LinkCompute(const std::string& source) {
|
||||
const GLuint c = Compile(GL_COMPUTE_SHADER, source.c_str());
|
||||
return c == 0 ? 0 : LinkShaders({c});
|
||||
}
|
||||
|
||||
GLuint MakeBuffer(GLenum target, const void* data, GLsizeiptr size) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(target, buffer);
|
||||
glBufferData(target, size, data, GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(target, 0);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Delete the buffer still bound at `target` point 0, then take the dead buffer's slot
|
||||
// back at a newer generation: every new buffer is defined with content, which is what
|
||||
// makes the server twin it (and so move its live generation) at once.
|
||||
void DeleteWhileBoundAndRecycle(GLuint& bound) {
|
||||
glDeleteBuffers(1, &bound);
|
||||
bound = 0;
|
||||
std::vector<unsigned int> payload(64);
|
||||
for (int i = 0; i < kRecycledBuffers; ++i) {
|
||||
for (auto& word : payload) word = static_cast<unsigned int>(i);
|
||||
m_owned.push_back(MakeBuffer(GL_ARRAY_BUFFER, payload.data(),
|
||||
static_cast<GLsizeiptr>(payload.size() * sizeof(unsigned int))));
|
||||
}
|
||||
}
|
||||
|
||||
// The draw path first (Espryt walks the whole storage window at every draw), then the
|
||||
// frame must show the quad.
|
||||
void DrawTheQuadAndExpectIt(const char* when) {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_quad);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
unsigned char px[4] = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
||||
EXPECT_GT(px[1], 200) << "the quad was not drawn " << when;
|
||||
EXPECT_LT(px[0], 50) << when;
|
||||
}
|
||||
|
||||
// Dispatch the target's program once; returns {marker, value}.
|
||||
std::array<unsigned int, 2> Dispatch(unsigned int read) {
|
||||
const unsigned int zero[2] = {0u, 0u};
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
|
||||
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(zero), zero);
|
||||
glUseProgram(m_compute);
|
||||
glUniform1ui(glGetUniformLocation(m_compute, "u_read"), read);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
|
||||
GL_ATOMIC_COUNTER_BARRIER_BIT);
|
||||
std::array<unsigned int, 2> out = {0xDEADBEEFu, 0xDEADBEEFu};
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(out), out.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
// The three shader-buffer targets share one shape.
|
||||
void RunShaderBufferTarget(GLenum target, const char* declaration, const char* read,
|
||||
unsigned int freshSeed, unsigned int expectedRead) {
|
||||
m_compute = LinkCompute(ComputeSource(declaration, read));
|
||||
ASSERT_NE(m_compute, 0u) << m_log;
|
||||
|
||||
// 1. A buffer at point 0, used once so the window naming it crosses.
|
||||
const unsigned int seed[4] = {freshSeed, 0u, 0u, 0u};
|
||||
GLuint doomed = MakeBuffer(target, seed, sizeof(seed));
|
||||
glBindBufferBase(target, 0, doomed);
|
||||
auto first = Dispatch(0);
|
||||
EXPECT_EQ(first[0], kMarker) << "the dispatch with the buffer bound did not run";
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// 2. Deleted while bound; its slot recycled at a newer generation.
|
||||
DeleteWhileBoundAndRecycle(doomed);
|
||||
GLint stillBound = -1;
|
||||
glGetIntegeri_v(target == GL_SHADER_STORAGE_BUFFER ? GL_SHADER_STORAGE_BUFFER_BINDING
|
||||
: target == GL_UNIFORM_BUFFER ? GL_UNIFORM_BUFFER_BINDING
|
||||
: GL_ATOMIC_COUNTER_BUFFER_BINDING,
|
||||
0, &stillBound);
|
||||
EXPECT_EQ(stillBound, 0) << "glDeleteBuffers did not unbind indexed point 0";
|
||||
|
||||
// 3. The SAME program dispatched again - no glUseProgram of another program in
|
||||
// between, because the uniform window's shutter mixes the program identity in and a
|
||||
// program change would re-send the window anyway - then a draw. Both must run.
|
||||
auto after = Dispatch(0);
|
||||
EXPECT_EQ(after[0], kMarker)
|
||||
<< "the dispatch that declares the deleted buffer's point did not run: its window still named "
|
||||
"the dead handle (Magma drops the dispatch; Espryt would have died with "
|
||||
"Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} before this line)";
|
||||
DrawTheQuadAndExpectIt("after the bound buffer was deleted and its slot reused");
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// 4. A fresh buffer at the same point reads correctly.
|
||||
const unsigned int fresh[4] = {freshSeed, 0u, 0u, 0u};
|
||||
const GLuint replacement = MakeBuffer(target, fresh, sizeof(fresh));
|
||||
m_owned.push_back(replacement);
|
||||
glBindBufferBase(target, 0, replacement);
|
||||
auto read1 = Dispatch(1);
|
||||
EXPECT_EQ(read1[0], kMarker);
|
||||
EXPECT_EQ(read1[1], expectedRead) << "the buffer bound after the delete was not the one read";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
glBindBufferBase(target, 0, 0);
|
||||
}
|
||||
|
||||
GLuint m_quad = 0;
|
||||
GLuint m_compute = 0;
|
||||
GLuint m_capture = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_result = 0;
|
||||
std::vector<GLuint> m_owned;
|
||||
std::string m_log;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DeletedBoundBufferScenario, AStorageBufferDeletedWhileBoundLeavesNoStaleBinding) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunShaderBufferTarget(GL_SHADER_STORAGE_BUFFER,
|
||||
"layout(std430, binding = 0) buffer Target { uint word; };", "word",
|
||||
kFreshWord, kFreshWord);
|
||||
}
|
||||
|
||||
TEST_F(DeletedBoundBufferScenario, AUniformBufferDeletedWhileBoundLeavesNoStaleBinding) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunShaderBufferTarget(GL_UNIFORM_BUFFER, "layout(std140, binding = 0) uniform Target { uint word; };",
|
||||
"word", kFreshWord, kFreshWord);
|
||||
}
|
||||
|
||||
// The counter's pre-increment value is what the shader reads, and the buffer then holds seed + 1.
|
||||
TEST_F(DeletedBoundBufferScenario, AnAtomicCounterBufferDeletedWhileBoundLeavesNoStaleBinding) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
GLint counters = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
|
||||
if (counters < 1) GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters;
|
||||
RunShaderBufferTarget(GL_ATOMIC_COUNTER_BUFFER, "layout(binding = 0, offset = 0) uniform atomic_uint counter;",
|
||||
"atomicCounterIncrement(counter)", 41u, 41u);
|
||||
}
|
||||
|
||||
// Transform feedback: nothing re-sends a capture window at a draw today (bit 17 is computed and
|
||||
// not emitted), so the stale-handle failure cannot reach the backend through this target; the
|
||||
// case pins the behaviour anyway - the delete unbinds, a draw after the slot is reused runs,
|
||||
// and a fresh capture buffer at the same point receives exactly the captured values.
|
||||
TEST_F(DeletedBoundBufferScenario, ATransformFeedbackBufferDeletedWhileBoundLeavesNoStaleBinding) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
m_capture = Link(kCaptureVertexSource, kQuadFragmentSource, "vs_value");
|
||||
ASSERT_NE(m_capture, 0u) << m_log;
|
||||
constexpr int kPoints = 4;
|
||||
const float poison[kPoints + 2] = {-7.f, -7.f, -7.f, -7.f, -7.f, -7.f};
|
||||
|
||||
auto capture = [&](GLuint into) {
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, into);
|
||||
glUseProgram(m_capture);
|
||||
glBindVertexArray(m_vao);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kPoints);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
};
|
||||
|
||||
GLuint doomed = MakeBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, poison, sizeof(poison));
|
||||
capture(doomed);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
DeleteWhileBoundAndRecycle(doomed);
|
||||
GLint stillBound = -1;
|
||||
glGetIntegeri_v(GL_TRANSFORM_FEEDBACK_BUFFER_BINDING, 0, &stillBound);
|
||||
EXPECT_EQ(stillBound, 0) << "glDeleteBuffers did not unbind transform feedback point 0";
|
||||
DrawTheQuadAndExpectIt("after the bound capture buffer was deleted and its slot reused");
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const GLuint fresh = MakeBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, poison, sizeof(poison));
|
||||
m_owned.push_back(fresh);
|
||||
capture(fresh);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
float captured[kPoints + 2] = {};
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, fresh);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(captured), captured);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
for (int i = 0; i < kPoints + 2; ++i) {
|
||||
const float expected = i < kPoints ? static_cast<float>(i) * 2.f + 1.f : -7.f;
|
||||
EXPECT_EQ(captured[i], expected) << "capture record " << i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,378 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.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
|
||||
//
|
||||
// Scenario - ONE FRAME THAT NEEDS THOUSANDS OF DISTINCT DESCRIPTOR SETS.
|
||||
//
|
||||
// DirectVulkan takes a draw's descriptor set from a per-frame-slot cache and adds a pool to the
|
||||
// slot when the cache runs dry (UniformManager). Two things went wrong with a burst like
|
||||
// Minecraft rd12's loading frame (15,016 sets in one slot):
|
||||
// - Growth was linear. The next pool was sized at twice the slot's FIRST pool of the flavour,
|
||||
// which is always its oldest and smallest, so every grown pool came out the same size: the
|
||||
// loading slot ended with 59 pools of 256 sets.
|
||||
// - The burst was kept forever. The cache only rewinds, so the slot held all 15k sets (and the
|
||||
// pools behind them) for the life of the process, while steady frames used a few dozen.
|
||||
// Now each pool doubles the slot's largest one (capped), and a slot whose pools stay under a
|
||||
// quarter full for MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES frames is handed back whole and re-grows.
|
||||
//
|
||||
// Each of kDraws draws paints its own pixel of a 64x64 target and samples one of kTextures
|
||||
// textures, a different one from each of the previous draws' so neither the renderer's per-draw
|
||||
// descriptor reuse memo (four entries) nor its dynamic-offset-only rebind can let two draws share
|
||||
// a set: one frame needs kDraws sets. Every pixel must hold its own draw's texture colour, on both
|
||||
// backends. On DirectVulkan the cases also read the pools through GetDescriptorPoolCensus - which
|
||||
// only the desktop build can reach, since the integration harness links the static library there
|
||||
// and the hidden-visibility shipping library on Android.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Backend/DirectVulkan/DescriptorPoolCensus.h>
|
||||
#endif
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kSide = 64;
|
||||
constexpr int kDraws = kSide * kSide; // well under the 16384-draw command-buffer split budget
|
||||
// More than the descriptor reuse memo's four entries, so the texture a draw samples is never
|
||||
// one of the last four draws'.
|
||||
constexpr int kTextures = 8;
|
||||
// The base pool size (VulkanRenderer's kDescriptorSetsPerFrame) and the growth cap
|
||||
// (UniformManager::kMaxDescriptorPoolSets).
|
||||
constexpr std::uint64_t kBaseSets = 64;
|
||||
constexpr std::uint64_t kMaxPoolSets = 4096;
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform int uIndex;
|
||||
void main() {
|
||||
vec2 cell = vec2(float(uIndex % 64), float(uIndex / 64));
|
||||
gl_Position = vec4((cell + aPos) / 32.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texture(uTex, vec2(0.5)); }
|
||||
)";
|
||||
|
||||
// Texture t's single texel; every channel differs between any two textures.
|
||||
Rgba8 TextureColor(int t) {
|
||||
return Rgba8{static_cast<std::uint8_t>(16 + 32 * t), static_cast<std::uint8_t>(240 - 32 * t),
|
||||
static_cast<std::uint8_t>(8 + 29 * t), 255};
|
||||
}
|
||||
|
||||
std::string Describe(const Rgba8& c) {
|
||||
std::ostringstream os;
|
||||
os << "rgba(" << static_cast<int>(c.r) << ", " << static_cast<int>(c.g) << ", " << static_cast<int>(c.b)
|
||||
<< ", " << static_cast<int>(c.a) << ")";
|
||||
return os.str();
|
||||
}
|
||||
|
||||
unsigned long EnvUnsigned(const char* name, unsigned long fallback) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr || *value == '\0') return fallback;
|
||||
char* end = nullptr;
|
||||
const unsigned long parsed = std::strtoul(value, &end, 10);
|
||||
return (end != nullptr && *end == '\0') ? parsed : fallback;
|
||||
}
|
||||
|
||||
// P11 M2: HOW MANY OF THE BURST'S SETS ONE FRAME SLOT HOLDS AT ONCE. All kDraws of them on
|
||||
// the monolith arm. Under a split transport the server draws through the WIRE arm, which
|
||||
// rewinds a slot's set cursors every UniformManager::kWireDescriptorSetBudget (2048) sets once
|
||||
// it has proved the GPU done with them (VulkanRenderer::RewindWireDescriptorSetsIfDue), so its
|
||||
// cache holds the burst 2048 sets at a time: the burst still needs a set per draw, grows the
|
||||
// slot geometrically to that and is given back by the same trim, only the census counts the
|
||||
// sets held rather than the draws. (The census itself is in-process - inproc it is read on the
|
||||
// server's apply thread; spawn and tcp answer unavailable - see GetDescriptorPoolCensus.)
|
||||
constexpr std::uint64_t kWireDescriptorSetBudget = 2048;
|
||||
std::uint64_t BurstSetsHeldAtOnce() {
|
||||
const std::uint64_t draws = static_cast<std::uint64_t>(kDraws);
|
||||
return PeekSplitRuntime().transportResolved ? std::min(draws, kWireDescriptorSetBudget) : draws;
|
||||
}
|
||||
|
||||
// Pools a geometric policy adds for n sets from an empty flavour: 64, 128, ... up to the cap,
|
||||
// then one per cap. The old first-pool rule needed n/128 or n/256 of them.
|
||||
std::uint64_t GeometricPoolBound(std::uint64_t n) {
|
||||
std::uint64_t pools = 0, capacity = 0, size = kBaseSets;
|
||||
while (capacity < n) {
|
||||
capacity += size;
|
||||
++pools;
|
||||
size = std::min(size * 2, kMaxPoolSets);
|
||||
}
|
||||
return pools;
|
||||
}
|
||||
|
||||
struct Census {
|
||||
bool available = false;
|
||||
std::uint64_t pools = 0;
|
||||
std::uint64_t capacitySets = 0;
|
||||
std::uint64_t cachedSets = 0;
|
||||
std::uint64_t slotTrims = 0;
|
||||
};
|
||||
|
||||
Census ReadCensus() {
|
||||
Census out;
|
||||
#if !defined(__ANDROID__)
|
||||
// P11 M2: THE POOLS ARE THE SERVER RENDERER'S under a split transport, and under
|
||||
// run-ahead a swap - or a draw - returns here before the server has applied it: a census
|
||||
// taken right after the burst's swap read the pools as they were before the burst
|
||||
// (cachedSets=0, measured under load). So first the harness's applied-seq fence (the
|
||||
// TriangleScenario / MagmaWireReclaimScenario shape): every record this client emitted
|
||||
// is applied. Inproc only - the census is in-process, spawn and tcp answer unavailable
|
||||
// - and the read itself then runs on the apply thread (GetDescriptorPoolCensus).
|
||||
// The fence's default 5 s is not enough here: the burst is 4096 draws with a queue-idle
|
||||
// rewind in the middle, and on a loaded host lavapipe applies it in more than that
|
||||
// (measured: 21 of 40 runs past 5 s beside the retrace lane).
|
||||
if (const SplitRuntimeState runtime = PeekSplitRuntime();
|
||||
runtime.transportResolved && runtime.transportName == "inproc") {
|
||||
EXPECT_TRUE(WaitForSplitAppliedForTesting(runtime.emitSeq, 120000))
|
||||
<< "the server never applied record " << runtime.emitSeq << " before the census";
|
||||
}
|
||||
const auto census = MobileGL::MG_Backend::DirectVulkan::GetDescriptorPoolCensus();
|
||||
out.available = census.available;
|
||||
out.pools = census.pools;
|
||||
out.capacitySets = census.capacitySets;
|
||||
out.cachedSets = census.cachedSets;
|
||||
out.slotTrims = census.slotTrims;
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Describe(const Census& c) {
|
||||
std::ostringstream os;
|
||||
os << "pools=" << c.pools << " capacity=" << c.capacitySets << " cachedSets=" << c.cachedSets
|
||||
<< " slotTrims=" << c.slotTrims;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
::testing::AssertionResult EveryPixelHoldsItsDrawsTexture(const Image& image, const char* when) {
|
||||
int wrong = 0;
|
||||
int firstX = -1, firstY = -1;
|
||||
Rgba8 firstSeen{};
|
||||
for (int y = 0; y < kSide; ++y) {
|
||||
for (int x = 0; x < kSide; ++x) {
|
||||
const Rgba8 expected = TextureColor((y * kSide + x) % kTextures);
|
||||
const Rgba8 seen = image.At(x, y);
|
||||
if (seen != expected) {
|
||||
if (wrong == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
firstSeen = seen;
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrong == 0) return ::testing::AssertionSuccess();
|
||||
const int draw = firstY * kSide + firstX;
|
||||
return ::testing::AssertionFailure()
|
||||
<< when << ": " << wrong << " of " << kDraws << " pixels do not hold their own draw's texture. "
|
||||
<< "First: (" << firstX << ", " << firstY << ") = " << Describe(firstSeen) << ", drawn by draw "
|
||||
<< draw << " with texture " << (draw % kTextures) << " = "
|
||||
<< Describe(TextureColor(draw % kTextures)) << " (a pixel no draw reached holds 0,0,0,0)";
|
||||
}
|
||||
|
||||
class DescriptorPoolGrowthScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
static const float kVertices[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
m_textures.resize(kTextures);
|
||||
glGenTextures(kTextures, m_textures.data());
|
||||
for (int t = 0; t < kTextures; ++t) {
|
||||
const Rgba8 c = TextureColor(t);
|
||||
const std::uint8_t texel[4] = {c.r, c.g, c.b, c.a};
|
||||
glBindTexture(GL_TEXTURE_2D, m_textures[t]);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
BindDefaultFramebuffer();
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
::testing::AssertionResult Prepare() {
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
if (m_program == 0) return ::testing::AssertionFailure() << error;
|
||||
m_indexLocation = glGetUniformLocation(m_program, "uIndex");
|
||||
m_target = MakeColorFbo(kSide, kSide);
|
||||
if (m_target.fbo == 0) return ::testing::AssertionFailure() << "MakeColorFbo failed";
|
||||
glUseProgram(m_program);
|
||||
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindVertexArray(m_vao);
|
||||
BindFbo(m_target);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// Draw i paints pixel (i % 64, i / 64) with texture i % kTextures.
|
||||
void Draw(int i) {
|
||||
glBindTexture(GL_TEXTURE_2D, m_textures[i % kTextures]);
|
||||
glUniform1i(m_indexLocation, i);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
// The burst: every pixel, one draw and one descriptor set each.
|
||||
void DrawBurst() {
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
for (int i = 0; i < kDraws; ++i) Draw(i);
|
||||
}
|
||||
|
||||
static bool IsVulkan() { return Gl().BackendName() == std::string("DirectVulkan"); }
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_program = 0;
|
||||
GLint m_indexLocation = -1;
|
||||
std::vector<GLuint> m_textures;
|
||||
ColorFbo m_target;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// One frame, kDraws distinct sets. Every pixel lands, and the frame slot grows O(log N) pools
|
||||
// for it, not N / 128.
|
||||
TEST_F(DescriptorPoolGrowthScenario, AFrameOfDistinctSetsLandsWholeAndGrowsItsPoolsGeometrically) {
|
||||
if (!Ready()) return;
|
||||
ASSERT_TRUE(Prepare());
|
||||
|
||||
const Census before = ReadCensus();
|
||||
DrawBurst();
|
||||
const Image image = ReadPixels(kSide, kSide);
|
||||
const Census after = ReadCensus();
|
||||
|
||||
EXPECT_TRUE(EveryPixelHoldsItsDrawsTexture(image, "one frame of distinct descriptor sets"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
RecordProperty("census_checked", (IsVulkan() && after.available) ? "yes" : "no");
|
||||
if (!IsVulkan() || !after.available) return;
|
||||
// Falsifiability first: had the renderer's memos collapsed the draws onto shared sets, the
|
||||
// slot would never have needed to grow and the bound below would pass on any policy.
|
||||
const std::uint64_t held = BurstSetsHeldAtOnce();
|
||||
ASSERT_GE(after.cachedSets, before.cachedSets + held)
|
||||
<< "the frame did not allocate one descriptor set per draw, so it does not exercise growth. Before: "
|
||||
<< Describe(before) << "; after: " << Describe(after);
|
||||
const std::uint64_t added = after.pools - before.pools;
|
||||
RecordProperty("census_before", Describe(before));
|
||||
RecordProperty("census_after", Describe(after));
|
||||
EXPECT_LE(added, GeometricPoolBound(held + 1))
|
||||
<< "a frame needing " << held << " descriptor sets at once added " << added << " pools; doubling the "
|
||||
<< "slot's largest pool needs at most " << GeometricPoolBound(held + 1) << ". Before: " << Describe(before)
|
||||
<< "; after: " << Describe(after);
|
||||
}
|
||||
|
||||
// Burst, then quiet frames until the renderer hands the burst back, then the burst again while
|
||||
// the quiet frames are still in flight. Nothing waits between frames except where noted, so the
|
||||
// trim runs with other frame slots' work outstanding.
|
||||
TEST_F(DescriptorPoolGrowthScenario, AGivenBackBurstLeavesNoStaleSetBehind) {
|
||||
if (!Ready()) return;
|
||||
ASSERT_TRUE(Prepare());
|
||||
const unsigned long trimFrames = EnvUnsigned("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120);
|
||||
const unsigned long framesInFlight = EnvUnsigned("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3);
|
||||
// The trim fires once trimFrames quiet frames have followed the burst, at the burst slot's next
|
||||
// turn; past this many frames it is not coming.
|
||||
const unsigned long quietFrameLimit = trimFrames + 2 * framesInFlight + 16;
|
||||
|
||||
DrawBurst();
|
||||
Gl().EndFrame();
|
||||
const Census afterBurst = ReadCensus();
|
||||
|
||||
// Quiet frames: one draw each, the same pixel and texture the burst gave draw 0.
|
||||
const bool observe = IsVulkan() && afterBurst.available && trimFrames != 0;
|
||||
unsigned long quietFrames = 0;
|
||||
Census afterQuiet = afterBurst;
|
||||
for (; quietFrames < quietFrameLimit; ++quietFrames) {
|
||||
Draw(0);
|
||||
Gl().EndFrame();
|
||||
if (observe) {
|
||||
afterQuiet = ReadCensus();
|
||||
if (afterQuiet.slotTrims > afterBurst.slotTrims) {
|
||||
++quietFrames;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The burst again, straight away. The slot it lands in re-grows from its base pool.
|
||||
DrawBurst();
|
||||
const Image image = ReadPixels(kSide, kSide);
|
||||
const Census afterSecondBurst = ReadCensus();
|
||||
|
||||
EXPECT_TRUE(EveryPixelHoldsItsDrawsTexture(image, "the burst drawn after the first one was given back"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
RecordProperty("census_checked", observe ? "yes" : "no");
|
||||
if (!observe) return;
|
||||
RecordProperty("census_after_burst", Describe(afterBurst));
|
||||
RecordProperty("quiet_frames", std::to_string(quietFrames));
|
||||
RecordProperty("census_after_quiet", Describe(afterQuiet));
|
||||
RecordProperty("census_after_second_burst", Describe(afterSecondBurst));
|
||||
const std::uint64_t held = BurstSetsHeldAtOnce();
|
||||
ASSERT_GE(afterBurst.cachedSets, held)
|
||||
<< "the burst did not allocate one descriptor set per draw: " << Describe(afterBurst);
|
||||
EXPECT_GT(afterQuiet.slotTrims, afterBurst.slotTrims)
|
||||
<< quietFrames << " frames of one draw each followed a " << kDraws << "-set frame, but no frame slot was "
|
||||
<< "trimmed (MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES=" << trimFrames << "). After the burst: "
|
||||
<< Describe(afterBurst) << "; after the quiet frames: " << Describe(afterQuiet);
|
||||
EXPECT_LE(afterQuiet.cachedSets + held / 2, afterBurst.cachedSets)
|
||||
<< "the trim did not give the burst's sets back. After the burst: " << Describe(afterBurst)
|
||||
<< "; after the quiet frames: " << Describe(afterQuiet);
|
||||
EXPECT_LT(afterQuiet.pools, afterBurst.pools)
|
||||
<< "the trim did not give the burst's pools back. After the burst: " << Describe(afterBurst)
|
||||
<< "; after the quiet frames: " << Describe(afterQuiet);
|
||||
EXPECT_GE(afterSecondBurst.cachedSets, afterQuiet.cachedSets + held)
|
||||
<< "the second burst was not served by freshly allocated sets: " << Describe(afterSecondBurst);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/SplitLane.h"
|
||||
#include "../Harness/WireDeclinePeek.h"
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
@@ -659,6 +660,12 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearDepth(0.5);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
// P8-SV: the decline is a WireDeclines.def row now (MipmapDepthStencilAspect), so the server's
|
||||
// MGWIRE-DECLINES dump sees it. Readable where the server is this process: the inproc arm.
|
||||
// Red once (executed, reverted): the site back to a bare MGLOG_E_ONCE - the count stays 0.
|
||||
unsigned long long declinesBefore = 0;
|
||||
const bool counted = PeekSplitRuntime().transportName == "inproc" &&
|
||||
PeekWireDeclineCount("MipmapDepthStencilAspect", &declinesBefore);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.DepthStencilMip.error";
|
||||
// The session is what the retired Fatal used to take with it. A readback is a round trip
|
||||
@@ -669,12 +676,77 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) {
|
||||
glReadPixels(1, 1, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &level0);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.DepthStencilMip.readback";
|
||||
EXPECT_NEAR(level0, 0.5f, 0.002f) << "F1.DepthStencilMip.level0";
|
||||
if (counted) {
|
||||
unsigned long long declinesAfter = 0;
|
||||
ASSERT_TRUE(PeekWireDeclineCount("MipmapDepthStencilAspect", &declinesAfter));
|
||||
EXPECT_EQ(declinesAfter - declinesBefore, 1u) << "F1.DepthStencilMip.tally: one named decline per refused "
|
||||
"glGenerateMipmap (WireDeclines.def MipmapDepthStencilAspect)";
|
||||
}
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||
glDeleteTextures(1, &depthStencil);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glClearDepth(1.0);
|
||||
}
|
||||
|
||||
// P8-F: `Magma:mipmap-shader-format-or-shape` IS A NAMED DECLINE ON THE WIRE ARM NOW, which is
|
||||
// the monolith arm's answer for a chain it cannot build (VulkanRenderer.cpp's GenerateMipmap
|
||||
// declines a format without blit support and leaves the chain unwritten). It used to end the
|
||||
// session. A 3D texture is a shape neither wire shader arm takes (the colour pass samples a
|
||||
// sampler2D), so with the native blit forced off (MGITEST_MAGMA_FORCE_SHADER_MIPMAP) it is what
|
||||
// reaches the refusal on this host - lavapipe blits a 3D RGBA8 chain natively otherwise.
|
||||
//
|
||||
// What this case pins: no GL error, the session still answering (two readback round trips), level
|
||||
// 0 untouched, and level 1 still holding the application's own texels - the decline wrote nothing.
|
||||
// Magma only, and knob-gated, like GenerateMipmapWithoutNativeBlitPixels above.
|
||||
// Red once (executed, reverted): restore the MagmaWireFatal and this case dies with
|
||||
// Fatal{UnmigratedVerb, "Magma:mipmap-shader-format-or-shape ..."}.
|
||||
TEST_F(F1WireScenario, GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession) {
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != "DirectVulkan")
|
||||
GTEST_SKIP() << "Magma wire-arm decline (P8-F, mipmap-shader-format-or-shape): the knob and "
|
||||
"the shader mip arms it forces are Magma's; Espryt ("
|
||||
<< Gl().BackendName() << ") generates this chain itself";
|
||||
const std::string tier = SplitLane::MarkerValue("MGITEST_MAGMA_FORCE_SHADER_MIPMAP");
|
||||
if (tier.empty())
|
||||
GTEST_SKIP() << "MGITEST_MAGMA_FORCE_SHADER_MIPMAP is unset: this driver blits a 3D RGBA8 "
|
||||
"chain natively, so the decline under test is not entered";
|
||||
GLuint volume = 0;
|
||||
glGenTextures(1, &volume);
|
||||
glBindTexture(GL_TEXTURE_3D, volume);
|
||||
glTexStorage3D(GL_TEXTURE_3D, 3, GL_RGBA8, 4, 4, 4);
|
||||
const std::array<GLubyte, 4> base{200, 40, 80, 255};
|
||||
const std::array<GLubyte, 4> sentinel{10, 220, 30, 255};
|
||||
const auto fill = [](int side, const std::array<GLubyte, 4>& texel) {
|
||||
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4));
|
||||
for (size_t at = 0; at < pixels.size(); at += 4) std::copy(texel.begin(), texel.end(), pixels.begin() + at);
|
||||
return pixels;
|
||||
};
|
||||
const auto level0 = fill(4, base);
|
||||
const auto level1 = fill(2, sentinel);
|
||||
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 4, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
|
||||
glTexSubImage3D(GL_TEXTURE_3D, 1, 0, 0, 0, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, level1.data());
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.setup";
|
||||
const auto before = PeekSplitRuntime().emitSeq;
|
||||
glGenerateMipmap(GL_TEXTURE_3D);
|
||||
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ShaderMipDecline.wire";
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.error";
|
||||
for (const int level : {0, 1}) {
|
||||
const int side = 4 >> level;
|
||||
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4), 0x5a);
|
||||
glGetTexImage(GL_TEXTURE_3D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.readback level " << level;
|
||||
const std::array<GLubyte, 4>& expected = level == 0 ? base : sentinel;
|
||||
size_t wrong = 0;
|
||||
for (size_t at = 0; at < pixels.size(); at += 4)
|
||||
if (!std::equal(expected.begin(), expected.end(), pixels.begin() + at)) ++wrong;
|
||||
EXPECT_EQ(wrong, 0u) << "F1.ShaderMipDecline.level" << level << " tier=" << tier
|
||||
<< ": a declined mip must leave every level as the application wrote it";
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_3D, 0);
|
||||
glDeleteTextures(1, &volume);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
}
|
||||
|
||||
TEST_F(F1WireScenario, GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels) {
|
||||
if (!Ready()) return;
|
||||
const std::array<GLubyte, 4> colors[5] = {
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FencePollScenario.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
|
||||
//
|
||||
// P10 (A, MG_Remote/CONTRACT-P10.md §1): FENCE POLLS ANSWERED FROM THE REVERSE CHANNEL.
|
||||
//
|
||||
// A glClientWaitSync / glGetSynciv(GL_SYNC_STATUS) used to be a kWaitReply record: its answer
|
||||
// came back only after the server had applied every record before it, so a poll was a full
|
||||
// pipeline drain and, over TCP, a round trip - once per frame in Minecraft's fence use. Now the
|
||||
// server reports each fence the GPU finishes (kEventFenceSignaled) and the client answers polls
|
||||
// from those reports; the Nth unanswered zero-timeout poll in a row (MOBILEGL_IPC_POLL_ESCALATE)
|
||||
// still crosses, which is what bounds a loop whose report is late.
|
||||
//
|
||||
// Three cases. The first is ordinary GL - the idiom's two poll loops end - and its flush-bit arm
|
||||
// runs on the monolith lanes too. The other two are about the wire and skip where there is none.
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
constexpr auto kLoopBound = std::chrono::seconds(10);
|
||||
|
||||
class FencePollScenario : public ScenarioTest {
|
||||
protected:
|
||||
// Some GPU work for a fence to wait behind.
|
||||
static void QueueWork() {
|
||||
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
};
|
||||
|
||||
// The GL idiom: `while (glClientWaitSync(s, flags, 0) == GL_TIMEOUT_EXPIRED) {}` and the
|
||||
// glGetSynciv twin. With nothing else in the loop, no record would ever be published if
|
||||
// the poll were not a doorbell point, and a local "not yet" would be repeated for ever if
|
||||
// nothing made it true - so the loop ending, bounded, is the property.
|
||||
//
|
||||
// WITHOUT GL_SYNC_FLUSH_COMMANDS_BIT GL itself does not promise the loop ends (the fence
|
||||
// may sit unflushed in the driver), so that arm is split-only: there the transport
|
||||
// promises it, because the poll flushes the link and an idle server submits.
|
||||
TEST_F(FencePollScenario, ZeroTimeoutPollLoopsEndInBoundedTime) {
|
||||
if (!Ready()) return;
|
||||
const bool split = SplitRuntimeSkipReason().empty();
|
||||
for (const GLbitfield flags : {GLbitfield(GL_SYNC_FLUSH_COMMANDS_BIT), GLbitfield(0)}) {
|
||||
if (flags == 0 && !split) continue;
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const auto start = Clock::now();
|
||||
GLenum status = GL_TIMEOUT_EXPIRED;
|
||||
unsigned long long polls = 0;
|
||||
while ((status = glClientWaitSync(sync, flags, 0)) == GL_TIMEOUT_EXPIRED &&
|
||||
Clock::now() - start < kLoopBound) {
|
||||
++polls;
|
||||
}
|
||||
EXPECT_TRUE(status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED)
|
||||
<< "glClientWaitSync(flags 0x" << std::hex << flags << std::dec << ", 0) still "
|
||||
<< status << " after " << polls << " polls";
|
||||
glDeleteSync(sync);
|
||||
}
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const auto start = Clock::now();
|
||||
GLint value = GL_UNSIGNALED;
|
||||
while (value != GL_SIGNALED && Clock::now() - start < kLoopBound) {
|
||||
glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value);
|
||||
}
|
||||
EXPECT_EQ(value, GL_SIGNALED) << "glGetSynciv(GL_SYNC_STATUS) never read signaled";
|
||||
glDeleteSync(sync);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// THE WIRE HALF: once a fence is known signaled, polling it costs nothing - no reply-slot
|
||||
// record, no round trip - however often it is asked. MOBILEGL_IPC_POLL_ESCALATE=0 puts the
|
||||
// round trip back and must fail both counters (its red-once).
|
||||
TEST_F(FencePollScenario, PollsOfASignaledFenceCrossNothing) {
|
||||
if (!Ready()) return;
|
||||
const std::string why = SplitRuntimeSkipReason();
|
||||
if (!why.empty()) GTEST_SKIP() << why;
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const GLenum waited = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 5000000000ull);
|
||||
ASSERT_TRUE(waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED) << waited;
|
||||
|
||||
constexpr unsigned kPolls = 100;
|
||||
const SplitRuntimeState before = PeekSplitRuntime();
|
||||
for (unsigned i = 0; i < kPolls; ++i) {
|
||||
EXPECT_EQ(glClientWaitSync(sync, 0, 0), static_cast<GLenum>(GL_ALREADY_SIGNALED));
|
||||
GLint value = GL_UNSIGNALED;
|
||||
glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value);
|
||||
EXPECT_EQ(value, GL_SIGNALED);
|
||||
}
|
||||
const SplitRuntimeState after = PeekSplitRuntime();
|
||||
glDeleteSync(sync);
|
||||
EXPECT_EQ(after.replyPostings - before.replyPostings, 0u)
|
||||
<< "polling a signaled fence published reply-slot records (MOBILEGL_IPC_POLL_ESCALATE=0 "
|
||||
"does this on purpose)";
|
||||
EXPECT_EQ(after.fenceRoundTrips - before.fenceRoundTrips, 0u);
|
||||
EXPECT_EQ(after.fenceLocalAnswers - before.fenceLocalAnswers, 2ull * kPolls);
|
||||
}
|
||||
|
||||
// THE SERVER'S HALF: a fence the GPU finishes while the client sends nothing - it only
|
||||
// polls - is still reported. The apply thread is idle then; it must submit what the driver
|
||||
// holds and keep re-checking rather than park for ever, or the only way out of the loop is
|
||||
// the escalation. The polls are paced (10 ms apart) so the escalation cannot come first by
|
||||
// rate: 64 of them is 640 ms, far more than a clear takes. Red-once: take the idle
|
||||
// re-check out of ServerLoop and this case escalates.
|
||||
TEST_F(FencePollScenario, AFenceThatFinishesWhileTheClientOnlyPollsIsReported) {
|
||||
if (!Ready()) return;
|
||||
const std::string why = SplitRuntimeSkipReason();
|
||||
if (!why.empty()) GTEST_SKIP() << why;
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const SplitRuntimeState before = PeekSplitRuntime();
|
||||
const auto start = Clock::now();
|
||||
GLenum status = GL_TIMEOUT_EXPIRED;
|
||||
while ((status = glClientWaitSync(sync, 0, 0)) == GL_TIMEOUT_EXPIRED &&
|
||||
Clock::now() - start < kLoopBound) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
const SplitRuntimeState after = PeekSplitRuntime();
|
||||
glDeleteSync(sync);
|
||||
EXPECT_TRUE(status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) << status;
|
||||
EXPECT_EQ(after.fenceEscalations - before.fenceEscalations, 0u)
|
||||
<< "the loop needed an escalated round trip: the server did not report the fence on its "
|
||||
"own while the client only polled";
|
||||
EXPECT_GE(after.fenceServerReports - before.fenceServerReports, 1u);
|
||||
}
|
||||
|
||||
// P10 A's PAIRED RECORD - a measurement, not a gate: it skips unless
|
||||
// MGITEST_FENCE_PACE_BENCH=1. Minecraft's shape, isolated (in the replay corpus only
|
||||
// improved-transparency-minecraft-26.3 carries timed fence waits): every frame queues work
|
||||
// and a fence, swaps, and waits - with a timeout - on the fence it made two frames earlier.
|
||||
// The same entry run with MOBILEGL_IPC_POLL_ESCALATE=0 and with the default is the A/B; it
|
||||
// prints frames, wall time, reply-slot records and the fence counters.
|
||||
TEST_F(FencePollScenario, PacedFrameFenceWaitsBench) {
|
||||
if (!Ready()) return;
|
||||
const char* bench = std::getenv("MGITEST_FENCE_PACE_BENCH");
|
||||
if (bench == nullptr || std::string(bench) != "1") {
|
||||
GTEST_SKIP() << "a measurement: set MGITEST_FENCE_PACE_BENCH=1";
|
||||
}
|
||||
const std::string why = SplitRuntimeSkipReason();
|
||||
if (!why.empty()) GTEST_SKIP() << why;
|
||||
constexpr int kFrames = 600;
|
||||
// MGITEST_FENCE_PACE_LAG: how many frames back the waited fence is (default 2).
|
||||
const char* lagEnv = std::getenv("MGITEST_FENCE_PACE_LAG");
|
||||
const size_t lag = lagEnv != nullptr && std::atoi(lagEnv) > 0 ? static_cast<size_t>(std::atoi(lagEnv)) : 2;
|
||||
std::deque<GLsync> inFlight;
|
||||
const SplitRuntimeState before = PeekSplitRuntime();
|
||||
const auto start = Clock::now();
|
||||
for (int frame = 0; frame < kFrames; ++frame) {
|
||||
QueueWork();
|
||||
inFlight.push_back(glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0));
|
||||
Gl().EndFrame();
|
||||
if (inFlight.size() > lag) {
|
||||
GLsync old = inFlight.front();
|
||||
inFlight.pop_front();
|
||||
const GLenum waited = glClientWaitSync(old, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
|
||||
EXPECT_TRUE(waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED) << waited;
|
||||
glDeleteSync(old);
|
||||
}
|
||||
}
|
||||
const double ms = std::chrono::duration<double, std::milli>(Clock::now() - start).count();
|
||||
const SplitRuntimeState after = PeekSplitRuntime();
|
||||
for (GLsync sync : inFlight) glDeleteSync(sync);
|
||||
const char* escalate = std::getenv("MOBILEGL_IPC_POLL_ESCALATE");
|
||||
std::printf("P10BENCH transport=%s poll-escalate=%s lag=%zu frames=%d ms=%.1f ms/frame=%.3f "
|
||||
"reply-records=%llu fence-round-trips=%llu fence-local=%llu fence-reports=%llu\n",
|
||||
after.transportName.c_str(), escalate != nullptr ? escalate : "(default)", lag, kFrames, ms,
|
||||
ms / kFrames, after.replyPostings - before.replyPostings,
|
||||
after.fenceRoundTrips - before.fenceRoundTrips,
|
||||
after.fenceLocalAnswers - before.fenceLocalAnswers,
|
||||
after.fenceServerReports - before.fenceServerReports);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,555 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glGenerateMipmap ON THE FORMATS ESPRYT DOES NOT HAND STRAIGHT TO THE DRIVER
|
||||
// (P8-B, docs/Disaggregated/notes/p8/B.md).
|
||||
//
|
||||
// Espryt generates R11F_G11F_B10F and depth chains with per-level blits, and RGB16F / RGB32F
|
||||
// chains outside glGenerateMipmap because ES refuses a three-channel float format there. Until
|
||||
// P8-B the split arms of both were named refusals:
|
||||
// B1 Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the texture's descriptor
|
||||
// held fewer levels than the full chain from level 0 - a MAX_LEVEL below the chain, or a
|
||||
// glTexStorage2D with fewer levels - which is legal GL (the generated window is
|
||||
// BASE_LEVEL+1 .. min(p, MAX_LEVEL), and immutable storage bounds it too);
|
||||
// B2 Fatal{UnmigratedEmulation, "generate-mipmap-cpu-filter"} on every RGB16F / RGB32F
|
||||
// generation, because the monolith filters the frontend's level shadows on the CPU.
|
||||
//
|
||||
// EVERY CASE CHECKS THE GL ANSWER BY SAMPLING. The base level is split into two halves (bottom
|
||||
// rows A, top rows B) so a generation that loses the row order, or reads another level, cannot
|
||||
// average to the right colour. Each generated level is read back through texelFetch into an RGBA8
|
||||
// target, and the levels outside the window are checked unchanged (or still undefined).
|
||||
//
|
||||
// ARMS. Both backends, monolith and Split / Spawn / Tcp; Magma is the control. The forced-CPU arm
|
||||
// (MGITEST_ESPRYT_FORCE_CPU_MIPMAP=1, read by the SERVER) runs the three-channel cases through
|
||||
// Espryt's server-side CPU filter, which a host driver never needs: llvmpipe generates both
|
||||
// formats natively. Under the knob the server log must name that arm, or the entry proved nothing.
|
||||
//
|
||||
// THE CPU ARM'S STORE ROUTE (P8-SE). When the driver cannot read the base level back, the server's
|
||||
// CPU arm filters the staged store's copy of it - but only for a texture the driver never wrote
|
||||
// (P8-E's sticky mark), because a render target's level 0 exists only on the driver and the store
|
||||
// still holds what crossed before the draw. The `CpuMipRefused.` entries add
|
||||
// MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=16x16 (P8-E's knob, also read by the SERVER) to the
|
||||
// forced-CPU knob: a render-target base must decline by name (generate-mipmap-store-declined) and
|
||||
// never filter the stale store, and an uploaded-only base must still be filtered from the store.
|
||||
//
|
||||
// MONOLITH DEFECTS, recorded for dev (ID-P8-3) and skipped by name on the monolith arm they live
|
||||
// on unless MGITEST_RUN_MONOLITH_MIP_DEFECTS=1 is set (which is how each was shown red):
|
||||
// M1 Espryt: the RGB16F / RGB32F CPU filter reads the frontend's level shadow, which a GPU
|
||||
// write to level 0 leaves stale (DirectGLES.cpp GenerateThreeChannelFloatMipmapOnCpu);
|
||||
// M2 Espryt: the R11F / depth blit chains start at level 1 and run to the end of the chain,
|
||||
// ignoring BASE_LEVEL and MAX_LEVEL, and the storage grow before them defines levels past
|
||||
// MAX_LEVEL (DirectGLES.cpp GenerateColorTexture2DMipmap / GenerateDepthTexture2DMipmap /
|
||||
// EnsureGenerateMipmapStorageAllocated);
|
||||
// M3 Magma: a colour format without BLIT_SRC|BLIT_DST (R11F_G11F_B10F on lavapipe) is skipped
|
||||
// with a warning and its levels stay undefined texels - the monolith has no shader arm,
|
||||
// which the wire arm has had since P7 (VulkanRenderer.cpp GenerateMipmap).
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kEdge = 16;
|
||||
constexpr const char* kForceCpuMipmap = "MGITEST_ESPRYT_FORCE_CPU_MIPMAP";
|
||||
constexpr const char* kCpuMipmapMarker = "generate-mipmap-server-cpu";
|
||||
constexpr const char* kRunMonolithDefects = "MGITEST_RUN_MONOLITH_MIP_DEFECTS";
|
||||
// P8-SE: E's server-read knob; the entries set it to kEdge x kEdge, the base level's extent.
|
||||
constexpr const char* kRefuseReadback = "MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT";
|
||||
constexpr const char* kStoreDeclinedMarker = "generate-mipmap-store-declined";
|
||||
constexpr const char* kStoreFilteredMarker = "filtering the staged store's copy";
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
constexpr const char* kSampleSource = R"(#version 430 core
|
||||
uniform sampler2D u_texture;
|
||||
uniform int u_level;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy), u_level); }
|
||||
)";
|
||||
constexpr const char* kPaintSource = R"(#version 430 core
|
||||
uniform vec4 u_bottom;
|
||||
uniform vec4 u_top;
|
||||
uniform float u_split;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = gl_FragCoord.y < u_split ? u_bottom : u_top; }
|
||||
)";
|
||||
|
||||
using Rgb = std::array<float, 3>;
|
||||
// Exact in R11F_G11F_B10F, RGB16F and RGB32F alike, and apart in every channel.
|
||||
constexpr Rgb kBottom = {0.25f, 0.5f, 0.75f};
|
||||
constexpr Rgb kTop = {0.875f, 0.375f, 0.125f};
|
||||
constexpr Rgb kSentinel = {1.0f, 0.0f, 1.0f};
|
||||
constexpr Rgb kLevelZero = {1.0f, 0.0f, 0.0f};
|
||||
constexpr Rgb kLevelTwo = {0.0f, 0.0f, 1.0f};
|
||||
constexpr Rgb kLevelThree = {1.0f, 1.0f, 0.0f};
|
||||
constexpr float kDepthBottom = 0.25f;
|
||||
constexpr float kDepthTop = 0.75f;
|
||||
constexpr float kDepthLevelZero = 0.125f;
|
||||
constexpr float kDepthLevelTwo = 0.5f;
|
||||
constexpr float kDepthLevelThree = 1.0f;
|
||||
|
||||
struct Format {
|
||||
GLenum internalFormat;
|
||||
bool depth;
|
||||
const char* name;
|
||||
};
|
||||
constexpr Format kPackedFloat = {GL_R11F_G11F_B10F, false, "R11F_G11F_B10F"};
|
||||
constexpr Format kDepth32F = {GL_DEPTH_COMPONENT32F, true, "DEPTH_COMPONENT32F"};
|
||||
constexpr Format kRgb16F = {GL_RGB16F, false, "RGB16F"};
|
||||
constexpr Format kRgb32F = {GL_RGB32F, false, "RGB32F"};
|
||||
|
||||
int ToByte(float value) { return static_cast<int>(std::lround(value * 255.0f)); }
|
||||
|
||||
class GenerateMipmapServerScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_wire = SplitRuntimeSkipReason().empty();
|
||||
m_espryt = Gl().BackendName() == "DirectGLES";
|
||||
m_forcedCpu = std::getenv(kForceCpuMipmap) != nullptr;
|
||||
m_refusedReadback = std::getenv(kRefuseReadback) != nullptr;
|
||||
std::string error;
|
||||
m_sampleProgram = CompileProgram(kVertexSource, kSampleSource, &error);
|
||||
ASSERT_NE(m_sampleProgram, 0u) << error;
|
||||
m_paintProgram = CompileProgram(kVertexSource, kPaintSource, &error);
|
||||
ASSERT_NE(m_paintProgram, 0u) << error;
|
||||
m_target = MakeColorFbo(kEdge, kEdge);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the sampling target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (m_sampleProgram != 0) glDeleteProgram(m_sampleProgram);
|
||||
if (m_paintProgram != 0) glDeleteProgram(m_paintProgram);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
BindDefaultFramebuffer();
|
||||
if (m_target.fbo != 0) DestroyColorFbo(m_target);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
// Whether this case meets a recorded monolith defect on this arm (see the header). The
|
||||
// knob runs it anyway, which is how each defect is shown red and how dev's fix is checked.
|
||||
bool MonolithDefect(bool onEspryt, bool onMagma) const {
|
||||
if (m_wire || std::getenv(kRunMonolithDefects) != nullptr) return false;
|
||||
return m_espryt ? onEspryt : onMagma;
|
||||
}
|
||||
static constexpr const char* kM1 =
|
||||
"monolith defect M1 (recorded for dev, ID-P8-3): Espryt's CPU filter reads the frontend's "
|
||||
"level shadow, which the draw into level 0 left stale";
|
||||
static constexpr const char* kM2 =
|
||||
"monolith defect M2 (recorded for dev, ID-P8-3): Espryt's blit chain ignores BASE_LEVEL and "
|
||||
"MAX_LEVEL and its storage grow defines levels past MAX_LEVEL";
|
||||
static constexpr const char* kM3 =
|
||||
"monolith defect M3 (recorded for dev, ID-P8-3): Magma skips a format without native blit "
|
||||
"and leaves the generated levels empty";
|
||||
|
||||
GLuint NewTexture() {
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
return m_texture;
|
||||
}
|
||||
|
||||
// A level `edge` x `edge` whose rows below the middle are `bottom` and above it `top`.
|
||||
static std::vector<float> Halves(const Format& format, int edge, const Rgb& bottom, const Rgb& top,
|
||||
float depthBottom, float depthTop) {
|
||||
const int channels = format.depth ? 1 : 3;
|
||||
std::vector<float> texels(static_cast<std::size_t>(edge * edge * channels));
|
||||
for (int y = 0; y < edge; ++y) {
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
float* texel = texels.data() + static_cast<std::size_t>((y * edge + x) * channels);
|
||||
const bool low = y < edge / 2;
|
||||
if (format.depth) {
|
||||
texel[0] = low ? depthBottom : depthTop;
|
||||
} else {
|
||||
for (int c = 0; c < 3; ++c) texel[c] = low ? bottom[c] : top[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
static std::vector<float> Flat(const Format& format, int edge, const Rgb& colour, float depth) {
|
||||
return Halves(format, edge, colour, colour, depth, depth);
|
||||
}
|
||||
|
||||
static void DefineLevel(const Format& format, int level, int edge, const std::vector<float>& texels) {
|
||||
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), edge, edge, 0,
|
||||
format.depth ? GL_DEPTH_COMPONENT : GL_RGB, GL_FLOAT, texels.data());
|
||||
}
|
||||
|
||||
static void UploadLevel(const Format& format, int level, int edge, const std::vector<float>& texels) {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, edge, edge, format.depth ? GL_DEPTH_COMPONENT : GL_RGB,
|
||||
GL_FLOAT, texels.data());
|
||||
}
|
||||
|
||||
static int LevelWidth(int level) {
|
||||
GLint width = 0;
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
|
||||
return width;
|
||||
}
|
||||
|
||||
// texelFetch's level is relative to BASE_LEVEL and bounded by MAX_LEVEL, so the window is
|
||||
// reopened over every defined level before any of them is read.
|
||||
Image SampleLevel(int level, int maxLevel) {
|
||||
const int edge = std::max(kEdge >> level, 1);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glUseProgram(m_sampleProgram);
|
||||
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_texture"), 0);
|
||||
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_level"), level);
|
||||
BindFbo(m_target);
|
||||
glViewport(0, 0, edge, edge);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
return ReadPixelsRect(0, 0, edge, edge);
|
||||
}
|
||||
|
||||
// Every row of `image` against the half it belongs to. A depth level is read in its red
|
||||
// channel only, and only in its bottom and top rows: the rows next to the boundary are
|
||||
// where a shader-based depth mip is entitled to a half-texel offset.
|
||||
static void ExpectHalves(const Image& image, const Format& format, const Rgb& bottom, const Rgb& top,
|
||||
float depthBottom, float depthTop, const std::string& what) {
|
||||
const int height = image.Height();
|
||||
for (int y = 0; y < height; ++y) {
|
||||
if (format.depth && y != 0 && y != height - 1) continue;
|
||||
const bool low = y < height / 2;
|
||||
for (int x = 0; x < image.Width(); ++x) {
|
||||
const Rgba8 got = image.At(x, y);
|
||||
if (format.depth) {
|
||||
const int want = ToByte(low ? depthBottom : depthTop);
|
||||
EXPECT_NEAR(got.r, want, 2) << what << " depth at (" << x << "," << y << ") got " << got;
|
||||
if (std::abs(got.r - want) > 2) return;
|
||||
continue;
|
||||
}
|
||||
const Rgb& colour = low ? bottom : top;
|
||||
const int want[3] = {ToByte(colour[0]), ToByte(colour[1]), ToByte(colour[2])};
|
||||
const int have[3] = {got.r, got.g, got.b};
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
EXPECT_NEAR(have[c], want[c], 2) << what << " channel " << c << " at (" << x << ","
|
||||
<< y << ") got " << got;
|
||||
if (std::abs(have[c] - want[c]) > 2) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ExpectFlat(const Image& image, const Format& format, const Rgb& colour, float depth,
|
||||
const std::string& what) {
|
||||
ExpectHalves(image, format, colour, colour, depth, depth, what);
|
||||
}
|
||||
|
||||
void Generate() {
|
||||
m_mark = PipeStatsWindow::MarkLaneLog();
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glGenerateMipmap";
|
||||
}
|
||||
|
||||
// Under the knob the SERVER must say it filtered on the CPU; without that line the entry
|
||||
// ran the native arm and its green says nothing about the arm it is named for.
|
||||
void ExpectCpuArmNamed() {
|
||||
if (!m_forcedCpu || !m_wire || !m_espryt) return;
|
||||
const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark);
|
||||
EXPECT_NE(log.find(kCpuMipmapMarker), std::string::npos)
|
||||
<< kForceCpuMipmap << " is set but the server log after glGenerateMipmap carries no '"
|
||||
<< kCpuMipmapMarker << "' line: the knob did not reach the server, or the three-channel "
|
||||
<< "generation never took the server's CPU arm";
|
||||
}
|
||||
|
||||
// ---- B1: the generated window, on the two formats Espryt blits level by level ----------
|
||||
|
||||
// Levels 0..2 defined, MAX_LEVEL = 2: the descriptor holds three of the chain's five levels.
|
||||
void MutableMaxLevelTwo(const Format& format) {
|
||||
if (MonolithDefect(true, !format.depth)) GTEST_SKIP() << (m_espryt ? kM2 : kM3);
|
||||
NewTexture();
|
||||
DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, kDepthBottom, kDepthTop));
|
||||
DefineLevel(format, 1, kEdge / 2, Flat(format, kEdge / 2, kSentinel, kDepthLevelZero));
|
||||
DefineLevel(format, 2, kEdge / 4, Flat(format, kEdge / 4, kSentinel, kDepthLevelZero));
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "define levels 0..2";
|
||||
Generate();
|
||||
EXPECT_EQ(LevelWidth(3), 0) << format.name << ": level 3 is past MAX_LEVEL and must stay undefined";
|
||||
for (int level = 1; level <= 2; ++level)
|
||||
ExpectHalves(SampleLevel(level, 2), format, kBottom, kTop, kDepthBottom, kDepthTop,
|
||||
std::string(format.name) + " MAX_LEVEL=2 level " + std::to_string(level));
|
||||
ExpectHalves(SampleLevel(0, 2), format, kBottom, kTop, kDepthBottom, kDepthTop,
|
||||
std::string(format.name) + " MAX_LEVEL=2 level 0 (the base, unchanged)");
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// glTexStorage2D with three of the chain's five levels.
|
||||
void PartialStorage(const Format& format) {
|
||||
if (MonolithDefect(false, !format.depth)) GTEST_SKIP() << kM3;
|
||||
NewTexture();
|
||||
glTexStorage2D(GL_TEXTURE_2D, 3, format.internalFormat, kEdge, kEdge);
|
||||
UploadLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, kDepthBottom, kDepthTop));
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "partial storage";
|
||||
Generate();
|
||||
for (int level = 1; level <= 2; ++level)
|
||||
ExpectHalves(SampleLevel(level, 2), format, kBottom, kTop, kDepthBottom, kDepthTop,
|
||||
std::string(format.name) + " three-level storage level " + std::to_string(level));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// BASE_LEVEL = 1, MAX_LEVEL = 2 over four defined levels: only level 2 is generated, from
|
||||
// level 1; levels 0 and 3 keep what they held and level 4 is never defined.
|
||||
void MutableBaseLevelOne(const Format& format) {
|
||||
if (MonolithDefect(true, !format.depth)) GTEST_SKIP() << (m_espryt ? kM2 : kM3);
|
||||
NewTexture();
|
||||
DefineLevel(format, 0, kEdge, Flat(format, kEdge, kLevelZero, kDepthLevelZero));
|
||||
DefineLevel(format, 1, kEdge / 2,
|
||||
Halves(format, kEdge / 2, kBottom, kTop, kDepthBottom, kDepthTop));
|
||||
DefineLevel(format, 2, kEdge / 4, Flat(format, kEdge / 4, kLevelTwo, kDepthLevelTwo));
|
||||
DefineLevel(format, 3, kEdge / 8, Flat(format, kEdge / 8, kLevelThree, kDepthLevelThree));
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "define levels 0..3";
|
||||
Generate();
|
||||
EXPECT_EQ(LevelWidth(4), 0) << format.name << ": level 4 is past MAX_LEVEL and must stay undefined";
|
||||
const std::string name(format.name);
|
||||
ExpectFlat(SampleLevel(0, 3), format, kLevelZero, kDepthLevelZero, name + " level 0 (below BASE_LEVEL)");
|
||||
ExpectHalves(SampleLevel(1, 3), format, kBottom, kTop, kDepthBottom, kDepthTop, name + " level 1 (base)");
|
||||
ExpectHalves(SampleLevel(2, 3), format, kBottom, kTop, kDepthBottom, kDepthTop,
|
||||
name + " level 2 (generated from level 1)");
|
||||
ExpectFlat(SampleLevel(3, 3), format, kLevelThree, kDepthLevelThree,
|
||||
name + " level 3 (past MAX_LEVEL)");
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// ---- B2: RGB16F / RGB32F ----------------------------------------------------------------
|
||||
|
||||
void UploadedBase(const Format& format) {
|
||||
NewTexture();
|
||||
DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, 0, 0));
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "define level 0";
|
||||
Generate();
|
||||
const int last = 4;
|
||||
for (int level = 1; level < last; ++level)
|
||||
ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0,
|
||||
std::string(format.name) + " uploaded base, level " + std::to_string(level));
|
||||
EXPECT_EQ(LevelWidth(last), 1) << format.name << ": the generated chain ends at 1x1";
|
||||
ExpectCpuArmNamed();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// Complementary's colortex2 shape: the base is a render target the GPU wrote after its one
|
||||
// upload, so the chain must come from the GPU's level 0 and never from the uploaded texels.
|
||||
void RenderTargetBase(const Format& format) {
|
||||
if (MonolithDefect(true, false)) GTEST_SKIP() << kM1;
|
||||
PaintSentinelBaseAsRenderTarget(format);
|
||||
if (HasFatalFailure() || IsSkipped()) return;
|
||||
Generate();
|
||||
const int last = 4;
|
||||
for (int level = 0; level < last; ++level)
|
||||
ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0,
|
||||
std::string(format.name) + " render-target base, level " + std::to_string(level));
|
||||
ExpectCpuArmNamed();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// ---- P8-SE: the CPU arm's store route under a refused readback ----------------------
|
||||
|
||||
// Only the `CpuMipRefused.` entries carry both server knobs; anywhere else the store route
|
||||
// is unreachable on a host (its driver reads every level back) and the case says so.
|
||||
void SkipUnlessTheStoreRouteIsArmed() {
|
||||
if (m_wire && m_espryt && m_forcedCpu && m_refusedReadback) return;
|
||||
GTEST_SKIP() << "the store route needs Espryt's split server with " << kForceCpuMipmap << " and "
|
||||
<< kRefuseReadback << "=" << kEdge << "x" << kEdge << " (the CpuMipRefused. entries)";
|
||||
}
|
||||
|
||||
// The render-target base again, with the driver refusing to read level 0 back: the store
|
||||
// holds the uploaded sentinel, the driver the drawn halves, and P8-E's mark says so. The
|
||||
// arm must decline by name - a stale chain of sentinel texels is the defect.
|
||||
void RefusedReadbackOfADrawnBase(const Format& format) {
|
||||
SkipUnlessTheStoreRouteIsArmed();
|
||||
if (IsSkipped()) return;
|
||||
PaintSentinelBaseAsRenderTarget(format);
|
||||
if (HasFatalFailure() || IsSkipped()) return;
|
||||
Generate();
|
||||
const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark);
|
||||
EXPECT_NE(log.find(kStoreDeclinedMarker), std::string::npos)
|
||||
<< "the base level was drawn and its readback refused, but the server log after glGenerateMipmap "
|
||||
"has no '" << kStoreDeclinedMarker << "' line";
|
||||
EXPECT_EQ(log.find(kStoreFilteredMarker), std::string::npos)
|
||||
<< "the CPU arm filtered the staged store's copy of a level the driver has since drawn over";
|
||||
const int last = 4;
|
||||
ExpectHalves(SampleLevel(0, last), format, kBottom, kTop, 0, 0,
|
||||
std::string(format.name) + " drawn base (untouched by the decline)");
|
||||
for (int level = 1; level < last; ++level) {
|
||||
const Image image = SampleLevel(level, last);
|
||||
const Rgba8 got = image.At(0, 0);
|
||||
EXPECT_FALSE(std::abs(got.r - ToByte(kSentinel[0])) <= 2 && std::abs(got.g - ToByte(kSentinel[1])) <= 2 &&
|
||||
std::abs(got.b - ToByte(kSentinel[2])) <= 2)
|
||||
<< format.name << " level " << level << " is the uploaded sentinel " << got
|
||||
<< ": the chain was filtered from the stale staged store";
|
||||
}
|
||||
ExpectCpuArmNamed();
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
// The control: a base that only ever crossed as an upload IS what the store holds, so a
|
||||
// refused readback is answered from the store and the chain is right. This is what tells
|
||||
// the check above from "the store route always declines" - the generation's own
|
||||
// driver-write note must not count against the level it reads.
|
||||
void RefusedReadbackOfAnUploadedBase(const Format& format) {
|
||||
SkipUnlessTheStoreRouteIsArmed();
|
||||
if (IsSkipped()) return;
|
||||
NewTexture();
|
||||
DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, 0, 0));
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "define level 0";
|
||||
Generate();
|
||||
const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark);
|
||||
EXPECT_NE(log.find(kStoreFilteredMarker), std::string::npos)
|
||||
<< "the uploaded base's readback was refused, but the server log has no '" << kStoreFilteredMarker
|
||||
<< "' line: the store route did not answer";
|
||||
EXPECT_EQ(log.find(kStoreDeclinedMarker), std::string::npos)
|
||||
<< "the store route declined a level that only ever crossed as an upload";
|
||||
const int last = 4;
|
||||
for (int level = 1; level < last; ++level)
|
||||
ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0,
|
||||
std::string(format.name) + " uploaded base, store-filtered level " +
|
||||
std::to_string(level));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// Level 0 uploaded as the sentinel, then drawn over as a render target (bottom / top halves).
|
||||
void PaintSentinelBaseAsRenderTarget(const Format& format) {
|
||||
NewTexture();
|
||||
DefineLevel(format, 0, kEdge, Flat(format, kEdge, kSentinel, 0));
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
FirstGLError();
|
||||
GTEST_SKIP() << format.name << " is not colour-renderable on " << Gl().BackendName()
|
||||
<< " (status 0x" << std::hex << status << ")";
|
||||
}
|
||||
glViewport(0, 0, kEdge, kEdge);
|
||||
glUseProgram(m_paintProgram);
|
||||
glUniform4f(glGetUniformLocation(m_paintProgram, "u_bottom"), kBottom[0], kBottom[1], kBottom[2], 1);
|
||||
glUniform4f(glGetUniformLocation(m_paintProgram, "u_top"), kTop[0], kTop[1], kTop[2], 1);
|
||||
glUniform1f(glGetUniformLocation(m_paintProgram, "u_split"), kEdge / 2.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "draw into level 0";
|
||||
}
|
||||
|
||||
bool m_wire = false;
|
||||
bool m_espryt = false;
|
||||
bool m_forcedCpu = false;
|
||||
bool m_refusedReadback = false;
|
||||
GLuint m_sampleProgram = 0;
|
||||
GLuint m_paintProgram = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_texture = 0;
|
||||
ColorFbo m_target{};
|
||||
PipeStatsWindow::LogMark m_mark{};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// B1.
|
||||
TEST_F(GenerateMipmapServerScenario, PackedFloatMutableMaxLevelBelowTheChain) {
|
||||
if (!Ready()) return;
|
||||
MutableMaxLevelTwo(kPackedFloat);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, PackedFloatStorageWithFewerLevelsThanTheChain) {
|
||||
if (!Ready()) return;
|
||||
PartialStorage(kPackedFloat);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, PackedFloatMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow) {
|
||||
if (!Ready()) return;
|
||||
MutableBaseLevelOne(kPackedFloat);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, DepthMutableMaxLevelBelowTheChain) {
|
||||
if (!Ready()) return;
|
||||
MutableMaxLevelTwo(kDepth32F);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, DepthStorageWithFewerLevelsThanTheChain) {
|
||||
if (!Ready()) return;
|
||||
PartialStorage(kDepth32F);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, DepthMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow) {
|
||||
if (!Ready()) return;
|
||||
MutableBaseLevelOne(kDepth32F);
|
||||
}
|
||||
|
||||
// B2.
|
||||
TEST_F(GenerateMipmapServerScenario, Rgb16fUploadedBaseGeneratesItsChain) {
|
||||
if (!Ready()) return;
|
||||
UploadedBase(kRgb16F);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, Rgb16fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) {
|
||||
if (!Ready()) return;
|
||||
RenderTargetBase(kRgb16F);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, Rgb32fUploadedBaseGeneratesItsChain) {
|
||||
if (!Ready()) return;
|
||||
UploadedBase(kRgb32F);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, Rgb32fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) {
|
||||
if (!Ready()) return;
|
||||
RenderTargetBase(kRgb32F);
|
||||
}
|
||||
|
||||
// P8-SE: the CPU arm's store route (header; the CpuMipRefused. entries).
|
||||
TEST_F(GenerateMipmapServerScenario, Rgb16fDrawnBaseWithARefusedReadbackDeclinesByName) {
|
||||
if (!Ready()) return;
|
||||
RefusedReadbackOfADrawnBase(kRgb16F);
|
||||
}
|
||||
TEST_F(GenerateMipmapServerScenario, Rgb16fUploadedBaseWithARefusedReadbackIsFilteredFromTheStore) {
|
||||
if (!Ready()) return;
|
||||
RefusedReadbackOfAnUploadedBase(kRgb16F);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,800 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.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
|
||||
//
|
||||
// Scenario - DRAW INPUTS A SHADER WROTE, READ BY A BACKEND ON THE CPU (P8-C).
|
||||
//
|
||||
// Espryt emulates five desktop-GL draw features on the CPU, and each one reads a buffer's bytes
|
||||
// on the side that applies the draw:
|
||||
//
|
||||
// * the primitive-restart rewrite (an application restart index that is not the all-ones value
|
||||
// of the index type): the whole element array buffer, rewritten into a scratch copy;
|
||||
// * the multi-draw rebase tier, which a batch with such a restart index is forced onto;
|
||||
// * *IndirectCount: the count word of the parameter buffer (GLES has no counted indirect draw);
|
||||
// * a native indirect draw's gl_BaseVertex, fed to the shader as a uniform from the command;
|
||||
// * a GL_DOUBLE vertex array, narrowed to float from the source buffer's bytes.
|
||||
//
|
||||
// Under split those bytes are the server's R-11 staged copy, which only uploads fill. A buffer a
|
||||
// shader WROTE on the server - storage block, atomic counter, buffer image, transform feedback -
|
||||
// is therefore read stale unless the server refreshes its copy first. Every case below writes
|
||||
// the buffer on the GPU only (the staged copy holds a poison the correct picture cannot come
|
||||
// from), so a stale read is a wrong picture, never a lucky one.
|
||||
//
|
||||
// MONOLITH ARMS KNOWN BROKEN HERE SKIP BY NAME (SkipBrokenMonolith). Espryt reads three from the
|
||||
// frontend shadow without SyncGpuWrites: *IndirectCount (OPEN-QUESTIONS 15), an indirect
|
||||
// gl_BaseVertex (the comment beside it in DirectGLES.cpp says so) and a GL_DOUBLE vertex array
|
||||
// (the M-3 deviation declared in Managers.cpp). Magma reads MultiDrawArraysIndirectCount's count
|
||||
// the same way, and its restart rewrite and GL_DOUBLE conversion call SyncGpuWrites while the
|
||||
// draw is being recorded, so the draw then records into an ended command buffer (SIGSEGV in
|
||||
// lavapipe). Those arms are dev's to fix (ID-P8-3); the skip keeps the ambient monolith lane
|
||||
// honest without hiding the split arms, which run the same cases under DirectGLES.Split/Spawn/Tcp,
|
||||
// DirectVulkan.*.Full and the inproc A/B.
|
||||
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kSurface = 64;
|
||||
|
||||
constexpr const char* kFlatVertex = R"(#version 430 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFlatFragment = R"(#version 430 core
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// Green when the shader saw gl_BaseVertex == 9 (the value the compute shader wrote),
|
||||
// red for anything else - in particular 3, the value the staged copy still holds.
|
||||
constexpr const char* kBaseVertexVertex = R"(#version 450 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
layout(location = 0) in vec2 aPos;
|
||||
flat out vec4 vColor;
|
||||
void main() {
|
||||
vColor = gl_BaseVertexARB == 9 ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kBaseVertexFragment = R"(#version 450 core
|
||||
flat in vec4 vColor;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vColor; }
|
||||
)";
|
||||
|
||||
// ---- the restart scene (PrimitiveRestartScenario's geometry) ----
|
||||
//
|
||||
// Two triangles with a gap between them, plus two spare vertices at the origin. Index 7 is
|
||||
// the application's restart index and a legal vertex, so a dropped restart welds the strip
|
||||
// across the gap (caught by the gap probe) instead of reading out of bounds.
|
||||
constexpr GLfloat kRestartVertices[] = {
|
||||
-0.9f, -0.9f, -0.1f, -0.9f, -0.9f, 0.9f, // 0-2: left triangle
|
||||
0.1f, -0.9f, 0.9f, -0.9f, 0.9f, 0.9f, // 3-5: right triangle
|
||||
0.0f, 0.0f, 0.0f, 0.0f, // 6, 7: spares
|
||||
};
|
||||
constexpr GLuint kRestartIndex = 7;
|
||||
constexpr GLuint kRestartIndexCount = 7;
|
||||
// What the compute shader writes: {0, 1, 2, 7, 3, 4, 5}.
|
||||
constexpr const char* kWriteRestartIndices = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Indices { uint value[]; };
|
||||
void main() {
|
||||
value[0] = 0u; value[1] = 1u; value[2] = 2u; value[3] = 7u;
|
||||
value[4] = 3u; value[5] = 4u; value[6] = 5u;
|
||||
}
|
||||
)";
|
||||
// The staged poison: every index names spare vertex 6, a strip of degenerate triangles
|
||||
// that paints nothing.
|
||||
const std::vector<GLuint> kRestartPoison(kRestartIndexCount, 6u);
|
||||
|
||||
// The same seven indices, produced by a TRANSFORM FEEDBACK capture: one point per index.
|
||||
constexpr const char* kCaptureIndicesVertex = R"(#version 430 core
|
||||
flat out uint idx;
|
||||
const uint kTable[7] = uint[7](0u, 1u, 2u, 7u, 3u, 4u, 5u);
|
||||
void main() {
|
||||
idx = kTable[gl_VertexID];
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 1.0;
|
||||
}
|
||||
)";
|
||||
// ...and by a capture with a HOLE after every index (gl_SkipComponents1), which ES cannot
|
||||
// express and Espryt therefore scatters itself. The captured words land at the even
|
||||
// slots; the odd slots keep what the application staged there.
|
||||
constexpr const char* kCaptureScatteredVertex = R"(#version 430 core
|
||||
flat out uint idx;
|
||||
const uint kTable[4] = uint[4](0u, 2u, 3u, 5u);
|
||||
void main() {
|
||||
idx = kTable[gl_VertexID];
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 1.0;
|
||||
}
|
||||
)";
|
||||
constexpr const char* kCaptureFragment = R"(#version 430 core
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vec4(1.0); }
|
||||
)";
|
||||
// Even slots are the capture's (poison 6 until it lands), odd slots the holes: after the
|
||||
// scatter the first seven words are {0, 1, 2, 7, 3, 4, 5}; before it, a strip of
|
||||
// degenerate triangles.
|
||||
constexpr GLuint kScatterStaged[8] = {6u, 1u, 6u, 7u, 6u, 4u, 6u, 6u};
|
||||
|
||||
// ...and by IMAGE STORES into a buffer texture over the element array buffer.
|
||||
constexpr const char* kImageStoreRestartIndices = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(r32ui, binding = 0) uniform writeonly uimageBuffer indices;
|
||||
const uint kTable[7] = uint[7](0u, 1u, 2u, 7u, 3u, 4u, 5u);
|
||||
void main() {
|
||||
for (int i = 0; i < 7; ++i) imageStore(indices, i, uvec4(kTable[i]));
|
||||
}
|
||||
)";
|
||||
|
||||
// NDC (-0.5, -0.5), (0.6, -0.5), (0.2, -0.5) on the 64x64 surface.
|
||||
constexpr int kLeftX = 16, kRightX = 51, kGapX = 38, kProbeY = 16;
|
||||
|
||||
// ---- the indirect scene (DrawParametersScenario's geometry) ----
|
||||
//
|
||||
// Three pad vertices at the origin, then the left half of the surface as two triangles
|
||||
// (vertices 3-8), then the right half (9-14). Element indices 0..5 address one half
|
||||
// through the command's baseVertex.
|
||||
constexpr int kLeftFirst = 3;
|
||||
constexpr int kRightFirst = 9;
|
||||
constexpr int kHalfCount = 6;
|
||||
|
||||
std::vector<GLfloat> HalvesVertices() {
|
||||
std::vector<GLfloat> v(6, 0.0f);
|
||||
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
|
||||
for (const auto& half : bounds) {
|
||||
const float x0 = half[0];
|
||||
const float x1 = half[1];
|
||||
const float quad[] = {x0, -1.0f, x1, -1.0f, x1, 1.0f, x0, -1.0f, x1, 1.0f, x0, 1.0f};
|
||||
v.insert(v.end(), std::begin(quad), std::end(quad));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
struct ElementsCommand {
|
||||
std::uint32_t count, instanceCount, firstIndex;
|
||||
std::int32_t baseVertex;
|
||||
std::uint32_t baseInstance;
|
||||
};
|
||||
struct ArraysCommand {
|
||||
std::uint32_t count, instanceCount, first, baseInstance;
|
||||
};
|
||||
|
||||
bool IsGreen(const Rgba8& p) { return p.g > 128 && p.r < 128; }
|
||||
|
||||
class GpuWrittenDrawInputScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_target = MakeColorFbo(kSurface, kSurface);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "the 64x64 colour target did not build";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
m_texture = 0;
|
||||
for (GLuint* buffer : {&m_vbo, &m_ebo, &m_commands, &m_parameters}) {
|
||||
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
||||
*buffer = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
for (GLuint program : m_programs) glDeleteProgram(program);
|
||||
m_programs.clear();
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// THE ARMS THIS FILE SKIPS, BY NAME: the monolith arm of a backend whose reason is
|
||||
// non-null. Every split arm, and the inproc A/B, runs the case.
|
||||
bool SkipBrokenMonolith(const char* espryt, const char* magma) {
|
||||
if (PeekSplitRuntime().transportName != "monolith") return false;
|
||||
const std::string backend = Gl().BackendName();
|
||||
const char* why = backend == "DirectGLES" ? espryt : backend == "DirectVulkan" ? magma : nullptr;
|
||||
if (why == nullptr) return false;
|
||||
m_skipReason = "monolith " + backend + ": " + why +
|
||||
" (dev's fix, ID-P8-3) - this case gates the split arms";
|
||||
return true;
|
||||
}
|
||||
static constexpr const char* kUnsyncedCount =
|
||||
"the *IndirectCount count is read from the frontend shadow without SyncGpuWrites "
|
||||
"(OPEN-QUESTIONS 15)";
|
||||
static constexpr const char* kUnsyncedBaseVertex =
|
||||
"an indirect command's gl_BaseVertex is read from the frontend shadow without "
|
||||
"SyncGpuWrites";
|
||||
static constexpr const char* kUnsyncedDoubles =
|
||||
"a GL_DOUBLE vertex array is narrowed from the client shadow without SyncGpuWrites "
|
||||
"(the M-3 deviation declared in Managers.cpp)";
|
||||
static constexpr const char* kSyncMidRecording =
|
||||
"SyncGpuWrites inside the draw (the restart rewrite, the GL_DOUBLE vertex conversion) "
|
||||
"submits the frame mid-recording and the draw then records into the ended command "
|
||||
"buffer (SIGSEGV)";
|
||||
|
||||
GLuint Program(const char* vertex, const char* fragment) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(vertex, fragment, &error);
|
||||
EXPECT_NE(program, 0u) << error;
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint ComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
char log[4096]{};
|
||||
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char log[4096]{};
|
||||
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Buffer `name` at `target` with `bytes` bytes of `data` (null: an orphaned store).
|
||||
static void Store(GLuint& name, GLenum target, GLsizeiptr bytes, const void* data) {
|
||||
if (name == 0) glGenBuffers(1, &name);
|
||||
glBindBuffer(target, name);
|
||||
glBufferData(target, bytes, data, GL_DYNAMIC_COPY);
|
||||
}
|
||||
|
||||
void BindVertices(const GLfloat* vertices, GLsizeiptr bytes) {
|
||||
glBindVertexArray(m_vao);
|
||||
Store(m_vbo, GL_ARRAY_BUFFER, bytes, vertices);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
|
||||
}
|
||||
|
||||
// One dispatch of `source` with `first` (and `second`, if non-zero) bound as storage
|
||||
// buffers 0 and 1, then the barrier the draw that consumes them needs.
|
||||
void WriteWithCompute(const char* source, GLuint first, GLuint second, GLbitfield barrier) {
|
||||
const GLuint compute = ComputeProgram(source);
|
||||
ASSERT_NE(compute, 0u);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, first);
|
||||
if (second != 0) glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, second);
|
||||
glUseProgram(compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(barrier | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
template <typename DrawFn>
|
||||
Image Render(GLuint program, DrawFn&& draw) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
draw();
|
||||
return ReadPixels(kSurface, kSurface);
|
||||
}
|
||||
|
||||
// The element array buffer of the restart scene, written by a compute shader over the
|
||||
// poison (or over an orphaned store when `orphaned`).
|
||||
void BuildRestartScene(bool orphaned) {
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint),
|
||||
orphaned ? nullptr : kRestartPoison.data());
|
||||
WriteWithCompute(kWriteRestartIndices, m_ebo, 0, GL_ELEMENT_ARRAY_BARRIER_BIT);
|
||||
ArmRestart();
|
||||
}
|
||||
|
||||
// The element array buffer bound to the scene's VAO, restart index 7 on.
|
||||
void ArmRestart() {
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
}
|
||||
|
||||
// A vertex + fragment program whose `varyings` are captured INTERLEAVED.
|
||||
GLuint XfbProgram(const char* vertex, const char* fragment, const char* const* varyings,
|
||||
GLsizei count) {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &vertex, nullptr);
|
||||
glCompileShader(vs);
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &fragment, nullptr);
|
||||
glCompileShader(fs);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glTransformFeedbackVaryings(program, count, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char log[4096]{};
|
||||
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
ADD_FAILURE() << "the capture program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// `vertices` points drawn with rasterization off, captured into m_ebo from byte 0.
|
||||
void CaptureIntoElementBuffer(GLuint program, GLsizei vertices) {
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_ebo);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, vertices);
|
||||
glEndTransformFeedback();
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
static void ExpectSplitStrip(const Image& image, const char* what) {
|
||||
EXPECT_TRUE(IsGreen(image.At(kLeftX, kProbeY)))
|
||||
<< what << ": the left triangle did not render (" << image.At(kLeftX, kProbeY)
|
||||
<< ") - the backend drew the indices it had staged, not the ones the compute "
|
||||
"shader wrote";
|
||||
EXPECT_TRUE(IsGreen(image.At(kRightX, kProbeY)))
|
||||
<< what << ": the right triangle did not render (" << image.At(kRightX, kProbeY) << ")";
|
||||
EXPECT_FALSE(IsGreen(image.At(kGapX, kProbeY)))
|
||||
<< what << ": the gap is covered, so the restart at index 7 was dropped";
|
||||
}
|
||||
|
||||
// Left and right half of the halves scene, sampled at their centres.
|
||||
static Rgba8 Half(const Image& image, int half) {
|
||||
return image.At(kSurface * (1 + 2 * half) / 4, kSurface / 2);
|
||||
}
|
||||
|
||||
ColorFbo m_target;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_commands = 0;
|
||||
GLuint m_parameters = 0;
|
||||
GLuint m_texture = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_skipReason;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- the primitive-restart rewrite (DirectGLES.cpp ScopedRestartIndexSubstitution) ----
|
||||
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
BuildRestartScene(/*orphaned=*/false);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over compute-written restart indices");
|
||||
}
|
||||
|
||||
// The same indices in a store the application ORPHANED (glBufferData(NULL)): nothing was
|
||||
// ever staged for it, so the only copy of its bytes is the one the GPU holds.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteIntoAnOrphanedStoreSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
BuildRestartScene(/*orphaned=*/true);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over compute-written restart indices in an orphaned store");
|
||||
}
|
||||
|
||||
// ---- the multi-draw rebase tier (MultiDraw.cpp RunRebasedDrawElements) ----
|
||||
//
|
||||
// An arbitrary restart index forces a multi-draw onto the one tier that rewrites the stream,
|
||||
// and that tier reads the whole element array buffer on the CPU. Sub-draw 0 is {0,1,2} (the
|
||||
// left triangle); sub-draw 1 is {7,3,4,5} (a restart, then the right triangle).
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteSplitEveryMultiDrawStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
BuildRestartScene(/*orphaned=*/false);
|
||||
const GLsizei counts[2] = {3, 4};
|
||||
const void* offsets[2] = {nullptr, reinterpret_cast<const void*>(3 * sizeof(GLuint))};
|
||||
const Image image = Render(program, [&] {
|
||||
glMultiDrawElements(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_INT, offsets, 2);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glMultiDrawElements over compute-written restart indices");
|
||||
}
|
||||
|
||||
// ---- *IndirectCount (DirectGLES.cpp MultiDraw{Elements,Arrays}IndirectCount) ----
|
||||
//
|
||||
// The compute shader writes BOTH commands and the count; the staged copies hold zeroes, so a
|
||||
// stale count draws nothing and a stale command (on a device without native indirect) draws
|
||||
// nothing either.
|
||||
TEST_F(GpuWrittenDrawInputScenario, ACountAComputeShaderWroteDrivesMultiDrawElementsIndirectCount) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedCount, nullptr)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
const ElementsCommand zeroCommands[2] = {};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
|
||||
const GLuint zeroCount = 0;
|
||||
Store(m_parameters, GL_PARAMETER_BUFFER, sizeof(zeroCount), &zeroCount);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
layout(std430, binding = 1) buffer Parameters { uint count; };
|
||||
void main() {
|
||||
command[0] = 6u; command[1] = 1u; command[2] = 0u; command[3] = 3u; command[4] = 0u;
|
||||
command[5] = 6u; command[6] = 1u; command[7] = 0u; command[8] = 9u; command[9] = 0u;
|
||||
count = 2u;
|
||||
}
|
||||
)",
|
||||
m_commands, m_parameters, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
|
||||
const Image image = Render(program, [] {
|
||||
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 2, 0);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "command 0 did not draw (" << Half(image, 0)
|
||||
<< "): the count was read from a copy the compute shader never reached";
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
|
||||
}
|
||||
|
||||
TEST_F(GpuWrittenDrawInputScenario, ACountAComputeShaderWroteDrivesMultiDrawArraysIndirectCount) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedCount, kUnsyncedCount)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const ArraysCommand zeroCommands[2] = {};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
|
||||
const GLuint zeroCount = 0;
|
||||
Store(m_parameters, GL_PARAMETER_BUFFER, sizeof(zeroCount), &zeroCount);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
layout(std430, binding = 1) buffer Parameters { uint count; };
|
||||
void main() {
|
||||
command[0] = 6u; command[1] = 1u; command[2] = 3u; command[3] = 0u;
|
||||
command[4] = 6u; command[5] = 1u; command[6] = 9u; command[7] = 0u;
|
||||
count = 2u;
|
||||
}
|
||||
)",
|
||||
m_commands, m_parameters, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
|
||||
const Image image = Render(program, [] {
|
||||
glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 0, 2, 0);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "command 0 did not draw (" << Half(image, 0)
|
||||
<< "): the count was read from a copy the compute shader never reached";
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
|
||||
}
|
||||
|
||||
// ---- a native indirect draw's gl_BaseVertex (DirectGLES.cpp ExecuteIndexedIndirectCommands) ----
|
||||
//
|
||||
// The GPU executes the command itself, so the geometry is right whatever the backend's copy
|
||||
// says; gl_BaseVertex is the one field Espryt feeds from the CPU. The staged command names
|
||||
// the left half (baseVertex 3), the compute shader moves it to the right half (9).
|
||||
TEST_F(GpuWrittenDrawInputScenario, ABaseVertexAComputeShaderWroteReachesGlBaseVertex) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedBaseVertex, nullptr)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kBaseVertexVertex, kBaseVertexFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
const ElementsCommand staged{kHalfCount, 1, 0, kLeftFirst, 0};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(staged), &staged);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { int command[]; };
|
||||
void main() { command[3] = 9; }
|
||||
)",
|
||||
m_commands, 0, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1)))
|
||||
<< "the right half is " << Half(image, 1)
|
||||
<< ": red means the draw ran with the compute-written baseVertex but gl_BaseVertex was "
|
||||
"fed from the staged command; black means the draw did not run there at all";
|
||||
EXPECT_FALSE(IsGreen(Half(image, 0))) << "the left half drew: the GPU saw the staged command";
|
||||
}
|
||||
|
||||
// ---- indirect commands with no staged copy at all (DirectGLES.cpp ResolveIndirectCommandBytes) ----
|
||||
//
|
||||
// The command buffer is ORPHANED and only the compute shader ever writes it - the GPU-culling
|
||||
// shape. Nothing was staged for it, so a backend that needs a CPU copy before it will issue
|
||||
// the draw has to make one from its own GL buffer.
|
||||
TEST_F(GpuWrittenDrawInputScenario, IndirectCommandsAComputeShaderWroteIntoAnOrphanedStoreDraw) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(ElementsCommand), nullptr);
|
||||
WriteWithCompute(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
void main() { command[0] = 6u; command[1] = 1u; command[2] = 0u; command[3] = 3u; command[4] = 0u; }
|
||||
)",
|
||||
m_commands, 0, GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "the compute-written command did not draw (" << Half(image, 0)
|
||||
<< "): the backend refused a draw whose command buffer it had no CPU copy of";
|
||||
EXPECT_FALSE(IsGreen(Half(image, 1))) << "the right half drew: the command was not the one written";
|
||||
}
|
||||
|
||||
// ---- the other GPU writers: transform feedback, buffer images, atomic counters ----
|
||||
//
|
||||
// The same restart reader over indices a CAPTURE produced. Espryt maps the captured range
|
||||
// back at glEndTransformFeedback for the client; the server's own copy has to take it too.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesATransformFeedbackCapturedSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const char* varyings[] = {"idx"};
|
||||
const GLuint capture = XfbProgram(kCaptureIndicesVertex, kCaptureFragment, varyings, 1);
|
||||
ASSERT_NE(capture, 0u);
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint), kRestartPoison.data());
|
||||
CaptureIntoElementBuffer(capture, kRestartIndexCount);
|
||||
ArmRestart();
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over restart indices a transform feedback captured");
|
||||
}
|
||||
|
||||
// ...through the SCATTER Espryt runs for a layout with holes: the server composes the range
|
||||
// from its pre-capture bytes and the captured words, and its own copy has to keep the result.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAScatteredTransformFeedbackCapturedSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const char* varyings[] = {"idx", "gl_SkipComponents1"};
|
||||
const GLuint capture = XfbProgram(kCaptureScatteredVertex, kCaptureFragment, varyings, 2);
|
||||
ASSERT_NE(capture, 0u);
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(kScatterStaged), kScatterStaged);
|
||||
CaptureIntoElementBuffer(capture, 4);
|
||||
ArmRestart();
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over restart indices a scattered capture composed");
|
||||
}
|
||||
|
||||
// ...and over indices IMAGE STORES wrote through a buffer texture on the element buffer.
|
||||
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesImageStoresWroteSplitTheStrip) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const GLuint compute = ComputeProgram(kImageStoreRestartIndices);
|
||||
ASSERT_NE(compute, 0u);
|
||||
BindVertices(kRestartVertices, sizeof(kRestartVertices));
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint), kRestartPoison.data());
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, m_texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_ebo);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glBindImageTexture(0, m_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R32UI);
|
||||
glUseProgram(compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ELEMENT_ARRAY_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||
glUseProgram(0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
ArmRestart();
|
||||
const Image image = Render(program, [] {
|
||||
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectSplitStrip(image, "glDrawElements over restart indices image stores wrote");
|
||||
}
|
||||
|
||||
// The GPU-culling shape at its most common: the draw count is an ATOMIC COUNTER the compute
|
||||
// shader bumped once per surviving command, and that counter buffer is the parameter buffer.
|
||||
TEST_F(GpuWrittenDrawInputScenario, ACountAnAtomicCounterProducedDrivesMultiDrawElementsIndirectCount) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedCount, nullptr)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
const GLuint compute = ComputeProgram(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint drawCount;
|
||||
layout(std430, binding = 0) buffer Commands { uint command[]; };
|
||||
void main() {
|
||||
const uint baseVertex[2] = uint[2](3u, 9u);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
uint slot = atomicCounterIncrement(drawCount);
|
||||
command[slot * 5u + 0u] = 6u;
|
||||
command[slot * 5u + 1u] = 1u;
|
||||
command[slot * 5u + 2u] = 0u;
|
||||
command[slot * 5u + 3u] = baseVertex[i];
|
||||
command[slot * 5u + 4u] = 0u;
|
||||
}
|
||||
}
|
||||
)");
|
||||
ASSERT_NE(compute, 0u);
|
||||
const std::vector<GLfloat> vertices = HalvesVertices();
|
||||
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
|
||||
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
|
||||
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
|
||||
const ElementsCommand zeroCommands[2] = {};
|
||||
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
|
||||
const GLuint zeroCount = 0;
|
||||
Store(m_parameters, GL_ATOMIC_COUNTER_BUFFER, sizeof(zeroCount), &zeroCount);
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, m_parameters);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_commands);
|
||||
glUseProgram(compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
|
||||
const Image image = Render(program, [] {
|
||||
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 2, 0);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)))
|
||||
<< "command 0 did not draw (" << Half(image, 0)
|
||||
<< "): the count was read from a copy the atomic counter never reached";
|
||||
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
|
||||
}
|
||||
|
||||
// ---- fp64 vertex narrowing (Managers.cpp SyncFloat64AttributeAsFloat32ByHandle) ----
|
||||
//
|
||||
// A GL_DOUBLE vertex array is narrowed to float on the CPU from the source buffer's bytes,
|
||||
// and the narrowed copy is memoised on the source's serial - which a GPU write never moves.
|
||||
// Two rounds: the first catches a stale read, the second a stale memo. The compute shader
|
||||
// writes each double as its two IEEE-754 words (low word 0), so it needs no fp64 itself.
|
||||
TEST_F(GpuWrittenDrawInputScenario, DoubleVerticesAComputeShaderWroteAreNarrowedFromTheWrittenBytes) {
|
||||
if (!Ready()) return;
|
||||
if (SkipBrokenMonolith(kUnsyncedDoubles, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
|
||||
const GLuint program = Program(kFlatVertex, kFlatFragment);
|
||||
ASSERT_NE(program, 0u);
|
||||
// Four vertices, x then y, all 100.0: a strip wholly off the surface.
|
||||
const double poison[8] = {100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0};
|
||||
glBindVertexArray(m_vao);
|
||||
Store(m_vbo, GL_ARRAY_BUFFER, sizeof(poison), poison);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_DOUBLE, GL_FALSE, 2 * sizeof(double), nullptr);
|
||||
|
||||
// High words: -1.0 = 0xBFF00000, 1.0 = 0x3FF00000, 0.0 = 0.
|
||||
const auto writeQuad = [&](const char* highWords) {
|
||||
const std::string source = std::string(R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Positions { uint word[]; };
|
||||
void main() {
|
||||
const uint hi[8] = uint[8]()") + highWords + R"();
|
||||
for (int i = 0; i < 8; ++i) { word[2 * i] = 0u; word[2 * i + 1] = hi[i]; }
|
||||
}
|
||||
)";
|
||||
WriteWithCompute(source.c_str(), m_vbo, 0, GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
|
||||
};
|
||||
|
||||
// Round 1: the whole surface, (-1,-1) (1,-1) (-1,1) (1,1).
|
||||
writeQuad("0xBFF00000u, 0xBFF00000u, 0x3FF00000u, 0xBFF00000u, 0xBFF00000u, 0x3FF00000u, "
|
||||
"0x3FF00000u, 0x3FF00000u");
|
||||
Image image = Render(program, [] { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); });
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0)) && IsGreen(Half(image, 1)))
|
||||
<< "round 1: the quad is " << Half(image, 0) << " / " << Half(image, 1)
|
||||
<< " - black means the doubles were narrowed from the staged poison, not the written bytes";
|
||||
|
||||
// Round 2: only the left half, (-1,-1) (0,-1) (-1,1) (0,1).
|
||||
writeQuad("0xBFF00000u, 0xBFF00000u, 0u, 0xBFF00000u, 0xBFF00000u, 0x3FF00000u, 0u, 0x3FF00000u");
|
||||
image = Render(program, [] { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); });
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(Half(image, 0))) << "round 2: the left half is " << Half(image, 0);
|
||||
EXPECT_FALSE(IsGreen(Half(image, 1)))
|
||||
<< "round 2: the right half still drew - the narrowed copy of round 1 was reused after the "
|
||||
"second GPU write";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,591 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageBindableRemintScenario.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
|
||||
//
|
||||
// Scenario - THE FIRST glBindImageTexture OF A TEXTURE THE GPU HAS ALREADY WRITTEN.
|
||||
//
|
||||
// ES only allows immutable storage on an image unit, and a few formats need a wider carrier on
|
||||
// top (TextureImpl::GetImageBindableStorageWidening), so Espryt re-mints a texture the first time
|
||||
// it is image-bound after it already has driver storage (RequireImageBindableStorage). The new
|
||||
// storage has to hold what the old one held. The re-mint used to fill it from the frontend's
|
||||
// shadow - the CLIENT's copy of each level - and the shadow never sees a GPU write: a render or
|
||||
// clear into the level, a glGenerateMipmap, a null-data definition only the GPU ever filled. Every
|
||||
// such level came back as the bytes the application last uploaded, or zeroes. That is the
|
||||
// ~26.9k-pixel iris-derivative difference: an RGBA16F render target, first image-bound after it
|
||||
// was rendered into.
|
||||
//
|
||||
// Every case gives a texture its content by one means, lets the backend create its storage (a
|
||||
// sampling draw, or the FBO attach of a GPU clear), writes it on the GPU where the case says so,
|
||||
// and then binds it to an image unit for the first time: a compute imageLoad copies the level into
|
||||
// a destination that was image-bound before it was ever synced (so the destination itself never
|
||||
// re-mints), and the destination is read back through an FBO. The expectation is the GL answer.
|
||||
//
|
||||
// The controls are the other half: levels the GPU never wrote (including formats whose driver
|
||||
// readback may be refused), levels the driver does not have yet because they were defined after
|
||||
// the last sync, and a pending client write that has to land on top of GPU content. Magma never
|
||||
// re-mints from a shadow (it copies image to image) and is the control backend.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr int kEdge = 8;
|
||||
|
||||
constexpr const char* kVS = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
constexpr const char* kSampleFS = R"(#version 430 core
|
||||
uniform sampler2D u_tex;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texelFetch(u_tex, ivec2(gl_FragCoord.xy) % 4, 0); }
|
||||
)";
|
||||
|
||||
// Copies an image into an RGBA8 image with imageLoad; a signed-normalized source is mapped
|
||||
// to [0, 1] so it survives the unsigned destination.
|
||||
std::string CopyCS(const char* srcFormat, bool snorm) {
|
||||
std::string s = "#version 430 core\nlayout(local_size_x = 8, local_size_y = 8) in;\n";
|
||||
s += std::string("layout(") + srcFormat + ", binding = 0) readonly uniform image2D u_src;\n";
|
||||
s += "layout(rgba8, binding = 1) writeonly uniform image2D u_dst;\n";
|
||||
s += "void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); vec4 v = imageLoad(u_src, p);\n";
|
||||
if (snorm) s += " v = v * 0.5 + 0.5;\n";
|
||||
s += " imageStore(u_dst, p, v); }\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<GLubyte> Pattern(int edge, GLubyte magic) {
|
||||
std::vector<GLubyte> t(static_cast<std::size_t>(edge * edge) * 4u);
|
||||
for (int y = 0; y < edge; ++y)
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
const std::size_t at = static_cast<std::size_t>((y * edge + x) * 4);
|
||||
t[at + 0] = magic;
|
||||
t[at + 1] = static_cast<GLubyte>(16 * (x + 1));
|
||||
t[at + 2] = static_cast<GLubyte>(16 * (y + 1));
|
||||
t[at + 3] = 0xFF;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
std::vector<GLubyte> Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) {
|
||||
std::vector<GLubyte> t(static_cast<std::size_t>(edge * edge) * 4u);
|
||||
for (std::size_t i = 0; i < t.size(); i += 4) {
|
||||
t[i] = r;
|
||||
t[i + 1] = g;
|
||||
t[i + 2] = b;
|
||||
t[i + 3] = a;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
const GLfloat kGreen[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||
|
||||
class ImageBindableRemintScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_sampleProgram = CompileProgram(kVS, kSampleFS, &error);
|
||||
ASSERT_NE(m_sampleProgram, 0u) << error;
|
||||
m_scratch = MakeColorFbo(4, 4);
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
if (m_sampleProgram) glDeleteProgram(m_sampleProgram);
|
||||
if (m_vao) glDeleteVertexArrays(1, &m_vao);
|
||||
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_scratch);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
GLuint NewTexture() {
|
||||
GLuint t = 0;
|
||||
glGenTextures(1, &t);
|
||||
m_textures.push_back(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
static void Nearest(GLenum target, GLuint tex, int maxLevel) {
|
||||
glBindTexture(target, tex);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, maxLevel);
|
||||
glBindTexture(target, 0);
|
||||
}
|
||||
|
||||
// A draw that samples the texture: the backend now holds storage for it.
|
||||
void SyncBySampling(GLuint tex) {
|
||||
BindFbo(m_scratch);
|
||||
glUseProgram(m_sampleProgram);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_tex"), 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
BindDefaultFramebuffer();
|
||||
glFinish();
|
||||
}
|
||||
|
||||
// A GPU write the frontend's shadow never sees. A layer >= 0 names one layer of an
|
||||
// array texture.
|
||||
void GpuClear(GLuint tex, int level, const GLfloat* color, int layer = -1) {
|
||||
if (!m_fbo) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
if (layer >= 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, level, layer);
|
||||
} else {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, level);
|
||||
}
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "the clear target is incomplete";
|
||||
glClearBufferfv(GL_COLOR, 0, color);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
BindDefaultFramebuffer();
|
||||
}
|
||||
|
||||
// The first image binding of (src, level[, layer]): an imageLoad copy into a fresh RGBA8
|
||||
// destination, read back through an FBO.
|
||||
std::vector<GLubyte> ImageCopy(GLuint src, int level, GLenum unitFormat, const char* glslFormat,
|
||||
bool snorm = false, int layer = 0) {
|
||||
const GLuint dst = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, dst);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindImageTexture(1, dst, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
|
||||
glBindImageTexture(0, src, level, GL_FALSE, layer, GL_READ_ONLY, unitFormat);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture";
|
||||
const std::string cs = CopyCS(glslFormat, snorm);
|
||||
const char* csp = cs.c_str();
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &csp, nullptr);
|
||||
glCompileShader(shader);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the copy program did not link";
|
||||
glUseProgram(program);
|
||||
// P11 M: A BARRIER THE SPEC DOES NOT ASK FOR, pinned for the reason
|
||||
// TextureRemintPullScenario's twin gives (ID-P9-11, DEBTS "待归因"): on llvmpipe the
|
||||
// kept-allocation case (GpuClearOverAnImmutableUploadSurvives - no re-mint to
|
||||
// serialize anything) read the pre-clear upload once on the push-monolith arm under
|
||||
// load, the same shape P9 measured 3/30 on the split arm. The cases ask what the
|
||||
// storage HOLDS after the first image binding, not how a clear orders against an
|
||||
// image load, so the ordering is pinned here.
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
if (!m_fbo) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0);
|
||||
std::vector<GLubyte> out(static_cast<std::size_t>(kEdge * kEdge) * 4u, 0xA5);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, out.data());
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
BindDefaultFramebuffer();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "copy/readback";
|
||||
return out;
|
||||
}
|
||||
|
||||
// The edge x edge corner of `got` against an RGBA8 expectation, the first `channels`
|
||||
// channels only, within 2 codes (a normalized value's round trip).
|
||||
static void ExpectImage(const std::vector<GLubyte>& got, int edge, const std::vector<GLubyte>& want,
|
||||
int channels = 4) {
|
||||
int bad = 0;
|
||||
std::ostringstream first;
|
||||
for (int y = 0; y < edge; ++y)
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
const std::size_t g = static_cast<std::size_t>((y * kEdge + x) * 4);
|
||||
const std::size_t w = static_cast<std::size_t>((y * edge + x) * 4);
|
||||
bool texelBad = false;
|
||||
for (int c = 0; c < channels; ++c)
|
||||
texelBad |= std::abs(int(got[g + c]) - int(want[w + c])) > 2;
|
||||
if (texelBad && bad++ == 0) {
|
||||
first << "(" << x << "," << y << ") got " << int(got[g]) << "," << int(got[g + 1]) << ","
|
||||
<< int(got[g + 2]) << "," << int(got[g + 3]) << " want " << int(want[w]) << ","
|
||||
<< int(want[w + 1]) << "," << int(want[w + 2]) << "," << int(want[w + 3]);
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0) << bad << " of " << edge * edge << " texels differ; first " << first.str();
|
||||
}
|
||||
|
||||
// One glTexSubImage of opaque blue per {x, y, w, h} box, left pending.
|
||||
static void WriteBlueBoxes(GLuint tex, const std::vector<std::array<int, 4>>& boxes) {
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
for (const auto& b : boxes) {
|
||||
std::vector<GLubyte> blue(static_cast<std::size_t>(b[2] * b[3]) * 4u, 0);
|
||||
for (std::size_t i = 0; i < blue.size(); i += 4) {
|
||||
blue[i + 2] = 255;
|
||||
blue[i + 3] = 255;
|
||||
}
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, b[0], b[1], b[2], b[3], GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// kEdge x kEdge green, blue inside the boxes.
|
||||
static std::vector<GLubyte> GreenWithBlueBoxes(const std::vector<std::array<int, 4>>& boxes) {
|
||||
auto want = Solid(kEdge, 0, 255, 0, 255);
|
||||
for (const auto& b : boxes)
|
||||
for (int y = b[1]; y < b[1] + b[3]; ++y)
|
||||
for (int x = b[0]; x < b[0] + b[2]; ++x) {
|
||||
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
|
||||
want[at] = 0;
|
||||
want[at + 1] = 0;
|
||||
want[at + 2] = 255;
|
||||
want[at + 3] = 255;
|
||||
}
|
||||
return want;
|
||||
}
|
||||
|
||||
GLuint m_sampleProgram = 0, m_vao = 0, m_fbo = 0;
|
||||
ColorFbo m_scratch{};
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
// Control: the shadow and the driver agree.
|
||||
TEST_F(ImageBindableRemintScenario, UploadedLevelKeepsItsUpload) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, p);
|
||||
}
|
||||
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearOverAnUploadSurvives) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 0, kGreen);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
// Immutable in a core image format: the storage is already what an image unit needs.
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearOverAnImmutableUploadSurvives) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 0, kGreen);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
// A null-data definition only the GPU ever filled: the shadow holds zeroes.
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearOfANullDefinitionSurvives) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 0, kGreen);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
// A client write still pending when the bind comes lands on top of the GPU's content,
|
||||
// and only inside its own box.
|
||||
TEST_F(ImageBindableRemintScenario, PendingSubImageLandsOnTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 0, kGreen);
|
||||
SyncBySampling(t);
|
||||
const auto blue = Solid(2, 0, 0, 255, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
auto want = Solid(kEdge, 0, 255, 0, 255);
|
||||
for (int y = 2; y < 4; ++y)
|
||||
for (int x = 2; x < 4; ++x) {
|
||||
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
|
||||
want[at] = 0;
|
||||
want[at + 1] = 0;
|
||||
want[at + 2] = 255;
|
||||
want[at + 3] = 255;
|
||||
}
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, want);
|
||||
}
|
||||
|
||||
// Several pending writes: only their texels land on the GPU's clear, never the gaps between
|
||||
// them. The re-mint merged the union box of the shadow over the readback whenever the rect
|
||||
// list was withheld, and kept the shadow outright when that box spanned the level.
|
||||
|
||||
// Two 3x7 columns with a one-texel gap: 42 of their union's 49 texels, past the 3/4 at which
|
||||
// the storage withholds its rect list.
|
||||
TEST_F(ImageBindableRemintScenario, PendingSubImagesFillingThreeQuartersOfTheirUnionLandOnTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
const std::vector<std::array<int, 4>> boxes = {{0, 0, 3, 7}, {4, 0, 3, 7}};
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 0, kGreen);
|
||||
SyncBySampling(t);
|
||||
WriteBlueBoxes(t, boxes);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, GreenWithBlueBoxes(boxes));
|
||||
}
|
||||
|
||||
// Three quadrants: the union box is the whole level; the fourth quadrant is the GPU's.
|
||||
TEST_F(ImageBindableRemintScenario, PendingSubImagesWhoseUnionSpansTheLevelLandOnTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
const std::vector<std::array<int, 4>> boxes = {{0, 0, 4, 4}, {4, 0, 4, 4}, {0, 4, 4, 4}};
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 0, kGreen);
|
||||
SyncBySampling(t);
|
||||
WriteBlueBoxes(t, boxes);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, GreenWithBlueBoxes(boxes));
|
||||
}
|
||||
|
||||
TEST_F(ImageBindableRemintScenario, GeneratedMipLevelSurvives) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto s = Solid(kEdge, 200, 100, 50, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, s.data());
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), kEdge / 2, Solid(kEdge / 2, 200, 100, 50, 255));
|
||||
}
|
||||
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearOfAnUploadedMipLevelSurvives) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto s0 = Solid(kEdge, 200, 100, 50, 255);
|
||||
const auto s1 = Solid(kEdge / 2, 10, 20, 30, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, s0.data());
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, s1.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 1);
|
||||
SyncBySampling(t);
|
||||
GpuClear(t, 1, kGreen);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), kEdge / 2, Solid(kEdge / 2, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
// Control: a level defined (null data) after the last sync is not on the driver yet, so
|
||||
// there is nothing to read back for it; the bound level 0 must still be the upload.
|
||||
TEST_F(ImageBindableRemintScenario, LevelDefinedAfterTheSyncDoesNotDisturbTheOthers) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, p);
|
||||
}
|
||||
|
||||
// Control: the same late level, partly uploaded - the shadow is the only copy of it.
|
||||
TEST_F(ImageBindableRemintScenario, LevelDefinedAndUploadedAfterTheSyncKeepsTheUpload) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
const auto blue = Solid(2, 0, 0, 255, 255);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), 2, blue);
|
||||
}
|
||||
|
||||
// GL_RG8 is not an ESSL image format, so the re-mint also WIDENS the storage (to RGBA8)
|
||||
// and cannot keep the old allocation even though it is immutable.
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearSurvivesAWideningRemint) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u, 0x33);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
const GLfloat c[4] = {0.5f, 0.25f, 0.0f, 1.0f};
|
||||
GpuClear(t, 0, c);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RG8, "rg8"), kEdge, Solid(kEdge, 128, 64, 0, 255), 2);
|
||||
}
|
||||
|
||||
// Control: widened, uploaded only.
|
||||
TEST_F(ImageBindableRemintScenario, UploadSurvivesAWideningRemint) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u);
|
||||
for (std::size_t i = 0; i < rg.size(); i += 2) {
|
||||
rg[i] = 0x33;
|
||||
rg[i + 1] = 0x99;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RG8, "rg8"), kEdge, Solid(kEdge, 0x33, 0x99, 0, 255), 2);
|
||||
}
|
||||
|
||||
// Control: a signed-normalized level (not colour-renderable on every ES driver, so its
|
||||
// readback may be refused and the shadow used instead), uploaded only.
|
||||
TEST_F(ImageBindableRemintScenario, SnormUploadSurvives) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
std::vector<GLbyte> s(static_cast<std::size_t>(kEdge * kEdge) * 4u);
|
||||
for (std::size_t i = 0; i < s.size(); i += 4) {
|
||||
s[i] = 127;
|
||||
s[i + 1] = 0;
|
||||
s[i + 2] = -127;
|
||||
s[i + 3] = 127;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, kEdge, kEdge, 0, GL_RGBA, GL_BYTE, s.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8_SNORM, "rgba8_snorm", true), kEdge, Solid(kEdge, 255, 128, 0, 255),
|
||||
3);
|
||||
}
|
||||
|
||||
// Control: RGBA16_SNORM is desktop-only as an image format, so this re-mint widens too.
|
||||
TEST_F(ImageBindableRemintScenario, Snorm16UploadSurvivesAWideningRemint) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
std::vector<GLshort> s(static_cast<std::size_t>(kEdge * kEdge) * 4u);
|
||||
for (std::size_t i = 0; i < s.size(); i += 4) {
|
||||
s[i] = 32767;
|
||||
s[i + 1] = 0;
|
||||
s[i + 2] = -32767;
|
||||
s[i + 3] = 32767;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA16_SNORM, kEdge, kEdge);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_SHORT, s.data());
|
||||
Nearest(GL_TEXTURE_2D, t, 0);
|
||||
SyncBySampling(t);
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA16_SNORM, "rgba16_snorm", true), kEdge,
|
||||
Solid(kEdge, 255, 128, 0, 255), 3);
|
||||
}
|
||||
|
||||
// An array texture is read back one layer at a time: the GPU-cleared layer and the
|
||||
// uploaded one both have to come through. The clear's FBO attach is what gives the
|
||||
// texture its driver storage here.
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearOfOneArrayLayerSurvivesBesideTheUploadedLayer) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
std::vector<GLubyte> layers(p);
|
||||
layers.insert(layers.end(), p.begin(), p.end());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, t);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kEdge, kEdge, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
layers.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
Nearest(GL_TEXTURE_2D_ARRAY, t, 0);
|
||||
GpuClear(t, 0, kGreen, /*layer=*/1);
|
||||
glFinish();
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8", false, /*layer=*/1), kEdge, Solid(kEdge, 0, 255, 0, 255));
|
||||
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8", false, /*layer=*/0), kEdge, p);
|
||||
}
|
||||
|
||||
// A view owns no storage, so its first image binding is the VIEWED texture's transition,
|
||||
// and the GPU's texels there have to come through it. Needs glTextureView, which Espryt
|
||||
// only has where the ES driver does (not on llvmpipe).
|
||||
TEST_F(ImageBindableRemintScenario, GpuClearSurvivesTheFirstImageBindingOfAView) {
|
||||
if (!Ready()) return;
|
||||
const GLuint owner = NewTexture();
|
||||
const auto p = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, owner);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
|
||||
Nearest(GL_TEXTURE_2D, owner, 0);
|
||||
SyncBySampling(owner);
|
||||
GpuClear(owner, 0, kGreen);
|
||||
SyncBySampling(owner);
|
||||
const GLuint view = NewTexture();
|
||||
FirstGLError();
|
||||
glTextureView(view, GL_TEXTURE_2D, owner, GL_RGBA8, 0, 1, 0, 1);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName() << " ("
|
||||
<< GLErrorName(error) << ")";
|
||||
}
|
||||
Nearest(GL_TEXTURE_2D, view, 0);
|
||||
SyncBySampling(view);
|
||||
ExpectImage(ImageCopy(view, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -49,8 +49,10 @@
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PersistentMapPeek.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitLane.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
@@ -94,6 +96,12 @@ void main() { word = 0xC0FFEEu; }
|
||||
// Set by the MapPersistentRoundtrips. ctest entry and by nothing else; a harness marker,
|
||||
// never read by the library.
|
||||
constexpr const char* kLaneMarker = "MGITEST_MPR_LANE";
|
||||
// P11 A1: set (to the tier the lane names, "T0") only by the `.AdoptTier0.` tcp entries,
|
||||
// which also set MOBILEGL_IPC_ADOPT_TIER=0; a harness marker, never read by the library.
|
||||
constexpr const char* kAdoptTierRefusalMarker = "MGITEST_ADOPT_TIER_REFUSAL";
|
||||
// P11 B2: set by the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries to the reason word the
|
||||
// server gives for not granting T0 ("no AHardwareBuffer" on a host, "disallowed" with its allow switch off).
|
||||
constexpr const char* kAdoptT0FallbackMarker = "MGITEST_ADOPT_T0_FALLBACK";
|
||||
// Draws issued against the arena inside the counted window. One definition, many draws:
|
||||
// "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different
|
||||
// numbers or the assertion cannot tell them apart.
|
||||
@@ -260,6 +268,27 @@ void main() { word = 0xC0FFEEu; }
|
||||
return px;
|
||||
}
|
||||
|
||||
// P11 A2: the arena's tier as the frontend object records it, against the lane's
|
||||
// declaration. Adopted = the resource owner minted a persistent coherent mapping and
|
||||
// the CPU shadow was released; emulated = the owner declined (T2) and the shadow is
|
||||
// the truth. Pixels cannot tell them apart - every case above is green on both.
|
||||
void ExpectDeclaredArm(const std::string& declared, const char* when) {
|
||||
bool adopted = false;
|
||||
ASSERT_TRUE(PeekBufferIsAdoptedPersistentMap(m_arena, &adopted))
|
||||
<< "no frontend BufferObject behind the arena " << when;
|
||||
const std::string landed = adopted ? "adopted" : "emulated";
|
||||
RecordProperty(std::string("arena_arm_") + (m_armPeeks++ == 0 ? "defined" : "used"),
|
||||
landed.c_str());
|
||||
EXPECT_EQ(declared, landed)
|
||||
<< "the lane declared the " << declared << " arm and the " << (kArenaBytes >> 20)
|
||||
<< " MiB arena is " << landed << " " << when
|
||||
<< ". Monolith lanes adopt on both backends (Espryt's handle arm mints through "
|
||||
"Ops_H_MapPersistentTracked, Magma's legacy AcquirePersistentMap); split lanes "
|
||||
"run T2, where every acquisition declines.";
|
||||
}
|
||||
|
||||
int m_armPeeks = 0;
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_compute = 0;
|
||||
unsigned int m_vao = 0;
|
||||
@@ -293,6 +322,44 @@ void main() { word = 0xC0FFEEu; }
|
||||
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
||||
}
|
||||
|
||||
// P11 B2: THE SAME REWRITE WITH NO READBACK BETWEEN THE TWO DRAWS. The case above reads
|
||||
// pixels after the first draw, which retires it before the SubData, so a store that took the
|
||||
// write IN PLACE (straight into pages the GPU reads - a T0 import is the client's
|
||||
// AHardwareBuffer, an adopted store is a coherent map) would still pass it. Here the first
|
||||
// draw is only queued when the arena is rewritten: it must still fetch the bytes it was
|
||||
// issued against (red, left half) and the second draw the new ones (green, right half).
|
||||
TEST_F(LargeArenaAdoptionScenario, ADrawQueuedBeforeASubDataKeepsItsOwnBytes) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLint w = Gl().Width();
|
||||
const GLint h = Gl().Height();
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
glViewport(0, 0, w, h);
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, w / 2, h);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
glScissor(w / 2, 0, w - w / 2, h);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
|
||||
std::array<unsigned char, 4> left = {0, 0, 0, 0};
|
||||
std::array<unsigned char, 4> right = {0, 0, 0, 0};
|
||||
glReadPixels(w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, left.data());
|
||||
glReadPixels(w / 2 + w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, right.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(left[0], 200) << "the draw queued BEFORE the rewrite lost its bytes: the SubData reached "
|
||||
"pages that draw had not read yet (a write that landed in place instead "
|
||||
"of GPU-ordered)";
|
||||
EXPECT_LT(left[1], 50) << "the draw queued before the rewrite fetched the rewritten bytes";
|
||||
EXPECT_GT(right[1], 200) << "the draw after the rewrite did not see the new bytes";
|
||||
EXPECT_LT(right[0], 50) << "the draw after the rewrite still fetched the old bytes";
|
||||
}
|
||||
|
||||
// Respecifying a frontend buffer preserves its VAO attachments even when the
|
||||
// backend replaces the adopted store's GL name. Keep every attribute binding
|
||||
// unchanged so a stale backend VAO cannot be repaired by a frontend rebind.
|
||||
@@ -341,6 +408,44 @@ void main() { word = 0xC0FFEEu; }
|
||||
glDeleteVertexArrays(1, &otherVao);
|
||||
}
|
||||
|
||||
// P11 B2: A RESPECIFY WHILE A DRAW OF THE OLD STORE IS STILL QUEUED. The case above reads pixels
|
||||
// before every respecify, so the old store is retired by then and a server that freed it at
|
||||
// once would still pass. Here the first draw is only queued when the arena is redefined: the
|
||||
// old store (a T0 import: the client's AHardwareBuffer, which the client lets go of at this
|
||||
// very respecify) must outlive it, and the draw must show the bytes it was issued against.
|
||||
TEST_F(LargeArenaAdoptionScenario, ARespecifyWithADrawQueuedKeepsThatDrawsStore) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLint w = Gl().Width();
|
||||
const GLint h = Gl().Height();
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
glViewport(0, 0, w, h);
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, w / 2, h);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
glScissor(w / 2, 0, w - w / 2, h);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
|
||||
std::array<unsigned char, 4> left = {0, 0, 0, 0};
|
||||
std::array<unsigned char, 4> right = {0, 0, 0, 0};
|
||||
glReadPixels(w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, left.data());
|
||||
glReadPixels(w / 2 + w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, right.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(left[0], 200) << "the draw queued before the respecify lost the store it was issued "
|
||||
"against (freed before that draw ran)";
|
||||
EXPECT_LT(left[1], 50) << "the draw queued before the respecify fetched the new store's bytes";
|
||||
EXPECT_GT(right[1], 200) << "the draw after the respecify did not fetch the new store";
|
||||
EXPECT_LT(right[0], 50) << "the draw after the respecify still fetched the old store";
|
||||
}
|
||||
|
||||
TEST_F(LargeArenaAdoptionScenario, RespecifiedIndexArenaKeepsVaoBinding) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
@@ -505,4 +610,140 @@ void main() { word = 0xC0FFEEu; }
|
||||
<< window.line;
|
||||
}
|
||||
|
||||
// P11 A2 (MG_Remote/CONTRACT-P11.md §2): WHICH TIER THE ARENA REALLY LANDED IN. The cases above
|
||||
// are green whether the arena adopted or stayed emulated, so the lane declares the arm
|
||||
// (MGITEST_PERSISTENT_MAP_ARM) and this case asserts it, the way
|
||||
// PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares does for a sub-16-MiB map.
|
||||
// Asked twice: after the NULL-data definition (TryAdoptLargeStorage's door) and after a frame
|
||||
// of use that ends in an SSBO binding and a dispatch (the storage-binding door,
|
||||
// EnsureGpuResidentStorage, which Magma's server walk opens). Lanes that declare nothing - the
|
||||
// ambient entries, which run under whatever transport the job exports - skip.
|
||||
TEST_F(LargeArenaAdoptionScenario, TheArenaLandsInTheTierItsLaneDeclares) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const std::string declared = SplitLane::DeclaredPersistentMapArm();
|
||||
if (declared.empty()) {
|
||||
GTEST_SKIP() << "this lane declares no arm: MGITEST_PERSISTENT_MAP_ARM is set only by the "
|
||||
"lanes that pin their transport (monolith: adopted; the split arms: "
|
||||
"emulated)";
|
||||
}
|
||||
if (!PersistentMapPeekAvailable()) {
|
||||
GTEST_SKIP() << "this lane declares the " << declared << " arm but Harness/PersistentMapPeek "
|
||||
"cannot look in this build (on Android this module links the shipping "
|
||||
"library, built -fvisibility=hidden). 'Could not look' is not 'it was "
|
||||
<< declared << "'.";
|
||||
}
|
||||
ExpectDeclaredArm(declared, "right after its NULL-data definition");
|
||||
if (HasFatalFailure()) return;
|
||||
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
DrawQuad();
|
||||
EXPECT_GT(CenterPixel()[0], 200) << "the draw from the arena never landed";
|
||||
Gl().EndFrame();
|
||||
|
||||
GLint maxComputeStorageBlocks = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
|
||||
if (maxComputeStorageBlocks >= 1) {
|
||||
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
|
||||
ASSERT_NE(compute, 0u) << m_buildLog;
|
||||
m_compute = glCreateProgram();
|
||||
glAttachShader(m_compute, compute);
|
||||
glLinkProgram(m_compute);
|
||||
glDeleteShader(compute);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
|
||||
glUseProgram(m_compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
unsigned int marker = 0;
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
|
||||
EXPECT_EQ(marker, 0xC0FFEEu) << "the dispatch through the arena's SSBO binding never landed";
|
||||
} else {
|
||||
RecordProperty("arena_arm_used", "not asked: no compute shader storage blocks");
|
||||
}
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectDeclaredArm(declared, "after a frame of draws, an SSBO binding and a dispatch");
|
||||
}
|
||||
|
||||
// P11 A1 (MG_Remote/CONTRACT-P11.md §1): THE TCP LANE WITH MOBILEGL_IPC_ADOPT_TIER=0. A stream
|
||||
// data plane has no memory the two sides share, so the client refuses the knob by name at its
|
||||
// handshake - exactly one `Refuse{AdoptTierOnStream, "T0"}` line in its log - and the session
|
||||
// runs T2: the other cases in the same `.AdoptTier0.` lane are green and the arena is emulated.
|
||||
// Before A1 this lane ran T2 silently. Everywhere else it skips.
|
||||
TEST_F(LargeArenaAdoptionScenario, AStreamSessionRefusesTheAdoptTierOnceByName) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const char* tier = std::getenv(kAdoptTierRefusalMarker);
|
||||
if (tier == nullptr || tier[0] == '\0') {
|
||||
GTEST_SKIP() << "runs only in the `.AdoptTier0.` tcp entries, which set "
|
||||
"MOBILEGL_IPC_ADOPT_TIER=0 and " << kAdoptTierRefusalMarker << "=T0";
|
||||
}
|
||||
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the client's log is "
|
||||
"where the refusal is written";
|
||||
}
|
||||
// A frame of the arena's work first: a refusal is only worth reading from a session that
|
||||
// then ran - and an armed split lane requires the case to move the record ordinal anyway.
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
DrawQuad();
|
||||
EXPECT_GT(CenterPixel()[1], 200) << "the session that refused the tier did not render";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
Gl().EndFrame();
|
||||
|
||||
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
|
||||
const std::string line = std::string("Refuse{AdoptTierOnStream, \"") + tier + "\"}";
|
||||
std::size_t count = 0;
|
||||
for (auto at = log.find(line); at != std::string::npos; at = log.find(line, at + 1)) ++count;
|
||||
RecordProperty("adopt_tier_refusals", static_cast<int>(count));
|
||||
EXPECT_EQ(count, 1u) << "the client names MOBILEGL_IPC_ADOPT_TIER on a stream exactly once, at "
|
||||
"its handshake (" << line << "), in " << PipeStatsWindow::LibraryLogPath();
|
||||
EXPECT_EQ(log.find("Fatal{UnimplementedAdoptTier"), std::string::npos) << log;
|
||||
}
|
||||
|
||||
// P11 B2 (MG_Remote/CONTRACT-P11.md B2): A SHARED-SEGMENT SESSION THAT ASKS T0 AND CANNOT RUN IT.
|
||||
// MOBILEGL_IPC_ADOPT_TIER=0 over shared segments asks T0 in the Hello (A1 made it die at the
|
||||
// handshake; ruling ID-P11-14 made it this). The lane's server cannot grant it - a host has no
|
||||
// AHardwareBuffer, or `.AdoptT0Disallowed.` turned its allow switch off - so it refuses by name
|
||||
// at its first native bind, publishes no kCapAdoptT0, and the client falls back to T2 with ONE
|
||||
// named line at its first map_persistent. Never a Fatal; the other cases in the lane are green
|
||||
// and the arena is emulated. Everywhere else it skips.
|
||||
TEST_F(LargeArenaAdoptionScenario, ASessionThatCannotRunT0FallsBackToT2OnceByName) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
const char* why = std::getenv(kAdoptT0FallbackMarker);
|
||||
if (why == nullptr || why[0] == '\0') {
|
||||
GTEST_SKIP() << "runs only in the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries, "
|
||||
"which set MOBILEGL_IPC_ADOPT_TIER=0 and " << kAdoptT0FallbackMarker;
|
||||
}
|
||||
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the two logs are where the "
|
||||
"fallback is written";
|
||||
}
|
||||
// A frame of the arena's work first: the fallback is only worth reading from a session that
|
||||
// then ran on T2 - and the client decides at its first map_persistent, which SetUp's
|
||||
// definition of the arena has already made.
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
DrawQuad();
|
||||
EXPECT_GT(CenterPixel()[1], 200) << "the session that fell back to T2 did not render";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
Gl().EndFrame();
|
||||
|
||||
const auto countOf = [](const std::string& text, const std::string& needle) {
|
||||
std::size_t n = 0;
|
||||
for (auto at = text.find(needle); at != std::string::npos; at = text.find(needle, at + 1)) ++n;
|
||||
return n;
|
||||
};
|
||||
const std::string client = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
|
||||
const std::string server = PipeStatsWindow::ReadServerLogSince(PipeStatsWindow::LogMark{});
|
||||
const std::string clientLine = "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}";
|
||||
const std::string serverLine = std::string("Refuse{AdoptT0Unavailable, \"") + why + "\"}";
|
||||
RecordProperty("t0_client_fallbacks", static_cast<int>(countOf(client, clientLine)));
|
||||
RecordProperty("t0_server_refusals", static_cast<int>(countOf(server, serverLine)));
|
||||
EXPECT_EQ(countOf(client, clientLine), 1u)
|
||||
<< "the client names a T0 it asked for and cannot run exactly once, at its first map_persistent ("
|
||||
<< clientLine << "), in " << PipeStatsWindow::LibraryLogPath();
|
||||
EXPECT_EQ(countOf(server, serverLine), 1u)
|
||||
<< "the server names why it did not grant T0 exactly once, at its first native bind (" << serverLine
|
||||
<< "), in " << PipeStatsWindow::ServerLibraryLogPath();
|
||||
EXPECT_EQ(client.find("Fatal{"), std::string::npos) << client;
|
||||
EXPECT_EQ(server.find("Fatal{"), std::string::npos) << server;
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,765 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.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
|
||||
//
|
||||
// Scenario - A FRAME WITH MORE DRAWS THAN ONE COMMAND BUFFER MAY HOLD IS SPLIT, AND LOSES NOTHING.
|
||||
//
|
||||
// DirectVulkan records a frame into one command buffer, and used to submit it only at Present.
|
||||
// A driver keeps a command buffer's GPU memory until the buffer is freed, and the Adreno 830
|
||||
// driver maps that memory into the process in 16 KiB chunks. Minecraft rd12's loading frame
|
||||
// records 981,654 draws (3.1M commands) into one buffer. It grew to ~40k such mappings and
|
||||
// aborted inside the driver's own calloc once vm.max_map_count (65530) was spent. The renderer
|
||||
// now submits the open buffer after MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER draws and goes
|
||||
// on recording into a fresh one (VulkanRenderer::SplitOversizedRecording).
|
||||
//
|
||||
// The first case writes each pixel of a 32x32 target with its own draw and its own uniform
|
||||
// value, so it pins that no draw is lost and no per-draw state is left behind on either side of
|
||||
// a split. The rest put GL state that one draw leaves for a later one ACROSS the split: a depth
|
||||
// occluder, a stencil mark, a blend destination, the samples of a multisample target, an
|
||||
// occlusion or primitives-generated query that is still open, and a transform-feedback capture
|
||||
// that is still active. Each draws its "before" part, then kFillers draws, then its "after"
|
||||
// part. kFillers is more than twice the SplitRecording lane's budget of 64, so at least one
|
||||
// split lands between the two parts wherever the recording's count stood.
|
||||
//
|
||||
// The ambient registrations run the same draws unsplit, because the default budget is far above
|
||||
// any of these frames; that is the control. The DirectVulkan.SplitRecording. entry pins the
|
||||
// budget to 64, and there every case also asserts the renderer's latched MGLOG_I - the one
|
||||
// assertion that is red on a renderer that never splits.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kSide = 32;
|
||||
constexpr int kDraws = kSide * kSide;
|
||||
// More than twice the SplitRecording lane's budget (64): wherever the count stands when
|
||||
// they start, at least one split lands among them.
|
||||
constexpr int kFillers = 150;
|
||||
// Draws inside the query and capture spans: also more than twice the budget.
|
||||
constexpr int kSpanDraws = 200;
|
||||
|
||||
// aPos spans the unit square; the uniform moves it onto cell (i % 32, i / 32) of a
|
||||
// 32x32 grid, which is exactly one pixel of a 32x32 target.
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform int uIndex;
|
||||
void main() {
|
||||
vec2 cell = vec2(float(uIndex % 32), float(uIndex / 32));
|
||||
gl_Position = vec4((cell + aPos) / 16.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Multiples of 8/255 survive an RGBA8 round trip exactly, so the readback bytes are
|
||||
// (8x, 8y, 255, 255) for the pixel at (x, y) and nothing else can produce them.
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
uniform int uIndex;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(float(uIndex % 32) * 8.0 / 255.0, float(uIndex / 32) * 8.0 / 255.0, 1.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The state cases: the unit-square vertices (or a triangle of them) stretched over an
|
||||
// NDC rectangle at a given depth, in a uniform colour. A zero rectangle makes a
|
||||
// zero-area primitive - a draw that reaches the renderer and rasterizes nothing.
|
||||
constexpr const char* kRectVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform vec4 uRect;
|
||||
uniform float uDepth;
|
||||
void main() { gl_Position = vec4(mix(uRect.xy, uRect.zw, aPos), uDepth, 1.0); }
|
||||
)";
|
||||
constexpr const char* kRectFS = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = uColor; }
|
||||
)";
|
||||
|
||||
// The capture case: one point per draw, carrying 2 * uIndex + 1.
|
||||
constexpr const char* kCaptureVS = R"(#version 330 core
|
||||
uniform int uIndex;
|
||||
out float vs_value;
|
||||
void main() {
|
||||
vs_value = float(uIndex) * 2.0 + 1.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
constexpr const char* kCaptureFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0); }
|
||||
)";
|
||||
|
||||
constexpr float kCapturePoison = -12345.0f;
|
||||
|
||||
// The budget the process was launched with, or 0 when it is unset or not a number.
|
||||
unsigned long PinnedDrawBudget() {
|
||||
const char* value = std::getenv("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER");
|
||||
if (value == nullptr || *value == '\0') return 0;
|
||||
char* end = nullptr;
|
||||
const unsigned long parsed = std::strtoul(value, &end, 10);
|
||||
return (end != nullptr && *end == '\0') ? parsed : 0;
|
||||
}
|
||||
|
||||
// The library log, for the arming assertion. MOBILEGL_LOG_FILE_PATH is read at log-init,
|
||||
// every process in the lane appends to it, and only bytes appended after the snapshot
|
||||
// count. P11 M2: ON A SPLIT BUILD IT IS A BASE NAME - the library writes one file per role
|
||||
// (PipeStatsWindow.h) - and the latched line comes from the RENDERER, which under a
|
||||
// transport is the server role: the apply thread under inproc, the spawned server process
|
||||
// under spawn (it inherits this entry's environment, budget and log base included). So the
|
||||
// mark and the read cover both roles, as PrimitivesGeneratedNoXfbScenario's arming does; a
|
||||
// read of the literal base found no file and reported an unsplit renderer. In the pull
|
||||
// build the client role is the whole log.
|
||||
std::string LibraryLogPath() { return PipeStatsWindow::LibraryLogPath(); }
|
||||
|
||||
PipeStatsWindow::LogMark LibraryLogSize() { return PipeStatsWindow::MarkLaneLog(); }
|
||||
|
||||
std::string LibraryLogSince(const PipeStatsWindow::LogMark& mark) {
|
||||
return PipeStatsWindow::ReadLaneLogSince(mark);
|
||||
}
|
||||
|
||||
std::string Describe(const Rgba8& c) {
|
||||
std::ostringstream os;
|
||||
os << "rgba(" << static_cast<int>(c.r) << ", " << static_cast<int>(c.g) << ", " << static_cast<int>(c.b)
|
||||
<< ", " << static_cast<int>(c.a) << ")";
|
||||
return os.str();
|
||||
}
|
||||
|
||||
::testing::AssertionResult EveryPixelHoldsItsOwnDraw(const Image& image) {
|
||||
int wrong = 0;
|
||||
int firstX = -1, firstY = -1;
|
||||
Rgba8 firstSeen{};
|
||||
for (int y = 0; y < kSide; ++y) {
|
||||
for (int x = 0; x < kSide; ++x) {
|
||||
const Rgba8 expected{static_cast<std::uint8_t>(8 * x), static_cast<std::uint8_t>(8 * y), 255, 255};
|
||||
const Rgba8 seen = image.At(x, y);
|
||||
if (seen != expected) {
|
||||
if (wrong == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
firstSeen = seen;
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrong == 0) {
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
return ::testing::AssertionFailure()
|
||||
<< wrong << " of " << kDraws << " pixels do not hold their own draw's colour. First: (" << firstX
|
||||
<< ", " << firstY << ") = " << firstSeen << ", drawn by draw " << (firstY * kSide + firstX)
|
||||
<< " (a pixel no draw reached still holds the clear colour 0,0,0,0)";
|
||||
}
|
||||
|
||||
// Every pixel of columns [x0, x1] (all rows) is `expected`, each channel within `tolerance`.
|
||||
::testing::AssertionResult ColumnsAre(const Image& image, int x0, int x1, const Rgba8& expected,
|
||||
int tolerance, const char* what) {
|
||||
int wrong = 0;
|
||||
int firstX = -1, firstY = -1;
|
||||
Rgba8 firstSeen{};
|
||||
for (int y = 0; y < kSide; ++y) {
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const Rgba8 seen = image.At(x, y);
|
||||
const bool close = std::abs(seen.r - expected.r) <= tolerance &&
|
||||
std::abs(seen.g - expected.g) <= tolerance &&
|
||||
std::abs(seen.b - expected.b) <= tolerance &&
|
||||
std::abs(seen.a - expected.a) <= tolerance;
|
||||
if (!close) {
|
||||
if (wrong == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
firstSeen = seen;
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrong == 0) return ::testing::AssertionSuccess();
|
||||
return ::testing::AssertionFailure()
|
||||
<< what << ": " << wrong << " pixels of columns " << x0 << ".." << x1 << " are not "
|
||||
<< Describe(expected) << "; first (" << firstX << ", " << firstY << ") = " << Describe(firstSeen);
|
||||
}
|
||||
|
||||
class OversizedRecordingSplitScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
// Taken before the case draws anything, so the line the arming check looks for
|
||||
// can only have been written by this case. The renderer latches it at its first
|
||||
// split, and ctest gives every case a process of its own.
|
||||
m_logBefore = LibraryLogSize();
|
||||
|
||||
// 0..3: the unit square as a strip. 4..6: its lower-left triangle. 7..9: its
|
||||
// upper-right triangle - the two share the diagonal, so together they cover every
|
||||
// sample of the square exactly once.
|
||||
static const float kVertices[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f,
|
||||
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFF);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
BindDefaultFramebuffer();
|
||||
if (m_rectProgram != 0) glDeleteProgram(m_rectProgram);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
for (GLuint rbo : m_renderbuffers) glDeleteRenderbuffers(1, &rbo);
|
||||
for (GLuint fbo : m_framebuffers) glDeleteFramebuffers(1, &fbo);
|
||||
for (ColorFbo& target : m_colorTargets) DestroyColorFbo(target);
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// In the SplitRecording lane (budget pinned below what every case draws, DirectVulkan,
|
||||
// a log to read), a split must have happened during this case. Elsewhere this case is
|
||||
// the unsplit control and there is nothing to arm.
|
||||
void ExpectTheRecordingWasSplitWhereTheBudgetIsPinned() {
|
||||
const unsigned long budget = PinnedDrawBudget();
|
||||
const bool splitExpected = budget > 0 && budget * 2 < static_cast<unsigned long>(kFillers) &&
|
||||
Gl().BackendName() == std::string("DirectVulkan") &&
|
||||
!LibraryLogPath().empty();
|
||||
RecordProperty("arming_checked", splitExpected ? "yes" : "no");
|
||||
if (!splitExpected) return;
|
||||
const std::string appended = LibraryLogSince(m_logBefore);
|
||||
EXPECT_NE(appended.find("draws into one command buffer; submitting it"), std::string::npos)
|
||||
<< "MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=" << budget << " and this case recorded more "
|
||||
<< "than " << kFillers << " draws in one frame, but DirectVulkan never reported splitting the "
|
||||
"recording. A renderer that does not split grows a single command buffer without bound "
|
||||
"(rd12 on Adreno: ~40k driver mappings, then SIGABRT). See "
|
||||
"VulkanRenderer::SplitOversizedRecording. Log appended by this case:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
bool BuildRectProgram(std::string* error) {
|
||||
m_rectProgram = CompileProgram(kRectVS, kRectFS, error);
|
||||
if (m_rectProgram == 0) return false;
|
||||
m_rectLoc = glGetUniformLocation(m_rectProgram, "uRect");
|
||||
m_depthLoc = glGetUniformLocation(m_rectProgram, "uDepth");
|
||||
m_colorLoc = glGetUniformLocation(m_rectProgram, "uColor");
|
||||
glUseProgram(m_rectProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The rectangle [x0, x1] x [y0, y1] in NDC; `first`/`count` pick the strip or a triangle.
|
||||
void DrawRect(float x0, float y0, float x1, float y1, float depth, float r, float g, float b, float a,
|
||||
GLenum mode = GL_TRIANGLE_STRIP, GLint first = 0, GLsizei count = 4) {
|
||||
glUniform4f(m_rectLoc, x0, y0, x1, y1);
|
||||
glUniform1f(m_depthLoc, depth);
|
||||
glUniform4f(m_colorLoc, r, g, b, a);
|
||||
glDrawArrays(mode, first, count);
|
||||
}
|
||||
|
||||
// One pixel of the 32x32 grid, by index.
|
||||
void DrawPixel(int index, float r, float g, float b, float a) {
|
||||
const float x = static_cast<float>(index % kSide);
|
||||
const float y = static_cast<float>(index / kSide);
|
||||
DrawRect(x / 16.0f - 1.0f, y / 16.0f - 1.0f, (x + 1.0f) / 16.0f - 1.0f, (y + 1.0f) / 16.0f - 1.0f, 0.0f,
|
||||
r, g, b, a);
|
||||
}
|
||||
|
||||
// Draws that reach the renderer and rasterize nothing: zero-area strips.
|
||||
void DrawFillers(int count) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
DrawRect(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// A kSide x kSide RGBA8 target, optionally with a DEPTH24_STENCIL8 renderbuffer.
|
||||
// Returns fbo 0 when the driver will not complete it.
|
||||
ColorFbo MakeTarget(bool withDepthStencil) {
|
||||
ColorFbo target = MakeColorFbo(kSide, kSide);
|
||||
if (target.fbo == 0) return target;
|
||||
m_colorTargets.push_back(target);
|
||||
if (withDepthStencil) {
|
||||
GLuint rbo = 0;
|
||||
glGenRenderbuffers(1, &rbo);
|
||||
m_renderbuffers.push_back(rbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSide, kSide);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
target.fbo = 0;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
Image ReadTarget(const ColorFbo& target) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
return ReadPixels(kSide, kSide);
|
||||
}
|
||||
|
||||
PipeStatsWindow::LogMark m_logBefore{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_rectProgram = 0;
|
||||
GLint m_rectLoc = -1;
|
||||
GLint m_depthLoc = -1;
|
||||
GLint m_colorLoc = -1;
|
||||
std::vector<ColorFbo> m_colorTargets;
|
||||
std::vector<GLuint> m_renderbuffers;
|
||||
std::vector<GLuint> m_framebuffers;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// kDraws single-pixel quads into a fresh target, one uniform value per draw, with no
|
||||
// readback, flush or rebind in between. Every pixel must hold its own draw's colour.
|
||||
TEST_F(OversizedRecordingSplitScenario, EveryDrawOfTheFrameLandsAcrossCommandBufferSplits) {
|
||||
if (!Ready()) return;
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
ColorFbo target = MakeTarget(false);
|
||||
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
|
||||
|
||||
BindFbo(target);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
|
||||
for (int i = 0; i < kDraws; ++i) {
|
||||
glUniform1i(indexLocation, i);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
const Image image = ReadPixels(kSide, kSide);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
|
||||
EXPECT_TRUE(EveryPixelHoldsItsOwnDraw(image));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (a) An occluder drawn before the split must still fail the depth test of a draw after it:
|
||||
// the depth attachment's contents have to survive the render pass ending and re-beginning.
|
||||
TEST_F(OversizedRecordingSplitScenario, ADepthOccluderDrawnBeforeTheSplitHidesADrawAfterIt) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildRectProgram(&error)) << error;
|
||||
ColorFbo target = MakeTarget(true);
|
||||
if (target.fbo == 0) GTEST_SKIP() << "no complete RGBA8 + DEPTH24_STENCIL8 target on this driver";
|
||||
|
||||
BindFbo(target);
|
||||
glClearDepth(1.0);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
|
||||
DrawRect(-1.0f, -1.0f, 0.0f, 1.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f); // left half, near, red
|
||||
DrawFillers(kFillers);
|
||||
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f); // everything, far, green
|
||||
|
||||
const Image image = ReadTarget(target);
|
||||
EXPECT_TRUE(ColumnsAre(image, 0, kSide / 2 - 1, Rgba8{255, 0, 0, 255}, 0,
|
||||
"the near occluder drawn before the split must hide the far draw after it"));
|
||||
EXPECT_TRUE(ColumnsAre(image, kSide / 2, kSide - 1, Rgba8{0, 255, 0, 255}, 0,
|
||||
"where nothing occludes, the far draw after the split must land"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (b) A stencil mark made before the split must gate a draw after it.
|
||||
TEST_F(OversizedRecordingSplitScenario, AStencilMarkMadeBeforeTheSplitGatesADrawAfterIt) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildRectProgram(&error)) << error;
|
||||
ColorFbo target = MakeTarget(true);
|
||||
if (target.fbo == 0) GTEST_SKIP() << "no complete RGBA8 + DEPTH24_STENCIL8 target on this driver";
|
||||
|
||||
BindFbo(target);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glStencilMask(0xFF);
|
||||
glClearStencil(0);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFF);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
DrawRect(-1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f); // left half: stencil 1, blue
|
||||
glStencilFunc(GL_EQUAL, 1, 0xFF);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||
DrawFillers(kFillers);
|
||||
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f); // everything, green, where stencil == 1
|
||||
|
||||
const Image image = ReadTarget(target);
|
||||
EXPECT_TRUE(ColumnsAre(image, 0, kSide / 2 - 1, Rgba8{0, 255, 0, 255}, 0,
|
||||
"where the mark was made before the split, the draw after it must pass"));
|
||||
EXPECT_TRUE(ColumnsAre(image, kSide / 2, kSide - 1, Rgba8{0, 0, 0, 255}, 0,
|
||||
"where no mark was made, the draw after the split must be stencilled out"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (c) Additive blending onto pixels written before the split: every one of kSpanDraws
|
||||
// full-target draws adds exactly 1/255 of red onto a target whose green was written first,
|
||||
// so the result counts every draw on both sides of every split.
|
||||
TEST_F(OversizedRecordingSplitScenario, BlendingAccumulatesOntoPixelsWrittenBeforeTheSplit) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildRectProgram(&error)) << error;
|
||||
ColorFbo target = MakeTarget(false);
|
||||
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
|
||||
|
||||
BindFbo(target);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendEquation(GL_FUNC_ADD);
|
||||
glBlendFunc(GL_ONE, GL_ONE);
|
||||
|
||||
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 55.0f / 255.0f, 0.0f, 0.0f);
|
||||
for (int i = 0; i < kSpanDraws; ++i) {
|
||||
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f / 255.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
const Image image = ReadTarget(target);
|
||||
EXPECT_TRUE(ColumnsAre(image, 0, kSide - 1, Rgba8{static_cast<std::uint8_t>(kSpanDraws), 55, 0, 0}, 1,
|
||||
"every additive draw, and the green written before any split, must be in the sum"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (d) A multisample target drawn on both sides of the split, then resolved. Before the
|
||||
// split a triangle covers the lower-left half of the target; after it, the complementary
|
||||
// triangle covers the upper-right half. On the diagonal each pixel's samples are shared
|
||||
// between the two, so only a target that kept every SAMPLE across the split resolves them to
|
||||
// red + green = 255. One that was resolved or re-broadcast at the split gives about 190
|
||||
// there, and one whose samples were dropped gives black or garbage.
|
||||
TEST_F(OversizedRecordingSplitScenario, AMultisampleTargetKeepsItsSamplesAcrossTheSplit) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildRectProgram(&error)) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
if (maxSamples < 2) {
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
||||
}
|
||||
const GLsizei samples = std::min<GLint>(4, maxSamples);
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
m_framebuffers.push_back(msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
m_renderbuffers.push_back(msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kSide, kSide);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
GTEST_SKIP() << "no complete " << samples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
ColorFbo resolved = MakeTarget(false);
|
||||
ASSERT_NE(resolved.fbo, 0u) << "MakeColorFbo failed";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glViewport(0, 0, kSide, kSide);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, GL_TRIANGLES, 4, 3); // lower-left, red
|
||||
DrawFillers(kFillers);
|
||||
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, GL_TRIANGLES, 7, 3); // upper-right, green
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
|
||||
glBlitFramebuffer(0, 0, kSide, kSide, 0, 0, kSide, kSide, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
const Image image = ReadTarget(resolved);
|
||||
|
||||
int wrong = 0;
|
||||
std::string first;
|
||||
for (int y = 0; y < kSide && wrong < 1000; ++y) {
|
||||
for (int x = 0; x < kSide; ++x) {
|
||||
const Rgba8 seen = image.At(x, y);
|
||||
bool ok = false;
|
||||
// The diagonal x + y = kSide in pixel units passes through the pixels whose
|
||||
// x + y == kSide - 1; every other pixel lies wholly on one side of it.
|
||||
if (x + y < kSide - 1) {
|
||||
ok = seen == Rgba8{255, 0, 0, 255};
|
||||
} else if (x + y > kSide - 1) {
|
||||
ok = seen == Rgba8{0, 255, 0, 255};
|
||||
} else {
|
||||
ok = seen.b == 0 && seen.a == 255 && seen.r > 0 && seen.g > 0 &&
|
||||
std::abs(static_cast<int>(seen.r) + static_cast<int>(seen.g) - 255) <= 3;
|
||||
}
|
||||
if (!ok) {
|
||||
if (wrong == 0) {
|
||||
std::ostringstream os;
|
||||
os << "(" << x << ", " << y << ") = " << Describe(seen);
|
||||
first = os.str();
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(wrong, 0) << wrong << " resolved pixels are wrong; first " << first
|
||||
<< ". Lower-left of the diagonal must be red, upper-right green, and a pixel on it "
|
||||
"red + green = 255 with both present (its samples were split between a draw before "
|
||||
"the command-buffer split and one after it)";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (e) Occlusion queries open across the split. GL_SAMPLES_PASSED counts kSpanDraws
|
||||
// single-pixel draws on both sides of it; GL_ANY_SAMPLES_PASSED is answered by the one draw
|
||||
// made before the split, the fillers after it pass nothing - with a fillers-only control
|
||||
// that must answer false.
|
||||
TEST_F(OversizedRecordingSplitScenario, OcclusionQueriesOpenAcrossTheSplitCountBothSides) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildRectProgram(&error)) << error;
|
||||
ColorFbo target = MakeTarget(false);
|
||||
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
|
||||
BindFbo(target);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
GLuint queries[3] = {};
|
||||
glGenQueries(3, queries);
|
||||
FirstGLError();
|
||||
|
||||
// Any samples: control first (fillers only), then one real pixel before the split.
|
||||
glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[0]);
|
||||
const unsigned int anyBeginError = FirstGLError();
|
||||
if (anyBeginError != 0u) {
|
||||
glDeleteQueries(3, queries);
|
||||
GTEST_SKIP() << Gl().BackendName() << " does not accept GL_ANY_SAMPLES_PASSED ("
|
||||
<< GLErrorName(anyBeginError) << ")";
|
||||
}
|
||||
DrawFillers(kFillers);
|
||||
glEndQuery(GL_ANY_SAMPLES_PASSED);
|
||||
glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[1]);
|
||||
DrawPixel(0, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
DrawFillers(kFillers);
|
||||
glEndQuery(GL_ANY_SAMPLES_PASSED);
|
||||
GLuint anyControl = 7, anyAcross = 7;
|
||||
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &anyControl);
|
||||
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &anyAcross);
|
||||
EXPECT_EQ(anyControl, static_cast<GLuint>(GL_FALSE)) << "fillers alone rasterize nothing";
|
||||
EXPECT_EQ(anyAcross, static_cast<GLuint>(GL_TRUE))
|
||||
<< "the only passing draw was made before the split; the query that spans it lost that answer";
|
||||
|
||||
// Samples passed: kSpanDraws distinct single-sample pixels, spanning at least one split.
|
||||
glBeginQuery(GL_SAMPLES_PASSED, queries[2]);
|
||||
const unsigned int samplesBeginError = FirstGLError();
|
||||
if (samplesBeginError == 0u) {
|
||||
for (int i = 0; i < kSpanDraws; ++i) {
|
||||
DrawPixel(i, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
}
|
||||
glEndQuery(GL_SAMPLES_PASSED);
|
||||
GLuint samplesPassed = 0;
|
||||
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &samplesPassed);
|
||||
if (Gl().BackendName() == std::string("DirectGLES")) {
|
||||
// OpenGL ES has no sample-counting occlusion query, only ANY_SAMPLES_PASSED*, so
|
||||
// DirectGLES answers GL_SAMPLES_PASSED with a 0/1 approximation (DirectGLES.cpp,
|
||||
// the occlusion-query result path). The exact count is not this backend's to give.
|
||||
RecordProperty("samples_passed", "0/1 approximation on OpenGL ES; exact count not checked");
|
||||
EXPECT_EQ(samplesPassed, 1u) << "samples passed, so the approximation must read 1";
|
||||
} else {
|
||||
EXPECT_EQ(samplesPassed, static_cast<GLuint>(kSpanDraws))
|
||||
<< "each of the " << kSpanDraws << " draws covers exactly one single-sample pixel";
|
||||
}
|
||||
} else {
|
||||
RecordProperty("samples_passed", std::string("not accepted: ") + GLErrorName(samplesBeginError));
|
||||
}
|
||||
glDeleteQueries(3, queries);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (e) GL_PRIMITIVES_GENERATED open across the split: kSpanDraws strips of two triangles.
|
||||
// A one-draw query first establishes that this stack counts at all.
|
||||
TEST_F(OversizedRecordingSplitScenario, APrimitivesGeneratedQueryOpenAcrossTheSplitCountsBothSides) {
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildRectProgram(&error)) << error;
|
||||
ColorFbo target = MakeTarget(false);
|
||||
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
|
||||
BindFbo(target);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
GLuint queries[2] = {};
|
||||
glGenQueries(2, queries);
|
||||
FirstGLError();
|
||||
glBeginQuery(GL_PRIMITIVES_GENERATED, queries[0]);
|
||||
const unsigned int beginError = FirstGLError();
|
||||
if (beginError != 0u) {
|
||||
glDeleteQueries(2, queries);
|
||||
GTEST_SKIP() << Gl().BackendName() << " does not accept GL_PRIMITIVES_GENERATED ("
|
||||
<< GLErrorName(beginError) << ")";
|
||||
}
|
||||
DrawPixel(0, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||
GLuint control = 0;
|
||||
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &control);
|
||||
if (control == 0u) {
|
||||
glDeleteQueries(2, queries);
|
||||
GTEST_SKIP() << "GL_PRIMITIVES_GENERATED reads 0 for a single unsplit strip on " << Gl().BackendName()
|
||||
<< " (" << Gl().RendererString() << "): nothing this stack counts, so nothing to carry";
|
||||
}
|
||||
ASSERT_EQ(control, 2u) << "one strip of four vertices is two triangles";
|
||||
|
||||
glBeginQuery(GL_PRIMITIVES_GENERATED, queries[1]);
|
||||
for (int i = 0; i < kSpanDraws; ++i) {
|
||||
DrawPixel(i, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
}
|
||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||
GLuint generated = 0;
|
||||
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &generated);
|
||||
EXPECT_EQ(generated, static_cast<GLuint>(2 * kSpanDraws))
|
||||
<< kSpanDraws << " strips of two triangles each, drawn across at least one split";
|
||||
glDeleteQueries(2, queries);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
// (f) Transform feedback active across the split: kSpanDraws one-point draws, draw i
|
||||
// capturing 2i + 1, must append in order into one buffer with nothing past the end, and
|
||||
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN must count every one.
|
||||
TEST_F(OversizedRecordingSplitScenario, ATransformFeedbackCaptureActiveAcrossTheSplitAppendsEveryDraw) {
|
||||
if (!Ready()) return;
|
||||
|
||||
auto compile = [](GLenum stage, const char* source, std::string* log) -> GLuint {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char buffer[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(buffer) - 1, nullptr, buffer);
|
||||
*log = buffer;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
};
|
||||
std::string log;
|
||||
const GLuint vs = compile(GL_VERTEX_SHADER, kCaptureVS, &log);
|
||||
ASSERT_NE(vs, 0u) << log;
|
||||
const GLuint fs = compile(GL_FRAGMENT_SHADER, kCaptureFS, &log);
|
||||
ASSERT_NE(fs, 0u) << log;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
const char* varyings[] = {"vs_value"};
|
||||
glTransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char buffer[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(buffer) - 1, nullptr, buffer);
|
||||
glDeleteProgram(program);
|
||||
FAIL() << "capture program failed to link: " << buffer;
|
||||
}
|
||||
|
||||
constexpr int kSlack = 4;
|
||||
std::vector<float> poison(kSpanDraws + kSlack, kCapturePoison);
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
||||
poison.data(), GL_STATIC_COPY);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
|
||||
GLuint written = 0;
|
||||
glGenQueries(1, &written);
|
||||
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, written);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
for (int i = 0; i < kSpanDraws; ++i) {
|
||||
glUniform1i(indexLocation, i);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
}
|
||||
glEndTransformFeedback();
|
||||
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
GLuint primitivesWritten = 0;
|
||||
glGetQueryObjectuiv(written, GL_QUERY_RESULT, &primitivesWritten);
|
||||
std::vector<float> captured(poison.size(), 0.0f);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(captured.size() * sizeof(float)),
|
||||
captured.data());
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteQueries(1, &written);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
glDeleteProgram(program);
|
||||
|
||||
EXPECT_EQ(primitivesWritten, static_cast<GLuint>(kSpanDraws));
|
||||
int wrong = 0;
|
||||
std::string first;
|
||||
for (int i = 0; i < static_cast<int>(captured.size()); ++i) {
|
||||
const float expected = i < kSpanDraws ? static_cast<float>(2 * i + 1) : kCapturePoison;
|
||||
if (captured[i] != expected) {
|
||||
if (wrong == 0) {
|
||||
std::ostringstream os;
|
||||
os << "record " << i << " = " << captured[i] << ", expected " << expected;
|
||||
first = os.str();
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(wrong, 0) << wrong << " of " << captured.size() << " records are wrong; first " << first
|
||||
<< " (records past " << kSpanDraws << " must keep the poison)";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -33,7 +33,7 @@
|
||||
// must see it too.
|
||||
// M-A Nothing produced kMGPipeBindSampler / kMGPipeBindShaderImage, so ImageBindableHint was
|
||||
// dead: the applier never saw a texture become image-bound, the metadata respecify
|
||||
// (ID-18 M4) had no live trigger, and the remint pull the hint exists to prevent was
|
||||
// (ID-18 M4) had no live trigger, and the re-mint the hint exists to prevent was
|
||||
// neither prevented nor counted. The case here reads the applier's record around a
|
||||
// glBindImageTexture: the hint arrives as a metadata update that keeps the pending upload
|
||||
// standing beside it, and the picture after the transition is the texels that upload
|
||||
@@ -537,9 +537,12 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
return;
|
||||
}
|
||||
|
||||
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR: a texture Espryt allocated BEFORE the
|
||||
// hint reached it is re-minted image-bindable at the bind and its levels replayed
|
||||
// from the client's shadow - one remint pull, counted. Arming the counter here is
|
||||
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR, on a texture Espryt allocated BEFORE the
|
||||
// hint reached it. An immutable RGBA8 allocation is already what an image unit needs,
|
||||
// so neither arm re-mints it and the counter does not move: the server has kept it
|
||||
// since P9 W2, and the monolith re-mint has had the same keep path since dev's
|
||||
// 568090f0 (merged in P11 M), where it used to re-mint from the shadow and count one.
|
||||
// A counted re-mint is TextureRemintPullScenario's A and H. Arming the counter here is
|
||||
// what makes it readable without a stats-enabled lane.
|
||||
unsigned long long pullsBefore = 0;
|
||||
const bool pullsReadable = PeekPipeStatsTextureRemintPulls(&pullsBefore);
|
||||
@@ -580,11 +583,10 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
|
||||
// THE TRANSITION. An immutable texture has no storage-defining respecify left, so the
|
||||
// hint can only arrive as a metadata update (ID-18 M4). Espryt syncs the texture
|
||||
// eagerly inside glBindImageTexture and the widening re-mints its storage, replaying
|
||||
// every defined level from the shadow (the remint pull the counter below counts), so
|
||||
// the standing upload is consumed by that regeneration here and the picture that
|
||||
// follows is blue whatever the metadata respecify did to the record - the KEPT
|
||||
// property is proved further down, on a texture no remint stands in front of.
|
||||
// eagerly inside glBindImageTexture and KEEPS the allocation (above), so the standing
|
||||
// upload is consumed by the kept storage's own sync and the picture that follows is
|
||||
// blue - the KEPT property of the record is proved further down, on a texture that is
|
||||
// image-bindable already.
|
||||
(void)uploadsBeforeBind;
|
||||
(void)uploadsReadable;
|
||||
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
@@ -608,14 +610,10 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
const auto runtime = PeekSplitRuntime();
|
||||
if (runtime.transportResolved && runtime.transportName == "inproc") {
|
||||
ASSERT_TRUE(runtime.sessionActive);
|
||||
EXPECT_EQ(pullsAfter, pullsBefore)
|
||||
<< "the server must preserve this already immutable RGBA8 allocation; "
|
||||
"image binding needs no remint pull";
|
||||
} else {
|
||||
EXPECT_EQ(pullsAfter, pullsBefore + 1)
|
||||
<< "the monolith re-mint of a texture allocated before its hint was not counted "
|
||||
"as a remint pull (trp= on the stats line is ROADMAP open question 2's number)";
|
||||
}
|
||||
EXPECT_EQ(pullsAfter, pullsBefore)
|
||||
<< "an already immutable RGBA8 allocation must be kept on every arm; image binding "
|
||||
"needs no re-mint (trp= on the stats line is ROADMAP open question 2's number)";
|
||||
}
|
||||
|
||||
// THE PREVENTION HALF, measured the other way round: a texture whose hint arrives at
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PackBufferReadbackScenario.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
|
||||
//
|
||||
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): READS INTO A PIXEL PACK BUFFER ARE ANSWERED BY NOTHING.
|
||||
//
|
||||
// Under a transport a glReadPixels / glGetTexImage whose destination is a bound
|
||||
// GL_PIXEL_PACK_BUFFER no longer waits for its pixels: it crosses as read_pixels_to_buffer /
|
||||
// get_texture_image_to_buffer, the server lands the rows in the buffer at the client's PACK
|
||||
// layout, and the client marks the buffer GPU-written so that the next CPU read of it - a
|
||||
// glGetBufferSubData, a glMapBufferRange, or a texture upload that uses it as the UNPACK source -
|
||||
// fetches the bytes through resource_readback.
|
||||
//
|
||||
// THE CASES ARE ORDINARY GL AND RUN ON THE MONOLITH ARMS TOO. Each one compares the pack buffer
|
||||
// against the same read into client memory under the same pack state, both destinations
|
||||
// pre-filled with the same byte, so the padding the pack state skips has to survive as well: the
|
||||
// monolith and every split arm must agree with themselves, and therefore with each other. The
|
||||
// one exception is the last case, which is about the WIRE (the read posted no reply and marked
|
||||
// the buffer) and skips where there is no wire to ask.
|
||||
//
|
||||
// Every image is POSITION-DEPENDENT, so a row landed at the wrong stride, a band at the wrong
|
||||
// offset or a byte swapped the wrong way cannot look right.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLubyte kFill = 0xAB;
|
||||
|
||||
// A texel that names its own position (and its layer), so nothing misplaced can match.
|
||||
std::array<GLubyte, 4> PatternRgba8(int x, int y, int layer) {
|
||||
return {static_cast<GLubyte>(x * 7 + layer * 31), static_cast<GLubyte>(y * 11 + layer),
|
||||
static_cast<GLubyte>((x ^ y) + layer * 5), static_cast<GLubyte>(0x80 | (x + y))};
|
||||
}
|
||||
|
||||
// Integers below 2^24 in every component, so float storage is exact and a byte swap of
|
||||
// any of them is visibly different from the value.
|
||||
std::array<GLfloat, 4> PatternRgba32f(int x, int y) {
|
||||
return {static_cast<GLfloat>(x + 1), static_cast<GLfloat>(y + 3),
|
||||
static_cast<GLfloat>(x * 64 + y), static_cast<GLfloat>(1000 + x * y)};
|
||||
}
|
||||
|
||||
struct PackState {
|
||||
const char* name;
|
||||
GLint alignment;
|
||||
GLint rowLength;
|
||||
GLint skipPixels;
|
||||
GLint skipRows;
|
||||
GLboolean swapBytes;
|
||||
};
|
||||
|
||||
// Neutral, then each PACK parameter that changes the layout, then all of them at once.
|
||||
constexpr PackState kPackStates[] = {
|
||||
{"neutral", 4, 0, 0, 0, GL_FALSE},
|
||||
{"alignment 1", 1, 0, 0, 0, GL_FALSE},
|
||||
{"alignment 8", 8, 0, 0, 0, GL_FALSE},
|
||||
{"row length 53", 4, 53, 0, 0, GL_FALSE},
|
||||
{"skip pixels 3, skip rows 2", 4, 0, 3, 2, GL_FALSE},
|
||||
{"everything", 8, 47, 5, 3, GL_FALSE},
|
||||
};
|
||||
|
||||
class PackBufferReadbackScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
ResetPack();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
ResetPack();
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
for (GLuint buffer : m_buffers) glDeleteBuffers(1, &buffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
for (GLuint texture : m_textures) glDeleteTextures(1, &texture);
|
||||
m_buffers.clear();
|
||||
m_textures.clear();
|
||||
m_fbo = 0;
|
||||
DrainErrors();
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void ResetPack() {
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_IMAGES, 0);
|
||||
glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
}
|
||||
|
||||
static void ApplyPack(const PackState& pack) {
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, pack.alignment);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, pack.rowLength);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, pack.skipPixels);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, pack.skipRows);
|
||||
glPixelStorei(GL_PACK_SWAP_BYTES, pack.swapBytes);
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// The bytes a pack state makes a width x height read occupy, from the first byte the
|
||||
// read may touch (GL 4.6 §8.4.4.1 / §18.2): the skips, then the padded rows, the last
|
||||
// row tight.
|
||||
static std::size_t PackedExtent(const PackState& pack, int width, int height, std::size_t bpp) {
|
||||
const std::size_t rowPixels = pack.rowLength > 0 ? static_cast<std::size_t>(pack.rowLength)
|
||||
: static_cast<std::size_t>(width);
|
||||
const std::size_t align = static_cast<std::size_t>(pack.alignment);
|
||||
const std::size_t stride = (rowPixels * bpp + align - 1) / align * align;
|
||||
return static_cast<std::size_t>(pack.skipRows) * stride + static_cast<std::size_t>(pack.skipPixels) * bpp +
|
||||
static_cast<std::size_t>(height - 1) * stride + static_cast<std::size_t>(width) * bpp;
|
||||
}
|
||||
|
||||
GLuint NewTexture() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint NewBuffer(GLenum target, std::size_t size, GLubyte fill) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
m_buffers.push_back(buffer);
|
||||
glBindBuffer(target, buffer);
|
||||
const std::vector<GLubyte> initial(size, fill);
|
||||
glBufferData(target, static_cast<GLsizeiptr>(size), initial.data(), GL_DYNAMIC_READ);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// An RGBA8 texture holding the pattern, attached as COLOR_ATTACHMENT0 for reading.
|
||||
GLuint AttachPatternRgba8(int width, int height) {
|
||||
std::vector<GLubyte> texels(static_cast<std::size_t>(width) * height * 4);
|
||||
for (int y = 0; y < height; ++y)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const auto t = PatternRgba8(x, y, 0);
|
||||
std::memcpy(texels.data() + (static_cast<std::size_t>(y) * width + x) * 4, t.data(), 4);
|
||||
}
|
||||
const GLuint texture = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
EXPECT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << "pattern upload";
|
||||
return texture;
|
||||
}
|
||||
|
||||
// glReadPixels of (x, y, width, height) under `pack` into client memory and into a
|
||||
// pack buffer at `offset`, both pre-filled with kFill; the buffer's bytes are then
|
||||
// read back by `readBack` and the two must be identical, byte for byte, padding
|
||||
// included - and the bytes in front of `offset` must still be kFill.
|
||||
template <typename ReadBack>
|
||||
void ExpectPackBufferMatchesClientMemory(const PackState& pack, int x, int y, int width, int height,
|
||||
GLenum type, std::size_t bpp, ReadBack&& readBack) {
|
||||
ApplyPack(pack);
|
||||
const std::size_t extent = PackedExtent(pack, width, height, bpp);
|
||||
std::vector<GLubyte> host(extent, kFill);
|
||||
glReadPixels(x, y, width, height, GL_RGBA, type, host.data());
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << pack.name << ": client-memory read";
|
||||
|
||||
constexpr std::size_t kOffset = 64;
|
||||
NewBuffer(GL_PIXEL_PACK_BUFFER, kOffset + extent, kFill);
|
||||
glReadPixels(x, y, width, height, GL_RGBA, type, reinterpret_cast<void*>(kOffset));
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << pack.name << ": pack-buffer read";
|
||||
std::vector<GLubyte> actual(kOffset + extent, 0);
|
||||
readBack(actual);
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << pack.name << ": buffer read-back";
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
ResetPack();
|
||||
|
||||
std::size_t lead = 0;
|
||||
for (std::size_t i = 0; i < kOffset; ++i) lead += actual[i] != kFill ? 1 : 0;
|
||||
EXPECT_EQ(lead, 0u) << pack.name << ": " << lead << " bytes in front of the read's offset moved";
|
||||
std::size_t bad = 0, first = 0;
|
||||
for (std::size_t i = 0; i < extent; ++i) {
|
||||
if (actual[kOffset + i] != host[i] && bad++ == 0) first = i;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << pack.name << ": " << bad << " of " << extent
|
||||
<< " bytes differ from the client-memory read, first at byte " << first
|
||||
<< " (buffer " << static_cast<int>(actual[kOffset + first]) << ", host "
|
||||
<< static_cast<int>(host[first]) << ")";
|
||||
}
|
||||
|
||||
GLuint m_fbo = 0;
|
||||
std::vector<GLuint> m_buffers;
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
TEST_F(PackBufferReadbackScenario, ReadPixelsIntoAPackBufferMatchesClientMemoryUnderEveryPackState) {
|
||||
if (!Ready()) return;
|
||||
AttachPatternRgba8(37, 23);
|
||||
if (HasFatalFailure()) return;
|
||||
for (const PackState& pack : kPackStates) {
|
||||
ExpectPackBufferMatchesClientMemory(pack, 2, 1, 31, 19, GL_UNSIGNED_BYTE, 4, [](std::vector<GLubyte>& out) {
|
||||
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(out.size()), out.data());
|
||||
});
|
||||
if (HasFatalFailure()) return;
|
||||
}
|
||||
}
|
||||
|
||||
// The map is the OTHER CPU reader, and the one an application that streams readbacks
|
||||
// through a PBO actually uses: glMapBufferRange(GL_MAP_READ_BIT) must see the pixels the
|
||||
// server landed, not the bytes the client shadow held before the read.
|
||||
TEST_F(PackBufferReadbackScenario, MapBufferRangeAfterAPackBufferReadSeesThePixels) {
|
||||
if (!Ready()) return;
|
||||
AttachPatternRgba8(37, 23);
|
||||
if (HasFatalFailure()) return;
|
||||
for (const PackState& pack : {kPackStates[0], kPackStates[5]}) {
|
||||
ExpectPackBufferMatchesClientMemory(pack, 0, 0, 37, 23, GL_UNSIGNED_BYTE, 4, [](std::vector<GLubyte>& out) {
|
||||
void* mapped = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(out.size()),
|
||||
GL_MAP_READ_BIT);
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
std::memcpy(out.data(), mapped, out.size());
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
});
|
||||
if (HasFatalFailure()) return;
|
||||
}
|
||||
}
|
||||
|
||||
// GL_PACK_SWAP_BYTES reverses every component's bytes; the client carries the group
|
||||
// width and the server swaps, so a wrong width or a skipped swap is a different float.
|
||||
TEST_F(PackBufferReadbackScenario, SwapBytesIntoAPackBufferMatchesClientMemory) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kWidth = 13, kHeight = 9;
|
||||
std::vector<GLfloat> texels(static_cast<std::size_t>(kWidth) * kHeight * 4);
|
||||
for (int y = 0; y < kHeight; ++y)
|
||||
for (int x = 0; x < kWidth; ++x) {
|
||||
const auto t = PatternRgba32f(x, y);
|
||||
std::memcpy(texels.data() + (static_cast<std::size_t>(y) * kWidth + x) * 4, t.data(), sizeof(t));
|
||||
}
|
||||
const GLuint texture = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, kWidth, kHeight);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_RGBA, GL_FLOAT, texels.data());
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const PackState swapped{"swap bytes, row length 17", 4, 17, 1, 1, GL_TRUE};
|
||||
ExpectPackBufferMatchesClientMemory(swapped, 0, 0, kWidth, kHeight, GL_FLOAT, 16, [](std::vector<GLubyte>& out) {
|
||||
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(out.size()), out.data());
|
||||
});
|
||||
}
|
||||
|
||||
// glGetTexImage into a pack buffer, 2D and layered: the layered case is the one whose
|
||||
// layout has an image stride (GL_PACK_IMAGE_HEIGHT) and a skip in the third dimension.
|
||||
TEST_F(PackBufferReadbackScenario, GetTexImageIntoAPackBufferMatchesClientMemory) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kWidth = 11, kHeight = 7, kLayers = 3;
|
||||
std::vector<GLubyte> texels(static_cast<std::size_t>(kWidth) * kHeight * kLayers * 4);
|
||||
for (int layer = 0; layer < kLayers; ++layer)
|
||||
for (int y = 0; y < kHeight; ++y)
|
||||
for (int x = 0; x < kWidth; ++x) {
|
||||
const auto t = PatternRgba8(x, y, layer);
|
||||
std::memcpy(texels.data() + ((static_cast<std::size_t>(layer) * kHeight + y) * kWidth + x) * 4,
|
||||
t.data(), 4);
|
||||
}
|
||||
const GLuint flat = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, flat);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
const GLuint layered = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, layered);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kWidth, kHeight, kLayers, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture upload";
|
||||
|
||||
struct Shape {
|
||||
const char* name;
|
||||
GLenum target;
|
||||
GLuint texture;
|
||||
int depth;
|
||||
GLint rowLength, alignment, imageHeight, skipImages;
|
||||
};
|
||||
const Shape shapes[] = {
|
||||
{"2D, row length 13, alignment 8", GL_TEXTURE_2D, flat, 1, 13, 8, 0, 0},
|
||||
{"2D array, image height 9, skip images 1", GL_TEXTURE_2D_ARRAY, layered, kLayers, 0, 4, 9, 1},
|
||||
};
|
||||
for (const Shape& shape : shapes) {
|
||||
glBindTexture(shape.target, shape.texture);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, shape.rowLength);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, shape.alignment);
|
||||
glPixelStorei(GL_PACK_IMAGE_HEIGHT, shape.imageHeight);
|
||||
glPixelStorei(GL_PACK_SKIP_IMAGES, shape.skipImages);
|
||||
const std::size_t rowPixels = shape.rowLength > 0 ? static_cast<std::size_t>(shape.rowLength) : kWidth;
|
||||
const std::size_t stride = (rowPixels * 4 + shape.alignment - 1) / shape.alignment * shape.alignment;
|
||||
const std::size_t imageRows = shape.imageHeight > 0 ? static_cast<std::size_t>(shape.imageHeight) : kHeight;
|
||||
const std::size_t imageStride = stride * imageRows;
|
||||
const std::size_t extent = static_cast<std::size_t>(shape.skipImages) * imageStride +
|
||||
static_cast<std::size_t>(shape.depth - 1) * imageStride +
|
||||
static_cast<std::size_t>(kHeight - 1) * stride + kWidth * 4u;
|
||||
std::vector<GLubyte> host(extent, kFill);
|
||||
glGetTexImage(shape.target, 0, GL_RGBA, GL_UNSIGNED_BYTE, host.data());
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << shape.name << ": client-memory read";
|
||||
|
||||
constexpr std::size_t kOffset = 32;
|
||||
NewBuffer(GL_PIXEL_PACK_BUFFER, kOffset + extent, kFill);
|
||||
glGetTexImage(shape.target, 0, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast<void*>(kOffset));
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << shape.name << ": pack-buffer read";
|
||||
std::vector<GLubyte> actual(kOffset + extent, 0);
|
||||
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(actual.size()), actual.data());
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
ResetPack();
|
||||
std::size_t bad = 0, first = 0;
|
||||
for (std::size_t i = 0; i < kOffset; ++i) bad += actual[i] != kFill ? 1 : 0;
|
||||
EXPECT_EQ(bad, 0u) << shape.name << ": bytes in front of the offset moved";
|
||||
bad = 0;
|
||||
for (std::size_t i = 0; i < extent; ++i)
|
||||
if (actual[kOffset + i] != host[i] && bad++ == 0) first = i;
|
||||
EXPECT_EQ(bad, 0u) << shape.name << ": " << bad << " of " << extent
|
||||
<< " bytes differ from the client-memory read, first at byte " << first;
|
||||
}
|
||||
}
|
||||
|
||||
// THE UNPACK HALF. A read into a buffer followed by an upload FROM that buffer is a
|
||||
// GPU-to-GPU copy an application expects to work; under a transport the upload reads the
|
||||
// client's shadow of the buffer, which the pack read did not touch, so this is exactly
|
||||
// the case that goes wrong if the upload path forgets that the buffer was GPU-written.
|
||||
TEST_F(PackBufferReadbackScenario, APackBufferReadFeedsAnUploadFromTheSameBuffer) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kWidth = 29, kHeight = 17;
|
||||
AttachPatternRgba8(kWidth, kHeight);
|
||||
if (HasFatalFailure()) return;
|
||||
const GLuint buffer = NewBuffer(GL_PIXEL_PACK_BUFFER, static_cast<std::size_t>(kWidth) * kHeight * 4, kFill);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
|
||||
const GLuint copy = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, copy);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << "read into the buffer, then upload from it";
|
||||
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, copy, 0);
|
||||
std::vector<GLubyte> actual(static_cast<std::size_t>(kWidth) * kHeight * 4, 0);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, actual.data());
|
||||
std::size_t bad = 0;
|
||||
std::string first;
|
||||
for (int y = 0; y < kHeight; ++y)
|
||||
for (int x = 0; x < kWidth; ++x) {
|
||||
const auto expected = PatternRgba8(x, y, 0);
|
||||
if (std::memcmp(actual.data() + (static_cast<std::size_t>(y) * kWidth + x) * 4, expected.data(), 4) != 0 &&
|
||||
bad++ == 0)
|
||||
first = "(" + std::to_string(x) + "," + std::to_string(y) + ")";
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << bad << " texels of the uploaded copy differ from the pattern, first at " << first;
|
||||
}
|
||||
|
||||
// ORDER AGAINST LATER WRITES. A glBufferSubData into part of the buffer AFTER the read
|
||||
// must win over the read's bytes there and leave the rest of the read alone - on the
|
||||
// server the pixels and the later write are two queued ranges of one store, drained in
|
||||
// record order.
|
||||
TEST_F(PackBufferReadbackScenario, ALaterBufferSubDataWinsOverThePackReadWhereTheyOverlap) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kWidth = 16, kHeight = 8;
|
||||
AttachPatternRgba8(kWidth, kHeight);
|
||||
if (HasFatalFailure()) return;
|
||||
const std::size_t size = static_cast<std::size_t>(kWidth) * kHeight * 4;
|
||||
std::vector<GLubyte> host(size, kFill);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, host.data());
|
||||
NewBuffer(GL_PIXEL_PACK_BUFFER, size, kFill);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
const std::vector<GLubyte> overwrite(40, 0x5A);
|
||||
glBufferSubData(GL_PIXEL_PACK_BUFFER, 100, static_cast<GLsizeiptr>(overwrite.size()), overwrite.data());
|
||||
std::vector<GLubyte> actual(size, 0);
|
||||
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(size), actual.data());
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
std::memcpy(host.data() + 100, overwrite.data(), overwrite.size());
|
||||
std::size_t bad = 0, first = 0;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
if (actual[i] != host[i] && bad++ == 0) first = i;
|
||||
EXPECT_EQ(bad, 0u) << bad << " bytes differ, first at " << first;
|
||||
}
|
||||
|
||||
// THE WIRE HALF, and the one case that is about the transport rather than the pixels:
|
||||
// under a transport a pack-buffer read posts NO reply-slot record and marks the buffer
|
||||
// GPU-written, once per read. MOBILEGL_IPC_PBO_READBACK_SYNC=1 puts the reply form back,
|
||||
// and this case must then fail on both counters - that is its red-once, recorded in
|
||||
// docs/Disaggregated/notes/p9/.
|
||||
TEST_F(PackBufferReadbackScenario, APackBufferReadPostsNoReplyAndMarksTheBuffer) {
|
||||
if (!Ready()) return;
|
||||
const std::string split = SplitRuntimeSkipReason();
|
||||
if (!split.empty()) GTEST_SKIP() << split;
|
||||
constexpr int kWidth = 24, kHeight = 12;
|
||||
AttachPatternRgba8(kWidth, kHeight);
|
||||
if (HasFatalFailure()) return;
|
||||
NewBuffer(GL_PIXEL_PACK_BUFFER, static_cast<std::size_t>(kWidth) * kHeight * 4, kFill);
|
||||
// A warm-up read, so every state record the read's fill has to publish once is out
|
||||
// of the way before the counted window opens.
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
constexpr int kReads = 4;
|
||||
const SplitRuntimeState before = PeekSplitRuntime();
|
||||
for (int i = 0; i < kReads; ++i) glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
const SplitRuntimeState after = PeekSplitRuntime();
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(after.replyPostings - before.replyPostings, 0u)
|
||||
<< "a read into a pack buffer published a reply-slot record: it took the reply form "
|
||||
"(MOBILEGL_IPC_PBO_READBACK_SYNC=1 does this on purpose)";
|
||||
EXPECT_EQ(after.packBufferReadbackMarks - before.packBufferReadbackMarks,
|
||||
static_cast<unsigned long long>(kReads + 1))
|
||||
<< "every pack-buffer read marks its buffer GPU-written exactly once";
|
||||
// And the bytes are still right after all of that.
|
||||
std::vector<GLubyte> actual(static_cast<std::size_t>(kWidth) * kHeight * 4, 0);
|
||||
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(actual.size()), actual.data());
|
||||
const auto corner = PatternRgba8(kWidth - 1, kHeight - 1, 0);
|
||||
EXPECT_EQ(std::memcmp(actual.data() + actual.size() - 4, corner.data(), 4), 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,600 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PartialUploadOverGpuContentScenario.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
|
||||
//
|
||||
// Scenario - A SMALL glTexSubImage INTO A LEVEL THE GPU HAS WRITTEN.
|
||||
//
|
||||
// Espryt keeps a CPU shadow of every level and syncs a pending client write by uploading the
|
||||
// region it covers (SyncMipmapsToBackend's sub-rect path). The shadow never sees a GPU write, so
|
||||
// only the region may go up: anything more puts the shadow's stale texels over what the GPU drew.
|
||||
// The region upload used to be skipped whenever the shadow's bytes had to be CONVERTED on the way
|
||||
// to the driver - a three-channel format the driver cannot render is stored four-channel and every
|
||||
// upload is repacked - and whenever the ES upload is shaped differently from the shadow (a 1D
|
||||
// array, stored as a 2D array one texel high): those levels went up WHOLE, and a 2x2 write erased
|
||||
// the rest of the level.
|
||||
//
|
||||
// Every case uploads a level, lets the GPU clear it, writes one 2x2 box, and reads the level back:
|
||||
// the clear outside the box, the write inside. The formats are picked by what they cost the ES
|
||||
// upload, which is per driver - on Mesa llvmpipe GL_SRGB8, GL_RGB8_SNORM and GL_RGB8UI are stored
|
||||
// four-channel (see ThreeChannelAttachmentScenario) while GL_RGB16F is native; ES has no renderable
|
||||
// three-channel format at all, so on a phone all four are converted. GL_RGBA8 is the control that
|
||||
// never converts. A format a backend cannot attach is skipped, not failed.
|
||||
//
|
||||
// The second half is about ORDER, with GL_RGBA8. Magma queues a clear until the texture's next
|
||||
// use and records texel uploads into a batch submitted ahead of the frame's recorded commands, so
|
||||
// a write issued after a clear of (or a draw into) the level reached the image FIRST and the
|
||||
// earlier clear or draw landed over it: the box read back green. Each order is covered - the
|
||||
// clear already recorded, still queued, synced by a draw or by the readback, the reverse order
|
||||
// in which the clear must win, and a draw instead of a clear.
|
||||
//
|
||||
// The last part is about SCATTER: several boxes written between two syncs, with the GPU's clear in
|
||||
// the gaps between them. Everything a backend has to go on is the union box and the storage's
|
||||
// rect list, and both describe more than the writes - the box spans the gaps, the list merges
|
||||
// touching rects into their bounding box, folds once it holds 96, and is withheld once its rects
|
||||
// fill 3/4 of the box; a box spanning the level went up as the whole level. Espryt also uploaded
|
||||
// the union box whenever its unpack ring was in use. Each case below reaches one of those: two
|
||||
// distant boxes, a hundred of them, boxes filling more than 3/4 of their union, and boxes whose
|
||||
// union spans the level.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr int kEdge = 8;
|
||||
constexpr int kBoxAt = 2;
|
||||
constexpr int kBoxEdge = 2;
|
||||
|
||||
constexpr const char* kVS = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// How a value in [0, 1] is spelled in the client data, and how the level is cleared and
|
||||
// read back. Integer levels spell 1 as kIntegerOne.
|
||||
enum class Encoding { Unorm8, Snorm8, Float32, Uint8 };
|
||||
constexpr unsigned kIntegerOne = 9;
|
||||
|
||||
struct Format {
|
||||
GLenum internalFormat;
|
||||
GLenum clientFormat;
|
||||
GLenum clientType;
|
||||
int components;
|
||||
Encoding encoding;
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> Fill(const Format& f, int texels, float r, float g, float b) {
|
||||
const float rgba[4] = {r, g, b, 1.0f};
|
||||
const std::size_t componentBytes = f.encoding == Encoding::Float32 ? 4u : 1u;
|
||||
std::vector<std::uint8_t> out(static_cast<std::size_t>(texels) * f.components * componentBytes);
|
||||
for (int t = 0; t < texels; ++t) {
|
||||
for (int c = 0; c < f.components; ++c) {
|
||||
std::uint8_t* dst = out.data() + (static_cast<std::size_t>(t) * f.components + c) * componentBytes;
|
||||
const float v = rgba[c];
|
||||
switch (f.encoding) {
|
||||
case Encoding::Unorm8: *dst = static_cast<std::uint8_t>(std::lround(v * 255.0f)); break;
|
||||
case Encoding::Snorm8:
|
||||
*dst = static_cast<std::uint8_t>(static_cast<std::int8_t>(std::lround(v * 127.0f)));
|
||||
break;
|
||||
case Encoding::Float32: std::memcpy(dst, &v, sizeof(v)); break;
|
||||
case Encoding::Uint8: *dst = static_cast<std::uint8_t>(std::lround(v * kIntegerOne)); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class PartialUploadOverGpuContentScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_scratch = MakeColorFbo(4, 4);
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
if (m_vao) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_scratch);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
GLuint NewTexture(GLenum target) {
|
||||
GLuint t = 0;
|
||||
glGenTextures(1, &t);
|
||||
m_textures.push_back(t);
|
||||
glBindTexture(target, t);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glBindTexture(target, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
// A draw that samples the texture, which is what syncs its pending writes to the
|
||||
// driver (an FBO attach alone may find the framebuffer already synced).
|
||||
void SyncBySampling(GLenum target, GLuint tex, const char* samplerType, const char* coord) {
|
||||
std::string fs = std::string("#version 430 core\nuniform ") + samplerType +
|
||||
" u_tex;\nout vec4 o_color;\nvoid main() { o_color = vec4(texelFetch(u_tex, " + coord +
|
||||
", 0)); }\n";
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kVS, fs.c_str(), &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
m_programs.push_back(program);
|
||||
BindFbo(m_scratch);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(target, tex);
|
||||
glUniform1i(glGetUniformLocation(program, "u_tex"), 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(target, 0);
|
||||
glUseProgram(0);
|
||||
BindDefaultFramebuffer();
|
||||
glFinish();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "sampling sync";
|
||||
}
|
||||
|
||||
// Attaches (tex, layer) to the scratch framebuffer; false when this backend cannot.
|
||||
bool Attach(GLuint tex, int layer) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
if (layer >= 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, layer);
|
||||
} else {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
||||
}
|
||||
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
}
|
||||
|
||||
void Detach() {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
BindDefaultFramebuffer();
|
||||
}
|
||||
|
||||
// The GPU write the shadow never sees: the whole level (or layer) to green.
|
||||
void GpuClearGreen(const Format& f) {
|
||||
if (f.encoding == Encoding::Uint8) {
|
||||
const GLuint green[4] = {0u, kIntegerOne, 0u, 1u};
|
||||
glClearBufferuiv(GL_COLOR, 0, green);
|
||||
} else {
|
||||
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, green);
|
||||
}
|
||||
}
|
||||
|
||||
// The attached image's rgb, in units of "one", width x height texels.
|
||||
std::vector<float> ReadRgb(const Format& f, int width, int height) {
|
||||
std::vector<float> rgb(static_cast<std::size_t>(width * height) * 3u);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
if (f.encoding == Encoding::Uint8) {
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(width * height) * 4u, 0xA5A5A5A5u);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
for (int i = 0; i < width * height; ++i)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
rgb[i * 3 + c] = static_cast<float>(texels[i * 4 + c]) / kIntegerOne;
|
||||
} else {
|
||||
std::vector<GLfloat> texels(static_cast<std::size_t>(width * height) * 4u, -7.0f);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, texels.data());
|
||||
for (int i = 0; i < width * height; ++i)
|
||||
for (int c = 0; c < 3; ++c) rgb[i * 3 + c] = texels[i * 4 + c];
|
||||
}
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "readback";
|
||||
return rgb;
|
||||
}
|
||||
|
||||
// Green everywhere but blue inside [boxX, boxX + boxW) x [boxY, boxY + boxH).
|
||||
static void ExpectClearAroundBox(const std::vector<float>& rgb, int width, int height, int boxX, int boxY,
|
||||
int boxW, int boxH) {
|
||||
int bad = 0;
|
||||
std::ostringstream first;
|
||||
for (int y = 0; y < height; ++y)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const bool inBox = x >= boxX && x < boxX + boxW && y >= boxY && y < boxY + boxH;
|
||||
const float want[3] = {0.0f, inBox ? 0.0f : 1.0f, inBox ? 1.0f : 0.0f};
|
||||
const float* got = &rgb[static_cast<std::size_t>(y * width + x) * 3u];
|
||||
bool texelBad = false;
|
||||
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
|
||||
if (texelBad && bad++ == 0) {
|
||||
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
|
||||
<< " want " << want[0] << "," << want[1] << "," << want[2];
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0) << bad << " of " << width * height << " texels wrong; first " << first.str()
|
||||
<< " (red outside the box: the stale shadow went back up; green inside it: the "
|
||||
"write landed before the clear or draw)";
|
||||
}
|
||||
|
||||
// Upload red, sync, clear green on the GPU, sync, write a blue 2x2 box, sync, read.
|
||||
void Run2D(const Format& f) {
|
||||
const GLuint t = NewTexture(GL_TEXTURE_2D);
|
||||
const bool integer = f.encoding == Encoding::Uint8;
|
||||
const char* sampler = integer ? "usampler2D" : "sampler2D";
|
||||
const auto red = Fill(f, kEdge * kEdge, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
|
||||
f.clientType, red.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "definition";
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
if (!Attach(t, -1)) {
|
||||
Detach();
|
||||
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
|
||||
}
|
||||
GpuClearGreen(f);
|
||||
Detach();
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
|
||||
blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
const auto rgb = ReadRgb(f, kEdge, kEdge);
|
||||
Detach();
|
||||
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge);
|
||||
}
|
||||
|
||||
// P11 M: TWO SCATTERED WRITES INTO ONE LEVEL, so the level carries a rect list (their
|
||||
// area is well under 3/4 of their union box) and not just a box. Under a transport
|
||||
// (and on the push builds' monolith arm) the list crosses in the client's record,
|
||||
// whose regions state their source pitch and offset in the CLIENT's layout; a level
|
||||
// whose upload is converted is cut from the conversion's wider buffer, so those
|
||||
// numbers must not address it (Managers.cpp, carriedLayoutApplies). The whole level
|
||||
// is uploaded green first, so every texel the union box spans outside the two boxes
|
||||
// is green in the shadow too: this case is about WHERE the boxes land, not about GPU
|
||||
// content between them. Green everywhere but the two blue boxes.
|
||||
void Run2DScatter(const Format& f) {
|
||||
constexpr int kFirstAt = 1, kSecondAt = 5;
|
||||
const GLuint t = NewTexture(GL_TEXTURE_2D);
|
||||
const bool integer = f.encoding == Encoding::Uint8;
|
||||
const char* sampler = integer ? "usampler2D" : "sampler2D";
|
||||
const auto green = Fill(f, kEdge * kEdge, 0.0f, 1.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
|
||||
f.clientType, green.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "definition";
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
if (!Attach(t, -1)) {
|
||||
Detach();
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
|
||||
}
|
||||
Detach();
|
||||
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, kFirstAt, kFirstAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
|
||||
blue.data());
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, kSecondAt, kSecondAt, kBoxEdge, kBoxEdge, f.clientFormat,
|
||||
f.clientType, blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
const auto rgb = ReadRgb(f, kEdge, kEdge);
|
||||
Detach();
|
||||
int bad = 0;
|
||||
std::ostringstream first;
|
||||
for (int y = 0; y < kEdge; ++y)
|
||||
for (int x = 0; x < kEdge; ++x) {
|
||||
const auto inBox = [&](int at) {
|
||||
return x >= at && x < at + kBoxEdge && y >= at && y < at + kBoxEdge;
|
||||
};
|
||||
const bool blueHere = inBox(kFirstAt) || inBox(kSecondAt);
|
||||
const float want[3] = {0.0f, blueHere ? 0.0f : 1.0f, blueHere ? 1.0f : 0.0f};
|
||||
const float* got = &rgb[static_cast<std::size_t>(y * kEdge + x) * 3u];
|
||||
bool texelBad = false;
|
||||
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
|
||||
if (texelBad && bad++ == 0) {
|
||||
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
|
||||
<< " want " << want[0] << "," << want[1] << "," << want[2];
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0) << bad << " of " << kEdge * kEdge << " texels wrong; first " << first.str()
|
||||
<< " (a box read at the wrong pitch or offset lands the wrong bytes)";
|
||||
}
|
||||
|
||||
// Building blocks of the RGBA8 ordering cases: the same texels, issued in the orders
|
||||
// a backend that defers work may get wrong.
|
||||
static Format Rgba8() { return {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8}; }
|
||||
|
||||
GLuint NewRedRgba8() {
|
||||
const GLuint t = NewTexture(GL_TEXTURE_2D);
|
||||
const auto red = Fill(Rgba8(), kEdge * kEdge, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "definition";
|
||||
return t;
|
||||
}
|
||||
|
||||
void SampleRgba8(GLuint t) { SyncBySampling(GL_TEXTURE_2D, t, "sampler2D", "ivec2(0)"); }
|
||||
|
||||
void ClearRgba8Green(GLuint t) {
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
GpuClearGreen(Rgba8());
|
||||
Detach();
|
||||
}
|
||||
|
||||
// A draw that writes green over the whole level.
|
||||
void DrawRgba8Green(GLuint t) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(
|
||||
kVS, "#version 430 core\nout vec4 o_color;\nvoid main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }\n",
|
||||
&error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
m_programs.push_back(program);
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
glViewport(0, 0, kEdge, kEdge);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
Detach();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
void WriteRgba8BlueBox(GLuint t) {
|
||||
const auto blue = Fill(Rgba8(), kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// Green everywhere, and blue in the box when `boxWins`.
|
||||
void ExpectRgba8(GLuint t, bool boxWins) {
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
const auto rgb = ReadRgb(Rgba8(), kEdge, kEdge);
|
||||
Detach();
|
||||
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, boxWins ? kBoxEdge : 0, boxWins ? kBoxEdge : 0);
|
||||
}
|
||||
|
||||
struct Box {
|
||||
int x, y, w, h;
|
||||
};
|
||||
|
||||
// An edge x edge RGBA8 level uploaded red, cleared green on the GPU, then written blue in
|
||||
// every box by one glTexSubImage each, with no sync in between; read back after a sync.
|
||||
// Blue inside the boxes, green everywhere else.
|
||||
void RunScatteredWrites(int edge, const std::vector<Box>& boxes) {
|
||||
const GLuint t = NewTexture(GL_TEXTURE_2D);
|
||||
const auto red = Fill(Rgba8(), edge * edge, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, edge, edge, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "definition";
|
||||
SampleRgba8(t);
|
||||
ClearRgba8Green(t);
|
||||
SampleRgba8(t);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
for (const Box& box : boxes) {
|
||||
const auto blue = Fill(Rgba8(), box.w * box.h, 0.0f, 0.0f, 1.0f);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, box.x, box.y, box.w, box.h, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
blue.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "writes";
|
||||
SampleRgba8(t);
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
const auto rgb = ReadRgb(Rgba8(), edge, edge);
|
||||
Detach();
|
||||
int bad = 0, stale = 0;
|
||||
std::ostringstream first;
|
||||
for (int y = 0; y < edge; ++y)
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
bool inBox = false;
|
||||
for (const Box& box : boxes)
|
||||
inBox |= x >= box.x && x < box.x + box.w && y >= box.y && y < box.y + box.h;
|
||||
const float want[3] = {0.0f, inBox ? 0.0f : 1.0f, inBox ? 1.0f : 0.0f};
|
||||
const float* got = &rgb[static_cast<std::size_t>(y * edge + x) * 3u];
|
||||
bool texelBad = false;
|
||||
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
|
||||
if (!texelBad) continue;
|
||||
stale += got[0] > 0.5f;
|
||||
if (bad++ == 0) {
|
||||
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
|
||||
<< " want " << want[0] << "," << want[1] << "," << want[2];
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0) << bad << " of " << edge * edge << " texels wrong, " << stale
|
||||
<< " of them red - the stale shadow went up outside the writes; first "
|
||||
<< first.str();
|
||||
}
|
||||
|
||||
GLuint m_vao = 0, m_fbo = 0;
|
||||
ColorFbo m_scratch{};
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
// Control: the shadow's bytes go to the driver as they are.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgba8SubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Srgb8SubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb8SnormSubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGB8_SNORM, GL_RGB, GL_BYTE, 3, Encoding::Snorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb16fSubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGB16F, GL_RGB, GL_FLOAT, 3, Encoding::Float32});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiSubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
|
||||
}
|
||||
|
||||
// P11 M: the rect-list shape of the same upload (Run2DScatter). RGBA8 is the control whose
|
||||
// record pitch does address the uploaded bytes; the other two are stored four-channel on
|
||||
// llvmpipe and on every ES driver, so their boxes are cut from the conversion's buffer.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgba8ScatteredSubImagesLandWhereWritten) {
|
||||
if (!Ready()) return;
|
||||
Run2DScatter({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Srgb8ScatteredSubImagesLandWhereWritten) {
|
||||
if (!Ready()) return;
|
||||
Run2DScatter({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiScatteredSubImagesLandWhereWritten) {
|
||||
if (!Ready()) return;
|
||||
Run2DScatter({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
|
||||
}
|
||||
|
||||
// A 1D array's ES image is a 2D array one texel high with the layers in depth, so the
|
||||
// upload is shaped differently from the shadow (layers as rows) even with no conversion.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, OneDimensionalArraySubImageKeepsTheGpuClearedLayer) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kLayers = 2;
|
||||
const Format f{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8};
|
||||
const GLuint t = NewTexture(GL_TEXTURE_1D_ARRAY);
|
||||
const auto red = Fill(f, kEdge * kLayers, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
|
||||
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kEdge, kLayers, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "definition";
|
||||
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
|
||||
if (!Attach(t, 1)) {
|
||||
Detach();
|
||||
GTEST_SKIP() << "a 1D array layer does not attach on backend " << Gl().BackendName();
|
||||
}
|
||||
GpuClearGreen(f);
|
||||
Detach();
|
||||
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
|
||||
const auto blue = Fill(f, kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
|
||||
glTexSubImage2D(GL_TEXTURE_1D_ARRAY, 0, kBoxAt, 1, kBoxEdge, 1, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
||||
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
|
||||
ASSERT_TRUE(Attach(t, 1));
|
||||
const auto layer1 = ReadRgb(f, kEdge, 1);
|
||||
Detach();
|
||||
ExpectClearAroundBox(layer1, kEdge, 1, kBoxAt, 0, kBoxEdge, 1);
|
||||
}
|
||||
|
||||
// The orders below matter to a backend that defers GPU work (Magma queues clears and runs
|
||||
// texel uploads in a batch ahead of the frame's recorded commands); every one is plain GL.
|
||||
|
||||
// The clear is still queued when the write arrives: it has to reach the level first.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClear) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
ClearRgba8Green(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
SampleRgba8(t);
|
||||
ExpectRgba8(t, true);
|
||||
}
|
||||
|
||||
// Same, with the readback as the first thing to sync the write.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClearForAReadback) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
ClearRgba8Green(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
ExpectRgba8(t, true);
|
||||
}
|
||||
|
||||
// The other order: the write is still pending when the clear arrives, and the clear wins.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, ClearAfterAPendingSubImageWins) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
ClearRgba8Green(t);
|
||||
SampleRgba8(t);
|
||||
ExpectRgba8(t, false);
|
||||
}
|
||||
|
||||
// A draw into the level instead of a clear.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterADrawIntoTheLevelLandsOnTheDraw) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
DrawRgba8Green(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
SampleRgba8(t);
|
||||
ExpectRgba8(t, true);
|
||||
}
|
||||
|
||||
// Two boxes in opposite corners: their union box is 7x7, of which the writes are 8 texels.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, TwoDistantSubImagesKeepTheGpuClearBetweenThem) {
|
||||
if (!Ready()) return;
|
||||
RunScatteredWrites(kEdge, {{0, 0, 2, 2}, {5, 5, 2, 2}});
|
||||
}
|
||||
|
||||
// A hundred 2x2 boxes two texels apart: more than the 96 rects the storage keeps, so its
|
||||
// list folds neighbours together, gaps and all.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, AHundredSubImagesKeepTheGpuClearBetweenThem) {
|
||||
if (!Ready()) return;
|
||||
std::vector<Box> boxes;
|
||||
for (int j = 0; j < 10; ++j)
|
||||
for (int i = 0; i < 10; ++i) boxes.push_back({4 * i + 1, 4 * j + 1, 2, 2});
|
||||
RunScatteredWrites(40, boxes);
|
||||
}
|
||||
|
||||
// Four 4x4 boxes around a one-texel cross: 64 of their union's 81 texels, past the 3/4 at
|
||||
// which the storage stops handing its rect list out.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImagesFillingThreeQuartersOfTheirUnionKeepTheGpuClearBetweenThem) {
|
||||
if (!Ready()) return;
|
||||
RunScatteredWrites(16, {{2, 2, 4, 4}, {7, 2, 4, 4}, {2, 7, 4, 4}, {7, 7, 4, 4}});
|
||||
}
|
||||
|
||||
// Three quadrants: the union box is the whole level, and the list merges the touching
|
||||
// boxes into it; the fourth quadrant is still the GPU's.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImagesWhoseUnionSpansTheLevelKeepTheGpuClearInTheGap) {
|
||||
if (!Ready()) return;
|
||||
RunScatteredWrites(kEdge, {{0, 0, 4, 4}, {4, 0, 4, 4}, {0, 4, 4, 4}});
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -30,7 +30,9 @@ TEST_F(SyncWireScenario, FenceWaitStatusAndDeletionCrossAndPreserveRenderedPixel
|
||||
glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(sync);
|
||||
EXPECT_FALSE(glIsSync(sync));
|
||||
EXPECT_GE(PeekSplitRuntime().emitSeq, before + 5);
|
||||
// Create, the server-side wait and the delete always cross; since P10 the two polls may be
|
||||
// answered from the server's fence report instead (FencePollScenario), so they are not counted.
|
||||
EXPECT_GE(PeekSplitRuntime().emitSeq, before + 3);
|
||||
GLubyte pixel[4]{};
|
||||
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||
const int expected[4] = {64, 128, 191, 255};
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureRemintPullScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A TEXTURE RE-MINT NEEDS NO BYTE FROM THE CLIENT (P9 W2, notes/p9/W2-REMINT.md).
|
||||
//
|
||||
// The name is the ROADMAP's: P4a named `texture-remint-pull` the one new stall class of the
|
||||
// split - a texture first bound to an image unit AFTER the backend allocated it has to be
|
||||
// re-minted in image-bindable (possibly channel-widened) storage, and design/04 §8.4 assumed the
|
||||
// server held no texels to fill it with, so the levels would be PULLED back from the client. P9
|
||||
// W2 measured the opposite on both backends: Espryt's server reads each level back from the old
|
||||
// GPU texture and replays its own staged store for what is still pending
|
||||
// (RequireImageBindableStorageByHandle), Magma copies the old image into the new one GPU to GPU
|
||||
// (PreserveTextureContentsOnRecreate). No pull protocol exists, and this scenario is what says
|
||||
// the re-mint is right without one.
|
||||
//
|
||||
// EVERY CASE HAS ONE SHAPE. Give a texture its content by one means, sync it to the backend with
|
||||
// a sampling draw (so the backend really holds storage when the bind arrives - a texture whose
|
||||
// first sync comes after the bind is allocated image-bindable up front and re-mints nothing),
|
||||
// optionally write it on the GPU, then glBindImageTexture it and copy it with imageLoad into a
|
||||
// destination that was image-bound BEFORE its own first sync (so the destination never re-mints),
|
||||
// and read the destination back through a framebuffer. The expectation is the GL answer, never
|
||||
// "what monolith does": the cases exist exactly where the arms can differ.
|
||||
//
|
||||
// A mutable, uploaded, synced the store and the GPU agree
|
||||
// B mutable, uploaded, then cleared on the GPU the store is STALE, the GPU holds it
|
||||
// C immutable RGBA8, uploaded, then cleared Espryt's split keeps the allocation
|
||||
// D null-defined, only ever cleared on the GPU the store has NO bytes
|
||||
// E cleared, then an unsynced partial upload GPU texels merged with the pending box
|
||||
// F glGenerateMipmap's level 1 GPU-generated, no bytes ever crossed
|
||||
// F2 an uploaded level 1, then cleared on the GPU as B, one level down
|
||||
// G level 1 null-defined AFTER the last sync the driver does not hold the level
|
||||
// G2 level 0 redefined larger, null, after the sync the driver holds the OLD extent
|
||||
// G3 G, plus a partial upload into that level the store holds the whole level
|
||||
// H widened rg8 immutable, uploaded, then cleared the carrier re-mint (Espryt promotes)
|
||||
// H2 widened rg8, uploaded only
|
||||
// I RGBA8_SNORM mutable, uploaded only
|
||||
// J RGBA16_SNORM immutable (widened), uploaded only
|
||||
//
|
||||
// G and G3 were Fatal{ResourceUnavailable, "image-promotion-readback"} on Espryt's split arms
|
||||
// until W2-a: the promotion read a level the driver never allocated.
|
||||
//
|
||||
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp, every case. The monolith
|
||||
// DirectGLES arm used to be skipped for B, C, D, E, F, F2 and H: its re-mint replayed the client's
|
||||
// shadow over texels the GPU wrote (W2-REMINT.md §6, the half of ID-P9-8 left to dev). Dev's
|
||||
// 568090f0 made it read the GPU level back first (AdoptDriverLevelIntoShadow), P11 M merged it,
|
||||
// and the skip is retired; E is also what holds the push builds' monolith arm to merging the
|
||||
// applier's pending boxes, which the frontend's dirty model no longer names there.
|
||||
//
|
||||
// W2-d's COUNTER. `trp=` (TextureRemintPulls) counts a re-mint of storage the backend already
|
||||
// held on BOTH Espryt arms now. The peek is process-wide, so it sees the server's count only
|
||||
// where the server is this process: monolith and inproc. A/H assert +1 there, C asserts +0 on
|
||||
// inproc (the kept allocation is not a re-mint).
|
||||
//
|
||||
// W2-b's CONTROL. MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE=1 (read by the SERVER) makes every
|
||||
// promotion readback fail, which a host driver cannot be made to do for real. The
|
||||
// `DirectGLES.{Split,Spawn}.RemintFallback.` entries run the uploaded-only cases under it: the
|
||||
// store covers those levels, so the pixels stay right and the server log names the fallback.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/P4aFinalFixPeek.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kEdge = 8;
|
||||
constexpr const char* kForceReadbackFailure = "MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE";
|
||||
constexpr const char* kFallbackMarker = "remint-readback-fallback";
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
// The sync: any draw that samples the texture makes the backend allocate and upload it.
|
||||
constexpr const char* kSampleSource = R"(#version 430 core
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
|
||||
)";
|
||||
|
||||
std::string CopyComputeSource(const char* sourceFormat, bool signedNormalized) {
|
||||
std::string source = "#version 430 core\nlayout(local_size_x = 8, local_size_y = 8) in;\n";
|
||||
source += std::string("layout(") + sourceFormat + ", binding = 0) readonly uniform image2D u_source;\n";
|
||||
source += "layout(rgba8, binding = 1) writeonly uniform image2D u_destination;\n";
|
||||
source += "void main() {\n ivec2 p = ivec2(gl_GlobalInvocationID.xy);\n";
|
||||
source += " vec4 v = imageLoad(u_source, p);\n";
|
||||
if (signedNormalized) source += " v = v * 0.5 + 0.5;\n";
|
||||
source += " imageStore(u_destination, p, v);\n}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
std::vector<GLubyte> Pattern(int edge, GLubyte magic) {
|
||||
std::vector<GLubyte> texels(static_cast<std::size_t>(edge * edge) * 4u);
|
||||
for (int y = 0; y < edge; ++y) {
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
const std::size_t at = static_cast<std::size_t>((y * edge + x) * 4);
|
||||
texels[at + 0] = magic;
|
||||
texels[at + 1] = static_cast<GLubyte>(16 * (x + 1));
|
||||
texels[at + 2] = static_cast<GLubyte>(16 * (y + 1));
|
||||
texels[at + 3] = 0xFF;
|
||||
}
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::vector<GLubyte> Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) {
|
||||
std::vector<GLubyte> texels(static_cast<std::size_t>(edge * edge) * 4u);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i] = r;
|
||||
texels[i + 1] = g;
|
||||
texels[i + 2] = b;
|
||||
texels[i + 3] = a;
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
const GLfloat kGreen[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||
|
||||
class TextureRemintPullScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
GLint computeImages = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &computeImages);
|
||||
FirstGLError();
|
||||
if (computeImages < 2) {
|
||||
GTEST_SKIP() << "the compute stage has fewer than two image uniforms";
|
||||
}
|
||||
m_wire = SplitRuntimeSkipReason().empty();
|
||||
m_inproc = m_wire && PeekSplitRuntime().transportName == "inproc";
|
||||
m_espryt = Gl().BackendName() == "DirectGLES";
|
||||
m_forced = std::getenv(kForceReadbackFailure) != nullptr;
|
||||
std::string error;
|
||||
m_sampleProgram = CompileProgram(kVertexSource, kSampleSource, &error);
|
||||
ASSERT_NE(m_sampleProgram, 0u) << error;
|
||||
m_scratch = MakeColorFbo(4, 4);
|
||||
ASSERT_NE(m_scratch.fbo, 0u) << "could not create the sampling target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (m_sampleProgram != 0) glDeleteProgram(m_sampleProgram);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (!m_textures.empty()) {
|
||||
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
}
|
||||
BindDefaultFramebuffer();
|
||||
if (m_scratch.fbo != 0) DestroyColorFbo(m_scratch);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
GLuint NewTexture() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static void NearestWithMaxLevel(GLuint texture, int maxLevel) {
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// A draw that samples the texture: from here on the backend holds storage for it.
|
||||
void SyncBySampling(GLuint texture) {
|
||||
BindFbo(m_scratch);
|
||||
glUseProgram(m_sampleProgram);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_texture"), 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
BindDefaultFramebuffer();
|
||||
glFinish();
|
||||
}
|
||||
|
||||
// A GPU write the client's shadow never sees.
|
||||
void ClearOnTheGpu(GLuint texture, int level, const GLfloat* color) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glClearBufferfv(GL_COLOR, 0, color);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
BindDefaultFramebuffer();
|
||||
}
|
||||
|
||||
// THE TRANSITION AND THE READ. The destination is image-bound before its first sync,
|
||||
// so only the source can re-mint; the copy goes through imageLoad, which reads the
|
||||
// re-minted storage and nothing else.
|
||||
std::vector<GLubyte> ImageCopy(GLuint source, int level, GLenum unitFormat, const char* glslFormat,
|
||||
bool signedNormalized = false) {
|
||||
const GLuint destination = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, destination);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindImageTexture(1, destination, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
|
||||
glBindImageTexture(0, source, level, GL_FALSE, 0, GL_READ_ONLY, unitFormat);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture raised a GL error";
|
||||
const std::string computeSource = CopyComputeSource(glslFormat, signedNormalized);
|
||||
const char* text = computeSource.c_str();
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the imageLoad copy did not link";
|
||||
glUseProgram(program);
|
||||
// A BARRIER THE SPEC DOES NOT ASK FOR, AND IT IS HERE ON PURPOSE. GL orders a
|
||||
// framebuffer write (the GPU clears above) before a later shader image load without
|
||||
// one, yet on the Espryt split arm the kept-allocation case (C, no re-mint to
|
||||
// serialize anything) read the pre-clear texels in 3 of 30 runs without it and 0 of
|
||||
// 30 with it, while every server-side decision was identical in passing and failing
|
||||
// runs (W2-REMINT.md §7). That ordering gap is not the re-mint's question; this case
|
||||
// asks what the re-minted storage HOLDS, so the ordering is pinned here.
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0);
|
||||
std::vector<GLubyte> read(static_cast<std::size_t>(kEdge * kEdge) * 4u, 0xA5);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
BindDefaultFramebuffer();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the copy or its readback raised a GL error";
|
||||
return read;
|
||||
}
|
||||
|
||||
// The `edge` x `edge` corner of the read against `want`, `channels` channels, within 2.
|
||||
void ExpectTexels(const char* what, const std::vector<GLubyte>& read, int edge,
|
||||
const std::vector<GLubyte>& want, int channels = 4) {
|
||||
int wrong = 0;
|
||||
int firstX = -1, firstY = -1;
|
||||
for (int y = 0; y < edge; ++y) {
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
for (int c = 0; c < channels; ++c) {
|
||||
const int got = read[static_cast<std::size_t>((y * kEdge + x) * 4 + c)];
|
||||
const int expected = want[static_cast<std::size_t>((y * edge + x) * 4 + c)];
|
||||
if (std::abs(got - expected) > 2 && wrong++ == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::size_t readAt = static_cast<std::size_t>((std::max(firstY, 0) * kEdge + std::max(firstX, 0)) * 4);
|
||||
const std::size_t wantAt = static_cast<std::size_t>((std::max(firstY, 0) * edge + std::max(firstX, 0)) * 4);
|
||||
EXPECT_EQ(wrong, 0) << what << ": " << wrong << " channel(s) differ; first at (" << firstX << ", "
|
||||
<< firstY << ") read " << int(read[readAt]) << "," << int(read[readAt + 1]) << ","
|
||||
<< int(read[readAt + 2]) << "," << int(read[readAt + 3]) << " want "
|
||||
<< int(want[wantAt]) << "," << int(want[wantAt + 1]) << ","
|
||||
<< int(want[wantAt + 2]) << "," << int(want[wantAt + 3]) << " (backend "
|
||||
<< Gl().BackendName() << ", " << (m_wire ? "split" : "monolith") << ")";
|
||||
}
|
||||
|
||||
// W2-d: where the peek can see the server's count (monolith, inproc) - and only on a
|
||||
// push build, which is where PipeStats exists; the pull build's peek answers false (P11
|
||||
// M: the skip retired above now runs H there too, and A always did).
|
||||
bool RemintCountVisible() const {
|
||||
unsigned long long ignored = 0;
|
||||
return m_espryt && (!m_wire || m_inproc) && PeekPipeStatsTextureRemintPulls(&ignored);
|
||||
}
|
||||
unsigned long long RemintCount() {
|
||||
unsigned long long count = 0;
|
||||
EXPECT_TRUE(PeekPipeStatsTextureRemintPulls(&count));
|
||||
return count;
|
||||
}
|
||||
|
||||
// W2-b: under the forced-failure knob the server must have named its fallback.
|
||||
void ExpectFallbackNamedSince(const PipeStatsWindow::LogMark& mark) {
|
||||
if (!m_forced) return;
|
||||
const std::string log = PipeStatsWindow::ReadServerLogSince(mark);
|
||||
EXPECT_NE(log.find(kFallbackMarker), std::string::npos)
|
||||
<< kForceReadbackFailure << " is set but the server log after the image bind carries no '"
|
||||
<< kFallbackMarker << "' line: either the knob did not reach the server (the entry "
|
||||
<< "proves nothing) or the promotion took no readback at all";
|
||||
}
|
||||
|
||||
bool m_wire = false;
|
||||
bool m_inproc = false;
|
||||
bool m_espryt = false;
|
||||
bool m_forced = false;
|
||||
GLuint m_sampleProgram = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_fbo = 0;
|
||||
ColorFbo m_scratch{};
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AnUploadedMutableTextureKeepsItsTexelsAcrossTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const bool counted = RemintCountVisible() && !m_forced;
|
||||
const unsigned long long before = counted ? RemintCount() : 0;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
ExpectTexels("an uploaded mutable texture", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge, pattern);
|
||||
ExpectFallbackNamedSince(mark);
|
||||
if (counted) {
|
||||
EXPECT_EQ(RemintCount(), before + 1)
|
||||
<< "the re-mint of a mutable texture the backend already held was not counted (trp=)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AGpuClearAfterTheUploadSurvivesTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
ClearOnTheGpu(texture, 0, kGreen);
|
||||
SyncBySampling(texture);
|
||||
ExpectTexels("a GPU clear after the upload", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge,
|
||||
Solid(kEdge, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AnImmutableTexturesGpuClearSurvivesTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const bool counted = RemintCountVisible() && m_inproc;
|
||||
const unsigned long long before = counted ? RemintCount() : 0;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
ClearOnTheGpu(texture, 0, kGreen);
|
||||
SyncBySampling(texture);
|
||||
ExpectTexels("an immutable texture's GPU clear", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge,
|
||||
Solid(kEdge, 0, 255, 0, 255));
|
||||
if (counted) {
|
||||
EXPECT_EQ(RemintCount(), before)
|
||||
<< "an immutable core-format allocation is KEPT by the server, not re-minted, and must "
|
||||
"not count (trp=)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, ANullDefinedTextureWrittenOnlyByTheGpuSurvivesTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
ClearOnTheGpu(texture, 0, kGreen);
|
||||
SyncBySampling(texture);
|
||||
ExpectTexels("a null-defined texture only the GPU wrote", ImageCopy(texture, 0, GL_RGBA8, "rgba8"),
|
||||
kEdge, Solid(kEdge, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, APendingPartialUploadMergesOverGpuWrittenTexels) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
ClearOnTheGpu(texture, 0, kGreen);
|
||||
SyncBySampling(texture);
|
||||
const auto blue = Solid(2, 0, 0, 255, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
auto want = Solid(kEdge, 0, 255, 0, 255);
|
||||
for (int y = 2; y < 4; ++y) {
|
||||
for (int x = 2; x < 4; ++x) {
|
||||
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
|
||||
want[at] = 0;
|
||||
want[at + 1] = 0;
|
||||
want[at + 2] = 255;
|
||||
}
|
||||
}
|
||||
ExpectTexels("a pending partial upload over GPU texels", ImageCopy(texture, 0, GL_RGBA8, "rgba8"),
|
||||
kEdge, want);
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AGeneratedMipLevelSurvivesTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto solid = Solid(kEdge, 200, 100, 50, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, solid.data());
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
SyncBySampling(texture);
|
||||
ExpectTexels("glGenerateMipmap's level 1", ImageCopy(texture, 1, GL_RGBA8, "rgba8"), kEdge / 2,
|
||||
Solid(kEdge / 2, 200, 100, 50, 255));
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AGpuClearedMipLevelSurvivesTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto level0 = Solid(kEdge, 200, 100, 50, 255);
|
||||
const auto level1 = Solid(kEdge / 2, 10, 20, 30, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, level1.data());
|
||||
NearestWithMaxLevel(texture, 1);
|
||||
SyncBySampling(texture);
|
||||
ClearOnTheGpu(texture, 1, kGreen);
|
||||
SyncBySampling(texture);
|
||||
ExpectTexels("a GPU-cleared level 1", ImageCopy(texture, 1, GL_RGBA8, "rgba8"), kEdge / 2,
|
||||
Solid(kEdge / 2, 0, 255, 0, 255));
|
||||
}
|
||||
|
||||
// W2-a. Level 1 exists in the store (a null-data respecify) and on no driver: the texture's
|
||||
// last sync predates it. The promotion must not read it back.
|
||||
TEST_F(TextureRemintPullScenario, ALevelDefinedWithoutDataAfterTheLastSyncDoesNotEndTheSession) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ExpectTexels("level 0 beside a level defined after the last sync",
|
||||
ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge, pattern);
|
||||
}
|
||||
|
||||
// W2-a's extent half: the driver holds level 0 at its OLD extent. The new contents are
|
||||
// undefined (GL 4.6 core 8.5), so only "no Fatal, no GL error" is asserted.
|
||||
TEST_F(TextureRemintPullScenario, ALevelRedefinedLargerWithoutDataAfterTheLastSyncDoesNotEndTheSession) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge / 2, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
(void)ImageCopy(texture, 0, GL_RGBA8, "rgba8");
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// W2-a's store half: the level is on no driver, but its whole staged run is in the store
|
||||
// (the client's level shadow, the partial upload inside it), and that is what replays.
|
||||
TEST_F(TextureRemintPullScenario, APartialUploadIntoALevelDefinedAfterTheLastSyncReachesTheImage) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
const auto pattern = Pattern(kEdge, 0x40);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
const auto blue = Solid(2, 0, 0, 255, 255);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
// Level 1 inside [BASE_LEVEL, MAX_LEVEL], or the image access is invalid (8.26).
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ExpectTexels("a partial upload into a level defined after the last sync",
|
||||
ImageCopy(texture, 1, GL_RGBA8, "rgba8"), 2, blue);
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AWidenedFormatsGpuClearSurvivesTheCarrierRemint) {
|
||||
if (!Ready()) return;
|
||||
const bool counted = RemintCountVisible();
|
||||
const unsigned long long before = counted ? RemintCount() : 0;
|
||||
const GLuint texture = NewTexture();
|
||||
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u, 0x33);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
const GLfloat halfQuarter[4] = {0.5f, 0.25f, 0.0f, 1.0f};
|
||||
ClearOnTheGpu(texture, 0, halfQuarter);
|
||||
SyncBySampling(texture);
|
||||
ExpectTexels("a widened rg8 texture's GPU clear", ImageCopy(texture, 0, GL_RG8, "rg8"), kEdge,
|
||||
Solid(kEdge, 128, 64, 0, 255), 2);
|
||||
if (counted) {
|
||||
EXPECT_EQ(RemintCount(), before + 1)
|
||||
<< "the carrier re-mint of a widened texture the backend already held was not counted "
|
||||
"(trp=)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AWidenedFormatsUploadSurvivesTheCarrierRemint) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u);
|
||||
for (std::size_t i = 0; i < rg.size(); i += 2) {
|
||||
rg[i] = 0x33;
|
||||
rg[i + 1] = 0x99;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
ExpectTexels("a widened rg8 upload", ImageCopy(texture, 0, GL_RG8, "rg8"), kEdge,
|
||||
Solid(kEdge, 0x33, 0x99, 0, 255), 2);
|
||||
ExpectFallbackNamedSince(mark);
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AnRgba8SnormUploadSurvivesTheImageBind) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
std::vector<GLbyte> texels(static_cast<std::size_t>(kEdge * kEdge) * 4u);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i] = 127;
|
||||
texels[i + 1] = 0;
|
||||
texels[i + 2] = -127;
|
||||
texels[i + 3] = 127;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, kEdge, kEdge, 0, GL_RGBA, GL_BYTE, texels.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
ExpectTexels("an RGBA8_SNORM upload", ImageCopy(texture, 0, GL_RGBA8_SNORM, "rgba8_snorm", true), kEdge,
|
||||
Solid(kEdge, 255, 128, 0, 255), 3);
|
||||
ExpectFallbackNamedSince(mark);
|
||||
}
|
||||
|
||||
TEST_F(TextureRemintPullScenario, AnRgba16SnormImmutableUploadSurvivesTheCarrierRemint) {
|
||||
if (!Ready()) return;
|
||||
const GLuint texture = NewTexture();
|
||||
std::vector<GLshort> texels(static_cast<std::size_t>(kEdge * kEdge) * 4u);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i] = 32767;
|
||||
texels[i + 1] = 0;
|
||||
texels[i + 2] = -32767;
|
||||
texels[i + 3] = 32767;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA16_SNORM, kEdge, kEdge);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_SHORT, texels.data());
|
||||
NearestWithMaxLevel(texture, 0);
|
||||
SyncBySampling(texture);
|
||||
const auto mark = PipeStatsWindow::MarkLaneLog();
|
||||
ExpectTexels("an RGBA16_SNORM immutable upload",
|
||||
ImageCopy(texture, 0, GL_RGBA16_SNORM, "rgba16_snorm", true), kEdge,
|
||||
Solid(kEdge, 255, 128, 0, 255), 3);
|
||||
ExpectFallbackNamedSince(mark);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,314 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnboundUniformBlockScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A PROGRAM DECLARES A UNIFORM BLOCK AND NOTHING IS BOUND AT ITS BINDING POINT.
|
||||
//
|
||||
// The uniform-block sibling of UnboundImageDescriptorScenario. A draw or dispatch whose program
|
||||
// declares a uniform block at an indexed point with no buffer on it raises no GL error - whether
|
||||
// the point was never bound or its buffer was deleted while bound (glDeleteBuffers unbinds it
|
||||
// from every indexed point of the current context). Only what the shader READS from the block is
|
||||
// undefined.
|
||||
//
|
||||
// DirectVulkan's ResolveUniformBufferPayload looked up the buffer at the block's point and
|
||||
// dereferenced it with nothing there: a SIGSEGV inside the draw or the dispatch, taking the whole
|
||||
// process with it. It now answers with a zeroed block of the reflected size.
|
||||
//
|
||||
// The block is STATICALLY USED in every case, because an unreferenced block is optimised out
|
||||
// before it ever reaches a descriptor and would prove nothing. Its one read sits behind a
|
||||
// uniform-controlled branch that is false whenever the point is empty, so no undefined value
|
||||
// reaches an assertion. Each case also binds a buffer at the same point and reads it back through
|
||||
// the same program - proof that the block is live and wired to that point - and then empties the
|
||||
// point again, so the transition back to "nothing bound" is covered as well as the first use.
|
||||
//
|
||||
// DirectGLES forwards the empty point to the GLES driver, which does what GL says, so it is the
|
||||
// control - every test here must stay green on both backends.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
// The point under test. Not 0, so a stray glBindBufferBase(GL_UNIFORM_BUFFER, 0, ...) from
|
||||
// an earlier scenario in this process is not what decides the case - and SetUp empties
|
||||
// this one explicitly anyway.
|
||||
constexpr GLuint kUniformPoint = 5;
|
||||
constexpr GLuint kResultPoint = 0;
|
||||
constexpr unsigned int kMarker = 0xC0FFEEu;
|
||||
constexpr unsigned int kFallback = 7u;
|
||||
constexpr unsigned int kBoundWord = 0x1234u;
|
||||
|
||||
// No vertex attributes: the quad's corners come from gl_VertexID, so nothing about the
|
||||
// vertex fetch can be confused with the descriptor question under test.
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Green unless u_read is set, in which case the block's tint.
|
||||
constexpr const char* kFragmentSource = R"(#version 430 core
|
||||
layout(std140, binding = 5) uniform Target { vec4 u_tint; };
|
||||
uniform int u_read;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_read != 0) {
|
||||
color = u_tint;
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The marker is unconditional, so a zero marker means the dispatch never ran; the value is
|
||||
// the fallback unless u_read is set, in which case it is the block's word.
|
||||
constexpr const char* kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Result { uint g_marker; uint g_value; };
|
||||
layout(std140, binding = 5) uniform Target { uint u_word; };
|
||||
uniform int u_read;
|
||||
void main() {
|
||||
uint value = 7u;
|
||||
if (u_read != 0) {
|
||||
value = u_word;
|
||||
}
|
||||
g_marker = 0xC0FFEEu;
|
||||
g_value = value;
|
||||
}
|
||||
)";
|
||||
|
||||
class UnboundUniformBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
|
||||
const unsigned int zero[2] = {0u, 0u};
|
||||
glGenBuffers(1, &m_result);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(zero), zero, GL_DYNAMIC_COPY);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kResultPoint, m_result);
|
||||
|
||||
// The harness shares one context across every scenario in the process, so an
|
||||
// earlier one may have left a buffer at this point. The whole subject here is that
|
||||
// nothing is bound, so say so rather than assume it.
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting up the scenario raised a GL error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kResultPoint, 0);
|
||||
if (m_block != 0) glDeleteBuffers(1, &m_block);
|
||||
if (m_result != 0) glDeleteBuffers(1, &m_result);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_draw != 0) glDeleteProgram(m_draw);
|
||||
if (m_compute != 0) glDeleteProgram(m_compute);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
unsigned int MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// The block must be ACTIVE and at kUniformPoint, and the guard uniform must exist:
|
||||
// if either were optimised away the program would never ask for the empty point and
|
||||
// every case below would pass without testing anything.
|
||||
static void ExpectTheBlockIsLiveAtThePoint(GLuint program) {
|
||||
const GLuint block = glGetUniformBlockIndex(program, "Target");
|
||||
ASSERT_NE(block, GL_INVALID_INDEX) << "the uniform block was optimised out - the case proves nothing";
|
||||
GLint binding = -1;
|
||||
glGetActiveUniformBlockiv(program, block, GL_UNIFORM_BLOCK_BINDING, &binding);
|
||||
ASSERT_EQ(binding, static_cast<GLint>(kUniformPoint)) << "the block is not at the point under test";
|
||||
ASSERT_NE(glGetUniformLocation(program, "u_read"), -1) << "the guard uniform was optimised out";
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "querying the block raised a GL error";
|
||||
}
|
||||
|
||||
static void ExpectThePointIsEmpty(const char* when) {
|
||||
GLint bound = -1;
|
||||
glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, kUniformPoint, &bound);
|
||||
ASSERT_EQ(bound, 0) << "uniform point " << kUniformPoint << " is not empty " << when;
|
||||
}
|
||||
|
||||
// A 16-byte std140 block at kUniformPoint holding `words`.
|
||||
void BindBlock(const std::array<unsigned int, 4>& words) {
|
||||
if (m_block == 0) glGenBuffers(1, &m_block);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, m_block);
|
||||
glBufferData(GL_UNIFORM_BUFFER, sizeof(words), words.data(), GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, m_block);
|
||||
}
|
||||
|
||||
// Runs the compute program once; returns {marker, value}.
|
||||
std::array<unsigned int, 2> Dispatch(int read) {
|
||||
const unsigned int zero[2] = {0u, 0u};
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
|
||||
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(zero), zero);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(m_compute);
|
||||
glUniform1i(glGetUniformLocation(m_compute, "u_read"), read);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::array<unsigned int, 2> out = {0xDEADBEEFu, 0xDEADBEEFu};
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(out), out.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
return out;
|
||||
}
|
||||
|
||||
void ExpectTheDispatchRunsOnAnEmptyPoint(const char* when) {
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectThePointIsEmpty(when));
|
||||
const auto result = Dispatch(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error " << when;
|
||||
EXPECT_EQ(result[0], kMarker) << "the dispatch did not run " << when;
|
||||
EXPECT_EQ(result[1], kFallback) << "the dispatch took the block's branch " << when;
|
||||
}
|
||||
|
||||
// Draws the full-target quad and expects every pixel to be `color`.
|
||||
void ExpectTheDraw(int read, const char* color, const std::string& when) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_draw);
|
||||
glUniform1i(glGetUniformLocation(m_draw, "u_read"), read);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error " << when;
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << "the readback came back empty " << when;
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, color, 0.0, when))
|
||||
<< "an all-black target means the draw was dropped";
|
||||
}
|
||||
|
||||
void ExpectTheDrawRunsOnAnEmptyPoint(const std::string& when) {
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectThePointIsEmpty(when.c_str()));
|
||||
ExpectTheDraw(0, "green", when);
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_result = 0;
|
||||
GLuint m_block = 0;
|
||||
unsigned int m_draw = 0;
|
||||
unsigned int m_compute = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(UnboundUniformBlockScenario, ADispatchDeclaringAnEmptyUniformPointRuns) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
m_compute = MakeComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_compute, 0u);
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_compute));
|
||||
|
||||
ExpectTheDispatchRunsOnAnEmptyPoint("with nothing ever bound at the point");
|
||||
|
||||
BindBlock({kBoundWord, 0u, 0u, 0u});
|
||||
const auto bound = Dispatch(1);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(bound[0], kMarker) << "the dispatch did not run with a buffer bound";
|
||||
EXPECT_EQ(bound[1], kBoundWord) << "the program did not read the buffer bound at the block's point";
|
||||
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
|
||||
ExpectTheDispatchRunsOnAnEmptyPoint("after the point was emptied again");
|
||||
}
|
||||
|
||||
TEST_F(UnboundUniformBlockScenario, ADrawDeclaringAnEmptyUniformPointPaintsTheFrame) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
std::string error;
|
||||
m_draw = CompileProgram(kQuadVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_draw, 0u) << error;
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_draw));
|
||||
|
||||
ExpectTheDrawRunsOnAnEmptyPoint("with nothing ever bound at the point");
|
||||
|
||||
// Opaque red: 1.0f and 0.0f as their IEEE-754 words, so the block is one vec4.
|
||||
BindBlock({0x3F800000u, 0u, 0u, 0x3F800000u});
|
||||
ExpectTheDraw(1, "red", "reading the buffer bound at the block's point");
|
||||
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
|
||||
ExpectTheDrawRunsOnAnEmptyPoint("after the point was emptied again");
|
||||
}
|
||||
|
||||
// The shape real applications reach it by: the buffer is bound and used, then deleted while
|
||||
// still bound, which empties the point without the application ever unbinding it.
|
||||
TEST_F(UnboundUniformBlockScenario, AUniformBufferDeletedWhileBoundLeavesAnEmptyPointBothPathsSurvive) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
m_compute = MakeComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_compute, 0u);
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_compute));
|
||||
std::string error;
|
||||
m_draw = CompileProgram(kQuadVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_draw, 0u) << error;
|
||||
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_draw));
|
||||
|
||||
BindBlock({kBoundWord, 0u, 0u, 0u});
|
||||
const auto bound = Dispatch(1);
|
||||
EXPECT_EQ(bound[0], kMarker) << "the dispatch did not run with a buffer bound";
|
||||
EXPECT_EQ(bound[1], kBoundWord) << "the program did not read the buffer bound at the block's point";
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glDeleteBuffers(1, &m_block);
|
||||
m_block = 0;
|
||||
ExpectTheDispatchRunsOnAnEmptyPoint("after the bound buffer was deleted");
|
||||
ExpectTheDrawRunsOnAnEmptyPoint("after the bound buffer was deleted");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,292 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P8-SV: THE MAGMA WIRE ARM ISSUES glDispatchComputeIndirect FROM THE WIRE STORE, AS THE MONOLITH
|
||||
// ARM DOES (docs/Disaggregated/notes/p8/SV.md).
|
||||
//
|
||||
// Before P8-SV the wire arm read the three group counts through ReadWireBuffer and dispatched them
|
||||
// from the CPU, so a dispatch buffer a shader had written cost a whole-GPU wait per call - the
|
||||
// dispatch twin of what P8-D removed for indirect draws (WireIndirectDrawScenario.cpp). The cases
|
||||
// pin both halves:
|
||||
// * THE GRID, on every arm and both backends - each dispatch marks exactly the work groups its
|
||||
// counts name, and GPU-written counts are the ones the dispatch sees (the buffer starts with
|
||||
// OTHER counts, so a stale read shows as the wrong grid);
|
||||
// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on the
|
||||
// inproc arm: every call issued as vkCmdDispatchIndirect (`wdsp`), no host wait (`whw`), and the
|
||||
// INDIRECT_COMMAND_READ barrier exactly once per shader write (`wibar`).
|
||||
// The counters are read BEFORE the grids are read back: that readback is a host read of a store a
|
||||
// shader wrote, so it takes a host wait of its own, which is the application's and not the
|
||||
// dispatch's.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#include "../Harness/WireIndirectPeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Grids up to 4 x 4 x 2: one cell per work group.
|
||||
constexpr std::uint32_t kGridX = 4, kGridY = 4, kGridZ = 2;
|
||||
constexpr std::uint32_t kCells = kGridX * kGridY * kGridZ;
|
||||
|
||||
// One invocation per work group, and each group marks its own cell.
|
||||
constexpr const char* kMarkSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 2) buffer Marks { uint marks[]; };
|
||||
void main() {
|
||||
uvec3 g = gl_WorkGroupID;
|
||||
atomicAdd(marks[(g.z * 4u + g.y) * 4u + g.x], 1u);
|
||||
}
|
||||
)";
|
||||
// Copies `source` into `words`, one word per invocation: the compute write under test.
|
||||
constexpr const char* kCopySource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Words { uint words[]; };
|
||||
layout(std430, binding = 1) readonly buffer Source { uint source[]; };
|
||||
void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; }
|
||||
)";
|
||||
|
||||
using Grid = std::array<std::uint32_t, 3>;
|
||||
|
||||
class WireIndirectDispatchScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_magma = Gl().BackendName() == "DirectVulkan";
|
||||
m_transport = PeekSplitRuntime().transportName;
|
||||
m_mark = CompileCompute(kMarkSource);
|
||||
ASSERT_NE(m_mark, 0u);
|
||||
m_copy = CompileCompute(kCopySource);
|
||||
ASSERT_NE(m_copy, 0u);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
// The counters are armed by the first peek, so it has to come before any dispatch.
|
||||
if (CountersReadable()) {
|
||||
ASSERT_TRUE(PeekWireIndirectCounters(&m_before))
|
||||
<< "Magma on the inproc arm must be able to read the wire arm's own counters";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, 0);
|
||||
for (GLuint& buffer : m_marks) {
|
||||
if (buffer != 0) glDeleteBuffers(1, &buffer);
|
||||
}
|
||||
for (GLuint* buffer : {&m_groupCounts, &m_source}) {
|
||||
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
||||
*buffer = 0;
|
||||
}
|
||||
if (m_mark != 0) glDeleteProgram(m_mark);
|
||||
if (m_copy != 0) glDeleteProgram(m_copy);
|
||||
}
|
||||
|
||||
static GLuint CompileCompute(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (ok != GL_TRUE) {
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||
if (ok != GL_TRUE) {
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
static void Fill(GLuint& name, GLenum target, const std::vector<std::uint32_t>& words) {
|
||||
if (name == 0) glGenBuffers(1, &name);
|
||||
glBindBuffer(target, name);
|
||||
glBufferData(target, static_cast<GLsizeiptr>(words.size() * sizeof(std::uint32_t)), words.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
}
|
||||
|
||||
// The dispatch-indirect store, holding CPU-written counts.
|
||||
void Parameters(const std::vector<std::uint32_t>& words) {
|
||||
Fill(m_groupCounts, GL_DISPATCH_INDIRECT_BUFFER, words);
|
||||
}
|
||||
|
||||
// The compute write: the parameter store (already holding its poison) receives `words`.
|
||||
void ComputeWriteParameters(const std::vector<std::uint32_t>& words) {
|
||||
Fill(m_source, GL_SHADER_STORAGE_BUFFER, words);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_groupCounts);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source);
|
||||
glUseProgram(m_copy);
|
||||
glDispatchCompute(static_cast<GLuint>(words.size()), 1, 1);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
}
|
||||
|
||||
// One indirect dispatch of the mark program into a fresh, zeroed cell buffer. Returns
|
||||
// the index of that buffer for ExpectGrid.
|
||||
size_t DispatchIndirect(GLintptr offset) {
|
||||
GLuint marks = 0;
|
||||
Fill(marks, GL_SHADER_STORAGE_BUFFER, std::vector<std::uint32_t>(kCells, 0u));
|
||||
m_marks.push_back(marks);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, marks);
|
||||
glUseProgram(m_mark);
|
||||
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, m_groupCounts);
|
||||
glDispatchComputeIndirect(offset);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0);
|
||||
return m_marks.size() - 1;
|
||||
}
|
||||
|
||||
// A round trip that reads no buffer store, so the server has applied every dispatch
|
||||
// above before the counters are read - and no host wait of its own is added.
|
||||
static void Settle() {
|
||||
BindDefaultFramebuffer();
|
||||
(void)ReadPixels(1, 1);
|
||||
}
|
||||
|
||||
void ExpectGrid(size_t which, const Grid& grid, const std::string& what) {
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<std::uint32_t> cells(kCells, 0xDEADu);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_marks[which]);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, static_cast<GLsizeiptr>(kCells * sizeof(std::uint32_t)),
|
||||
cells.data());
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << what << ": readback";
|
||||
size_t wrong = 0;
|
||||
std::string first;
|
||||
for (std::uint32_t z = 0; z < kGridZ; ++z) {
|
||||
for (std::uint32_t y = 0; y < kGridY; ++y) {
|
||||
for (std::uint32_t x = 0; x < kGridX; ++x) {
|
||||
const bool inside = x < grid[0] && y < grid[1] && z < grid[2];
|
||||
const std::uint32_t got = cells[(z * kGridY + y) * kGridX + x];
|
||||
if (got != (inside ? 1u : 0u) && wrong++ == 0) {
|
||||
first = "group (" + std::to_string(x) + "," + std::to_string(y) + "," +
|
||||
std::to_string(z) + ") = " + std::to_string(got);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(wrong, 0u) << what << ": expected exactly the " << grid[0] << "x" << grid[1] << "x"
|
||||
<< grid[2] << " grid marked once; first wrong " << first;
|
||||
}
|
||||
|
||||
// Magma's server in THIS process: the inproc arm. Spawn and tcp servers are other
|
||||
// processes, and the monolith arm never takes the wire path at all.
|
||||
bool CountersReadable() const { return m_magma && m_transport == "inproc"; }
|
||||
|
||||
WireIndirectCounters Delta() const {
|
||||
WireIndirectCounters now{};
|
||||
EXPECT_TRUE(PeekWireIndirectCounters(&now));
|
||||
WireIndirectCounters delta{};
|
||||
delta.nativeDraws = now.nativeDraws - m_before.nativeDraws;
|
||||
delta.cpuExpansions = now.cpuExpansions - m_before.cpuExpansions;
|
||||
delta.barriers = now.barriers - m_before.barriers;
|
||||
delta.hostWaits = now.hostWaits - m_before.hostWaits;
|
||||
delta.indirectWaits = now.indirectWaits - m_before.indirectWaits;
|
||||
delta.nativeDispatches = now.nativeDispatches - m_before.nativeDispatches;
|
||||
return delta;
|
||||
}
|
||||
|
||||
bool m_magma = false;
|
||||
std::string m_transport;
|
||||
WireIndirectCounters m_before{};
|
||||
GLuint m_mark = 0, m_copy = 0, m_groupCounts = 0, m_source = 0;
|
||||
std::vector<GLuint> m_marks;
|
||||
};
|
||||
|
||||
// CPU-written counts at two non-zero offsets of one store. On Magma/inproc both calls must
|
||||
// be issued natively, with no host wait and no barrier (no shader wrote the store).
|
||||
TEST_F(WireIndirectDispatchScenario, CpuWrittenGroupCountsAreReadByTheGpuAtTheirOffsets) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Parameters({1, 1, 1, 3, 2, 1, 4, 1, 2});
|
||||
const size_t first = DispatchIndirect(12);
|
||||
const size_t second = DispatchIndirect(24);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Settle();
|
||||
if (CountersReadable()) {
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDispatches, 2u) << "every indirect dispatch must be vkCmdDispatchIndirect";
|
||||
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write to the counts";
|
||||
EXPECT_EQ(delta.barriers, 0u) << "no shader wrote this store, so no barrier is due";
|
||||
}
|
||||
ExpectGrid(first, {3, 2, 1}, "glDispatchComputeIndirect at byte 12");
|
||||
ExpectGrid(second, {4, 1, 2}, "glDispatchComputeIndirect at byte 24");
|
||||
}
|
||||
|
||||
// Counts a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT. The
|
||||
// store starts with OTHER counts, so a stale read dispatches the wrong grid. On
|
||||
// Magma/inproc: issued natively and - the point of the change - NO host wait.
|
||||
TEST_F(WireIndirectDispatchScenario, ComputeWrittenGroupCountsDispatchWithoutAHostWait) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Parameters({1, 1, 1});
|
||||
ComputeWriteParameters({3, 2, 1});
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
||||
const size_t first = DispatchIndirect(0);
|
||||
|
||||
Parameters({1, 1, 1, 1, 1, 1});
|
||||
ComputeWriteParameters({1, 1, 1, 2, 2, 2});
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
||||
const size_t second = DispatchIndirect(12);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Settle();
|
||||
if (CountersReadable()) {
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDispatches, 2u) << "both dispatches must be issued natively";
|
||||
EXPECT_EQ(delta.hostWaits, 0u)
|
||||
<< "an indirect dispatch of shader-written counts waited for the GPU on the host";
|
||||
EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store";
|
||||
}
|
||||
ExpectGrid(first, {3, 2, 1}, "compute-written counts at byte 0");
|
||||
ExpectGrid(second, {2, 2, 2}, "compute-written counts at byte 12");
|
||||
}
|
||||
|
||||
// Flywheel's shape, one call over: the counts are written and dispatched with no
|
||||
// glMemoryBarrier between (GL leaves it undefined). The wire arm orders it anyway, once per
|
||||
// write, as P8-D does for draws. Magma's split arms only: the monolith arm records no
|
||||
// dependency here (P8-D recorded that half for dev), and Espryt's is its driver's.
|
||||
TEST_F(WireIndirectDispatchScenario, ShaderWrittenGroupCountsWithoutAnApplicationBarrierAreOrdered) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!m_magma || m_transport == "monolith") {
|
||||
GTEST_SKIP() << "the implicit INDIRECT_COMMAND_READ barrier is the Magma wire arm's (P8-D, P8-SV)";
|
||||
}
|
||||
Parameters({1, 1, 1});
|
||||
ComputeWriteParameters({3, 2, 1});
|
||||
const size_t first = DispatchIndirect(0);
|
||||
const size_t again = DispatchIndirect(0);
|
||||
ComputeWriteParameters({1, 2, 2});
|
||||
const size_t third = DispatchIndirect(0);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
Settle();
|
||||
if (CountersReadable()) {
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDispatches, 3u);
|
||||
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
|
||||
EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second dispatch reads no new write";
|
||||
}
|
||||
ExpectGrid(first, {3, 2, 1}, "the first dispatch after the write");
|
||||
ExpectGrid(again, {3, 2, 1}, "a second dispatch, no write between");
|
||||
ExpectGrid(third, {1, 2, 2}, "the dispatch after a second write");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,368 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P8-D: THE MAGMA WIRE ARM ISSUES INDIRECT DRAWS FROM THE WIRE STORE, AS THE MONOLITH ARM DOES.
|
||||
//
|
||||
// Before P8-D every wire indirect draw read its commands (and its count word) through
|
||||
// ReadWireBuffer and expanded them on the CPU, and a store a shader had written turned that read
|
||||
// into a whole-GPU wait per draw (create-indirect: 321 expansions, 327 waits, ~4-8 s of waiting
|
||||
// per replay on lavapipe). The cases pin both halves of the change:
|
||||
// * PIXELS, on every arm and both backends where the backend's own semantics allow it - each
|
||||
// indirect form draws exactly the strips its commands name, and GPU-written commands are the
|
||||
// ones the draw sees (the poison the buffers start with names the OTHER strips);
|
||||
// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on
|
||||
// the inproc arm, whose server is this process: every call issued natively, no CPU
|
||||
// expansion, no host wait, and the INDIRECT_COMMAND_READ barrier exactly once per write.
|
||||
//
|
||||
// Four strips across the viewport, one per quarter; a command draws one strip. Green = drawn,
|
||||
// red = the clear. The VBO opens with three padding vertices so a command that loses its `first`
|
||||
// or `firstIndex` draws a degenerate triangle rather than the right strip.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#include "../Harness/WireIndirectPeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kStrips = 4;
|
||||
constexpr std::uint32_t kPad = 3;
|
||||
constexpr std::uint32_t kStripVertices = 6;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
constexpr const char* kFragmentSource = R"(#version 430 core
|
||||
out vec4 oColor;
|
||||
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
// Copies `source` into `words`, one word per invocation: the compute write under test.
|
||||
constexpr const char* kCopySource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Words { uint words[]; };
|
||||
layout(std430, binding = 1) readonly buffer Source { uint source[]; };
|
||||
void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; }
|
||||
)";
|
||||
|
||||
struct ArraysCommand {
|
||||
std::uint32_t count, instanceCount, first, baseInstance;
|
||||
};
|
||||
struct ElementsCommand {
|
||||
std::uint32_t count, instanceCount, firstIndex;
|
||||
std::int32_t baseVertex;
|
||||
std::uint32_t baseInstance;
|
||||
};
|
||||
// A padded elements command, for the stride case: 32 bytes, the tail unread.
|
||||
struct PaddedElementsCommand {
|
||||
ElementsCommand command;
|
||||
std::uint32_t padding[3];
|
||||
};
|
||||
|
||||
ArraysCommand Arrays(int strip) { return {kStripVertices, 1, kPad + kStripVertices * strip, 0}; }
|
||||
ElementsCommand Elements(int strip) { return {kStripVertices, 1, kStripVertices * strip, 0, 0}; }
|
||||
|
||||
using Strips = std::array<bool, kStrips>;
|
||||
|
||||
class WireIndirectDrawScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_magma = Gl().BackendName() == "DirectVulkan";
|
||||
m_transport = PeekSplitRuntime().transportName;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
m_copy = CompileCompute(kCopySource);
|
||||
ASSERT_NE(m_copy, 0u);
|
||||
|
||||
std::vector<float> vertices(2 * kPad, 0.0f);
|
||||
for (int strip = 0; strip < kStrips; ++strip) {
|
||||
const float x0 = -1.0f + 0.5f * static_cast<float>(strip);
|
||||
const float x1 = x0 + 0.5f;
|
||||
const float quad[12] = {x0, -1.0f, x1, -1.0f, x1, 1.0f, x0, -1.0f, x1, 1.0f, x0, 1.0f};
|
||||
vertices.insert(vertices.end(), quad, quad + 12);
|
||||
}
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < kStripVertices * kStrips; ++i) indices.push_back(kPad + i);
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(float)),
|
||||
vertices.data(), GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glGenBuffers(1, &m_ebo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(indices.size() * sizeof(std::uint32_t)),
|
||||
indices.data(), GL_STATIC_DRAW);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
|
||||
// The counters are armed by the first peek, so it has to come before any draw.
|
||||
if (CountersReadable()) {
|
||||
ASSERT_TRUE(PeekWireIndirectCounters(&m_before))
|
||||
<< "Magma on the inproc arm must be able to read the wire arm's own counters";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
for (GLuint* buffer : {&m_vbo, &m_ebo, &m_indirect, &m_parameter, &m_source}) {
|
||||
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
||||
*buffer = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_copy != 0) glDeleteProgram(m_copy);
|
||||
}
|
||||
|
||||
static GLuint CompileCompute(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (ok != GL_TRUE) {
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||
if (ok != GL_TRUE) {
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void Fill(GLuint& name, GLenum target, const std::vector<T>& data) {
|
||||
if (name == 0) glGenBuffers(1, &name);
|
||||
glBindBuffer(target, name);
|
||||
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_DYNAMIC_COPY);
|
||||
}
|
||||
|
||||
// The compute write: `words` (already holding its poison) receives `data` from the GPU.
|
||||
template <typename T>
|
||||
void ComputeWrite(GLuint words, const std::vector<T>& data) {
|
||||
Fill(m_source, GL_SHADER_STORAGE_BUFFER, data);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, words);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source);
|
||||
glUseProgram(m_copy);
|
||||
glDispatchCompute(static_cast<GLuint>(data.size() * sizeof(T) / sizeof(std::uint32_t)), 1, 1);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
}
|
||||
|
||||
template <typename DrawFn>
|
||||
Image Render(DrawFn&& draw) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_indirect);
|
||||
draw();
|
||||
return ReadPixels(Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
static void ExpectStrips(const Image& image, const Strips& drawn, const std::string& what) {
|
||||
for (int strip = 0; strip < kStrips; ++strip) {
|
||||
const int x = image.Width() * (2 * strip + 1) / (2 * kStrips);
|
||||
EXPECT_STREQ(image.ColorName(x, image.Height() / 2), drawn[strip] ? "green" : "red")
|
||||
<< what << ": strip " << strip;
|
||||
}
|
||||
}
|
||||
|
||||
// Magma's server in THIS process: the inproc arm. Spawn and tcp servers are other
|
||||
// processes, and the monolith arm never takes the wire path at all.
|
||||
bool CountersReadable() const { return m_magma && m_transport == "inproc"; }
|
||||
|
||||
WireIndirectCounters Delta() const {
|
||||
WireIndirectCounters now{};
|
||||
EXPECT_TRUE(PeekWireIndirectCounters(&now));
|
||||
return {now.nativeDraws - m_before.nativeDraws, now.cpuExpansions - m_before.cpuExpansions,
|
||||
now.barriers - m_before.barriers, now.hostWaits - m_before.hostWaits,
|
||||
now.indirectWaits - m_before.indirectWaits};
|
||||
}
|
||||
|
||||
bool m_magma = false;
|
||||
std::string m_transport;
|
||||
WireIndirectCounters m_before{};
|
||||
GLuint m_program = 0, m_copy = 0, m_vao = 0, m_vbo = 0, m_ebo = 0;
|
||||
GLuint m_indirect = 0, m_parameter = 0, m_source = 0;
|
||||
};
|
||||
|
||||
// Every indirect form from CPU-written commands. On Magma/inproc the server must have
|
||||
// issued all six natively, expanded none, waited for nothing and recorded no barrier (no
|
||||
// shader ever wrote these stores).
|
||||
TEST_F(WireIndirectDrawScenario, EveryIndirectFormIsIssuedNativelyFromItsStore) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
// Arrays commands at 0.., elements commands after them: the offsets are byte offsets
|
||||
// into one store, as an application packs them.
|
||||
const std::vector<ArraysCommand> arrays = {Arrays(0), Arrays(1), Arrays(3), Arrays(2)};
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, arrays);
|
||||
Image image = Render([&] { glDrawArraysIndirect(GL_TRIANGLES, reinterpret_cast<const void*>(16)); });
|
||||
ExpectStrips(image, {false, true, false, false}, "glDrawArraysIndirect at byte 16");
|
||||
image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 3, 0); });
|
||||
ExpectStrips(image, {true, true, false, true}, "glMultiDrawArraysIndirect, 3 commands");
|
||||
|
||||
const std::vector<PaddedElementsCommand> padded = {{Elements(3), {}}, {Elements(0), {}}};
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, padded);
|
||||
image = Render([&] {
|
||||
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 2, sizeof(PaddedElementsCommand));
|
||||
});
|
||||
ExpectStrips(image, {true, false, false, true}, "glMultiDrawElementsIndirect, stride 32");
|
||||
image = Render([&] {
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(sizeof(PaddedElementsCommand)));
|
||||
});
|
||||
ExpectStrips(image, {true, false, false, false}, "glDrawElementsIndirect at byte 32");
|
||||
|
||||
// The count forms: three commands each, a count word that allows fewer.
|
||||
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{7, 2, 1});
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(0), Arrays(1), Arrays(2)});
|
||||
image = Render([&] {
|
||||
glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 4, 3, 0);
|
||||
});
|
||||
ExpectStrips(image, {true, true, false, false}, "glMultiDrawArraysIndirectCount, count word 2 of 3");
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(3), Elements(2), Elements(1)});
|
||||
image = Render([&] {
|
||||
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 8, 3, 0);
|
||||
});
|
||||
ExpectStrips(image, {false, false, false, true}, "glMultiDrawElementsIndirectCount, count word 1 of 3");
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
if (!CountersReadable()) return;
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDraws, 6u) << "every indirect call must be issued as vkCmdDraw*Indirect[Count]";
|
||||
EXPECT_EQ(delta.cpuExpansions, 0u) << "no indirect call may read its words on the CPU";
|
||||
EXPECT_EQ(delta.indirectWaits, 0u) << "an indirect call took a host wait";
|
||||
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write anywhere";
|
||||
EXPECT_EQ(delta.barriers, 0u) << "no shader wrote these stores, so no barrier is due";
|
||||
}
|
||||
|
||||
// Commands a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT.
|
||||
// The buffers start with commands for the OTHER strips, so a read of stale words shows.
|
||||
// On Magma/inproc: issued natively, and - the point of the package - NO host wait.
|
||||
TEST_F(WireIndirectDrawScenario, ComputeWrittenCommandsDrawWithoutAHostWait) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(2), Arrays(3)});
|
||||
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(0), Arrays(1)});
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
||||
Image image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
|
||||
ExpectStrips(image, {true, true, false, false}, "glMultiDrawArraysIndirect of compute-written commands");
|
||||
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(0), Elements(1)});
|
||||
ComputeWrite(m_indirect, std::vector<ElementsCommand>{Elements(3), Elements(2)});
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
||||
image = Render([&] { glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 2, 0); });
|
||||
ExpectStrips(image, {false, false, true, true}, "glMultiDrawElementsIndirect of compute-written commands");
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
if (!CountersReadable()) return;
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDraws, 2u) << "both draws must be issued natively";
|
||||
EXPECT_EQ(delta.cpuExpansions, 0u) << "no indirect call may read its words on the CPU";
|
||||
EXPECT_EQ(delta.indirectWaits, 0u)
|
||||
<< "an indirect draw of shader-written commands waited for the GPU on the host";
|
||||
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
|
||||
EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store";
|
||||
}
|
||||
|
||||
// The COUNT word a compute shader wrote. Espryt reads that word on the CPU from the
|
||||
// server's staged copy of the parameter buffer; P8-C refreshes that copy after a shader
|
||||
// write (SplitHostBytesForCpuRead), so its split arms answer here too (P8-SE). Espryt's
|
||||
// MONOLITH arm still reads the frontend shadow without a sync (OPEN-QUESTIONS 15, C.md's
|
||||
// monolith defects), left to P13 (ID-P8-13).
|
||||
TEST_F(WireIndirectDrawScenario, ComputeWrittenCountWordIsTheOneTheGpuReads) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!m_magma && m_transport == "monolith") {
|
||||
GTEST_SKIP() << "Espryt's monolith arm reads the count word from the frontend shadow without "
|
||||
"a sync (OQ15; recorded for P13, ID-P8-13)";
|
||||
}
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(0), Elements(3), Elements(2)});
|
||||
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{3});
|
||||
ComputeWrite(m_parameter, std::vector<std::uint32_t>{1});
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
||||
Image image = Render([&] {
|
||||
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 3, 0);
|
||||
});
|
||||
ExpectStrips(image, {true, false, false, false}, "glMultiDrawElementsIndirectCount, shader-written count 1");
|
||||
if (m_transport == "monolith") {
|
||||
// DirectVulkan.cpp's monolith MultiDrawArraysIndirectCount reads this word from the
|
||||
// frontend shadow without SyncGpuWrites: recorded for dev (ID-P8-3), not this lane's.
|
||||
return;
|
||||
}
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(1), Arrays(2), Arrays(3)});
|
||||
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{0});
|
||||
ComputeWrite(m_parameter, std::vector<std::uint32_t>{2});
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
||||
image = Render([&] { glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 0, 3, 0); });
|
||||
ExpectStrips(image, {false, true, true, false}, "glMultiDrawArraysIndirectCount, shader-written count 2");
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
if (!CountersReadable()) return;
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDraws, 2u);
|
||||
EXPECT_EQ(delta.cpuExpansions, 0u) << "a count word was read on the CPU";
|
||||
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
|
||||
EXPECT_EQ(delta.barriers, 2u) << "one barrier per draw whose count store a shader wrote";
|
||||
}
|
||||
|
||||
// Flywheel's shape (create-indirect's trace: 322 cull-then-draw rounds, no glMemoryBarrier
|
||||
// between them). GL leaves it undefined; the wire arm orders it anyway, once per write,
|
||||
// because the CPU path it replaced did (by waiting). Magma's split arms only: the monolith
|
||||
// arm records no dependency here (recorded for dev), and Espryt's is its driver's.
|
||||
TEST_F(WireIndirectDrawScenario, ShaderWrittenCommandsWithoutAnApplicationBarrierAreOrdered) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!m_magma || m_transport == "monolith") {
|
||||
GTEST_SKIP() << "the implicit INDIRECT_COMMAND_READ barrier is the Magma wire arm's (P8-D)";
|
||||
}
|
||||
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(3), Arrays(2)});
|
||||
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(0), Arrays(1)});
|
||||
Image image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
|
||||
ExpectStrips(image, {true, true, false, false}, "the first draw after the write");
|
||||
image = Render([&] { glDrawArraysIndirect(GL_TRIANGLES, reinterpret_cast<const void*>(16)); });
|
||||
ExpectStrips(image, {false, true, false, false}, "a second draw, no write between");
|
||||
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(2), Arrays(3)});
|
||||
image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
|
||||
ExpectStrips(image, {false, false, true, true}, "the draw after a second write");
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
if (!CountersReadable()) return;
|
||||
const WireIndirectCounters delta = Delta();
|
||||
EXPECT_EQ(delta.nativeDraws, 3u);
|
||||
EXPECT_EQ(delta.cpuExpansions, 0u);
|
||||
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
|
||||
EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second draw reads no new write";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,220 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P8-SV: TWO glGenerateMipmap SHAPES THE MAGMA WIRE ARM GOT WRONG (docs/Disaggregated/notes/p8/SV.md).
|
||||
//
|
||||
// * A 3D chain through the NATIVE blit. The wire arm passed the level's depth as the blit's
|
||||
// subresource layerCount on an image with one array layer (VUID-vkCmdBlitImage-srcImage-00240,
|
||||
// -srcSubresource-01707, -dstSubresource-01708). Lavapipe blits a 3D image by its z offsets and
|
||||
// ignores layerCount, so the PIXELS below are right with or without the fix on this host; what
|
||||
// goes red on the host is the validation layer, which the `.Mip3DValidation.` entry runs this
|
||||
// case under (inproc, so the server's Vulkan instance is this process and its stdout is ours).
|
||||
// The pixel half runs on both backends and every arm, and is what a driver that honours
|
||||
// layerCount would fail.
|
||||
// * A 1D DEPTH chain with no native blit. The depth shader arm renders 2D views, which a 1D image
|
||||
// does not admit; the wire arm handed it the chain anyway. It is now the named decline
|
||||
// MipmapShaderFormatOrShape, decided before the chain grows, and the session carries on.
|
||||
// Magma only, and reachable on this host only with MGITEST_MAGMA_FORCE_SHADER_MIPMAP (lavapipe
|
||||
// blits depth natively), which the SERVER reads: split and spawn entries only.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitLane.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#include "../Harness/WireDeclinePeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
using Texel = std::array<int, 4>;
|
||||
|
||||
// Is the Khronos validation layer loaded into THIS process? On the inproc arm the server's
|
||||
// Vulkan instance lives here, so this is the proof that a run "under the layer" really was.
|
||||
bool ValidationLayerMapped() {
|
||||
#if defined(__linux__)
|
||||
std::ifstream maps("/proc/self/maps");
|
||||
std::string line;
|
||||
while (std::getline(maps, line)) {
|
||||
if (line.find("VkLayer_khronos_validation") != std::string::npos) return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
class WireMipShapeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_magma = Gl().BackendName() == "DirectVulkan";
|
||||
m_transport = PeekSplitRuntime().transportName;
|
||||
}
|
||||
|
||||
// The Magma server's decline tally is readable where the server is this process.
|
||||
bool DeclinesReadable() const { return m_magma && m_transport == "inproc"; }
|
||||
|
||||
bool m_magma = false;
|
||||
std::string m_transport;
|
||||
};
|
||||
|
||||
// A 3D RGBA8 chain, 8x8x8 with its full storage, whose level-0 slices come in PAIRS of one
|
||||
// colour: level 1's slice k is then pair k's colour exactly, and every level below is the
|
||||
// average of two slices of the level above. A blit that loses the z axis (one slice only,
|
||||
// or slices read as array layers of a 2D image) cannot produce those numbers.
|
||||
TEST_F(WireMipShapeScenario, VolumeChainIsGeneratedAlongItsDepth) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
constexpr int kSide = 8, kLevels = 4;
|
||||
const std::array<Texel, 4> pairs = {Texel{200, 40, 80, 255}, Texel{40, 200, 120, 255},
|
||||
Texel{100, 100, 240, 255}, Texel{240, 160, 0, 255}};
|
||||
GLuint volume = 0;
|
||||
glGenTextures(1, &volume);
|
||||
glBindTexture(GL_TEXTURE_3D, volume);
|
||||
glTexStorage3D(GL_TEXTURE_3D, kLevels, GL_RGBA8, kSide, kSide, kSide);
|
||||
std::vector<GLubyte> level0(static_cast<size_t>(kSide * kSide * kSide * 4));
|
||||
for (int z = 0; z < kSide; ++z) {
|
||||
const Texel& texel = pairs[static_cast<size_t>(z / 2)];
|
||||
for (int at = 0; at < kSide * kSide; ++at) {
|
||||
for (int c = 0; c < 4; ++c)
|
||||
level0[static_cast<size_t>((z * kSide * kSide + at) * 4 + c)] = static_cast<GLubyte>(texel[c]);
|
||||
}
|
||||
}
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kSide, kSide, kSide, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "3D texture setup";
|
||||
glGenerateMipmap(GL_TEXTURE_3D);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "glGenerateMipmap(GL_TEXTURE_3D)";
|
||||
|
||||
const auto average = [](const Texel& a, const Texel& b) {
|
||||
return Texel{(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, (a[3] + b[3]) / 2};
|
||||
};
|
||||
// expected[level][slice]
|
||||
std::vector<std::vector<Texel>> expected(kLevels);
|
||||
expected[1].assign(pairs.begin(), pairs.end());
|
||||
expected[2] = {average(pairs[0], pairs[1]), average(pairs[2], pairs[3])};
|
||||
expected[3] = {average(expected[2][0], expected[2][1])};
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
for (int level = 1; level < kLevels; ++level) {
|
||||
const int side = kSide >> level;
|
||||
GLint width = 0, height = 0, depth = 0;
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_WIDTH, &width);
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_HEIGHT, &height);
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_DEPTH, &depth);
|
||||
EXPECT_EQ(width, side) << "level " << level;
|
||||
EXPECT_EQ(height, side) << "level " << level;
|
||||
EXPECT_EQ(depth, side) << "level " << level;
|
||||
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4), 0x5a);
|
||||
glGetTexImage(GL_TEXTURE_3D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "readback of level " << level;
|
||||
for (int z = 0; z < side; ++z) {
|
||||
const Texel& want = expected[static_cast<size_t>(level)][static_cast<size_t>(z)];
|
||||
size_t wrong = 0;
|
||||
Texel first{};
|
||||
for (int at = 0; at < side * side; ++at) {
|
||||
const size_t base = static_cast<size_t>((z * side * side + at) * 4);
|
||||
bool ok = true;
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const int got = pixels[base + static_cast<size_t>(c)];
|
||||
if (got < want[c] - 2 || got > want[c] + 2) ok = false;
|
||||
}
|
||||
if (!ok && wrong++ == 0) {
|
||||
for (int c = 0; c < 4; ++c) first[c] = pixels[base + static_cast<size_t>(c)];
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(wrong, 0u) << "level " << level << " slice " << z << ": expected (" << want[0] << ","
|
||||
<< want[1] << "," << want[2] << "," << want[3] << "), first wrong texel ("
|
||||
<< first[0] << "," << first[1] << "," << first[2] << "," << first[3] << ")";
|
||||
}
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_3D, 0);
|
||||
glDeleteTextures(1, &volume);
|
||||
|
||||
// The `.Mip3DValidation.` entry: the blit above ran under the Khronos validation layer,
|
||||
// whose VUID lines the entry's FAIL_REGULAR_EXPRESSION reads. Without the layer actually
|
||||
// loaded, a clean output would prove nothing, so the entry requires it.
|
||||
if (SplitLane::MarkerIsOne("MGITEST_REQUIRE_VK_VALIDATION_LAYER")) {
|
||||
ASSERT_TRUE(m_magma && m_transport == "inproc")
|
||||
<< "the validation entry reads the server's layer output in-process: Magma, inproc";
|
||||
EXPECT_TRUE(ValidationLayerMapped())
|
||||
<< "VK_LAYER_KHRONOS_validation is not loaded in this process, so the absence of "
|
||||
"VUID lines in this entry's output says nothing";
|
||||
}
|
||||
}
|
||||
|
||||
// A 1D DEPTH chain with the native blit forced off: no mip arm renders a 1D image, so the
|
||||
// wire arm declines by name (MipmapShaderFormatOrShape) before growing the chain. Pinned:
|
||||
// no GL error, the session answering (two readback round trips), level 0 as uploaded, level
|
||||
// 1 still the application's own sentinel - and, where the server is this process, exactly
|
||||
// one MipmapShaderFormatOrShape decline counted.
|
||||
TEST_F(WireMipShapeScenario, Depth1DChainWithoutANativeBlitDeclinesByName) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!m_magma)
|
||||
GTEST_SKIP() << "Magma wire-arm decline (P8-SV): the knob and the depth shader arm it forces are "
|
||||
"Magma's; Espryt (" << Gl().BackendName() << ") generates this chain itself";
|
||||
const std::string tier = SplitLane::MarkerValue("MGITEST_MAGMA_FORCE_SHADER_MIPMAP");
|
||||
if (tier.empty())
|
||||
GTEST_SKIP() << "MGITEST_MAGMA_FORCE_SHADER_MIPMAP is unset: this driver blits depth natively, "
|
||||
"so the depth shader arm this case refuses is not entered";
|
||||
constexpr int kWidth = 16;
|
||||
GLuint line = 0;
|
||||
glGenTextures(1, &line);
|
||||
glBindTexture(GL_TEXTURE_1D, line);
|
||||
glTexStorage1D(GL_TEXTURE_1D, 5, GL_DEPTH_COMPONENT32F, kWidth);
|
||||
const std::vector<GLfloat> level0(kWidth, 0.25f), level1(kWidth / 2, 0.75f);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glTexSubImage1D(GL_TEXTURE_1D, 0, 0, kWidth, GL_DEPTH_COMPONENT, GL_FLOAT, level0.data());
|
||||
glTexSubImage1D(GL_TEXTURE_1D, 1, 0, kWidth / 2, GL_DEPTH_COMPONENT, GL_FLOAT, level1.data());
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "1D depth texture setup";
|
||||
unsigned long long declinesBefore = 0;
|
||||
const bool counted =
|
||||
DeclinesReadable() && PeekWireDeclineCount("MipmapShaderFormatOrShape", &declinesBefore);
|
||||
glGenerateMipmap(GL_TEXTURE_1D);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "glGenerateMipmap(GL_TEXTURE_1D) on a depth chain";
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
for (const int level : {0, 1}) {
|
||||
const int width = kWidth >> level;
|
||||
std::vector<GLfloat> depths(static_cast<size_t>(width), -1.0f);
|
||||
glGetTexImage(GL_TEXTURE_1D, level, GL_DEPTH_COMPONENT, GL_FLOAT, depths.data());
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "readback of level " << level;
|
||||
const GLfloat want = level == 0 ? 0.25f : 0.75f;
|
||||
size_t wrong = 0;
|
||||
GLfloat first = 0.0f;
|
||||
for (const GLfloat depth : depths) {
|
||||
if (depth < want - 0.0001f || depth > want + 0.0001f) {
|
||||
if (wrong++ == 0) first = depth;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(wrong, 0u) << "level " << level << " tier=" << tier << ": a declined mip must leave "
|
||||
<< "every level as the application wrote it (expected " << want
|
||||
<< ", first wrong " << first << ")";
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_1D, 0);
|
||||
glDeleteTextures(1, &line);
|
||||
if (counted) {
|
||||
unsigned long long declinesAfter = 0;
|
||||
ASSERT_TRUE(PeekWireDeclineCount("MipmapShaderFormatOrShape", &declinesAfter));
|
||||
EXPECT_EQ(declinesAfter - declinesBefore, 1u)
|
||||
<< "the 1D depth chain must be refused once, by name, before the chain grows";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user