mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
770
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37cb446bda | ||
|
|
7bb5a9ccbe | ||
|
|
1161d489e2 | ||
|
|
92f1b9b231 | ||
|
|
265966a15d | ||
|
|
23ee8bdb49 | ||
|
|
37ecba200b | ||
|
|
7ec1448f03 | ||
|
|
211ca08b0c | ||
|
|
aabca3c171 | ||
|
|
0e3e2b7761 | ||
|
|
174a428e81 | ||
|
|
d3c10f15c3 | ||
|
|
66ef4076c8 | ||
|
|
67c4864ea1 | ||
|
|
b1f36a71b5 | ||
|
|
725a73e2b5 | ||
|
|
73d4511bc1 | ||
|
|
b808b68fd2 | ||
|
|
eb0830dfeb | ||
|
|
f9c5e0adbc | ||
|
|
5370d917d0 | ||
|
|
506ab1541d | ||
|
|
c4fb9397cb | ||
|
|
35c7b589f4 | ||
|
|
f0cd9b2c84 | ||
|
|
bc38b0f735 | ||
|
|
37d07cd38c | ||
|
|
652e7cd3fc | ||
|
|
fdb93e9e65 | ||
|
|
d1b3a3325f | ||
|
|
28f937d539 | ||
|
|
2afbcaaf4f | ||
|
|
e8dafacb1c | ||
|
|
6336943b87 | ||
|
|
a31eabd12b | ||
|
|
b9f940ac63 | ||
|
|
5980349b33 | ||
|
|
fdb747b06d | ||
|
|
8ecab66567 | ||
|
|
d6b0eba8fe | ||
|
|
cfbaffd757 | ||
|
|
735b705397 | ||
|
|
cd9f67763e | ||
|
|
8c2d5019c2 | ||
|
|
4e0989b99a | ||
|
|
4eb0dbc8a9 | ||
|
|
065d1582ba | ||
|
|
05d58a566a | ||
|
|
702f937ca9 | ||
|
|
6129d6e463 | ||
|
|
53f3b7a9af | ||
|
|
47601cbbb8 | ||
|
|
cd26f508df | ||
|
|
da6b172d08 | ||
|
|
171eed9ac0 | ||
|
|
09b315197b | ||
|
|
c9bf43b710 | ||
|
|
1962e0c603 | ||
|
|
acad001b34 | ||
|
|
8fff9363f7 | ||
|
|
3b0e6270b7 | ||
|
|
1469022b53 | ||
|
|
e98c5b4a6a | ||
|
|
e0193f0b3d | ||
|
|
35077242c3 | ||
|
|
cc23ed3c7e | ||
|
|
4cb65bdfdc | ||
|
|
c2dbcbee19 | ||
|
|
45f5bae8b3 | ||
|
|
b4da0e1a87 | ||
|
|
049c8a421c | ||
|
|
60ba52bfe4 | ||
|
|
b7d42faac0 | ||
|
|
2f312262ea | ||
|
|
4b5c315ea5 | ||
|
|
d4501e0c23 | ||
|
|
6e43ff8ccc | ||
|
|
82ef0053ea | ||
|
|
c83d27a668 | ||
|
|
454083bf57 | ||
|
|
b22c37f763 | ||
|
|
4c9ce56124 | ||
|
|
2eae028938 | ||
|
|
c881bc2396 | ||
|
|
db23b9d551 | ||
|
|
6d27499f3c | ||
|
|
7e544c2b78 | ||
|
|
ba6db60c09 | ||
|
|
106190e2ff | ||
|
|
bc3c4fbb6c | ||
|
|
9b5e15dfc8 | ||
|
|
363fce247f | ||
|
|
c639e17514 | ||
|
|
84b0c20512 | ||
|
|
14617a57df | ||
|
|
8800c6b068 | ||
|
|
7abc581551 | ||
|
|
a566342eed | ||
|
|
ef3178a98f | ||
|
|
03aa1528a8 | ||
|
|
e7cd3a3290 | ||
|
|
53877cfb6b | ||
|
|
2a1920adce | ||
|
|
b4f35cd67d | ||
|
|
cef575916e | ||
|
|
2afeec95cd | ||
|
|
a2801f9da4 | ||
|
|
e4aab8d0bc | ||
|
|
745f59bc3f | ||
|
|
5f9c3d6947 | ||
|
|
79afd8a8e0 | ||
|
|
b733eed45c | ||
|
|
166f47f115 | ||
|
|
96037fc502 | ||
|
|
2b277b01d4 | ||
|
|
71e442de02 | ||
|
|
fa56e1c671 | ||
|
|
2175ee08cf | ||
|
|
add15f0a17 | ||
|
|
f4f2bec711 | ||
|
|
f66d31c881 | ||
|
|
a6028ff38a | ||
|
|
3b01067a9b | ||
|
|
b5f9ab51ca | ||
|
|
f7550322d4 | ||
|
|
575be3532d | ||
|
|
0a206b0963 | ||
|
|
8ebb69d895 | ||
|
|
9a1d3062cb | ||
|
|
a759b4b72d | ||
|
|
4d3626215f | ||
|
|
e0b8053e28 | ||
|
|
a44a877080 | ||
|
|
d5ff64ded8 | ||
|
|
b48f55edfd | ||
|
|
1accc7f421 | ||
|
|
dce02ae80e | ||
|
|
e70925d35b | ||
|
|
e9201891f5 | ||
|
|
cee9178e22 | ||
|
|
0650608ddc | ||
|
|
0b536de3fb | ||
|
|
e072176c24 | ||
|
|
d4688d0037 | ||
|
|
13e8e095b4 | ||
|
|
286e4af582 | ||
|
|
4e055e4686 | ||
|
|
9f61016776 | ||
|
|
9f4d9095a9 | ||
|
|
6ffd2084fb | ||
|
|
c0ea142955 | ||
|
|
fe748773f3 | ||
|
|
1b2e817a97 | ||
|
|
c4032d2022 | ||
|
|
09ae776836 | ||
|
|
2b8c04373d | ||
|
|
fcf5ae5659 | ||
|
|
2bbb67e528 | ||
|
|
db346f2844 | ||
|
|
c85c9439a5 | ||
|
|
485dff1421 | ||
|
|
9ff97c2e7e | ||
|
|
afcc313d9f | ||
|
|
9eed5a27d7 | ||
|
|
8941eae008 | ||
|
|
533a256482 | ||
|
|
65d4b808c1 | ||
|
|
98b2c37384 | ||
|
|
d2acb75766 | ||
|
|
6933228d59 | ||
|
|
d786f1c072 | ||
|
|
79b6deacaf | ||
|
|
262c8985a9 | ||
|
|
e5ab9d4f06 | ||
|
|
129355961e | ||
|
|
6cecfcd22f | ||
|
|
16867c5c64 | ||
|
|
0b96a3efbf | ||
|
|
2fbcc22410 | ||
|
|
1771381057 | ||
|
|
2a1f1df6d1 | ||
|
|
cc5a4c0a60 | ||
|
|
7acf3ea8e1 | ||
|
|
db915f76f0 | ||
|
|
657722ff57 | ||
|
|
58a97bb7b8 | ||
|
|
e00db8e64f | ||
|
|
19a96929aa | ||
|
|
500b63ea58 | ||
|
|
c8a5abe581 | ||
|
|
49c6394dfd | ||
|
|
b4e209e12b | ||
|
|
6985e47025 | ||
|
|
3c61689dac | ||
|
|
efa415b00c | ||
|
|
4e27342e81 | ||
|
|
4a8ecf9aaf | ||
|
|
748ffc31c5 | ||
|
|
855b3b5009 | ||
|
|
4984da923f | ||
|
|
c7360b47e2 | ||
|
|
91f04ebc7c | ||
|
|
84ec06f3fc | ||
|
|
408b9fcade | ||
|
|
ddfbdab0e8 | ||
|
|
80c6aa39da | ||
|
|
92da2486c5 | ||
|
|
a0f4aa830e | ||
|
|
781a7446c4 | ||
|
|
86ab28355c | ||
|
|
e058a4507a | ||
|
|
ec973fba84 | ||
|
|
67f4f311d3 | ||
|
|
c24b3428d6 | ||
|
|
553eb86d31 | ||
|
|
785df78d77 | ||
|
|
0bd0abf652 | ||
|
|
ff53ba3b2b | ||
|
|
94705adccd | ||
|
|
81bdfb1f80 | ||
|
|
4fe2edb6fa | ||
|
|
032dcd240e | ||
|
|
c1497bb83e | ||
|
|
114bd70122 | ||
|
|
0158a115de | ||
|
|
8883e496bd | ||
|
|
0cc54748f0 | ||
|
|
80dc9d45d9 | ||
|
|
d07ec0255b | ||
|
|
4f4297f347 | ||
|
|
5788a6023e | ||
|
|
afcf8d96f9 | ||
|
|
e6281b26de | ||
|
|
eafe1179b3 | ||
|
|
4ef9705ee0 | ||
|
|
b556ec3134 | ||
|
|
0b0089e599 | ||
|
|
8346d80999 | ||
|
|
549a34bb1a | ||
|
|
2b0857b016 | ||
|
|
a9108e1a0b | ||
|
|
267211e8b4 | ||
|
|
51c1411696 | ||
|
|
d56179d876 | ||
|
|
f8a9466b93 | ||
|
|
242663c9ae | ||
|
|
4bac6255fd | ||
|
|
f4e98710ff | ||
|
|
4379ceed97 | ||
|
|
87e915382c | ||
|
|
db8484cbb9 | ||
|
|
f7360fe210 | ||
|
|
45513aef51 | ||
|
|
cd4f2db53c | ||
|
|
de6ab16756 | ||
|
|
9987e57f64 | ||
|
|
fa77b1874f | ||
|
|
556ff46601 | ||
|
|
9fa61b84a5 | ||
|
|
3fb0054e79 | ||
|
|
922507f8f8 | ||
|
|
9f88379141 | ||
|
|
a4c78fe10a | ||
|
|
5011c3d868 | ||
|
|
737ef5e980 | ||
|
|
dab827d54f | ||
|
|
ec7fc40ba5 | ||
|
|
8bc39c79d2 | ||
|
|
8abae647d0 | ||
|
|
dc1e95b4c3 | ||
|
|
887547c707 | ||
|
|
e822c933c9 | ||
|
|
3aed7ae047 | ||
|
|
e0113503af | ||
|
|
742d43aeb4 | ||
|
|
740dd301b9 | ||
|
|
d61a4240ee | ||
|
|
4c3e469ae4 | ||
|
|
c1e985c893 | ||
|
|
7fb18840dc | ||
|
|
a0b9ca0772 | ||
|
|
a37212eac9 | ||
|
|
acfee266f5 | ||
|
|
942bb6d7dd | ||
|
|
bc8595f27a | ||
|
|
6d73529f70 | ||
|
|
e0910ebd85 | ||
|
|
8efb7f492a | ||
|
|
a71c23bcf3 | ||
|
|
1cd35d3922 | ||
|
|
e9a353c324 | ||
|
|
557975bf2d | ||
|
|
aa5c12b08c | ||
|
|
7ac3ce0376 | ||
|
|
956e3847cd | ||
|
|
4ec65e801a | ||
|
|
23593c9f15 | ||
|
|
1961029043 | ||
|
|
dd7fbbd7d1 | ||
|
|
851251671c | ||
|
|
b1978890b4 | ||
|
|
ad47dc8c6f | ||
|
|
0b5e7fad82 | ||
|
|
8e76f9dcb7 | ||
|
|
ed7800a550 | ||
|
|
e04c6edb76 | ||
|
|
fe5324c900 | ||
|
|
f4be8c380f | ||
|
|
7f194a8579 | ||
|
|
bb45648c41 | ||
|
|
c285f350b1 | ||
|
|
0f5658f56e | ||
|
|
17e36589fa | ||
|
|
b13af729e4 | ||
|
|
b050c60b32 | ||
|
|
cb81610b2b | ||
|
|
1956d6cd6f | ||
|
|
c879776a26 | ||
|
|
8a2ddc8150 | ||
|
|
d00d5c9416 | ||
|
|
b26167a095 | ||
|
|
73a185f6d1 | ||
|
|
e544121eed | ||
|
|
c04347ef88 | ||
|
|
80bfc7ce02 | ||
|
|
a891d0f514 | ||
|
|
b965a18e15 | ||
|
|
22739a033c | ||
|
|
036d1ac36e | ||
|
|
2a8bb7e659 | ||
|
|
759bd8ffa5 | ||
|
|
06a4ebb645 | ||
|
|
b16dc20473 | ||
|
|
1d0f908b4a | ||
|
|
0fbcaed454 | ||
|
|
a6c02a1741 | ||
|
|
2f2a515d04 | ||
|
|
b3a8a1f47f | ||
|
|
6baa1bcdb4 | ||
|
|
462318c636 | ||
|
|
c3af0719f0 | ||
|
|
c641ed92fb | ||
|
|
3b441531b7 | ||
|
|
534f14e3f2 | ||
|
|
ec92ae4d08 | ||
|
|
5fdc1e46e2 | ||
|
|
cfd6c8730b | ||
|
|
4f5b4fa25d | ||
|
|
96c3aff78f | ||
|
|
88a11b812f | ||
|
|
f3a2fd9308 | ||
|
|
c86be0c274 | ||
|
|
2af4728e70 | ||
|
|
a7409e1e9a | ||
|
|
af84eac8ee | ||
|
|
081854653a | ||
|
|
a32ca0017d | ||
|
|
be86ca175e | ||
|
|
c0404018ec | ||
|
|
6cb6fa88f1 | ||
|
|
7643822606 | ||
|
|
75a9ea3f98 | ||
|
|
e35965d187 | ||
|
|
cef29781c5 | ||
|
|
520bafe823 | ||
|
|
3e2aff1d1d | ||
|
|
883b861490 | ||
|
|
938f4b111f | ||
|
|
3e36bd6c56 | ||
|
|
1d380a6ede | ||
|
|
4d2a244382 | ||
|
|
daf2b44c71 | ||
|
|
e2c9098648 | ||
|
|
a4d10dd9b6 | ||
|
|
640c73243d | ||
|
|
06362251b0 | ||
|
|
b9c1291f10 | ||
|
|
30ebae4346 | ||
|
|
5e547add80 | ||
|
|
a0bff7cee1 | ||
|
|
fc57a79bf1 | ||
|
|
915c87056c | ||
|
|
943aac6afb | ||
|
|
b981244672 | ||
|
|
7184a40a47 | ||
|
|
9e14c16d4c | ||
|
|
55de98a006 | ||
|
|
3e11c848e8 | ||
|
|
9f05e66591 | ||
|
|
68b56165da | ||
|
|
0a47038ae3 | ||
|
|
c6cb5dea68 | ||
|
|
5ff33cbb77 | ||
|
|
138a14019a | ||
|
|
c9085533a1 | ||
|
|
0cbb1a84d8 | ||
|
|
cde6512b08 | ||
|
|
959f518669 | ||
|
|
aee0632a82 | ||
|
|
1d1cfa6603 | ||
|
|
d409731632 | ||
|
|
039b342b97 | ||
|
|
4eee6ae251 | ||
|
|
ee9919ba39 | ||
|
|
5331106e90 | ||
|
|
ca981a8fc1 | ||
|
|
24889447cc | ||
|
|
d8edc8baa2 | ||
|
|
c90383db16 | ||
|
|
698f04574b | ||
|
|
f497ff9741 | ||
|
|
946e63d810 | ||
|
|
918d40dd4f | ||
|
|
f5b8fe48aa | ||
|
|
c5d25868cf | ||
|
|
c464947bdd | ||
|
|
4fb75f4e00 | ||
|
|
4d6904184d | ||
|
|
2f1a673513 | ||
|
|
88bc35b711 | ||
|
|
880be57bb6 | ||
|
|
70e4691b3a | ||
|
|
2d42414667 | ||
|
|
32d27b1491 | ||
|
|
78c5f8dae7 | ||
|
|
96bd540ba1 | ||
|
|
710beefd02 | ||
|
|
c1e957cb98 | ||
|
|
eb71b669b3 | ||
|
|
bb31f374a3 | ||
|
|
e5ee31b022 | ||
|
|
915882b293 | ||
|
|
963f4d8177 | ||
|
|
d627fca387 | ||
|
|
2de0d6dd25 | ||
|
|
66b47a101b | ||
|
|
4722fa38bb | ||
|
|
3a289222af | ||
|
|
18a5dba0e4 | ||
|
|
7218bacb49 | ||
|
|
4633ca223c | ||
|
|
0ef13ab0a6 | ||
|
|
287e1df8dc | ||
|
|
4f6f4125ac | ||
|
|
2a1889b817 | ||
|
|
07e7730085 | ||
|
|
03648c2783 | ||
|
|
f22ae625cb | ||
|
|
f52a546707 | ||
|
|
77c604061e | ||
|
|
36190b7cc0 | ||
|
|
cf763b4081 | ||
|
|
2acc6c41bb | ||
|
|
a84104a40f | ||
|
|
1dd888cb21 | ||
|
|
393002a46e | ||
|
|
25a76c1b20 | ||
|
|
a9f22ebb63 | ||
|
|
2fe011adf6 | ||
|
|
11668b02f2 | ||
|
|
30977c181f | ||
|
|
e5f097f9e1 | ||
|
|
90e63c9e2a | ||
|
|
af2b9f4c1c | ||
|
|
8302dd30bf | ||
|
|
cd58f048b6 | ||
|
|
a6d788191f | ||
|
|
0d89db36dd | ||
|
|
b7a6e6c538 | ||
|
|
f44888ca01 | ||
|
|
8245221755 | ||
|
|
b0dad54cf1 | ||
|
|
95aa618d22 | ||
|
|
5de6ecd4e1 | ||
|
|
7fd965ceda | ||
|
|
9e7305b885 | ||
|
|
bd4c5b5770 | ||
|
|
198d6feaf3 | ||
|
|
08f23543aa | ||
|
|
316ffe02d0 | ||
|
|
fdf9252b53 | ||
|
|
8024b645cb | ||
|
|
9f30fc500f | ||
|
|
e3120af6a9 | ||
|
|
c60fcc17c5 | ||
|
|
b06c2a9bf2 | ||
|
|
8b5295401f | ||
|
|
22266c47cf | ||
|
|
4a4e53d9a9 | ||
|
|
b7cc56dd44 | ||
|
|
7823bcd987 | ||
|
|
caa00995e7 | ||
|
|
1ff7ca5331 | ||
|
|
6125f941d1 | ||
|
|
bfa7987ca6 | ||
|
|
b1405e17ad | ||
|
|
b64c2de3c8 | ||
|
|
2b95a9697a | ||
|
|
228bd24c67 | ||
|
|
c822a9fef7 | ||
|
|
815217cc07 | ||
|
|
7a5b0755b3 | ||
|
|
3d69c37504 | ||
|
|
4f679d669d | ||
|
|
4095624f87 | ||
|
|
321e61ca0b | ||
|
|
552dd3d39b | ||
|
|
6c742eba8a | ||
|
|
69e96f5b8d | ||
|
|
d24abe9287 | ||
|
|
d6d75c9951 | ||
|
|
bb4e3e0ec6 | ||
|
|
233e2f8ced | ||
|
|
74919a3bc1 | ||
|
|
7090e31dee | ||
|
|
5accb0985c | ||
|
|
58685bc116 | ||
|
|
34090097a1 | ||
|
|
58c3d9bde5 | ||
|
|
d03fe4c9dd | ||
|
|
239fc228fc | ||
|
|
8da082f805 | ||
|
|
258b946086 | ||
|
|
16b07cf4b5 | ||
|
|
51c37bcbc9 | ||
|
|
5a7b23f661 | ||
|
|
15f9b27a42 | ||
|
|
82d96a63c9 | ||
|
|
7520e53b5e | ||
|
|
b5cab44100 | ||
|
|
672261764e | ||
|
|
0e314f645e | ||
|
|
350fc699a1 | ||
|
|
3d17e8c10b | ||
|
|
38842985f8 | ||
|
|
e06aabe812 | ||
|
|
67a97065b8 | ||
|
|
ec084959fa | ||
|
|
4b388950e3 | ||
|
|
7e76a98d36 | ||
|
|
15dbe78ce6 | ||
|
|
168808d1a7 | ||
|
|
75e1da4d1b | ||
|
|
cd610934d7 | ||
|
|
335637af38 | ||
|
|
4236b42a57 | ||
|
|
0b6842ac8b | ||
|
|
47db68d557 | ||
|
|
ce11c9accd | ||
|
|
7bb9d9829f | ||
|
|
ccf8eb6571 | ||
|
|
0b314ddae7 | ||
|
|
d5acece005 | ||
|
|
30fef23d52 | ||
|
|
cbb87b968a | ||
|
|
d554c87c45 | ||
|
|
7a48521d37 | ||
|
|
6946ff3a5c | ||
|
|
5a0f13fdfa | ||
|
|
ff2120616d | ||
|
|
5c6399e240 | ||
|
|
4ed1c9cef2 | ||
|
|
ebc552a048 | ||
|
|
6fb880cdf2 | ||
|
|
d52c031c50 | ||
|
|
4156ca6e36 | ||
|
|
2f64ede8c6 | ||
|
|
a5b463a5e8 | ||
|
|
32c79f4135 | ||
|
|
f458924360 | ||
|
|
0dad3fcaa1 | ||
|
|
75f83b20fc | ||
|
|
57781688ce | ||
|
|
7308668488 | ||
|
|
134791b4b9 | ||
|
|
c24926cafe | ||
|
|
c5fa722be7 | ||
|
|
b7aa8d2011 | ||
|
|
378020adf3 | ||
|
|
028cf4eb62 | ||
|
|
08124c99f1 | ||
|
|
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 |
@@ -0,0 +1,3 @@
|
||||
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
|
||||
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: anland-mobilegl-plasma
|
||||
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
|
||||
---
|
||||
|
||||
# MobileGL on anland KDE Plasma
|
||||
|
||||
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
|
||||
Human-readable runbook (Chinese, same content): `docs/archive/notes/anland/runbook-plasma.md`.
|
||||
Background: `docs/archive/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
|
||||
issues) and `plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
|
||||
parameters are documented at the top of each script and in `scripts/env.sh`.
|
||||
|
||||
**The phone may be shared with another session.** Before anything that restarts the app, the daemon,
|
||||
the container session or replaces a library, run the read-only `scripts/device/status.sh` and make sure
|
||||
nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2, c3, xd, xr, gbm).
|
||||
|
||||
## Architecture (10 lines)
|
||||
|
||||
1. Phone: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), rooted with **KernelSU** (`/data/adb/ksu`, `ksud`; `su -c` works). USB serial `HA27Q3LQ`, or wireless adb `ip:port`.
|
||||
2. anland APK `com.anland.consumer.mobilegl` (debuggable build of anland branch `legacy-mobilegl-unified`): UI process + private service process `:mobilegl` (`MobileGLWorker`), which loads the **server** `libMobileGL.so` (NDK, from MobileGL `feat/disaggregated`) and listens on abstract socket `@anland-mobilegl` from the first attached window on. One process serves every client (KWin, plasmashell, ksplash, Chrome...), one backend per process. `MobileGLWorker` is a **foreground service** (notification "Linux desktop is running" / action "Stop desktop"), so the server and the session outlive a hidden, locked or swiped-away window.
|
||||
3. Backend = the app setting "Renderer backend" (Settings > Connection > MobileGL desktop; file `/data/data/<pkg>/files/mobilegl-backend`, default DirectGLES; also `am start ... --es mobilegl_backend X`). Espryt = DirectGLES, Magma = DirectVulkan. The server reads it when its process starts; the app publishes it as `/data/local/tmp/anland-mobilegl/backend` and the session copies that into the container's `/etc/mobilegl/backend` (what every client reports; mismatch = "Hello.backendType disagrees with pinned backend"). `debug.mobilegl.backend`, if someone sets it, still overrides the setting until the next reboot.
|
||||
4. Experiment display daemon `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (display/input broker), started by the app (below). The module only starts the original one on `/data/local/tmp/display_daemon.sock` at boot; leave that alone.
|
||||
5. Container `arch-kde-mgl` (Droidspaces 6.4.5, Arch Linux ARM aarch64 glibc, host net, `rootfs.img` mounted at `/mnt/Droidspaces/arch-kde-mgl` on the Android side). Bind mounts: `/data/local/tmp/anland-mobilegl` -> `/run/anland-mobilegl`, `/data/local/tmp/display_daemon.sock` -> `/run/display.sock`.
|
||||
6. `desktop-session.service` (User=swung0x48, **enabled**: the container boots straight into it, as the original image does) has drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf` (anland `producers/kde/Arch_v5/desktop-session-mobilegl.conf`, installed + enabled by `sync-build-kwin.sh`): `ExecStartPre=mobilegl-startup.sh wait` (until `@anland-mobilegl` listens in `/proc/net/unix` and the daemon socket exists), `ExecStartPre=+... sync-backend`, `Restart=always`, then `ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma`, which deletes the sticky software-renderer key, sets `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0` session-wide (MobileGL never declines, so glvnd never falls through to another GL stack), writes a user drop-in for `plasma-kwin_wayland.service` (kwin_wayland_wrapper -> `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4`, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`) and runs `startplasma-wayland`.
|
||||
7. KWin 6.7.4 with anland's `anland` backend renders straight into the anland Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers its output down (DPMS Off + RenderLoop inhibited: no compositing, no frame callbacks) and back up when the window returns. Every other GL process is `MOBILEGL_IPC_SURFACE=offscreen` and presents through KWin.
|
||||
8. Client = `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64), the container's **system-wide** GL vendor (section below): glvnd EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines), GLX vendor `/usr/lib/libGLX_mobilegl.so.0`, settings from `/etc/mobilegl/client.conf` (`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`) + `/etc/mobilegl/backend`. No per-process environment is needed.
|
||||
9. Zero-copy windows: client `wl_egl_window` frames are server-allocated AHardwareBuffer "shared images" exported as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognises its own buffer by inode. KWin gets a DRM device via `GBM_BACKEND=mobilegl` -> `/usr/lib/gbm/mobilegl_gbm.so` (node is only an identity, never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
|
||||
10. X11 (`docs/archive/notes/anland/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
|
||||
|
||||
## First-time prerequisites (Windows host)
|
||||
|
||||
- Git Bash, `adb` (SDK platform-tools), CMake >= 3.22 and Ninja on PATH (seen: CMake 3.27.6, Ninja 1.13.2), Python 3 + Pillow (only `rec.sh`).
|
||||
- Android NDK **27.2.12479018** at `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018` (override `ANDROID_NDK`).
|
||||
- WSL distro **archlinux** with clang, lld, llvm, cmake, ninja. **lld's major version must equal clang/llvm-libs'** (lld 23 on llvm 22 broke `ld.lld`; working set: all 22.1.6). Then export the sysroot once: `bash scripts/xsysroot/refresh-sysroot.sh` (-> `~/sysroots/arch-kde-mgl` + `~/sysroots/aarch64-arch.cmake`; re-run after container package upgrades, then `xbuild.sh --reconfigure`).
|
||||
- MobileGL worktree (never build in the shared main checkout): `GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`, `git submodule update --init --recursive`, and copy `3rdparty/glslang/External/spirv-tools` from the main checkout (the submodule leaves it empty).
|
||||
- anland worktree on `legacy-mobilegl-unified` (KWin patch/backend, startup script, `mobilegl-tools/`).
|
||||
- Phone side that already exists and is NOT recreated by this skill: the APK (build: anland `consumers/anland_v5/android_consumer/MOBILEGL.md`, gradle `:app:assemblePlainDebug` with `MOBILEGL_DIST`, `-PmobileglApplicationId=com.anland.consumer.mobilegl -PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock`, from Git Bash with `MSYS_NO_PATHCONV=1`; signed with this machine's `~/.android/debug.keystore`, so `adb install -r` works; reinstalling with another key changes the UID and needs the KernelSU grant again - `mobilegl-tools/ksu_grant.c`), the anland KernelSU modules (`anland-daemon`, `anland-awl`), the container with KWin's configured build tree `/root/mobilegl-build/kwin-6.7.4/build` and pristine `/root/gbm-dev/kwin-a/src`.
|
||||
- Install the helpers once (container must be running): `bash scripts/push-tools.sh` (device/*.sh -> `/data/local/tmp/anl`, `mgrun` -> container `/usr/local/bin`, `kwprep.sh` -> container `/root`).
|
||||
|
||||
Run host scripts from Git Bash as `bash scripts/<x>.sh`. Raw adb with device paths needs `MSYS_NO_PATHCONV=1` and the whole root command in one quoted string: `adb shell 'su -c "..."'`. A device script can also be run without pushing: `MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`.
|
||||
|
||||
## Starting, idling, stopping - no manual steps
|
||||
|
||||
Nothing starts at phone boot (container `run_at_boot=0`). **Opening the app** (launcher icon = `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`, no extras; the APK's default socket is the experiment's, `-PanlandDefaultSocket`) calls `MobileGLDesktop.ensureStarted()` - the single trigger - which starts the foreground service and runs the APK's `assets/mobilegl-desktop.sh up` as root (`su`): display daemon if absent (the script leaves the app's cgroup first, so it survives the app) -> backend published -> container started if stopped -> `systemctl start --no-block desktop-session`. The window waits for the daemon socket; the server listens once the Surface is attached; the session's `wait` step then lets Plasma start. A reopen with everything up only reattaches the window. Log: `/data/local/tmp/anland-mobilegl/desktop.log`, logcat `AnlandMobileGL` ("MobileGL desktop up done in N ms").
|
||||
|
||||
Hidden window, screen off, lock screen or swiped-away task: server and session stay (foreground service); KWin turns the workspace's DPMS off (logs "viewer gone: output powered down"; windows get xdg_toplevel `suspended`, no frame callbacks) and back on ("viewer back"). Client swaps are paced by frame callbacks (`MG_Impl/EGLImpl/FrameThrottle.h`): interval >= 1 waits like any Wayland EGL; interval 0 (Chrome) drops to ~1 fps once a callback goes unanswered for 200 ms.
|
||||
|
||||
Measured 2026-10-03 (Espryt, Chrome on webglsamples aquarium; % of one core, phone = all of Android): open-to-desktop cold 7.5-12.5 s (daemon 0.5, server 1.0, container 1.5, kwin ~5, plasmashell ~6-7), after a phone reboot 8.5 s, reopen of a running desktop 0.2 s; visible phone 249% / kwin 13 / chrome 99 / server 57 / GPU 22%, hidden 54 / 0 / 6 / 2 / 0%, locked 38 / 0 / 8 / 3 / 0%. Stop desktop 1.7 s. Wireless adb does not come back by itself after a reboot (re-enable it on the phone). **Stop desktop** (notification action, or `am startservice -n <pkg>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`) runs `mobilegl-desktop.sh down` (session + container stopped, daemon stays), closes the windows, ends `:mobilegl`.
|
||||
|
||||
```sh
|
||||
adb connect <ip>:<port> # only for wireless debugging
|
||||
export MSYS_NO_PATHCONV=1
|
||||
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # after a reboot: wake, dismiss keyguard, open the app, print the timeline
|
||||
bash scripts/desktop-verify.sh push cold reopen idle # timed cold start, swipe-away + reopen, CPU/GPU visible/hidden/locked
|
||||
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
|
||||
```
|
||||
|
||||
`droidspaces` has no `status` command; `droidspaces show` lists running containers. `device/run-plasma.sh` is a developer restart (session + logind user, then the app), needed after a rebuild because KWin does not reconnect to a new server process. `device/desktop-reset.sh` takes everything down to the post-reboot state. The old manual sequence (setprop, start container, `restart-daemon.sh`, `run-plasma.sh`) is gone; `restart-daemon.sh` remains for daemon debugging only.
|
||||
|
||||
## Build and deploy loops
|
||||
|
||||
**Server (Android, NDK) -> APK lib**
|
||||
```sh
|
||||
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so (RelWithDebInfo, unstripped)
|
||||
bash scripts/cyc.sh s1 # clear sticky SW-renderer key, push, swap into APK, restart session, screenshot $ANL_DIR/s1.png, print fatals
|
||||
```
|
||||
`device/swap.sh` overwrites `/data/app/.../lib/arm64/libMobileGL.so` keeping owner/mode/SELinux context (original saved as `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`); no re-signing. To swap without the session restart: push to `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`, `su -c "sh /data/local/tmp/anl/swap.sh"`, then `run-plasma.sh` anyway (KWin does not reconnect to a new server process).
|
||||
|
||||
**Client + GBM backend (container glibc) via WSL cross-build**
|
||||
```sh
|
||||
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64; ~150 s full, seconds incremental
|
||||
bash scripts/xdeploy.sh # rename-into-place install of libMobileGL.so + mobilegl_gbm.so, aliases, vendor JSONs, client.conf, GLX/GBM env drop-ins
|
||||
```
|
||||
Running clients keep the old inode; restart the client, or `run-plasma.sh` for KWin/plasmashell. Build server and client from the **same commit** (both stamp `git rev-parse HEAD`), else the logs WARN "compatible wire with different build". Fallback in-container build (slow): `scripts/sync-src.sh` + anland `mobilegl-tools/anland-build-client.sh`.
|
||||
|
||||
**KWin (anland backend / kwin.patch) - built inside the container**
|
||||
```sh
|
||||
bash scripts/kwin-stage.sh build # stage CR-stripped patch+backend+startup script, kwprep.sh, background sync-build-kwin.sh
|
||||
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # wait for "== ALL DONE =="
|
||||
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
|
||||
```
|
||||
`sync-build-kwin.sh` (anland `producers/kde/Arch_v5/`) copies only the backend sources, then `ninja -j4`, installs `kwin_wayland`, `libkwin.so.6.7.4`, the patched screencast plugin and `/opt/mobilegl/bin/mobilegl-startup.sh` by atomic rename. `kwprep.sh` is what applies changed `kwin.patch` hunks to core KWin files. Repo-root `libdisplay_producer/` is not synced by either. `kwin-stage.sh` is assembled from those two scripts and was not run as one unit yet.
|
||||
|
||||
**Unit tests (no device)** - in WSL (the MSVC build of this branch is broken):
|
||||
```sh
|
||||
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
|
||||
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
|
||||
```
|
||||
`wsl-all.sh` builds and runs every unit test. Never run DirectVulkan integration tests; never two ctest runs at once.
|
||||
|
||||
## Switching backends
|
||||
|
||||
In the app: Settings > Connection > MobileGL desktop > Renderer backend, then Stop desktop and open the app. Or `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`, which does the same (saves the setting through the launch extra, STOPs the service, reopens; clears a stale `debug.mobilegl.backend`). No setprop, no file to edit in the container: the session copies the published backend into `/etc/mobilegl/backend` on its next start. `cyc.sh` keeps whatever is set.
|
||||
|
||||
## System-wide vendor and fallback
|
||||
|
||||
Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any process in the container gets MobileGL through the stock libglvnd/libgbm:
|
||||
|
||||
| File | Does |
|
||||
|---|---|
|
||||
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor, asked before the system's `50_*.json` |
|
||||
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | same JSON, for per-process forcing with `__EGL_VENDOR_LIBRARY_FILENAMES` |
|
||||
| `/etc/mobilegl/client.conf` | `KEY=value` defaults for every client (transport, data, endpoint); rewritten on deploy |
|
||||
| `/etc/mobilegl/backend` | the backend (the session's `sync-backend` step copies the app's choice in); wins over a `MOBILEGL_BACKEND_TYPE` line in client.conf |
|
||||
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` for the systemd user manager (all Plasma services and what they start, terminals included) |
|
||||
| `/etc/profile.d/mobilegl.sh` | the same two for login shells (`bash -l`, `su -l`, ssh, `droidspaces run bash -l`) |
|
||||
|
||||
Precedence is environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` name other files (empty = off); a server process never reads them.
|
||||
|
||||
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created. It no longer steps aside for `LIBGL_ALWAYS_SOFTWARE=1` (918d40dd): that is Mesa's software-renderer selection, not a reason to disable MobileGL's entry points, so a process that sets it (a software Xwayland included) is still served by MobileGL while a server answers. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
|
||||
```sh
|
||||
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
|
||||
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
|
||||
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # vendor = the other one, ~0.1 s
|
||||
```
|
||||
|
||||
## Test clients and how to verify
|
||||
|
||||
X11 on the GPU has its own driver, `scripts/x11-verify.sh` (preflight, measure before/after with glxgears at `vblank_mode=0` on both GLX paths (presentation throughput only: glxgears is fixed-function GL, which MobileGL does not implement, so its window is black by design), deploy-client/-kwin/-server, check, apps, stop); `scripts/device/x11-measure.sh` refuses to load the phone below 40% battery.
|
||||
|
||||
Inside the container as root (`printf '...' | bash scripts/ct.sh <chan>`), `mgrun offscreen <cmd>` runs a client as swung0x48 in the live session through a login shell, with nothing but the surface mode and the log path set (log `/tmp/mgl-run.client.log`, override base with `MGLOG=/tmp/x.log`); a plain `env -i ... bash -l -c '<cmd>'` as swung0x48 gets MobileGL the same way:
|
||||
```sh
|
||||
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
|
||||
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
|
||||
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
|
||||
```
|
||||
Chrome: `bash scripts/chrome-launch.sh <tag> [flags]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`; `CHROME_GPU=in-process` puts the GPU back in the browser, which cannot recover from a device loss).
|
||||
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labelled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's colour hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf. A vendor-agnostic replacement driver (MobileVA: VA-API on Android MediaCodec, decoding into server YUV images with no CPU copy, the current driver kept as an A/B fallback) is planned, not built: `docs/MobileVA/plan.md`.
|
||||
Device loss: `setprop debug.mobilegl.inject_device_lost_pid <pid>` (as root) loses the server session of the client whose pid - as it sees itself in the container - is `<pid>`, once per value, on any backend and whether or not it presents; `debug.mobilegl.inject_device_lost_at N` instead loses whichever session makes the N-th device check (Espryt presents and read-backs only). `bash scripts/chrome-recover.sh <in-process|process> [rounds]` checks that Chrome repaints after such a loss without a restart (battery-guarded, stops below 40%). `bash scripts/kwin-recover.sh [rounds]` loses KWin's own session (default twice) and checks that KWin restarts compositing on a fresh session - same KWin, plasmashell and client processes, the screen still animating (glmark2 on Wayland, glxgears on X11, `CHROME=1` adds Chrome) - which needs the anland backend + kwin.patch reset handling deployed (`kwin-stage.sh build`).
|
||||
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs), `yuvprobe.c` (YUV dma-buf sampling, `gcc -O1 /root/yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm -lm`), `cdpmedia.py` (which decoder a Chrome page uses); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
|
||||
GPU hangs: a shader that never ends stops the WHOLE GPU (every process, MobileGL or not) until the kernel resets it - ~2 s, or ~10 s ("Preemption Fault" in dmesg) when a higher-priority context waits; nothing in user space shortens one stall. What the server does is end the guilty session so it cannot hang the GPU again (Magma: event-bracketed submissions + `mgl-gpu-watch`, budget `MOBILEGL_GPU_HANG_BUDGET_MS` / `debug.mobilegl.gpu_hang_budget_ms`, default 2000; Espryt: the driver's GUILTY reset status, which it reports after the ~2 s reset only). Repro: `scripts/probes/hangclient.c` (Wayland + GL 3.3 core; `-H 60 -N 1 -E 60` hangs every 60 frames, `-H -1` is a frame-gap observer; build with wayland-scanner's xdg-shell files, `-lwayland-client -lwayland-egl -lEGL -lOpenGL -lm`), and `gpuprobe.c` (NDK, plain adb shell; `gpuprobe 30 high` is a high-priority GLES context outside MobileGL - it both measures the device-wide stall and forces the 10 s preemption path).
|
||||
|
||||
A result counts only if ALL hold:
|
||||
- client log says `presented through linux-dmabuf shared images` (not `wl_shm`; `could not be presented through a linux-dmabuf shared image` = fell back);
|
||||
- `bash scripts/shot.sh g 2 1` gives two different hashes while the client animates (a "fast" score can mean frames are not shown);
|
||||
- `adb logcat -b crash -d` and `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` are clean (a frozen window is usually a dead server);
|
||||
- for plasmashell verdicts, the sticky software renderer is off (below) and `/tmp/mgl-plasmashell.client.log` is freshly written.
|
||||
Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49, Magma 158 vs 34. Rerun any surprising pass.
|
||||
|
||||
## Logs
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| KWin client side | container `/tmp/mobilegl-compositor.client.log` |
|
||||
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`, `/tmp/mgl-ksplash.client.log` (user drop-ins `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
|
||||
| mgrun / Chrome | `/tmp/mgl-run.client.log`, `/tmp/mgl-chrome-<tag>.client.log` (+ `/tmp/chrome-<tag>-out.log`) |
|
||||
| Server | `adb logcat -s MobileGL`; file `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
|
||||
| Native crashes | `adb logcat -b crash -d`; container `coredumpctl list` |
|
||||
| Session / KWin stderr | container `journalctl -u desktop-session --no-pager`, `journalctl --no-pager --since -2min \| grep kwin_wayland` |
|
||||
| Display daemon | `/data/local/tmp/anland-mobilegl/display-daemon.log` |
|
||||
| Desktop start/stop (app's su script) | `/data/local/tmp/anland-mobilegl/desktop.log`, `container-start.log`; logcat `-s AnlandMobileGL` |
|
||||
| Session wait / backend sync | container `journalctl -u desktop-session --no-pager` ("waiting for the MobileGL server ...") |
|
||||
|
||||
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` produces `/tmp/x.client.log` (and `.server.log` for forwarded lines); files are truncated on open, so one base per process. Client builds log at INFO. Symbolize server frames with `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <offset>`.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **pkill -f self-match**: a `ct.sh`/`droidspaces run bash -lc "<script>"` process carries the script in its argv; `pkill -f chrome` kills the runner. Use `pkill -x`.
|
||||
- **Root-owned /tmp files**: a log created by a root-run client cannot be reopened by swung0x48 later (no log appears). `rm -f` it as root first.
|
||||
- **Sticky Plasma software renderer**: after any server crash Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`; plasmashell then renders on the CPU and screenshots still look fine (tray icon "正在使用的软件渲染器"). `mobilegl-startup.sh plasma` deletes it at every session start; within a running session that hit a crash, restart the session (`run-plasma.sh`) before any verdict.
|
||||
- **Lock screen**: autolock is off (`kscreenlockerrc [Daemon] Autolock=false`); a real lock can only be left by typing the password (logind lock/unlock is not supported for this session type - `device/unlock.sh` is unverified). Test the greeter with `/usr/lib/kscreenlocker_greet --testing` and kill it.
|
||||
- **CRLF**: `core.autocrlf=true` here; anland's backend sources and `sync-build-kwin.sh` are CRLF in the Windows checkout. Strip CR before anything runs on Linux/Android (`tr -d '\r'`; `ct.sh`, `push-tools.sh`, `kwin-stage.sh` do). This skill has a `.gitattributes` forcing LF.
|
||||
- **Display daemon**: the app starts it; the session's `wait` step keeps KWin from starting without it (a KWin that cannot reach it logs "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and exits in ~3 s). Anything started from the app's `su` must leave the app's cgroup (`echo $$ > /sys/fs/cgroup/cgroup.procs`) or a force-stop kills it.
|
||||
- **Device power/connection**: low battery or the screen turning off (30 s default) can kill the SurfaceView - `svc power stayon true`; wireless adb `ip:port` changes per pairing - never pin `-s HA27Q3LQ`, use `ANDROID_SERIAL` if several devices are attached.
|
||||
- **Container vs phone paths**: container `/tmp` is a different place from the phone's `/data/local/tmp`; move files via `/mnt/Droidspaces/arch-kde-mgl/root` (container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
|
||||
- **Never overwrite a mapped .so in place** (container or APK while running): copy to a new inode and `mv -f`; the APK swap force-stops the app first.
|
||||
- **Input**: plain `adb shell input` is blocked - use `su -c input`; anland grabs the touchscreen, so if `input tap` does nothing use `input mouse tap x y`. Never `debuggerd -b` a container process (signal 35 kills glibc processes).
|
||||
- **linux-dmabuf**: the client must use `create` + roundtrip, never `create_immed` (KWin answers a failed immediate import with a protocol error that kills the client).
|
||||
- **Cross-session bugs**: noise / another window's pixels after some client starts or exits = process-global state in the multi-session server (handles are per client, FBO/VAO per context). Repro with `kscreenlocker_greet --testing`.
|
||||
- `/etc/environment` leaks the baseline session's driver variables through pam_env (and, via `/usr/lib/environment.d/99-environment.conf`, into the user manager); `mobilegl-startup.sh` unsets them - keep that when editing it.
|
||||
- **Shared log path**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`; any line a process logs opens (truncates) that file. MobileGL stays silent in processes it does not serve, but give any extra client its own `MOBILEGL_LOG_FILE_PATH`.
|
||||
- **Stale per-process overrides**: an `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager's environment (`systemctl --user show-environment`) still wins over the system files; `run-plasma.sh` (terminate-user) clears the manager's.
|
||||
- `handoff` mentions a `kwin_wayland` logging shim and `/tmp/mgl-kwin-<pid>.log`: stale - `/opt/mobilegl/kwin/bin/kwin_wayland` is now the real binary and logs to `/tmp/mobilegl-compositor.client.log`.
|
||||
|
||||
## House rules
|
||||
|
||||
- Work in a git worktree, never in the shared main checkout or its build dirs; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
|
||||
- Commit as `CONTRIBUTING.md` describes (`[Type] (Scope): content`, no trailer), with `git commit --no-verify`.
|
||||
- Never run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
|
||||
- No external project names ("Mesa", "Zink", ...) in source code or comments; describe the technique.
|
||||
- Vendor-agnostic only on the zero-copy path: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, libgbm ABI - no kgsl/gralloc layouts/QCOM modifiers.
|
||||
- Identity caches key on monotonic lifetime ids, never GL names or pointers.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Reproducing the MobileGL + anland + KDE Plasma setup from source
|
||||
|
||||
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
|
||||
day-to-day loop once everything below exists. Paths and names that are specific to the original
|
||||
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
|
||||
|
||||
## 0. What you need
|
||||
|
||||
**Phone.**
|
||||
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
|
||||
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
|
||||
- About 20 GB free for the container image.
|
||||
|
||||
**Development host.**
|
||||
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
|
||||
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
|
||||
- Tools:
|
||||
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
|
||||
- Android NDK **27.2.12479018**.
|
||||
- JDK 17 and Android SDK platform 36 (for the APK).
|
||||
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
|
||||
|
||||
## 1. Sources
|
||||
|
||||
```sh
|
||||
# MobileGL: branch feat/disaggregated (everything here is on it)
|
||||
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
|
||||
git checkout feat/disaggregated
|
||||
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
|
||||
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
|
||||
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
|
||||
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
|
||||
|
||||
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
|
||||
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
|
||||
git checkout legacy-mobilegl-unified
|
||||
```
|
||||
|
||||
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
|
||||
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
|
||||
|
||||
## 2. Phone base: root, Droidspaces, display daemon
|
||||
|
||||
1. **Root** with KernelSU (or another `su` provider).
|
||||
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
|
||||
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
|
||||
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
|
||||
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
|
||||
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
|
||||
|
||||
## 3. The container
|
||||
|
||||
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
|
||||
|
||||
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
|
||||
```
|
||||
enable_gpu_mode=1
|
||||
net_mode=host
|
||||
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
|
||||
run_at_boot=0
|
||||
```
|
||||
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
|
||||
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
|
||||
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
|
||||
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
|
||||
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
|
||||
|
||||
## 4. MobileGL server and the anland APK
|
||||
|
||||
```sh
|
||||
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
|
||||
```
|
||||
The script runs, with the NDK toolchain:
|
||||
```
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
|
||||
```
|
||||
|
||||
APK, from anland `consumers/anland_v5/android_consumer` (details in its `MOBILEGL.md`):
|
||||
```sh
|
||||
mkdir -p dist/arm64-v8a && cp <worktree>/build-android/libMobileGL.so dist/arm64-v8a/
|
||||
MOBILEGL_DIST=$PWD/dist ./gradlew :app:assemblePlainDebug \
|
||||
-PanlandNdkVersion=27.2.12479018 -PmobileglApplicationId=com.anland.consumer.mobilegl \
|
||||
-PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock
|
||||
adb install -r app/build/outputs/apk/plain/debug/app-plain-debug.apk
|
||||
```
|
||||
- **Grant root:** the app needs root for its helper; approve it in KernelSU.
|
||||
- **Reinstall with another signing key:** that changes the UID, so grant root again (anland `mobilegl-tools/ksu_grant.c` does it from a shell). Don't uninstall to fix a signature clash: that wipes the app data.
|
||||
- **Later server-only changes:** swap just the library into the installed APK (`scripts/cyc.sh` / `device/swap.sh`) instead of rebuilding the APK.
|
||||
|
||||
## 5. MobileGL client in the container
|
||||
|
||||
Pick one of these two:
|
||||
- **Cross build** (fast, from WSL or Linux):
|
||||
1. `bash scripts/xsysroot/refresh-sysroot.sh` exports the container's headers and libraries to `~/sysroots/arch-kde-mgl` and installs `~/sysroots/aarch64-arch.cmake`. Re-run it after container package upgrades.
|
||||
2. `bash scripts/xbuild.sh`, then `bash scripts/xdeploy.sh`.
|
||||
- **Native build in the container** (slow, no WSL): `scripts/sync-src.sh` copies the worktree to `/root/mobilegl-unified-src`, then run anland `producers/kde/Arch_v5/mobilegl-tools/anland-build-client.sh` inside (g++, `-DMOBILEGL_BUILD_DISAGGREGATED=ON`, Release).
|
||||
|
||||
Either route installs the same files, renamed into place so running clients keep their old mapping:
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `/opt/mobilegl/lib/libMobileGL.so` (+ `libEGL`/`libGL` aliases) | the client library |
|
||||
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | system-wide EGL vendor; sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines (no server reachable) |
|
||||
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | per-process forcing |
|
||||
| `/usr/lib/libGLX_mobilegl.so.0` | GLX vendor |
|
||||
| `/usr/lib/gbm/mobilegl_gbm.so` | GBM backend (zero-copy window buffers) |
|
||||
| `/etc/mobilegl/client.conf` | `MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_DATA=shm`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl` |
|
||||
| `/etc/mobilegl/backend` | `DirectVulkan` (Magma) or `DirectGLES` (Espryt) |
|
||||
| `/etc/environment.d/10-mobilegl.conf`, `/etc/profile.d/mobilegl.sh` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` |
|
||||
|
||||
## 6. KWin 6.7.4 with the anland backend, and Xwayland
|
||||
|
||||
The MobileGL session runs a rebuilt KWin from `/opt/mobilegl/kwin`, so the packaged one stays untouched.
|
||||
|
||||
1. **Source tree:** in the container, unpack KWin 6.7.4 to `/root/mobilegl-build/kwin-6.7.4`, apply anland `producers/kde/Arch_v5/kwin.patch`, and overlay `producers/kde/anland_backend_Arch_v5/src/backends/anland` into `src/backends/anland`. Also take the current `common/` and `libdisplay_producer/`, as `mobilegl.md` there says.
|
||||
2. **Configure:**
|
||||
```sh
|
||||
cmake -S kwin-6.7.4 -B kwin-6.7.4/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF
|
||||
```
|
||||
3. **Build and install:** run `producers/kde/Arch_v5/sync-build-kwin.sh` (`ninja -j4`). It installs `kwin_wayland` + `libkwin.so.6.7.4` into `/opt/mobilegl/kwin/{bin,lib}`, the patched screencast plugin in place (the original is kept beside it), and `/opt/mobilegl/bin/mobilegl-startup.sh`.
|
||||
- From the dev host, `bash scripts/kwin-stage.sh build` does steps 1–3 incrementally: `kwprep.sh` applies changed patch hunks.
|
||||
4. **Xwayland:** the anland-patched Xwayland 24.1.13 comes from `producers/kde/Arch_v5/build.sh` (makepkg, as a normal user; it builds the anland KWin and Xwayland packages). A prebuilt anland rootfs already has it.
|
||||
5. **Session hook:** add `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`:
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma
|
||||
Environment=ANLAND_SOCKET=/run/anland-mobilegl/display.sock
|
||||
Environment=QT_LOGGING_RULES=kwin_*.info=true
|
||||
```
|
||||
`mobilegl-startup.sh plasma` writes the user drop-in for `plasma-kwin_wayland.service` (the `/opt/mobilegl/kwin` binary, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`, `GBM_BACKEND=mobilegl`) and runs `startplasma-wayland`.
|
||||
|
||||
## 7. Bring it up
|
||||
|
||||
Nothing has to be started by hand, and the order no longer matters: **open the app** (its launcher
|
||||
icon, or `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no extras).
|
||||
That is the single trigger. The app starts its foreground service and runs its bundled
|
||||
`mobilegl-desktop.sh up` as root, which:
|
||||
1. starts the display daemon if it is not running;
|
||||
2. publishes the backend (the app setting "Renderer backend", default DirectGLES) to
|
||||
`/data/local/tmp/anland-mobilegl/backend`;
|
||||
3. starts the container if it is stopped;
|
||||
4. starts `desktop-session`, which waits for the server and the daemon socket, copies the backend into
|
||||
`/etc/mobilegl/backend` and runs Plasma.
|
||||
|
||||
Cold start to a desktop takes about 8-12 s. `scripts/device/bringup.sh` wakes the phone, dismisses the
|
||||
keyguard, opens the app and prints the timeline. To change the backend, use the app setting and then
|
||||
Stop desktop and reopen, or run `scripts/device/switch.sh DirectGLES|DirectVulkan`. Do not use
|
||||
`setprop debug.mobilegl.backend`: it overrides the setting until the next reboot. **Stop desktop**
|
||||
(the notification action) stops the session and the container.
|
||||
|
||||
**After a phone reboot:** wireless adb does not come back by itself. Re-enable it on the phone
|
||||
(Developer options > Wireless debugging), then `adb connect <ip>:<port>`; the port changes. After
|
||||
that, open the app as above. See SKILL.md "Starting, idling, stopping".
|
||||
|
||||
## 8. Verify
|
||||
|
||||
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
|
||||
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
|
||||
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
|
||||
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
|
||||
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
|
||||
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
|
||||
|
||||
## 9. Machine-specific values to change
|
||||
|
||||
| Value | Original | Where |
|
||||
|---|---|---|
|
||||
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
|
||||
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
|
||||
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
|
||||
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
|
||||
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
|
||||
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
|
||||
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
|
||||
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
|
||||
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
|
||||
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
|
||||
|
||||
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-android.sh - build the Android SERVER libMobileGL.so (NDK, arm64-v8a) that the anland APK embeds.
|
||||
# Output: $MGL_WORKTREE/build-android/libMobileGL.so (RelWithDebInfo, unstripped, ~320 MB; keep it for
|
||||
# llvm-addr2line, it is what cyc.sh pushes). Stamp = worktree HEAD, so the container client
|
||||
# (xbuild.sh, same stamp) and the server agree on the build ("compatible wire with different build"
|
||||
# WARN otherwise). Env: MGL_WORKTREE, ANDROID_NDK, BUILD_DIR (default build-android), JOBS (12).
|
||||
# Log level is compiled to INFO. android-30 = the APK's minSdk: thread_local is then the platform's
|
||||
# own TLS rather than the emulated one (which cost 7-12% of an apply thread on the API 26 build).
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
W=$MGL_WORKTREE
|
||||
B=$W/${BUILD_DIR:-build-android}
|
||||
STAMP=$(git -C "$W" rev-parse HEAD)
|
||||
if [ ! -f "$B/build.ninja" ]; then
|
||||
cmake -S "$W" -B "$B" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DCMAKE_CXX_FLAGS=-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_STAMP="$STAMP"
|
||||
else
|
||||
cmake "$B" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
|
||||
fi
|
||||
cmake --build "$B" --target MobileGL -j"${JOBS:-12}"
|
||||
ls -la "$B/libMobileGL.so"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# chrome-launch.sh <tag> [extra chrome flags...] - (re)start Chrome inside the running Plasma session
|
||||
# through mobilegl-startup.sh's `chrome` mode (ANGLE GLES over MobileGL EGL), client log
|
||||
# /tmp/mgl-chrome-<tag>.client.log, then print its "presented through" line and save a screenshot
|
||||
# to $ANL_DIR/ch-<tag>.png. Kills the running chrome first (pkill -x, never -f). MobileGL is the
|
||||
# container's system-wide vendor (10_mobilegl.json + /etc/mobilegl/client.conf), so only the log is set here.
|
||||
# Env: WAIT (s before checking, default 20), CHANNEL (ct.sh channel, default chrome),
|
||||
# CHROME_GPU (process, the default: the GPU in its own process, which presents
|
||||
# through GBM dma-bufs and is restarted by Chrome after a device loss; in-process opts out -
|
||||
# needs the anland startup script that knows MOBILEGL_CHROME_GPU; in process mode the GPU process and the browser
|
||||
# write the one client log, the GPU process last); CHROME_GPU_SANDBOX=on keeps its GPU sandbox.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
TAG=${1:?usage: chrome-launch.sh <tag> [chrome flags...]}; shift
|
||||
bash "$here/ct.sh" "${CHANNEL:-chrome}" <<IN
|
||||
pkill -x chrome; sleep 2
|
||||
U=\$(id -u $MGL_DESKTOP_USER)
|
||||
rm -f /tmp/mgl-chrome-$TAG.client.log /tmp/chrome-$TAG-out.log
|
||||
sudo -u $MGL_DESKTOP_USER env XDG_RUNTIME_DIR=/run/user/\$U WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$U/bus MOBILEGL_LOG_FILE_PATH=/tmp/mgl-chrome-$TAG.log MOBILEGL_CHROME_GPU=${CHROME_GPU:-process} MOBILEGL_CHROME_GPU_SANDBOX=${CHROME_GPU_SANDBOX:-off} bash -c 'nohup /opt/mobilegl/bin/mobilegl-startup.sh chrome --restore-last-session $* > /tmp/chrome-$TAG-out.log 2>&1 &'
|
||||
sleep ${WAIT:-20}
|
||||
grep -ah 'presented through' /tmp/mgl-chrome-$TAG.client.log | head -3
|
||||
IN
|
||||
adb_ shell "su -c 'screencap -p $DEV_SHARE/ch-$TAG.png'"
|
||||
adb_ pull "$DEV_SHARE/ch-$TAG.png" "$ANL_DIR/ch-$TAG.png" >/dev/null && echo "screenshot: $ANL_DIR/ch-$TAG.png"
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# chrome-recover.sh <in-process|process> [rounds] - does Chrome repaint after a GPU device loss,
|
||||
# without a restart? Starts Chrome (chrome-launch.sh, CHROME_GPU=<mode>) on an animated WebGL page,
|
||||
# then per round: screenshots (the page must animate), loses the session of Chrome's GPU client with
|
||||
# the server's pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid; the pid as Chrome sees
|
||||
# itself in the container), waits, and screenshots again. A round PASSES when the server latched
|
||||
# that pid's session, the browser process is the same one (no restart), a GPU client is drawing on a
|
||||
# NEW server session, and the screen still animates. Stops early below MIN_BATTERY percent.
|
||||
#
|
||||
# in-process the GPU in the browser process (the startup script's default today): the target is
|
||||
# the browser itself; recovery needs ANGLE to make a new native context.
|
||||
# process the GPU in its own process (MOBILEGL_CHROME_GPU=process): the target is the GPU
|
||||
# process; Chrome restarts it after the loss, so the next round's target is a new pid.
|
||||
#
|
||||
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
|
||||
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
|
||||
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
|
||||
# STARTUP=<anland worktree>/producers/kde/Arch_v5/mobilegl-startup.sh bash scripts/chrome-recover.sh deploy-startup
|
||||
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 8), MIN_BATTERY (default 40),
|
||||
# CHANNEL (ct.sh channel, default crec), TAG (log tag, default crec-<mode>).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
MODE=${1:?usage: chrome-recover.sh <in-process|process|deploy-startup> [rounds]}
|
||||
ROUNDS=${2:-${ROUNDS:-2}}
|
||||
SETTLE=${SETTLE:-8}
|
||||
MIN_BATTERY=${MIN_BATTERY:-40}
|
||||
CH=${CHANNEL:-crec}
|
||||
TAG=${TAG:-crec-$MODE}
|
||||
PAGE=/tmp/mgl-spin.html
|
||||
|
||||
ct() { bash "$here/ct.sh" "$CH"; }
|
||||
|
||||
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
|
||||
check_battery() {
|
||||
local level; level=$(battery)
|
||||
echo "battery: $level%"
|
||||
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
|
||||
echo "STOP: battery below $MIN_BATTERY%"; cleanup; exit 3
|
||||
fi
|
||||
}
|
||||
cleanup() { adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true; }
|
||||
|
||||
if [ "$MODE" = deploy-startup ]; then
|
||||
# The anland startup script that knows MOBILEGL_CHROME_GPU, installed by rename (a running
|
||||
# session keeps its open copy).
|
||||
src=${STARTUP:?STARTUP=<path to the anland mobilegl-startup.sh>}
|
||||
tr -d '\r' < "$src" > "$ANL_DIR/mobilegl-startup.sh"
|
||||
adb_ push "$ANL_DIR/mobilegl-startup.sh" "$DEV_SHARE/mobilegl-startup.sh.new" >/dev/null
|
||||
ct <<'IN'
|
||||
install -m 755 /run/anland-mobilegl/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh.new
|
||||
mv -f /opt/mobilegl/bin/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh
|
||||
grep -c MOBILEGL_CHROME_GPU /opt/mobilegl/bin/mobilegl-startup.sh
|
||||
IN
|
||||
exit 0
|
||||
fi
|
||||
case "$MODE" in in-process|process) ;; *) echo "mode must be in-process or process" >&2; exit 2;; esac
|
||||
trap cleanup EXIT
|
||||
|
||||
check_battery
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
|
||||
cleanup
|
||||
|
||||
# The page: WebGL clears that change colour every frame plus a moving block, so a frame that
|
||||
# reaches the screen changes the screenshot.
|
||||
ct <<IN
|
||||
cat > $PAGE <<'HTML'
|
||||
<!doctype html><html><body style="margin:0;background:#000">
|
||||
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
|
||||
<script>
|
||||
const gl = document.getElementById('c').getContext('webgl');
|
||||
let t = 0, lost = 0;
|
||||
document.getElementById('c').addEventListener('webglcontextlost', e => { e.preventDefault(); lost++; });
|
||||
function frame() {
|
||||
t++;
|
||||
if (gl && !gl.isContextLost()) {
|
||||
gl.clearColor((t % 60) / 60, ((t + 20) % 60) / 60, ((t + 40) % 60) / 60, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.enable(gl.SCISSOR_TEST); gl.scissor((t * 7) % 600, 100, 40, 160);
|
||||
gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.SCISSOR_TEST);
|
||||
}
|
||||
document.title = 'spin ' + t + ' lost ' + lost;
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
</script></body></html>
|
||||
HTML
|
||||
chmod 644 $PAGE
|
||||
IN
|
||||
|
||||
WAIT=${WAIT:-20} CHROME_GPU=$MODE CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file://$PAGE"
|
||||
|
||||
# The target pid inside the container: the browser (in-process) or the GPU process.
|
||||
target_pid() {
|
||||
ct <<'IN' | tr -d '\r' | tail -1
|
||||
for p in $(pgrep -x chrome); do
|
||||
args=$(tr '\0' ' ' < /proc/$p/cmdline)
|
||||
case "$args" in
|
||||
*--type=gpu-process*) echo "gpu $p" ;;
|
||||
*--type=*) ;;
|
||||
*) echo "browser $p" ;;
|
||||
esac
|
||||
done | sort > /tmp/mgl-crec-pids
|
||||
cat /tmp/mgl-crec-pids >&2
|
||||
echo "$(awk '/^browser/ {print $2; exit}' /tmp/mgl-crec-pids) $(awk '/^gpu/ {print $2; exit}' /tmp/mgl-crec-pids)"
|
||||
IN
|
||||
}
|
||||
|
||||
pass=0
|
||||
for round in $(seq 1 "$ROUNDS"); do
|
||||
echo "=== round $round/$ROUNDS ($MODE)"
|
||||
check_battery
|
||||
read -r browser gpu < <(target_pid)
|
||||
if [ "$MODE" = process ]; then target=$gpu; else target=$browser; fi
|
||||
echo "browser=$browser gpu=${gpu:-none} target=$target"
|
||||
if [ -z "$target" ]; then echo "FAIL: no target process"; break; fi
|
||||
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
|
||||
adb_ logcat -c
|
||||
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $target'"
|
||||
sleep "$SETTLE"
|
||||
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
|
||||
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
|
||||
latched=$(adb_ logcat -d | grep -c "injected:pid=$target" || true)
|
||||
started=$(adb_ logcat -d | grep -c "session #[0-9]* started" || true)
|
||||
adb_ logcat -d | grep -E "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)" | cut -c1-220 | head -12 || true
|
||||
read -r browser2 gpu2 < <(target_pid)
|
||||
echo "after: browser=$browser2 gpu=${gpu2:-none}"
|
||||
ct <<IN || true
|
||||
echo "--- client log ($TAG):"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|failed to make|session #" /tmp/mgl-chrome-$TAG.client.log | tail -8
|
||||
echo "--- chrome stderr:"; grep -aiE "context lost|GPU process|make.*current|lost" /tmp/chrome-$TAG-out.log | tail -8
|
||||
IN
|
||||
adb_ logcat -b crash -d | tail -5
|
||||
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
|
||||
verdict=PASS
|
||||
[ "$latched" -ge 1 ] || { echo " - the server never latched pid $target"; verdict=FAIL; }
|
||||
[ "$browser2" = "$browser" ] || { echo " - the browser process changed ($browser -> $browser2): Chrome restarted"; verdict=FAIL; }
|
||||
[ "$started" -ge 1 ] || { echo " - no new server session started after the loss"; verdict=FAIL; }
|
||||
if [ "$MODE" = process ]; then
|
||||
[ -n "$gpu2" ] && [ "$gpu2" != "$gpu" ] || { echo " - no new GPU process"; verdict=FAIL; }
|
||||
fi
|
||||
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
|
||||
echo "round $round: $verdict"
|
||||
[ "$verdict" = PASS ] && pass=$((pass + 1))
|
||||
[ "$verdict" = PASS ] || break
|
||||
done
|
||||
echo "=== $pass/$ROUNDS rounds passed ($MODE); screenshots in $ANL_DIR/$TAG-r*-*.png"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# kwprep.sh - (container, as root) bring the live KWin tree /root/mobilegl-build/kwin-6.7.4 in line with
|
||||
# anland's kwin.patch + anland backend sources, touching ONLY files whose content changed (keeps the
|
||||
# ninja build incremental). It re-applies the whole patch to a pristine copy of KWin's src/
|
||||
# (/root/gbm-dev/kwin-a/src) in /root/kp and copies every patched file that differs into the live tree,
|
||||
# then copies changed backend files. Build + install afterwards with anland's sync-build-kwin.sh.
|
||||
#
|
||||
# Input: /root/kwsync.tgz, a tar made in the anland worktree's producers/kde directory:
|
||||
# tar czf kwsync.tgz Arch_v5/kwin.patch Arch_v5/mobilegl-startup.sh anland_backend_Arch_v5/src/backends/anland/
|
||||
# and pushed to the container's /root (e.g. via /mnt/Droidspaces/arch-kde-mgl/root/ on the Android side).
|
||||
# The pristine tree /root/gbm-dev/kwin-a/src was set up by hand earlier (unverified that it is exactly
|
||||
# upstream 6.7.4); patch hunks that fail are reported by `patch`.
|
||||
set -e
|
||||
rm -rf /root/kwsync && mkdir -p /root/kwsync && tar xzf /root/kwsync.tgz -C /root/kwsync
|
||||
P=/root/kwsync/Arch_v5/kwin.patch
|
||||
rm -rf /root/kp && mkdir -p /root/kp && cp -a /root/gbm-dev/kwin-a/src /root/kp/
|
||||
cd /root/kp && patch -p1 -s < $P
|
||||
LIVE=/root/mobilegl-build/kwin-6.7.4
|
||||
FILES=$(grep -E '^\+\+\+ b/' $P | sed 's|^+++ b/||' | awk '{print $1}' | sort -u)
|
||||
for f in $FILES; do
|
||||
if [ -f "/root/kp/$f" ] && ! cmp -s "/root/kp/$f" "$LIVE/$f"; then cp "/root/kp/$f" "$LIVE/$f"; echo "updated $f"; fi
|
||||
done
|
||||
for f in /root/kwsync/anland_backend_Arch_v5/src/backends/anland/*; do
|
||||
# libdisplay_producer is a symlink in git; a Windows checkout turns it into a small text file. The live
|
||||
# tree has the real directory (repo-root libdisplay_producer/), which neither script syncs.
|
||||
[ -f "$f" ] && [ "$(basename $f)" != libdisplay_producer ] || continue
|
||||
b=$(basename $f); if ! cmp -s "$f" "$LIVE/src/backends/anland/$b"; then cp "$f" "$LIVE/src/backends/anland/$b"; echo "updated anland/$b"; fi
|
||||
done
|
||||
echo PREP_DONE
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
|
||||
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
|
||||
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
|
||||
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
|
||||
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
|
||||
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
|
||||
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
|
||||
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
|
||||
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
|
||||
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
|
||||
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
|
||||
S=$1; shift
|
||||
DU=${DESKTOP_USER:-swung0x48}
|
||||
U=$(id -u "$DU")
|
||||
cd /tmp
|
||||
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
|
||||
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
|
||||
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
|
||||
bash -lc 'exec "$@"' mgrun "$@"
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# ct.sh <channel> [script-file] - run a bash script as ROOT inside the container (login shell),
|
||||
# reading the script from the file or from stdin. Generic form of the old c.sh/c2.sh/c3.sh.
|
||||
#
|
||||
# printf '%s\n' 'ls -la /tmp/*.log' | bash ct.sh mychan
|
||||
# bash ct.sh mychan ./something.cmd
|
||||
#
|
||||
# How: the script is written to $ANL_DIR/<channel>.cmd, pushed to $DEV_TOOLS/<channel>.cmd together with
|
||||
# a one-line <channel>.sh that does `droidspaces --name=<container> run bash -lc "$(cat <channel>.cmd)"`,
|
||||
# and that runs under `su`. Quoting through adb + su + droidspaces is thereby avoided entirely.
|
||||
#
|
||||
# Pick a channel name of your own: other sessions on the same phone use c, c2, c3, xd, xr, gbm...;
|
||||
# writing to their channel files races with them. CR characters are stripped (Windows checkouts).
|
||||
# Inside, run user-side commands with `sudo -u <desktop user> env XDG_RUNTIME_DIR=/run/user/<uid> ...`.
|
||||
# Never `pkill -f <word>` in a channel script: the `bash -lc "<script>"` process carries the whole
|
||||
# script on its command line and kills itself. Use `pkill -x <name>`.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
ch=${1:?usage: ct.sh <channel> [script-file]}
|
||||
src=${2:--}
|
||||
if [ "$src" = - ]; then tr -d '\r' > "$ANL_DIR/$ch.cmd"; else tr -d '\r' < "$src" > "$ANL_DIR/$ch.cmd"; fi
|
||||
printf '%s\n' "$DS_BIN --name=$MGL_CONTAINER run bash -lc \"\$(cat $DEV_TOOLS/$ch.cmd)\"" > "$ANL_DIR/$ch.sh"
|
||||
adb_ shell "mkdir -p $DEV_TOOLS"
|
||||
adb_ push "$ANL_DIR/$ch.cmd" "$DEV_TOOLS/$ch.cmd" >/dev/null
|
||||
adb_ push "$ANL_DIR/$ch.sh" "$DEV_TOOLS/$ch.sh" >/dev/null
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/$ch.sh'"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# cyc.sh <shot-name> - one server iteration: clear Plasma's sticky software-renderer key, push the
|
||||
# NDK-built server lib, swap it into the installed APK (device/swap.sh), restart app + Plasma session
|
||||
# (device/run-plasma.sh, ~90 s), screenshot to $ANL_DIR/<shot-name>.png, print fatal logcat lines.
|
||||
# Env: SERVER_LIB (default $MGL_WORKTREE/build-android/libMobileGL.so), WAIT (s after the session
|
||||
# restart, default 15), N (logcat lines, default 20), CHANNEL (ct.sh channel, default cyc).
|
||||
# Needs device/swap.sh + device/run-plasma.sh in $DEV_TOOLS (push-tools.sh). RESTARTS THE SESSION.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
shot=${1:?usage: cyc.sh <shot-name>}
|
||||
LIB=${SERVER_LIB:-$MGL_WORKTREE/build-android/libMobileGL.so}
|
||||
printf '%s\n' "sudo -u $MGL_DESKTOP_USER kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete; rm -f /tmp/mgl-plasmashell.client.log" \
|
||||
| bash "$here/ct.sh" "${CHANNEL:-cyc}"
|
||||
adb_ push "$LIB" "$DEV_SHARE/libMobileGL.new.so" >/dev/null
|
||||
adb_ logcat -c
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/swap.sh >/dev/null && sh $DEV_TOOLS/run-plasma.sh >/dev/null 2>&1; sleep ${WAIT:-15}; screencap -p $DEV_SHARE/$shot.png'"
|
||||
adb_ pull "$DEV_SHARE/$shot.png" "$ANL_DIR/$shot.png" >/dev/null && echo "screenshot: $ANL_DIR/$shot.png"
|
||||
adb_ logcat -d | grep -E "Fatal signal|FATAL|Abort message|Magma wire buffer|E MobileGL" | cut -c1-260 | head -"${N:-20}" || true
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# desktop-verify.sh <phase>... - check that the MobileGL desktop starts, idles and stops by itself
|
||||
# (anland app: foreground service + mobilegl-desktop.sh; container: desktop-session.service).
|
||||
# Phases run in the order given; screenshots go to $ANL_DIR/dv-<phase>.png.
|
||||
#
|
||||
# push install device/*.sh into $DEV_TOOLS (push-tools.sh's device half)
|
||||
# reset everything down: app force-stopped (its service with it), container stopped,
|
||||
# experiment display daemon killed - the state after a phone reboot
|
||||
# cold reset, then open the app like its icon does and time each piece (desktop-timeline.sh)
|
||||
# open open the app without resetting (after a reboot: the reboot was the reset)
|
||||
# chrome (re)start Chrome in the session on an animated WebGL page (kills a running Chrome)
|
||||
# idle CPU/GPU for visible, hidden (HOME), locked (screen off), visible again (measure.sh)
|
||||
# reopen swipe the app away (remove its task), check KWin survives, open it again, time it
|
||||
# stop Stop desktop from the notification (the service's STOP action) and check it all ends
|
||||
# rtest lock/unlock + window resize regression ($ANL_DIR/rtest.sh)
|
||||
# Env: SECS (idle window, default 20), URL (chrome page).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
SECS=${SECS:-20}
|
||||
URL=${URL:-https://webglsamples.org/aquarium/aquarium.html}
|
||||
SOCK=$DEV_SHARE/display.sock
|
||||
su_() { adb_ shell "su -c '$*'"; }
|
||||
shot() {
|
||||
su_ "screencap -p $DEV_SHARE/dv-$1.png"
|
||||
adb_ pull "$DEV_SHARE/dv-$1.png" "$ANL_DIR/dv-$1.png" > /dev/null && echo "screenshot $ANL_DIR/dv-$1.png"
|
||||
}
|
||||
kwin_log() { # the anland backend's lines since <time>
|
||||
printf '%s\n' "journalctl --no-pager --since '$1' 2>/dev/null | grep -a -E 'viewer (gone|back)|consumer (reconnected|disconnected)|failed to connect' | tail -${2:-8}" |
|
||||
bash "$here/ct.sh" dv 2>/dev/null | grep -v pushed || true
|
||||
}
|
||||
task_id() {
|
||||
su_ "am stack list" | tr -d '\r' | grep -m1 "$MGL_PKG/" | sed -n 's/.*taskId=\([0-9]*\).*/\1/p'
|
||||
}
|
||||
wake() {
|
||||
su_ "input keyevent KEYCODE_WAKEUP"; sleep 1; su_ "wm dismiss-keyguard"; sleep 2
|
||||
}
|
||||
|
||||
for phase in "$@"; do
|
||||
echo "=== $phase"
|
||||
case $phase in
|
||||
push)
|
||||
for f in "$here"/device/*.sh; do
|
||||
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
|
||||
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" > /dev/null
|
||||
done
|
||||
rm -f "$ANL_DIR/push.tmp"; echo "device scripts in $DEV_TOOLS" ;;
|
||||
reset)
|
||||
su_ "sh $DEV_TOOLS/desktop-reset.sh" ;;
|
||||
cold)
|
||||
bash "$0" reset
|
||||
wake
|
||||
su_ "sh $DEV_TOOLS/desktop-timeline.sh 240"
|
||||
sleep 3; shot cold ;;
|
||||
open)
|
||||
wake
|
||||
su_ "sh $DEV_TOOLS/desktop-timeline.sh 300"
|
||||
sleep 3; shot open ;;
|
||||
chrome)
|
||||
WAIT=15 bash "$here/chrome-launch.sh" dv "$URL" ;;
|
||||
idle)
|
||||
su_ "sh $DEV_TOOLS/measure.sh visible $SECS"
|
||||
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
su_ "input keyevent KEYCODE_HOME"; sleep 4
|
||||
su_ "sh $DEV_TOOLS/measure.sh hidden $SECS"
|
||||
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
|
||||
su_ "input keyevent KEYCODE_SLEEP"; sleep 4
|
||||
su_ "sh $DEV_TOOLS/measure.sh locked $SECS"
|
||||
wake
|
||||
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
|
||||
su_ "sh $DEV_TOOLS/measure.sh visible2 $SECS"
|
||||
shot idle
|
||||
kwin_log "$t" 12 ;;
|
||||
reopen)
|
||||
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
id=$(task_id); echo "removing task $id"
|
||||
[ -n "$id" ] && su_ "am stack remove $id"
|
||||
sleep 8
|
||||
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG) kwin: \$(pidof kwin_wayland) plasmashell: \$(pidof plasmashell)"
|
||||
su_ "sh $DEV_TOOLS/measure.sh closed 10"
|
||||
su_ "sh $DEV_TOOLS/desktop-timeline.sh 120"
|
||||
sleep 3; shot reopen
|
||||
kwin_log "$t" ;;
|
||||
stop)
|
||||
su_ "am startservice -n $MGL_PKG/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP" || true
|
||||
sleep 25
|
||||
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG); $DS_BIN show"
|
||||
su_ "tail -3 $DEV_SHARE/desktop.log" ;;
|
||||
rtest)
|
||||
bash "$ANL_DIR/rtest.sh" dv ;;
|
||||
*)
|
||||
echo "unknown phase $phase" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
@@ -0,0 +1,7 @@
|
||||
# bringup.sh - (Android root shell) bring the MobileGL desktop up after a phone reboot. There is
|
||||
# nothing left to do by hand: opening the anland app (its launcher icon, or this am start) starts
|
||||
# the display daemon, the MobileGL server, the container and its Plasma session. Prints the timeline.
|
||||
# Run: su -c "sh /data/local/tmp/anl/bringup.sh" Env: as desktop-timeline.sh.
|
||||
input keyevent KEYCODE_WAKEUP
|
||||
wm dismiss-keyguard
|
||||
sh "$(dirname "$0")/desktop-timeline.sh" 300
|
||||
@@ -0,0 +1,17 @@
|
||||
# desktop-reset.sh - (Android root shell) take the MobileGL desktop fully down, as after a phone
|
||||
# reboot: the app force-stopped (its foreground service and server with it), the container
|
||||
# stopped, the experiment display daemon killed. Leaves the original anland daemon alone.
|
||||
# Run: su -c "sh /data/local/tmp/anl/desktop-reset.sh" Env: PKG, CONTAINER, SOCK, DS.
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
am force-stop $P
|
||||
$D --name=$C stop > /dev/null 2>&1
|
||||
for p in $(pidof display_daemon); do
|
||||
tr '\0' ' ' < /proc/$p/cmdline | grep -qF " $SOCK " && kill $p
|
||||
done
|
||||
rm -f $SOCK
|
||||
sleep 2
|
||||
echo "app: $(pidof $P) server: $(pidof $P:mobilegl) daemon: $(ls $SOCK 2>/dev/null)"
|
||||
$D show
|
||||
@@ -0,0 +1,30 @@
|
||||
# desktop-timeline.sh [timeout-s] - (Android root shell) open the anland app the way its launcher
|
||||
# icon does (am start, no extras) and print when each piece of the MobileGL desktop comes up,
|
||||
# in seconds after the open: display daemon socket, the app's MobileGL server listening, the
|
||||
# container, kwin_wayland, plasmashell, and the splash (ksplashqml) gone = desktop on screen.
|
||||
# Pieces that already run show 0.0. Read-only apart from the am start.
|
||||
# Run: su -c "sh /data/local/tmp/anl/desktop-timeline.sh 180" Env: PKG, CONTAINER, SOCK, DS.
|
||||
T=${1:-180}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
now() { awk '{print $1}' /proc/uptime; }
|
||||
since() { awk -v a="$1" -v b="$(now)" 'BEGIN{printf "%.1f", b - a}'; }
|
||||
listening() { grep -q '00010000 0001 01 [0-9]* @anland-mobilegl$' /proc/net/unix; }
|
||||
t0=$(now)
|
||||
am start -n $P/com.anland.consumer.MainActivity > /dev/null
|
||||
for k in daemon server container kwin shell splash_seen shown; do eval "t_$k="; done
|
||||
while :; do
|
||||
[ -z "$t_daemon" ] && [ -S "$SOCK" ] && t_daemon=$(since $t0) && echo "daemon socket +${t_daemon}s"
|
||||
[ -z "$t_server" ] && listening && t_server=$(since $t0) && echo "server listening +${t_server}s"
|
||||
[ -z "$t_container" ] && [ -n "$($D --name=$C pid 2>/dev/null | tr -dc 0-9)" ] && t_container=$(since $t0) && echo "container up +${t_container}s"
|
||||
[ -z "$t_kwin" ] && pidof kwin_wayland > /dev/null && t_kwin=$(since $t0) && echo "kwin_wayland +${t_kwin}s"
|
||||
[ -z "$t_shell" ] && pidof plasmashell > /dev/null && t_shell=$(since $t0) && echo "plasmashell +${t_shell}s"
|
||||
[ -z "$t_splash_seen" ] && pidof ksplashqml > /dev/null && t_splash_seen=1
|
||||
if [ -n "$t_shell" ] && [ -z "$t_shown" ] && ! pidof ksplashqml > /dev/null; then
|
||||
t_shown=$(since $t0); echo "splash gone +${t_shown}s (desktop on screen)"; break
|
||||
fi
|
||||
[ "$(since $t0 | cut -d. -f1)" -ge "$T" ] && { echo "TIMEOUT after ${T}s"; break; }
|
||||
sleep 0.3
|
||||
done
|
||||
@@ -0,0 +1,47 @@
|
||||
# measure.sh <label> [seconds] - (Android root shell) CPU and GPU load of the MobileGL desktop over a
|
||||
# window: % of ONE core (so 800 = all 8 cores) for the whole phone, KWin (+Xwayland), plasmashell,
|
||||
# Chrome (all its processes), the app's :mobilegl server and its UI process, plus the mean of the
|
||||
# kgsl gpu_busy_percentage samples (1/s). The container's processes are visible from Android's
|
||||
# /proc (Droidspaces only gives them their own pid namespace).
|
||||
# Run: su -c "sh /data/local/tmp/anl/measure.sh visible 20" Env: PKG.
|
||||
L=${1:-sample}
|
||||
N=${2:-20}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
|
||||
|
||||
pids_named() { # exact process names (comm or argv0 basename)
|
||||
for n in "$@"; do pidof "$n"; done
|
||||
}
|
||||
pids_matching() { # argv contains the word
|
||||
for d in /proc/[0-9]*; do
|
||||
{ tr '\0' ' ' < $d/cmdline; } 2>/dev/null | grep -q "$1" && echo ${d#/proc/}
|
||||
done
|
||||
}
|
||||
ticks() { # utime+stime of the pids, in clock ticks
|
||||
t=0
|
||||
for p in "$@"; do
|
||||
[ -r /proc/$p/stat ] || continue
|
||||
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
|
||||
t=$((t + v))
|
||||
done
|
||||
echo $t
|
||||
}
|
||||
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
|
||||
|
||||
KW="$(pids_named kwin_wayland Xwayland)"
|
||||
SH="$(pids_named plasmashell)"
|
||||
CH="$(pids_matching /opt/google/chrome/)"
|
||||
SV="$(pidof $P:mobilegl)"
|
||||
UI="$(pidof $P)"
|
||||
|
||||
a0=$(total); k0=$(ticks $KW); s0=$(ticks $SH); c0=$(ticks $CH); v0=$(ticks $SV); u0=$(ticks $UI)
|
||||
g=0; i=0
|
||||
while [ $i -lt $N ]; do
|
||||
sleep 1
|
||||
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
|
||||
done
|
||||
a1=$(total); k1=$(ticks $KW); s1=$(ticks $SH); c1=$(ticks $CH); v1=$(ticks $SV); u1=$(ticks $UI)
|
||||
# CLK_TCK is 100: ticks per second == percent of one core.
|
||||
pct() { echo $(( ($2 - $1) / N )); }
|
||||
printf '%-10s %3ss phone %4s%% kwin %3s%% plasmashell %3s%% chrome %3s%% server %3s%% app-ui %3s%% gpu %3s%%\n' \
|
||||
"$L" "$N" "$(pct $a0 $a1)" "$(pct $k0 $k1)" "$(pct $s0 $s1)" "$(pct $c0 $c1)" "$(pct $v0 $v1)" "$(pct $u0 $u1)" "$((g / N))"
|
||||
@@ -0,0 +1,20 @@
|
||||
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
|
||||
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
|
||||
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
|
||||
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
|
||||
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
|
||||
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
|
||||
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
|
||||
DIR=$(dirname $SOCK)
|
||||
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
|
||||
sleep 1
|
||||
rm -f $SOCK
|
||||
mkdir -p $DIR
|
||||
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
|
||||
sleep 1
|
||||
chmod 777 $DIR
|
||||
chmod 666 $SOCK
|
||||
echo "daemon restarted:"
|
||||
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
|
||||
@@ -0,0 +1,27 @@
|
||||
# run-plasma.sh - (Android root shell) DEVELOPER restart of the whole MobileGL desktop, e.g. after a
|
||||
# server, client or KWin rebuild: ends the Plasma session (and the user's logind session, which takes
|
||||
# a while to wind down), then restarts the anland app; opening it brings up the server, starts
|
||||
# desktop-session.service again and the session waits for the server. Normal use needs none of this:
|
||||
# opening the app is enough. ~60 s. Prints the unit status, plasma user units and recent coredumps.
|
||||
# Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
|
||||
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), DS (droidspaces binary).
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
U=${DESKTOP_USER:-swung0x48}
|
||||
$D --name=$C run bash -lc "
|
||||
systemctl stop desktop-session.service 2>/dev/null
|
||||
loginctl terminate-user $U 2>/dev/null; sleep 30
|
||||
systemctl reset-failed desktop-session.service
|
||||
rm -f /tmp/mobilegl-compositor.log
|
||||
date +%s > /tmp/plasma-start
|
||||
"
|
||||
am force-stop $P
|
||||
am start -n $P/com.anland.consumer.MainActivity > /dev/null
|
||||
sleep 30
|
||||
$D --name=$C run bash -lc "
|
||||
systemctl --no-pager status desktop-session.service | head -8
|
||||
UID_=\$(id -u $U)
|
||||
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
|
||||
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
|
||||
"
|
||||
@@ -0,0 +1,13 @@
|
||||
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
|
||||
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
|
||||
echo "backend: app setting=$(cat /data/data/$P/files/mobilegl-backend 2>/dev/null || echo 'DirectGLES (default)') published=$(cat ${SOCK%/*}/backend 2>/dev/null) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null) prop override=$(getprop debug.mobilegl.backend)"
|
||||
echo "desktop service log: $(tail -1 ${SOCK%/*}/desktop.log 2>/dev/null)"
|
||||
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
|
||||
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
|
||||
$D show
|
||||
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
|
||||
@@ -0,0 +1,17 @@
|
||||
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
|
||||
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
|
||||
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
|
||||
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
|
||||
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
|
||||
set -e
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
|
||||
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
|
||||
ls -laZ $D
|
||||
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
|
||||
am force-stop $P
|
||||
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
|
||||
OWN=$(stat -c %u:%g $D/libMobileGL.so)
|
||||
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
|
||||
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
|
||||
ls -laZ $D/libMobileGL.so
|
||||
@@ -0,0 +1,17 @@
|
||||
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) change the MobileGL backend the anland
|
||||
# app's server runs (Espryt = DirectGLES, Magma = DirectVulkan). The choice is the app's setting
|
||||
# (Settings > Connection > MobileGL desktop, saved in the app's files/mobilegl-backend); this does
|
||||
# what a user does there: save it, Stop desktop, open the app again. The new server publishes the
|
||||
# backend and the container's session copies it to /etc/mobilegl/backend before it starts, so
|
||||
# server and clients agree. ~30-60 s. No setprop: debug.mobilegl.backend, if set, still overrides
|
||||
# the setting until the next reboot - this clears it.
|
||||
# Env: PKG, SOCK.
|
||||
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
[ -n "$(getprop debug.mobilegl.backend)" ] && setprop debug.mobilegl.backend ""
|
||||
if pidof $P:mobilegl > /dev/null; then
|
||||
am startservice -n $P/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP > /dev/null
|
||||
i=0; while pidof $P:mobilegl > /dev/null && [ $i -lt 60 ]; do sleep 1; i=$((i + 1)); done
|
||||
fi
|
||||
am start -n $P/com.anland.consumer.MainActivity --es mobilegl_backend $B > /dev/null
|
||||
echo "backend $B saved; the desktop is starting (watch: sh $(dirname $0)/status.sh)"
|
||||
@@ -0,0 +1,8 @@
|
||||
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
|
||||
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
|
||||
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
|
||||
# Env: DS, CONTAINER, DESKTOP_USER.
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
U=${DESKTOP_USER:-swung0x48}
|
||||
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
|
||||
@@ -0,0 +1,47 @@
|
||||
# x11-measure.sh <label> <app-process-name> [seconds] - (Android root shell) CPU of the X11 path while
|
||||
# an X11 app runs: % of ONE core (800 = all 8 cores) for the whole phone, the app, Xwayland,
|
||||
# kwin_wayland, the app's :mobilegl server; mean kgsl gpu_busy_percentage; and the number of dma-buf
|
||||
# descriptors Xwayland and KWin hold (window pixmaps / client buffers as dma-bufs). Refuses to run
|
||||
# below 40% battery: the phone drains faster under load than USB charges it.
|
||||
# Run: su -c "sh /data/local/tmp/anl/x11-measure.sh glamor glxgears 10" Env: PKG, MIN_BATTERY.
|
||||
L=${1:-sample}
|
||||
APP=${2:-glxgears}
|
||||
N=${3:-10}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
|
||||
level=$(dumpsys battery | sed -n 's/^ *level: *//p')
|
||||
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
|
||||
echo "battery ${level}% < ${MIN_BATTERY:-40}%: not loading the phone; charge first" >&2
|
||||
exit 3
|
||||
fi
|
||||
ticks() {
|
||||
t=0
|
||||
for p in "$@"; do
|
||||
[ -r /proc/$p/stat ] || continue
|
||||
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
|
||||
t=$((t + v))
|
||||
done
|
||||
echo $t
|
||||
}
|
||||
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
|
||||
dmabufs() { # dma-buf descriptors held by the pids (Android kernels link them to /dmabuf:...)
|
||||
n=0
|
||||
for p in "$@"; do n=$((n + $(ls -l /proc/$p/fd 2>/dev/null | grep -c dmabuf))); done
|
||||
echo $n
|
||||
}
|
||||
AP="$(pidof $APP)"
|
||||
XW="$(pidof Xwayland)"
|
||||
KW="$(pidof kwin_wayland)"
|
||||
SV="$(pidof $P:mobilegl)"
|
||||
[ -n "$AP" ] || { echo "no running process named $APP" >&2; exit 2; }
|
||||
a0=$(total); p0=$(ticks $AP); x0=$(ticks $XW); k0=$(ticks $KW); v0=$(ticks $SV)
|
||||
g=0; i=0
|
||||
while [ $i -lt $N ]; do
|
||||
sleep 1
|
||||
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
|
||||
done
|
||||
a1=$(total); p1=$(ticks $AP); x1=$(ticks $XW); k1=$(ticks $KW); v1=$(ticks $SV)
|
||||
pct() { echo $(( ($2 - $1) / N )); }
|
||||
printf '%-12s %3ss phone %4s%% %s %3s%% Xwayland %3s%% kwin %3s%% server %3s%% gpu %3s%% dmabufs Xwayland %s kwin %s battery %s%%\n' \
|
||||
"$L" "$N" "$(pct $a0 $a1)" "$APP" "$(pct $p0 $p1)" "$(pct $x0 $x1)" "$(pct $k0 $k1)" "$(pct $v0 $v1)" "$((g / N))" \
|
||||
"$(dmabufs $XW)" "$(dmabufs $KW)" "$level"
|
||||
@@ -0,0 +1,41 @@
|
||||
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
|
||||
# Sourced, never run. Every value can be overridden from the environment before calling a script.
|
||||
#
|
||||
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
|
||||
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
|
||||
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
|
||||
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
|
||||
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
|
||||
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
|
||||
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
|
||||
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
|
||||
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
|
||||
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
|
||||
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
|
||||
# (USB serial or a wireless "ip:port").
|
||||
|
||||
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
if [ -z "${MGL_WORKTREE:-}" ]; then
|
||||
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
|
||||
fi
|
||||
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
|
||||
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
|
||||
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
|
||||
: "${MGL_CONTAINER:=arch-kde-mgl}"
|
||||
: "${MGL_DESKTOP_USER:=swung0x48}"
|
||||
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
|
||||
: "${DEV_TOOLS:=/data/local/tmp/anl}"
|
||||
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
|
||||
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
|
||||
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
|
||||
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
|
||||
|
||||
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
|
||||
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
|
||||
export MSYS_NO_PATHCONV=1
|
||||
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
|
||||
|
||||
# C:/x/y -> /mnt/c/x/y (for WSL).
|
||||
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
|
||||
|
||||
mkdir -p "$ANL_DIR"
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# kwin-recover.sh [rounds] - does the desktop keep working when KWin's OWN MobileGL session is lost?
|
||||
# Starts animated clients (glmark2-es2-wayland: Wayland, zero-copy shared images; glxgears: X11 through
|
||||
# Xwayland's GLX; with CHROME=1 also Chrome on an animated WebGL page), then per round loses the server
|
||||
# session of KWin with the pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid, KWin's pid as
|
||||
# it sees itself in the container), waits, and checks. A round PASSES when:
|
||||
# - the server latched KWin's session (the server log file: "injected:pid=<kwin>"; logcat is too small);
|
||||
# - KWin, plasmashell and every test client are the SAME processes (nothing restarted);
|
||||
# - KWin restarted compositing on a fresh session (its journal: "compositing restarted on a fresh
|
||||
# MobileGL session"; its client log: one more "RECOVERED");
|
||||
# - the screen still animates (shot.sh hashes differ) and the clients keep presenting;
|
||||
# - no native crash (logcat -b crash).
|
||||
# Between rounds the knob is reset to 0 while the clients run, which re-arms it for the same pid.
|
||||
# Stops early below MIN_BATTERY percent (the phone drains faster under load than USB charges it).
|
||||
#
|
||||
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
|
||||
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
|
||||
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
|
||||
# ANLAND_WORKTREE=<anland worktree> bash scripts/kwin-stage.sh build # kwin.patch + anland backend
|
||||
# (wait for "== ALL DONE ==" in /root/kwin-build.log), then: adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
|
||||
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 10), REARM (s with the knob at 0 between
|
||||
# rounds, default 4), MIN_BATTERY (default 40), CHROME (1: add Chrome), CHANNEL (ct.sh channel,
|
||||
# default krec), TAG (screenshot/log tag, default krec).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
ROUNDS=${1:-${ROUNDS:-2}}
|
||||
SETTLE=${SETTLE:-10}
|
||||
REARM=${REARM:-4}
|
||||
MIN_BATTERY=${MIN_BATTERY:-40}
|
||||
CH=${CHANNEL:-krec}
|
||||
TAG=${TAG:-krec}
|
||||
COMPOSITOR_LOG=/tmp/mobilegl-compositor.client.log
|
||||
SERVER_LOG=/data/data/$MGL_PKG/files/mobilegl-server.server.log
|
||||
|
||||
ct() { bash "$here/ct.sh" "$CH"; }
|
||||
|
||||
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
|
||||
stop_clients() {
|
||||
ct <<'IN' || true
|
||||
pkill -x glmark2-es2-way 2>/dev/null; pkill -x glmark2-es2-wayland 2>/dev/null; pkill -x glxgears 2>/dev/null
|
||||
true
|
||||
IN
|
||||
}
|
||||
cleanup() {
|
||||
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true
|
||||
stop_clients
|
||||
}
|
||||
check_battery() {
|
||||
local level; level=$(battery)
|
||||
echo "battery: $level%"
|
||||
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
|
||||
echo "STOP: battery below $MIN_BATTERY%"; exit 3
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
check_battery
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
|
||||
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
|
||||
|
||||
# The clients, each with its own log (mgrun's MGLOG), in the background for the whole run.
|
||||
ct <<'IN'
|
||||
rm -f /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log /tmp/krec-gm.out /tmp/krec-gx.out
|
||||
MGLOG=/tmp/mgl-krec-gm.log nohup timeout 900 mgrun offscreen glmark2-es2-wayland -s 960x540 --run-forever \
|
||||
> /tmp/krec-gm.out 2>&1 < /dev/null &
|
||||
MGLOG=/tmp/mgl-krec-gx.log nohup timeout 900 mgrun offscreen glxgears -info > /tmp/krec-gx.out 2>&1 < /dev/null &
|
||||
sleep 8
|
||||
echo "glmark2: $(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland) glxgears: $(pgrep -x glxgears)"
|
||||
grep -aE "presented through|could not be presented" /tmp/mgl-krec-gm.client.log | head -2
|
||||
grep -aE "presented through|readback" /tmp/mgl-krec-gx.client.log | head -2
|
||||
IN
|
||||
if [ "${CHROME:-0}" = 1 ]; then
|
||||
ct <<'IN'
|
||||
cat > /tmp/mgl-krec-spin.html <<'HTML'
|
||||
<!doctype html><html><body style="margin:0;background:#000">
|
||||
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
|
||||
<script>
|
||||
const gl = document.getElementById('c').getContext('webgl'); let t = 0;
|
||||
function frame() { t++; if (gl && !gl.isContextLost()) { gl.clearColor((t % 60) / 60, 0.2, 0.6, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
|
||||
requestAnimationFrame(frame); }
|
||||
frame();
|
||||
</script></body></html>
|
||||
HTML
|
||||
chmod 644 /tmp/mgl-krec-spin.html
|
||||
IN
|
||||
WAIT=${WAIT:-20} CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file:///tmp/mgl-krec-spin.html"
|
||||
fi
|
||||
|
||||
# KWin, plasmashell and the clients, as pids inside the container; how often KWin recovered so far.
|
||||
snapshot() {
|
||||
ct <<IN | tr -d '\r' | tail -1
|
||||
k=\$(pgrep -x kwin_wayland | head -1); p=\$(pgrep -x plasmashell | head -1)
|
||||
g=\$(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland); x=\$(pgrep -x glxgears | head -1)
|
||||
c=\$(pgrep -x chrome | head -1)
|
||||
r=\$(grep -ac "RECOVERED" $COMPOSITOR_LOG 2>/dev/null || true)
|
||||
f=\$(grep -ac "FPS" /tmp/krec-gm.out 2>/dev/null || true)
|
||||
echo "\${k:-none} \${p:-none} \${g:-none} \${x:-none} \${c:-none} \${r:-0} \${f:-0}"
|
||||
IN
|
||||
}
|
||||
|
||||
pass=0
|
||||
for round in $(seq 1 "$ROUNDS"); do
|
||||
echo "=== round $round/$ROUNDS"
|
||||
check_battery
|
||||
read -r kwin plasma gm gx chrome recovered fps < <(snapshot)
|
||||
echo "kwin=$kwin plasmashell=$plasma glmark2=$gm glxgears=$gx chrome=$chrome recoveries=$recovered fps-lines=$fps"
|
||||
if [ "$kwin" = none ]; then echo "FAIL: KWin is not running"; break; fi
|
||||
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
|
||||
srvlines=$(adb_ shell "su -c 'wc -l < $SERVER_LOG'" | tr -dc '0-9')
|
||||
adb_ logcat -b crash -c || true
|
||||
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $kwin'"
|
||||
sleep "$SETTLE"
|
||||
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
|
||||
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
|
||||
read -r kwin2 plasma2 gm2 gx2 chrome2 recovered2 fps2 < <(snapshot)
|
||||
echo "after: kwin=$kwin2 plasmashell=$plasma2 glmark2=$gm2 glxgears=$gx2 chrome=$chrome2 recoveries=$recovered2 fps-lines=$fps2"
|
||||
adb_ shell "su -c 'tail -n +$((srvlines + 1)) $SERVER_LOG'" > "$ANL_DIR/$TAG-r$round-server.log"
|
||||
latched=$(grep -c "injected:pid=$kwin" "$ANL_DIR/$TAG-r$round-server.log" || true)
|
||||
grep -aE "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)|owner=server|lease" \
|
||||
"$ANL_DIR/$TAG-r$round-server.log" | cut -c1-220 | head -14 || true
|
||||
ct <<IN || true
|
||||
echo "--- KWin's MobileGL client log:"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|re-created on the fresh|Refuse" $COMPOSITOR_LOG | tail -6
|
||||
echo "--- KWin journal:"; journalctl --no-pager --since -40s 2>/dev/null | grep -aE "kwin_wayland" | grep -aiE "reset|restarted|MobileGL swap|context|fatal|abort" | tail -10
|
||||
echo "--- clients:"; grep -aE "DEVICE LOST|lost|Fatal" /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log | tail -4
|
||||
IN
|
||||
crashes=$(adb_ logcat -b crash -d | grep -c "Fatal signal\|Abort message" || true)
|
||||
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
|
||||
verdict=PASS
|
||||
[ "$latched" -ge 1 ] || { echo " - the server never latched KWin's session (pid $kwin)"; verdict=FAIL; }
|
||||
[ "$kwin2" = "$kwin" ] || { echo " - KWin is a different process ($kwin -> $kwin2): it restarted"; verdict=FAIL; }
|
||||
[ "$plasma2" = "$plasma" ] || { echo " - plasmashell restarted ($plasma -> $plasma2)"; verdict=FAIL; }
|
||||
[ "$gm2" = "$gm" ] && [ "$gx2" = "$gx" ] || { echo " - a test client restarted or died"; verdict=FAIL; }
|
||||
[ "${CHROME:-0}" != 1 ] || [ "$chrome2" = "$chrome" ] || { echo " - Chrome restarted"; verdict=FAIL; }
|
||||
[ "$recovered2" -gt "$recovered" ] || { echo " - KWin's client log shows no new RECOVERED session"; verdict=FAIL; }
|
||||
[ "$fps2" -gt "$fps" ] || { echo " - glmark2 stopped reporting frames"; verdict=FAIL; }
|
||||
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
|
||||
[ "$crashes" -eq 0 ] || { echo " - native crash(es):"; adb_ logcat -b crash -d | tail -8; verdict=FAIL; }
|
||||
echo "round $round: $verdict"
|
||||
[ "$verdict" = PASS ] && pass=$((pass + 1))
|
||||
[ "$verdict" = PASS ] || break
|
||||
# Re-arm the knob for the same pid: a session must observe 0 before the next value counts.
|
||||
sleep "$REARM"
|
||||
done
|
||||
echo "=== $pass/$ROUNDS rounds passed; screenshots in $ANL_DIR/$TAG-r*-*.png"
|
||||
[ "$pass" = "$ROUNDS" ]
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# kwin-stage.sh [build] - stage anland's KWin changes (kwin.patch, anland backend sources,
|
||||
# mobilegl-startup.sh, sync-build-kwin.sh) for the in-container KWin rebuild; with "build", also run
|
||||
# kwprep.sh (apply changed patch hunks / backend files to the live tree) and start sync-build-kwin.sh
|
||||
# (ninja -j4 + atomic install into /opt/mobilegl/kwin, screencast plugin, startup script) in the
|
||||
# background, log /root/kwin-build.log. KWin is NOT restarted: run device/run-plasma.sh afterwards.
|
||||
# Poll: printf 'tail -5 /root/kwin-build.log\n' | bash ct.sh kw
|
||||
#
|
||||
# Two staging places, because the two container scripts read different ones:
|
||||
# /root/kwsync.tgz <- kwprep.sh (installed by push-tools.sh)
|
||||
# $DEV_SHARE/kwin-sync = /run/anland-mobilegl/kwin-sync <- sync-build-kwin.sh (as kwin-sync/sync-build.sh)
|
||||
# All files are CR-stripped: the Windows checkout of anland has CRLF backend sources and a CRLF
|
||||
# sync-build-kwin.sh (bash then fails on $'\r').
|
||||
# Assembled from kwprep.sh + sync-build-kwin.sh; not exercised as one unit yet.
|
||||
# Env: ANLAND_WORKTREE (anland checkout on branch legacy-mobilegl-unified, Windows form),
|
||||
# CHANNEL (ct.sh channel, default kw).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified}"
|
||||
K=$ANLAND_WORKTREE/producers/kde
|
||||
st=$ANL_DIR/kwstage
|
||||
rm -rf "$st"; mkdir -p "$st/kwsync/Arch_v5" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland" "$st/kwin-sync/anland" "$st/kwin-sync/misc"
|
||||
lf() { tr -d '\r' < "$1" > "$2"; }
|
||||
for f in "$K"/anland_backend_Arch_v5/src/backends/anland/*; do
|
||||
b=$(basename "$f"); [ "$b" = libdisplay_producer ] && continue
|
||||
lf "$f" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland/$b"
|
||||
lf "$f" "$st/kwin-sync/anland/$b"
|
||||
done
|
||||
for f in kwin.patch mobilegl-startup.sh desktop-session-mobilegl.conf; do
|
||||
lf "$K/Arch_v5/$f" "$st/kwsync/Arch_v5/$f"
|
||||
lf "$K/Arch_v5/$f" "$st/kwin-sync/misc/$f"
|
||||
done
|
||||
lf "$K/Arch_v5/sync-build-kwin.sh" "$st/kwin-sync/sync-build.sh"
|
||||
(cd "$st/kwsync" && tar --force-local -czf ../kwsync.tgz Arch_v5 anland_backend_Arch_v5)
|
||||
tar --force-local -C "$st" -czf "$st/kwin-sync.tgz" kwin-sync
|
||||
adb_ push "$st/kwsync.tgz" "$DEV_TOOLS/kwsync.tgz" >/dev/null
|
||||
adb_ push "$st/kwin-sync.tgz" "$DEV_TOOLS/kwin-sync.tgz" >/dev/null
|
||||
adb_ shell "su -c 'cp $DEV_TOOLS/kwsync.tgz $CT_ROOT/root/kwsync.tgz && tar xzf $DEV_TOOLS/kwin-sync.tgz -C $DEV_SHARE && chmod -R a+rX $DEV_SHARE/kwin-sync && rm -f $DEV_TOOLS/kwsync.tgz $DEV_TOOLS/kwin-sync.tgz && echo staged'"
|
||||
if [ "${1:-}" = build ]; then
|
||||
bash "$here/ct.sh" "${CHANNEL:-kw}" <<'IN'
|
||||
bash /root/kwprep.sh || exit 1
|
||||
nohup bash /run/anland-mobilegl/kwin-sync/sync-build.sh > /root/kwin-build.log 2>&1 < /dev/null &
|
||||
echo "kwin build started (pid $!), log /root/kwin-build.log; it ends with '== ALL DONE =='"
|
||||
IN
|
||||
fi
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
# cdpmedia.py [port] [seconds] - ask a Chrome started with --remote-debugging-port=<port> (and a
|
||||
# non-default --user-data-dir) which video decoder its first page's players use, and evaluate
|
||||
# `window.__mglStats` / the element `#t` text of the page. Standard library only (no websocket
|
||||
# module in the container): a minimal RFC 6455 client.
|
||||
# python3 cdpmedia.py 9333 6
|
||||
# Prints kVideoDecoderName / kIsPlatformVideoDecoder / kVideoTracks properties and media errors as
|
||||
# they arrive, then the page's stats text.
|
||||
import base64, json, os, socket, struct, sys, time, urllib.request
|
||||
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9333
|
||||
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0
|
||||
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
|
||||
page = next(t for t in targets if t.get("type") == "page")
|
||||
url = page["webSocketDebuggerUrl"]
|
||||
path = url.split(f"127.0.0.1:{port}", 1)[1]
|
||||
sock = socket.create_connection(("127.0.0.1", port))
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
|
||||
response = b""
|
||||
while b"\r\n\r\n" not in response:
|
||||
response += sock.recv(4096)
|
||||
buffer = response.split(b"\r\n\r\n", 1)[1]
|
||||
|
||||
|
||||
def send(message):
|
||||
data = json.dumps(message).encode()
|
||||
header = bytes([0x81])
|
||||
mask = os.urandom(4)
|
||||
if len(data) < 126:
|
||||
header += bytes([0x80 | len(data)])
|
||||
elif len(data) < 65536:
|
||||
header += bytes([0x80 | 126]) + struct.pack(">H", len(data))
|
||||
else:
|
||||
header += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
|
||||
sock.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||||
|
||||
|
||||
def recv_exact(n):
|
||||
global buffer
|
||||
while len(buffer) < n:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
raise EOFError
|
||||
buffer += chunk
|
||||
out, buffer = buffer[:n], buffer[n:]
|
||||
return out
|
||||
|
||||
|
||||
def recv():
|
||||
first, second = recv_exact(2)
|
||||
length = second & 0x7F
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", recv_exact(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", recv_exact(8))[0]
|
||||
payload = recv_exact(length)
|
||||
return json.loads(payload) if first & 0x0F == 1 else None
|
||||
|
||||
|
||||
send({"id": 1, "method": "Media.enable"})
|
||||
# Players created before the domain was enabled report nothing: reload so the page makes new ones.
|
||||
if os.environ.get("CDP_NAVIGATE"):
|
||||
send({"id": 3, "method": "Page.navigate", "params": {"url": os.environ["CDP_NAVIGATE"]}})
|
||||
elif os.environ.get("CDP_RELOAD", "1") == "1":
|
||||
send({"id": 3, "method": "Page.reload"})
|
||||
deadline = time.time() + seconds
|
||||
sock.settimeout(0.5)
|
||||
seen = set()
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
message = recv()
|
||||
except socket.timeout:
|
||||
continue
|
||||
if not message or "method" not in message:
|
||||
continue
|
||||
if message["method"] == "Media.playerPropertiesChanged":
|
||||
for prop in message["params"]["properties"]:
|
||||
if prop["name"] in ("kVideoDecoderName", "kIsPlatformVideoDecoder", "kVideoTracks", "kResolution"):
|
||||
line = f"{prop['name']} = {prop['value']}"
|
||||
if line not in seen:
|
||||
seen.add(line)
|
||||
print(line)
|
||||
elif message["method"] in ("Media.playerErrorsRaised",):
|
||||
print("media error:", json.dumps(message["params"])[:300])
|
||||
send({"id": 2, "method": "Runtime.evaluate",
|
||||
"params": {"expression": os.environ.get("CDP_EXPR", "(document.getElementById('t')||{}).textContent"), "returnByValue": True}})
|
||||
sock.settimeout(3)
|
||||
while True:
|
||||
message = recv()
|
||||
if message and message.get("id") == 2:
|
||||
print("page:", message.get("result", {}).get("result", {}).get("value"))
|
||||
break
|
||||
@@ -0,0 +1,34 @@
|
||||
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
char dir[64];
|
||||
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
|
||||
DIR* d = opendir(dir);
|
||||
if (!d) { perror("opendir"); return 1; }
|
||||
struct dirent* e;
|
||||
while ((e = readdir(d))) {
|
||||
char p[128], l[128] = {0};
|
||||
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
||||
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
|
||||
int fd = open(p, O_RDONLY);
|
||||
if (fd < 0) continue;
|
||||
struct stat st; fstat(fd, &st);
|
||||
if (st.st_size < (1 << 20)) { close(fd); continue; }
|
||||
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
||||
if (m == MAP_FAILED) { printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino); close(fd); continue; }
|
||||
uint64_t h = 1469598103934665603ull; unsigned nz = 0;
|
||||
for (off_t i = 0; i < st.st_size; i += 64) { h = (h ^ m[i]) * 1099511628211ull; nz += m[i] != 0; }
|
||||
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
|
||||
(unsigned long long)h, nz);
|
||||
munmap((void*)m, st.st_size); close(fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// gpuprobe - a plain Android process (outside MobileGL) that measures whether the GPU keeps executing
|
||||
// other contexts' work: a pbuffer GLES context clears + draws and glFinish()es every ~10 ms, printing any
|
||||
// round trip over 50 ms with a CLOCK_REALTIME timestamp. Usage: gpuprobe <seconds> [priority: low|med|high]
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef EGL_CONTEXT_PRIORITY_LEVEL_IMG
|
||||
#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
|
||||
#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
|
||||
#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
|
||||
#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
|
||||
#endif
|
||||
|
||||
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int seconds = argc > 1 ? atoi(argv[1]) : 20;
|
||||
EGLint prio = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
|
||||
if (argc > 2 && !strcmp(argv[2], "high")) prio = EGL_CONTEXT_PRIORITY_HIGH_IMG;
|
||||
if (argc > 2 && !strcmp(argv[2], "low")) prio = EGL_CONTEXT_PRIORITY_LOW_IMG;
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
eglInitialize(d, NULL, NULL);
|
||||
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_NONE};
|
||||
EGLConfig c; EGLint n;
|
||||
eglChooseConfig(d, ca, &c, 1, &n);
|
||||
const EGLint pa[] = {EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE};
|
||||
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
|
||||
const EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_CONTEXT_PRIORITY_LEVEL_IMG, prio, EGL_NONE};
|
||||
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
|
||||
if (x == EGL_NO_CONTEXT) { printf("no ctx 0x%x\n", eglGetError()); return 1; }
|
||||
eglMakeCurrent(d, s, s, x);
|
||||
EGLint got = 0; eglQueryContext(d, x, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &got);
|
||||
const double t0 = now_ms(); double maxRt = 0; long iters = 0, slow = 0;
|
||||
printf("[probe] start %.0f prio=0x%x\n", t0, got);
|
||||
while (now_ms() - t0 < seconds * 1000.0) {
|
||||
const double a = now_ms();
|
||||
glClearColor((iters & 1) ? 1.f : 0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glFinish();
|
||||
const double rt = now_ms() - a;
|
||||
if (rt > maxRt) maxRt = rt;
|
||||
if (rt > 50) { ++slow; printf("[probe] glFinish %.0f ms ending at %.0f\n", rt, now_ms()); }
|
||||
++iters;
|
||||
usleep(10000);
|
||||
}
|
||||
printf("[probe] END iters=%ld max=%.0f ms slow=%ld\n", iters, maxRt, slow);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// hangclient - a Wayland xdg-toplevel, EGL + desktop GL 3.3 core client that animates and, on request,
|
||||
// submits fragment work that never ends (the driver has to reset it). Prints per-frame gaps so the same
|
||||
// binary measures "other app" frame pacing (frame callbacks come from the compositor) when run with -H -1.
|
||||
//
|
||||
// hangclient -T tag [-H hang_at_frame] [-N hang_repeat] [-I iters] [-S seconds] [-W w] [-X h] [-F]
|
||||
// -H n frame at which the hanging draw is submitted (-1: never; default -1)
|
||||
// -N k how many consecutive hanging frames (default 1)
|
||||
// -E e re-arm a hang every e frames after the first (0: once)
|
||||
// -I i loop count of the hanging shader (default 2^31-1)
|
||||
// -S s run time in seconds (default 30)
|
||||
// -F glFinish after every frame (puts the wait inside the client's own session)
|
||||
#define GL_GLEXT_PROTOTYPES 1
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#include <wayland-client.h>
|
||||
#include <wayland-egl.h>
|
||||
#include "xdg-shell-client-protocol.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static struct wl_compositor* g_comp;
|
||||
static struct xdg_wm_base* g_wm;
|
||||
static int g_configured, g_closed;
|
||||
static int g_w = 480, g_h = 360;
|
||||
|
||||
static void reg_global(void* d, struct wl_registry* r, uint32_t name, const char* iface, uint32_t ver) {
|
||||
(void)d;
|
||||
if (!strcmp(iface, "wl_compositor")) g_comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
|
||||
else if (!strcmp(iface, "xdg_wm_base")) g_wm = wl_registry_bind(r, name, &xdg_wm_base_interface, 1);
|
||||
}
|
||||
static void reg_remove(void* d, struct wl_registry* r, uint32_t n) { (void)d; (void)r; (void)n; }
|
||||
static const struct wl_registry_listener reg_l = {reg_global, reg_remove};
|
||||
static void wm_ping(void* d, struct xdg_wm_base* wm, uint32_t s) { (void)d; xdg_wm_base_pong(wm, s); }
|
||||
static const struct xdg_wm_base_listener wm_l = {wm_ping};
|
||||
static void xs_conf(void* d, struct xdg_surface* s, uint32_t serial) { (void)d; xdg_surface_ack_configure(s, serial); g_configured = 1; }
|
||||
static const struct xdg_surface_listener xs_l = {xs_conf};
|
||||
static void tl_conf(void* d, struct xdg_toplevel* t, int32_t w, int32_t h, struct wl_array* st) { (void)d; (void)t; (void)st; (void)w; (void)h; }
|
||||
static void tl_close(void* d, struct xdg_toplevel* t) { (void)d; (void)t; g_closed = 1; }
|
||||
static const struct xdg_toplevel_listener tl_l = {tl_conf, tl_close};
|
||||
|
||||
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
|
||||
|
||||
static GLuint sh(GLenum type, const char* src) {
|
||||
GLuint s = glCreateShader(type);
|
||||
glShaderSource(s, 1, &src, NULL);
|
||||
glCompileShader(s);
|
||||
GLint ok = 0; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) { char log[2048]; glGetShaderInfoLog(s, sizeof log, NULL, log); fprintf(stderr, "shader: %s\n", log); exit(2); }
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const char* tag = "hc"; int hangAt = -1, hangN = 1, every = 0, seconds = 30, finish = 0; long iters = 2147483647L;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (!strcmp(argv[i], "-T") && i + 1 < argc) tag = argv[++i];
|
||||
else if (!strcmp(argv[i], "-H") && i + 1 < argc) hangAt = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-N") && i + 1 < argc) hangN = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-E") && i + 1 < argc) every = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-I") && i + 1 < argc) iters = atol(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-S") && i + 1 < argc) seconds = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-W") && i + 1 < argc) g_w = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-X") && i + 1 < argc) g_h = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-F")) finish = 1;
|
||||
}
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
struct wl_display* dpy = wl_display_connect(NULL);
|
||||
if (!dpy) { fprintf(stderr, "no wayland\n"); return 1; }
|
||||
struct wl_registry* reg = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(reg, ®_l, NULL);
|
||||
wl_display_roundtrip(dpy);
|
||||
if (!g_comp || !g_wm) { fprintf(stderr, "no compositor/xdg_wm_base\n"); return 1; }
|
||||
xdg_wm_base_add_listener(g_wm, &wm_l, NULL);
|
||||
struct wl_surface* surf = wl_compositor_create_surface(g_comp);
|
||||
struct xdg_surface* xs = xdg_wm_base_get_xdg_surface(g_wm, surf);
|
||||
xdg_surface_add_listener(xs, &xs_l, NULL);
|
||||
struct xdg_toplevel* tl = xdg_surface_get_toplevel(xs);
|
||||
xdg_toplevel_add_listener(tl, &tl_l, NULL);
|
||||
xdg_toplevel_set_title(tl, tag);
|
||||
wl_surface_commit(surf);
|
||||
while (!g_configured) wl_display_dispatch(dpy);
|
||||
|
||||
EGLDisplay ed = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, dpy, NULL);
|
||||
if (!eglInitialize(ed, NULL, NULL)) { fprintf(stderr, "eglInitialize\n"); return 1; }
|
||||
eglBindAPI(EGL_OPENGL_API);
|
||||
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
|
||||
EGLConfig cfg; EGLint n = 0;
|
||||
if (!eglChooseConfig(ed, ca, &cfg, 1, &n) || n < 1) { fprintf(stderr, "no config\n"); return 1; }
|
||||
const EGLint xa[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
|
||||
EGLContext ctx = eglCreateContext(ed, cfg, EGL_NO_CONTEXT, xa);
|
||||
if (ctx == EGL_NO_CONTEXT) { fprintf(stderr, "no context 0x%x\n", eglGetError()); return 1; }
|
||||
struct wl_egl_window* ew = wl_egl_window_create(surf, g_w, g_h);
|
||||
EGLSurface es = eglCreatePlatformWindowSurface(ed, cfg, ew, NULL);
|
||||
eglMakeCurrent(ed, es, es, ctx);
|
||||
eglSwapInterval(ed, 1);
|
||||
|
||||
const char* vs = "#version 330 core\nlayout(location=0) in vec2 p; uniform float t; out vec2 uv;\n"
|
||||
"void main(){ uv=p*0.5+0.5; gl_Position=vec4(p*0.4+vec2(0.5*sin(t),0.0),0.0,1.0);}";
|
||||
const char* fs = "#version 330 core\nin vec2 uv; uniform int iters; uniform float t; out vec4 c;\n"
|
||||
"void main(){ float a=uv.x; for(int i=0;i<iters;++i){ a=fract(a*1.0001+0.37+uv.y); }\n"
|
||||
" c=vec4(uv, 0.5+0.5*sin(t)+a*1e-6, 1.0);}";
|
||||
GLuint prog = glCreateProgram();
|
||||
glAttachShader(prog, sh(GL_VERTEX_SHADER, vs));
|
||||
glAttachShader(prog, sh(GL_FRAGMENT_SHADER, fs));
|
||||
glLinkProgram(prog);
|
||||
GLint ok = 0; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||
if (!ok) { fprintf(stderr, "link failed\n"); return 2; }
|
||||
GLint uIt = glGetUniformLocation(prog, "iters"), uT = glGetUniformLocation(prog, "t");
|
||||
GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao);
|
||||
const float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
|
||||
glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
|
||||
|
||||
const double t0 = now_ms(); double last = t0, maxGap = 0; long frames = 0, gaps100 = 0;
|
||||
printf("[%s] start %.0f renderer=%s hangAt=%d N=%d every=%d iters=%ld\n", tag, t0, glGetString(GL_RENDERER), hangAt, hangN, every, iters);
|
||||
while (!g_closed && now_ms() - t0 < seconds * 1000.0) {
|
||||
wl_display_dispatch_pending(dpy);
|
||||
const double t = (now_ms() - t0) / 1000.0;
|
||||
int hang = 0;
|
||||
if (hangAt >= 0 && frames >= hangAt) {
|
||||
long rel = frames - hangAt;
|
||||
if (every > 0) rel %= every;
|
||||
hang = rel < hangN;
|
||||
}
|
||||
glViewport(0, 0, g_w, g_h);
|
||||
glClearColor(0.5f + 0.5f * (float)sin(t * 3.0), 0.2f, 0.4f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(prog);
|
||||
glUniform1i(uIt, hang ? (int)iters : 0);
|
||||
glUniform1f(uT, (float)t);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
if (hang) printf("[%s] HANG submitted frame %ld at %.0f\n", tag, frames, now_ms());
|
||||
if (finish) glFinish();
|
||||
if (!eglSwapBuffers(ed, es)) printf("[%s] swap failed 0x%x at %.0f\n", tag, eglGetError(), now_ms());
|
||||
const GLenum rs = glGetError();
|
||||
if (rs != GL_NO_ERROR) printf("[%s] glGetError 0x%x at %.0f\n", tag, rs, now_ms());
|
||||
const double nowv = now_ms(), gap = nowv - last;
|
||||
last = nowv; ++frames;
|
||||
if (gap > maxGap) maxGap = gap;
|
||||
if (gap > 100.0) { ++gaps100; printf("[%s] GAP %.0f ms ending at %.0f (frame %ld)\n", tag, gap, nowv, frames); }
|
||||
if (frames % 120 == 0) printf("[%s] frame %ld t=%.1fs fps=%.1f\n", tag, frames, (nowv - t0) / 1e3, frames * 1e3 / (nowv - t0));
|
||||
}
|
||||
const double total = now_ms() - t0;
|
||||
printf("[%s] END frames=%ld fps=%.1f maxGap=%.0f ms gaps>100ms=%ld\n", tag, frames, frames * 1e3 / total, maxGap, gaps100);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
|
||||
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
|
||||
* left half red and the right half WHITE; a black right half means the alpha was lost. */
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#define P(T, n) T n = (T)eglGetProcAddress(#n)
|
||||
static const char* vs = "#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
|
||||
static const char* fs = "#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
|
||||
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
|
||||
int main(void) {
|
||||
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(d, 0, 0); eglBindAPI(EGL_OPENGL_ES_API);
|
||||
EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
|
||||
EGLConfig c; EGLint n; eglChooseConfig(d, ca, &c, 1, &n);
|
||||
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
|
||||
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||
EGLSurface s = eglCreatePbufferSurface(d, c, pa); eglMakeCurrent(d, s, s, x);
|
||||
P(PFNGLCREATESHADERPROC, glCreateShader); P(PFNGLSHADERSOURCEPROC, glShaderSource); P(PFNGLCOMPILESHADERPROC, glCompileShader);
|
||||
P(PFNGLCREATEPROGRAMPROC, glCreateProgram); P(PFNGLATTACHSHADERPROC, glAttachShader); P(PFNGLLINKPROGRAMPROC, glLinkProgram);
|
||||
P(PFNGLUSEPROGRAMPROC, glUseProgram); P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); P(PFNGLUNIFORM1IPROC, glUniform1i);
|
||||
P(PFNGLGENBUFFERSPROC, glGenBuffers); P(PFNGLBINDBUFFERPROC, glBindBuffer); P(PFNGLBUFFERDATAPROC, glBufferData);
|
||||
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
|
||||
P(PFNGLDRAWARRAYSPROC, glDrawArrays); P(PFNGLCLEARCOLORPROC, glClearColor); P(PFNGLCLEARPROC, glClear);
|
||||
P(PFNGLREADPIXELSPROC, glReadPixels); P(PFNGLVIEWPORTPROC, glViewport); P(PFNGLGENTEXTURESPROC, glGenTextures);
|
||||
P(PFNGLBINDTEXTUREPROC, glBindTexture); P(PFNGLTEXIMAGE2DPROC, glTexImage2D); P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
|
||||
P(PFNGLENABLEPROC, glEnable); P(PFNGLBLENDFUNCPROC, glBlendFunc); P(PFNGLGETERRORPROC, glGetError);
|
||||
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D); P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D); P(PFNGLGETSTRINGPROC, glGetString);
|
||||
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
|
||||
printf("BGRA ext: %s\n", strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
|
||||
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(v, 1, &vs, 0); glCompileShader(v); glShaderSource(f, 1, &fs, 0); glCompileShader(f);
|
||||
GLuint pr = glCreateProgram(); glAttachShader(pr, v); glAttachShader(pr, f); glLinkProgram(pr); glUseProgram(pr);
|
||||
float q[] = {-1,-1, 1,-1, -1,1, 1,1};
|
||||
GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0); glEnableVertexAttribArray(0);
|
||||
GLint lm = glGetUniformLocation(pr, "mode");
|
||||
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
struct { const char* name; GLint ifmt; GLenum fmt; int bpp; int mode; int storage; GLenum sized; unsigned char px[8]; } cases[] = {
|
||||
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255,0,0,255, 0,0,0,0}},
|
||||
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0,0,255,255, 0,0,0,0}},
|
||||
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0,0,255,255, 0,0,0,0}},
|
||||
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255,0,0,255, 0,0,0,0}},
|
||||
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
|
||||
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255,255, 0,0}},
|
||||
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
|
||||
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
|
||||
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);
|
||||
while (glGetError()) {}
|
||||
if (cases[i].storage) {
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
|
||||
} else {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
|
||||
}
|
||||
GLenum err = glGetError();
|
||||
glUniform1i(lm, cases[i].mode);
|
||||
glViewport(0, 0, 64, 64); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
unsigned char l[4], r[4];
|
||||
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
|
||||
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
|
||||
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
|
||||
}
|
||||
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
|
||||
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
|
||||
struct { const char* name; GLenum sized, fmt; int bpp; GLint sw[4]; unsigned char px[4]; unsigned char wantL[3], wantR[3]; } sz[] = {
|
||||
{"R8 swizzled as ALPHA8", GL_R8, GL_RED, 1, {GL_ZERO, GL_ZERO, GL_ZERO, GL_RED}, {255, 0}, {0,0,0}, {255,255,255}},
|
||||
{"RG8 swizzled as LUM_ALPHA", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {255,255, 0,0}, {255,255,255}, {255,255,255}},
|
||||
{"RG8 as LUM_ALPHA (lum 0)", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {0,255, 0,0}, {0,0,0}, {255,255,255}},
|
||||
};
|
||||
glUniform1i(lm, 0);
|
||||
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
|
||||
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
|
||||
GLenum err = glGetError();
|
||||
glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
unsigned char l[4], r[4];
|
||||
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
|
||||
int ok = 1;
|
||||
for (int k = 0; k < 3; ++k) ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
|
||||
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
|
||||
//
|
||||
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
|
||||
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
|
||||
//
|
||||
// What it does, in an ES 3.0 context on a pbuffer:
|
||||
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
|
||||
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
|
||||
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
|
||||
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
|
||||
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
|
||||
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
|
||||
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
|
||||
// again: the new order must show (the server refreshes a foreign buffer at each frame).
|
||||
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
|
||||
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
|
||||
// check is that every call succeeds).
|
||||
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
|
||||
#define _GNU_SOURCE
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <fcntl.h>
|
||||
#include <gbm.h>
|
||||
#include <linux/dma-buf.h>
|
||||
#include <linux/dma-heap.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef DRM_FORMAT_NV12
|
||||
#define DRM_FORMAT_NV12 0x3231564E
|
||||
#endif
|
||||
#ifndef DRM_FORMAT_P010
|
||||
#define DRM_FORMAT_P010 0x30313050
|
||||
#endif
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL: " __VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
|
||||
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
|
||||
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
|
||||
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
|
||||
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
|
||||
static PFNEGLCREATESYNCKHRPROC pCreateSync;
|
||||
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
|
||||
static EGLDisplay g_display;
|
||||
|
||||
enum { W = 256, H = 128, BARS = 4 };
|
||||
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
|
||||
|
||||
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
|
||||
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
|
||||
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
|
||||
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
|
||||
const double y = kr * r + kg * g + kb * b;
|
||||
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
|
||||
const double scale = (double)(1 << (bits - 8));
|
||||
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
|
||||
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
|
||||
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
|
||||
}
|
||||
|
||||
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
|
||||
// samples with the 10 significant bits high.
|
||||
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
|
||||
const int bits = bytes == 1 ? 8 : 10;
|
||||
for (int y = 0; y < H; ++y) {
|
||||
for (int x = 0; x < W; ++x) {
|
||||
int bar = x * BARS / W;
|
||||
if (reverse) bar = BARS - 1 - bar;
|
||||
int code[3];
|
||||
Encode709(kBars[bar], bits, code);
|
||||
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
|
||||
if (bytes == 1) py[0] = (uint8_t)code[0];
|
||||
else *(uint16_t*)py = (uint16_t)(code[0] << 6);
|
||||
if ((x & 1) == 0 && (y & 1) == 0) {
|
||||
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
|
||||
if (bytes == 1) {
|
||||
pc[0] = (uint8_t)code[1];
|
||||
pc[1] = (uint8_t)code[2];
|
||||
} else {
|
||||
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
|
||||
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint Compile(GLenum type, const char* source) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, NULL);
|
||||
glCompileShader(shader);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
|
||||
printf("shader: %s\n", log);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
static GLuint g_program, g_fbo, g_rgba;
|
||||
|
||||
static void SetUpDraw(void) {
|
||||
const char* vs = "#version 300 es\n"
|
||||
"out vec2 uv;\n"
|
||||
"void main() {\n"
|
||||
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
|
||||
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
|
||||
" uv = p;\n"
|
||||
"}\n";
|
||||
const char* fs = "#version 300 es\n"
|
||||
"#extension GL_OES_EGL_image_external_essl3 : require\n"
|
||||
"precision mediump float;\n"
|
||||
"uniform samplerExternalOES tex;\n"
|
||||
"in vec2 uv;\n"
|
||||
"out vec4 color;\n"
|
||||
"void main() { color = texture(tex, uv); }\n";
|
||||
g_program = glCreateProgram();
|
||||
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
|
||||
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
|
||||
glLinkProgram(g_program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
|
||||
CHECK(linked, "the samplerExternalOES program did not link");
|
||||
GLint type = 0, size = 0;
|
||||
char name[64];
|
||||
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
|
||||
printf("uniform 0: %s type 0x%04x\n", name, type);
|
||||
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
|
||||
glGenTextures(1, &g_rgba);
|
||||
glBindTexture(GL_TEXTURE_2D, g_rgba);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
|
||||
glGenFramebuffers(1, &g_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
|
||||
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
|
||||
GLuint vao;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
}
|
||||
|
||||
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
|
||||
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
|
||||
glViewport(0, 0, W, H);
|
||||
glUseProgram(g_program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
static unsigned char pixels[W * H * 4];
|
||||
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
GLenum error = glGetError();
|
||||
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
|
||||
if (tolerance < 0) {
|
||||
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
|
||||
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
|
||||
pixels[(H / 2 * W + W / 2) * 4 + 3]);
|
||||
return;
|
||||
}
|
||||
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
|
||||
// so any row does; take two to catch a broken chroma row pitch.
|
||||
const int rows[2] = {H / 4, (3 * H) / 4};
|
||||
for (int r = 0; r < 2; ++r) {
|
||||
for (int bar = 0; bar < BARS; ++bar) {
|
||||
const int x = bar * (W / BARS) + W / (2 * BARS);
|
||||
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
|
||||
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
|
||||
int worst = 0;
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
int d = abs((int)p[c] - (int)want[c]);
|
||||
if (d > worst) worst = d;
|
||||
}
|
||||
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
|
||||
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
|
||||
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int AllocateDmaHeap(size_t size) {
|
||||
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
|
||||
if (heap < 0) {
|
||||
perror("open /dev/dma_heap/system");
|
||||
return -1;
|
||||
}
|
||||
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
|
||||
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
|
||||
perror("DMA_HEAP_IOCTL_ALLOC");
|
||||
close(heap);
|
||||
return -1;
|
||||
}
|
||||
close(heap);
|
||||
return (int)data.fd;
|
||||
}
|
||||
|
||||
static void Sync(int fd, int start) {
|
||||
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
|
||||
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
|
||||
}
|
||||
|
||||
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
|
||||
EGLint attribs[64];
|
||||
int n = 0;
|
||||
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
|
||||
A(EGL_WIDTH, W);
|
||||
A(EGL_HEIGHT, H);
|
||||
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
|
||||
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
|
||||
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
|
||||
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
|
||||
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
|
||||
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
|
||||
if (modifier) {
|
||||
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
|
||||
}
|
||||
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
|
||||
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
|
||||
attribs[n] = EGL_NONE;
|
||||
#undef A
|
||||
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
|
||||
return image;
|
||||
}
|
||||
|
||||
static GLuint ExternalTexture(EGLImageKHR image) {
|
||||
GLuint texture;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
|
||||
GLenum error = glGetError();
|
||||
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
|
||||
GLint bound = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
|
||||
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A frame boundary as an offscreen producer makes one: a native fence.
|
||||
static void EndFrame(void) {
|
||||
EGLint attribs[] = {EGL_NONE};
|
||||
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
|
||||
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
|
||||
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
|
||||
}
|
||||
|
||||
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
|
||||
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
|
||||
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
|
||||
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
|
||||
const uint32_t offsetC = pitchY * (H + 16);
|
||||
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
|
||||
int fd = AllocateDmaHeap(size);
|
||||
if (fd < 0) {
|
||||
CHECK(0, "%s: no dma-buf to test with", what);
|
||||
return;
|
||||
}
|
||||
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
|
||||
if (map == MAP_FAILED) return;
|
||||
Sync(fd, 1);
|
||||
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
|
||||
Sync(fd, 0);
|
||||
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
|
||||
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
|
||||
if (image == EGL_NO_IMAGE_KHR) return;
|
||||
GLuint texture = ExternalTexture(image);
|
||||
DrawAndCheck(texture, what, 0, tolerance);
|
||||
EndFrame();
|
||||
// New content, same buffer: shows after the frame boundary.
|
||||
Sync(fd, 1);
|
||||
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
|
||||
Sync(fd, 0);
|
||||
EndFrame();
|
||||
DrawAndCheck(texture, what, 1, tolerance);
|
||||
glDeleteTextures(1, &texture);
|
||||
pDestroyImage(g_display, image);
|
||||
munmap(map, size);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void GbmTest(void) {
|
||||
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
|
||||
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
|
||||
struct gbm_device* gbm = gbm_create_device(node);
|
||||
if (gbm == NULL) {
|
||||
CHECK(0, "gbm_create_device failed");
|
||||
return;
|
||||
}
|
||||
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
|
||||
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
|
||||
printf("gbm NV12 supported: %d, P010: %d\n", supported,
|
||||
gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
|
||||
CHECK(supported == 1, "gbm does not support NV12");
|
||||
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
|
||||
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
|
||||
if (bo != NULL) {
|
||||
const int planes = gbm_bo_get_plane_count(bo);
|
||||
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
|
||||
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
|
||||
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
|
||||
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
|
||||
int fd = gbm_bo_get_fd(bo);
|
||||
EGLImageKHR image =
|
||||
Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
|
||||
gbm_bo_get_stride_for_plane(bo, 1), 0);
|
||||
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
|
||||
if (image != EGL_NO_IMAGE_KHR) {
|
||||
GLuint texture = ExternalTexture(image);
|
||||
DrawAndCheck(texture, "gbm NV12", 0, -1);
|
||||
glDeleteTextures(1, &texture);
|
||||
pDestroyImage(g_display, image);
|
||||
}
|
||||
close(fd);
|
||||
gbm_bo_destroy(bo);
|
||||
}
|
||||
gbm_device_destroy(gbm);
|
||||
close(node);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
EGLint major, minor;
|
||||
if (!eglInitialize(g_display, &major, &minor)) {
|
||||
printf("FAIL: eglInitialize\n");
|
||||
return 2;
|
||||
}
|
||||
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
|
||||
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
|
||||
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
|
||||
eglBindAPI(EGL_OPENGL_ES_API);
|
||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
|
||||
EGLConfig config;
|
||||
EGLint count = 0;
|
||||
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
|
||||
if (count < 1) {
|
||||
printf("FAIL: no config\n");
|
||||
return 2;
|
||||
}
|
||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
|
||||
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
|
||||
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
||||
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
|
||||
printf("FAIL: no ES 3 context\n");
|
||||
return 2;
|
||||
}
|
||||
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
|
||||
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
|
||||
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
|
||||
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
|
||||
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
|
||||
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
|
||||
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
|
||||
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
|
||||
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
|
||||
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
|
||||
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
|
||||
|
||||
EGLint formats[32], formatCount = 0;
|
||||
pQueryFormats(g_display, 32, formats, &formatCount);
|
||||
int nv12 = 0;
|
||||
for (int i = 0; i < formatCount; ++i) {
|
||||
printf("dma-buf format %.4s", (const char*)&formats[i]);
|
||||
EGLuint64KHR modifiers[4];
|
||||
EGLBoolean externalOnly[4];
|
||||
EGLint modifierCount = 0;
|
||||
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
|
||||
for (int m = 0; m < modifierCount; ++m)
|
||||
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
|
||||
printf("\n");
|
||||
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
|
||||
}
|
||||
CHECK(nv12, "NV12 is not a listed dma-buf format");
|
||||
|
||||
SetUpDraw();
|
||||
ForeignTest(DRM_FORMAT_NV12, 1, 12);
|
||||
ForeignTest(DRM_FORMAT_P010, 2, 16);
|
||||
GbmTest();
|
||||
|
||||
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
|
||||
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglTerminate(g_display);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# push-tools.sh - install this skill's device- and container-side helpers on the phone.
|
||||
# device/*.sh -> $DEV_TOOLS (run as root: adb shell 'su -c "sh $DEV_TOOLS/<x>.sh"')
|
||||
# container/mgrun -> <container>/usr/local/bin/mgrun
|
||||
# container/kwprep.sh -> <container>/root/kwprep.sh
|
||||
# Needs the container running (its rootfs is mounted at $CT_ROOT only then).
|
||||
# Overwrites same-named files other sessions may be using in $DEV_TOOLS; check first on a shared phone.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
adb_ shell "mkdir -p $DEV_TOOLS"
|
||||
for f in "$here"/device/*.sh; do
|
||||
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
|
||||
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" >/dev/null
|
||||
echo "pushed $DEV_TOOLS/$(basename "$f")"
|
||||
done
|
||||
for f in mgrun kwprep.sh; do
|
||||
tr -d '\r' < "$here/container/$f" > "$ANL_DIR/push.tmp"
|
||||
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$f" >/dev/null
|
||||
done
|
||||
rm -f "$ANL_DIR/push.tmp"
|
||||
adb_ shell "su -c 'cp $DEV_TOOLS/mgrun $CT_ROOT/usr/local/bin/mgrun && chmod 755 $CT_ROOT/usr/local/bin/mgrun && cp $DEV_TOOLS/kwprep.sh $CT_ROOT/root/kwprep.sh && echo installed mgrun kwprep.sh'"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# rec.sh <name> - record 3 s of the screen while opening Kickoff, extract frames inside the container
|
||||
# with ffmpeg, pull them to $ANL_DIR/af_<name>/ and build a 3x4 contact sheet $ANL_DIR/grid_<name>.png
|
||||
# (needs Python + Pillow on Windows). Use it to catch flicker/black frames a single screenshot misses.
|
||||
# Env: TAP="x y" Kickoff button position (default "69 1349" = bottom-left on the 2560x1600 tablet;
|
||||
# device specific). Note: anland grabs the touchscreen (EVIOCGRAB); if `input tap` does nothing, use
|
||||
# `input mouse tap x y`. Env CHANNEL (default rec).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
N=${1:?usage: rec.sh <name>}; TAP=${TAP:-69 1349}
|
||||
adb_ shell "su -c 'input keyevent 111; sleep 1.5; (screenrecord --time-limit 3 --bit-rate 20000000 $DEV_SHARE/$N.mp4 &); sleep 0.7; input tap $TAP; sleep 3; cp $DEV_SHARE/$N.mp4 $CT_ROOT/root/$N.mp4'"
|
||||
bash "$here/ct.sh" "${CHANNEL:-rec}" <<IN
|
||||
rm -rf /root/af_$N; mkdir -p /root/af_$N; ffmpeg -loglevel quiet -i /root/$N.mp4 -vf scale=1000:-1 -fps_mode passthrough /root/af_$N/f%03d.png; ls /root/af_$N | wc -l
|
||||
IN
|
||||
adb_ shell "su -c 'rm -rf $DEV_SHARE/af_$N; cp -r $CT_ROOT/root/af_$N $DEV_SHARE/ && chmod -R 777 $DEV_SHARE/af_$N'"
|
||||
rm -rf "$ANL_DIR/af_$N"; adb_ pull "$DEV_SHARE/af_$N" "$ANL_DIR/" >/dev/null
|
||||
python - "$ANL_DIR/af_$N" "$ANL_DIR/grid_$N.png" <<'P'
|
||||
import sys, os
|
||||
from PIL import Image
|
||||
d, out = sys.argv[1], sys.argv[2]
|
||||
fs = sorted(os.listdir(d)); n = len(fs)
|
||||
pick = [fs[min(n-1, int(i * n / 12))] for i in range(12)]
|
||||
ims = [Image.open(os.path.join(d, f)) for f in pick]
|
||||
w, h = ims[0].size; tw, th = w // 2, h // 2
|
||||
g = Image.new('RGB', (tw * 3, th * 4))
|
||||
for i, im in enumerate(ims): g.paste(im.resize((tw, th)), ((i % 3) * tw, (i // 3) * th))
|
||||
g.save(out); print(n, pick)
|
||||
P
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# shot.sh <name> [count] [interval-s] - take <count> (default 2) phone screenshots <interval> s apart
|
||||
# (default 1), pull them to $ANL_DIR/<name>-<i>.png and print their md5. Identical hashes while an
|
||||
# animated client (glmark2, a video) runs mean the frames are NOT reaching the screen.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
name=${1:?usage: shot.sh <name> [count] [interval]}; n=${2:-2}; dt=${3:-1}
|
||||
cmd=""; i=1
|
||||
while [ "$i" -le "$n" ]; do cmd="$cmd screencap -p $DEV_SHARE/$name-$i.png; sleep $dt;"; i=$((i+1)); done
|
||||
adb_ shell "su -c '$cmd'"
|
||||
i=1
|
||||
while [ "$i" -le "$n" ]; do
|
||||
adb_ pull "$DEV_SHARE/$name-$i.png" "$ANL_DIR/$name-$i.png" >/dev/null
|
||||
md5sum "$ANL_DIR/$name-$i.png"; i=$((i+1))
|
||||
done
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-src.sh - FALLBACK path only (the WSL cross build, xbuild.sh, replaced it). Pushes the files the
|
||||
# worktree changed since the container's source stamp into the container's MobileGL source tree
|
||||
# (/root/mobilegl-unified-src), keeping mtimes so the in-container ninja build stays incremental;
|
||||
# then build/install inside the container with anland's anland-build-client.sh
|
||||
# (producers/kde/Arch_v5/mobilegl-tools/, gcc, -j4, slow on the phone).
|
||||
# A full re-sync of the tree is anland's anland-sync-source.sh (MOBILEGL_REPO=<worktree>).
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
SRC=$CT_ROOT/root/mobilegl-unified-src
|
||||
STAMP=$(adb_ shell "su -c 'cat $SRC/.anland-build-stamp'" | tr -d '\r\n')
|
||||
cd "$MGL_WORKTREE"
|
||||
FILES=$( (git diff --name-only --diff-filter=d "$STAMP"; git ls-files --others --exclude-standard) \
|
||||
| grep -v -E '^(3rdparty/|build-android|\.anland-build-stamp|\.claude/)' | sort -u)
|
||||
echo "$(echo "$FILES" | wc -l) files since $STAMP"
|
||||
tar --force-local -czf "$ANL_DIR/src-delta.tgz" $FILES
|
||||
adb_ push "$ANL_DIR/src-delta.tgz" /data/local/tmp/src-delta.tgz >/dev/null
|
||||
HEAD=$(git rev-parse HEAD)
|
||||
adb_ shell "su -c 'tar xzf /data/local/tmp/src-delta.tgz -C $SRC && echo $HEAD > $SRC/.anland-build-stamp && echo synced'"
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# wsl-all.sh - run INSIDE WSL archlinux after wsl-test.sh configured $B: build every *Test target except
|
||||
# DirectVulkan*/Benchmark* and run the unit label. NEVER run the DirectVulkan integration tests
|
||||
# (they need a live device + manual intervention); never run two ctest suites at once.
|
||||
B=${B:-$HOME/mgl-anl-bld}
|
||||
cd $B
|
||||
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
|
||||
ninja -j14 MobileGLServer $T 2>&1 | grep -E "error:|FAILED" | head -10
|
||||
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" 2>&1 | tail -25
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# wsl-test.sh <targets...> - run INSIDE WSL archlinux: configure (once) and build MobileGL unit-test
|
||||
# targets for the host (clang, disaggregated + in-process server ON). From Git Bash:
|
||||
# MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest
|
||||
# then ctest in $B (see wsl-all.sh). Env: W (worktree, WSL path; default: derived from this file),
|
||||
# B (build dir, default ~/mgl-anl-bld). The Windows MSVC build of feat/disaggregated is broken; use this.
|
||||
set -e
|
||||
W=${W:-$(cd "$(dirname "$0")/../../../.." && pwd)}
|
||||
B=${B:-$HOME/mgl-anl-bld}
|
||||
if [ ! -f $B/build.ninja ]; then
|
||||
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl
|
||||
fi
|
||||
cmake --build $B --target "$@" -j16
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/archive/notes/anland/
|
||||
# plan-x11-gpu.md): Xwayland drawing with glamor on MobileGL, and GLX windows presented through
|
||||
# DRI3 + Present from shared images. Steps, in the order a full run takes them:
|
||||
#
|
||||
# preflight read-only: battery, stack status, Xwayland package, render node, xcb libs,
|
||||
# test apps, Xwayland's extensions and its GLX vendor
|
||||
# measure <label> [s] glxgears 1280x720 (GEARS_ARGS; vblank_mode=0) for s seconds (10): its fps, then CPU/GPU/dma-buf
|
||||
# counts (device/x11-measure.sh); also with MOBILEGL_GLX_PRESENT=readback
|
||||
# deploy-client xbuild.sh + xdeploy.sh of this worktree's client (MGL_XBUILD_DIR)
|
||||
# deploy-kwin kwin-stage.sh build from ANLAND_WORKTREE; waits for "== ALL DONE =="
|
||||
# deploy-server build-android.sh + cyc.sh x11-server (swaps the APK lib, RESTARTS the session)
|
||||
# check the evidence: Xwayland took glamor (journal), its MobileGL log, GLX's path
|
||||
# line, dma-buf counts in Xwayland/KWin, the server's import lines
|
||||
# apps X11 clients on screen: konsole and kate on Qt's xcb platform (glamor-drawn
|
||||
# core X), glxgears (GLX), xterm/xeyes when installed; two screenshots
|
||||
# stop kill the test apps
|
||||
#
|
||||
# Typical run: preflight; measure before; deploy-client; deploy-kwin; deploy-server; check; measure
|
||||
# after; apps; stop. Battery: every load step refuses below MIN_BATTERY (40) %.
|
||||
# Env: CHANNEL (ct.sh channel, default x11), ANLAND_WORKTREE (anland x11-gpu worktree, Windows
|
||||
# form), MGL_XBUILD_DIR (default /home/swung/mgl-xbuild-x11), MIN_BATTERY.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
CH=${CHANNEL:-x11}
|
||||
: "${MGL_XBUILD_DIR:=/home/swung/mgl-xbuild-x11}"
|
||||
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/x11-gpu}"
|
||||
export MGL_XBUILD_DIR ANLAND_WORKTREE
|
||||
|
||||
battery_ok() {
|
||||
local level
|
||||
level=$(adb_ shell "dumpsys battery" | sed -n 's/^ *level: *//p' | tr -d '\r')
|
||||
echo "battery: ${level}%"
|
||||
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
|
||||
echo "below ${MIN_BATTERY:-40}%: stopping GPU-heavy work; charge the phone first" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
push_measure() {
|
||||
tr -d '\r' < "$here/device/x11-measure.sh" > "$ANL_DIR/x11-measure.sh"
|
||||
adb_ push "$ANL_DIR/x11-measure.sh" "$DEV_TOOLS/x11-measure.sh" >/dev/null
|
||||
}
|
||||
|
||||
step=${1:?usage: x11-verify.sh <preflight|measure|deploy-client|deploy-kwin|deploy-server|check|apps|stop>}
|
||||
shift
|
||||
case "$step" in
|
||||
preflight)
|
||||
battery_ok || true
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
|
||||
bash "$here/ct.sh" "$CH" <<'IN'
|
||||
echo "== packages"; pacman -Q xorg-xwayland libxcb mesa-utils xorg-xdpyinfo qt6-base 2>&1 | sed 's/^/ /'
|
||||
echo "== render nodes"; ls -l /dev/dri/ 2>&1 | sed 's/^/ /'
|
||||
U=swung0x48; for n in /dev/dri/renderD*; do sudo -u $U test -r "$n" -a -w "$n" && echo " $U can open $n"; done
|
||||
echo "== KWin's thin node"; tr '\0' '\n' < /proc/$(pgrep -x kwin_wayland)/environ 2>/dev/null | grep -E "MOBILEGL_GBM_NODE|ANLAND_XWAYLAND_GLAMOR|GBM_BACKEND" | sed 's/^/ /'
|
||||
echo "== xcb libraries"; for l in libxcb-dri3.so.0 libxcb-present.so.0 libxcb-xfixes.so.0 libxcb-shm.so.0; do ls /usr/lib/$l >/dev/null 2>&1 && echo " $l ok" || echo " $l MISSING"; done
|
||||
echo "== Xwayland"; pgrep -a -x Xwayland | sed 's/^/ /'
|
||||
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM|XFIXES|GLX)$" | sed 's/^/ ext/'
|
||||
mgrun offscreen glxinfo -B 2>&1 | grep -E "vendor|renderer|direct rendering" | sed 's/^/ /'
|
||||
IN
|
||||
;;
|
||||
measure)
|
||||
label=${1:?usage: x11-verify.sh measure <label> [seconds]}
|
||||
secs=${2:-10}
|
||||
battery_ok
|
||||
push_measure
|
||||
for mode in auto readback; do
|
||||
printf '%s\n' \
|
||||
"pkill -x glxgears; rm -f /tmp/mgl-x11-gears.client.log /tmp/x11-gears.out" \
|
||||
"MGLOG=/tmp/mgl-x11-gears.log timeout $((secs + 8)) mgrun offscreen env vblank_mode=${VBLANK_MODE:-0} MOBILEGL_GLX_PRESENT=$mode glxgears ${GEARS_ARGS:--geometry 1280x720} > /tmp/x11-gears.out 2>&1 &" \
|
||||
"sleep 3" | bash "$here/ct.sh" "$CH" >/dev/null
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/x11-measure.sh $label-$mode glxgears $secs'" || true
|
||||
sleep 6
|
||||
printf '%s\n' "grep -a 'frames in' /tmp/x11-gears.out | tail -3; grep -a 'presented through' /tmp/mgl-x11-gears.client.log | head -1; pkill -x glxgears; true" \
|
||||
| bash "$here/ct.sh" "$CH"
|
||||
done
|
||||
;;
|
||||
deploy-client)
|
||||
bash "$here/xbuild.sh"
|
||||
CHANNEL=$CH bash "$here/xdeploy.sh"
|
||||
;;
|
||||
deploy-kwin)
|
||||
CHANNEL=$CH bash "$here/kwin-stage.sh" build
|
||||
for _ in $(seq 1 120); do
|
||||
out=$(printf 'tail -3 /root/kwin-build.log\n' | bash "$here/ct.sh" "$CH" 2>/dev/null || true)
|
||||
echo "$out" | tail -1
|
||||
echo "$out" | grep -q "== ALL DONE ==" && exit 0
|
||||
echo "$out" | grep -qiE "error|failed" && { echo "KWin build failed" >&2; exit 1; }
|
||||
sleep 30
|
||||
done
|
||||
echo "KWin build did not finish in an hour" >&2; exit 1
|
||||
;;
|
||||
deploy-server)
|
||||
battery_ok
|
||||
bash "$here/build-android.sh"
|
||||
CHANNEL=$CH bash "$here/cyc.sh" x11-server
|
||||
;;
|
||||
check)
|
||||
bash "$here/ct.sh" "$CH" <<'IN'
|
||||
echo "== Xwayland's command line (no -shm with glamor)"; pgrep -a -x Xwayland | sed 's/^/ /'
|
||||
echo "== Xwayland's glamor (journal)"
|
||||
journalctl --no-pager --since -30min 2>/dev/null | grep -aiE "glamor|Xwayland.*(EGL|gbm|dri3|falling back)" | tail -10 | sed 's/^/ /'
|
||||
echo "== Xwayland's MobileGL client log"; grep -aE "presented|shared image|declin|Fatal|ERROR" /tmp/mgl-xwayland.client.log /tmp/mgl-xwayland.client 2>/dev/null | tail -8 | sed 's/^/ /' || echo " (none)"
|
||||
X=$(pgrep -x Xwayland); K=$(pgrep -x kwin_wayland)
|
||||
echo "== dma-bufs held: Xwayland $(ls -l /proc/$X/fd 2>/dev/null | grep -c dmabuf), kwin $(ls -l /proc/$K/fd 2>/dev/null | grep -c dmabuf)"
|
||||
echo "== Xwayland's GLX vendor and extensions"
|
||||
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM)$" | sed 's/^/ ext/'
|
||||
echo "== a GLX window's path"
|
||||
rm -f /tmp/mgl-x11-check.client.log
|
||||
MGLOG=/tmp/mgl-x11-check.log timeout 6 mgrun offscreen glxgears >/dev/null 2>&1
|
||||
grep -a "presented through\|leaves DRI3" /tmp/mgl-x11-check.client.log | sed 's/^/ /'
|
||||
IN
|
||||
adb_ logcat -d | grep -aE "MobileGL.*(shared image|dma-buf import|refused)" | tail -8 || true
|
||||
adb_ logcat -b crash -d | tail -5 || true
|
||||
;;
|
||||
apps)
|
||||
battery_ok
|
||||
printf '%s\n' \
|
||||
"pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate" \
|
||||
"MGLOG=/tmp/mgl-x11-konsole.log mgrun offscreen env QT_QPA_PLATFORM=xcb konsole >/dev/null 2>&1 &" \
|
||||
"MGLOG=/tmp/mgl-x11-kate.log mgrun offscreen env QT_QPA_PLATFORM=xcb kate >/dev/null 2>&1 &" \
|
||||
"command -v xterm >/dev/null && MGLOG=/tmp/mgl-x11-xterm.log mgrun offscreen xterm >/dev/null 2>&1 &" \
|
||||
"command -v xeyes >/dev/null && MGLOG=/tmp/mgl-x11-xeyes.log mgrun offscreen xeyes >/dev/null 2>&1 &" \
|
||||
"MGLOG=/tmp/mgl-x11-gears.log mgrun offscreen glxgears >/dev/null 2>&1 &" \
|
||||
"sleep 6; pgrep -a -x konsole; pgrep -a -x kate; pgrep -a -x xterm; pgrep -a -x xeyes; pgrep -a -x glxgears; true" | bash "$here/ct.sh" "$CH"
|
||||
bash "$here/shot.sh" x11-apps 2 1
|
||||
;;
|
||||
stop)
|
||||
printf '%s\n' "pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate; true" | bash "$here/ct.sh" "$CH"
|
||||
;;
|
||||
*)
|
||||
echo "unknown step $step" >&2; exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# xbuild.sh [--reconfigure] [extra ninja targets...] - cross-build the container CLIENT: libMobileGL.so
|
||||
# (glibc aarch64, glvnd EGL/GLX vendor) + mobilegl_gbm.so (GBM backend), in WSL archlinux with
|
||||
# clang + lld against a sysroot exported from the container (xsysroot/refresh-sysroot.sh).
|
||||
# Full build ~150 s, incremental seconds. Deploy with xdeploy.sh.
|
||||
# Env: MGL_WORKTREE (source, Windows form), MGL_XBUILD_DIR (WSL build dir, default ~/mgl-xbuild-a64),
|
||||
# MGL_JOBS (16), WSL_DISTRO (archlinux).
|
||||
# Prereq in WSL: clang, lld, llvm (lld major MUST equal clang/llvm-libs major - lld 23 on llvm 22
|
||||
# broke ld.lld), cmake, ninja; ~/sysroots/aarch64-arch.cmake + ~/sysroots/arch-kde-mgl.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
STAMP=$(git -C "$MGL_WORKTREE" rev-parse HEAD)
|
||||
RECONF=0
|
||||
if [ "${1:-}" = "--reconfigure" ]; then RECONF=1; shift; fi
|
||||
EXTRA="$*"
|
||||
SRC_WSL=$(win2wsl "$MGL_WORKTREE")
|
||||
wsl -d "${WSL_DISTRO:-archlinux}" -- env SRC="$SRC_WSL" STAMP="$STAMP" RECONF="$RECONF" EXTRA="$EXTRA" \
|
||||
OUT="${MGL_XBUILD_DIR:-}" JOBS="${MGL_JOBS:-16}" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
|
||||
set -euo pipefail
|
||||
OUT=${OUT:-$HOME/mgl-xbuild-a64}
|
||||
TC=$HOME/sysroots/aarch64-arch.cmake
|
||||
[ -f "$TC" ] || { echo "missing toolchain $TC (run xsysroot/refresh-sysroot.sh)"; exit 1; }
|
||||
ld.lld --version >/dev/null || { echo "ld.lld is not runnable in WSL (lld/llvm version mismatch?)"; exit 1; }
|
||||
t0=$(date +%s)
|
||||
if [ "$RECONF" = 1 ] || [ ! -f "$OUT/build.ninja" ]; then
|
||||
rm -rf "$OUT/CMakeCache.txt" "$OUT/CMakeFiles"
|
||||
cmake -S "$SRC" -B "$OUT" -G Ninja --toolchain "$TC" -DMOBILEGL_BUILD_STAMP="$STAMP" \
|
||||
-DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_ENABLE_LTO=OFF
|
||||
elif ! grep -qx "MOBILEGL_BUILD_STAMP:STRING=$STAMP" "$OUT/CMakeCache.txt"; then
|
||||
cmake -S "$SRC" -B "$OUT" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
|
||||
fi
|
||||
t1=$(date +%s)
|
||||
cmake --build "$OUT" --target MobileGL mobilegl_gbm $EXTRA -j"$JOBS"
|
||||
t2=$(date +%s)
|
||||
echo "stamp: $STAMP"
|
||||
echo "configure: $((t1-t0))s build: $((t2-t1))s"
|
||||
for f in "$OUT/libMobileGL.so" "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so"; do
|
||||
ls -la "$f"; echo " windows: $(wslpath -w "$f")"
|
||||
done
|
||||
IN
|
||||
exit "${PIPESTATUS[0]}"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# xdeploy.sh - install the WSL cross-built libMobileGL.so + mobilegl_gbm.so into the container, exactly
|
||||
# like anland's anland-build-client.sh: copy to a NEW inode and rename into place (overwriting a mapped
|
||||
# .so in place crashes every running client, KWin included), refresh the libEGL/libGL aliases, the
|
||||
# glvnd GLX vendor link, the EGL vendor JSON (system-wide 10_mobilegl.json + the /opt copy), the client
|
||||
# config /etc/mobilegl/client.conf and the GLX/GBM selection drop-ins. Does not restart anything: already-running clients
|
||||
# keep the old inode; restart the client (or the session) to pick up the new library.
|
||||
# Env: MGL_XBUILD_DIR (WSL build dir), CHANNEL (ct.sh channel, default xd), WSL_DISTRO.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
mkdir -p "$ANL_DIR/xdist"
|
||||
# 1. Copy the build outputs out of WSL.
|
||||
wsl -d "${WSL_DISTRO:-archlinux}" -- env OUT="${MGL_XBUILD_DIR:-}" D="$(win2wsl "$ANL_DIR")/xdist" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
|
||||
set -e; OUT=${OUT:-$HOME/mgl-xbuild-a64}
|
||||
cp -f "$OUT/libMobileGL.so" "$D/libMobileGL.so"; cp -f "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so" "$D/mobilegl_gbm.so"
|
||||
sha256sum "$D/libMobileGL.so" "$D/mobilegl_gbm.so"
|
||||
IN
|
||||
# 2. Push to the phone and stage them in the container's /root/xdeploy (the container's /tmp and the
|
||||
# phone's /data/local/tmp are different places; the rootfs is reachable at $CT_ROOT).
|
||||
adb_ shell "mkdir -p $DEV_TOOLS/xdist"
|
||||
adb_ push "$ANL_DIR/xdist/libMobileGL.so" "$DEV_TOOLS/xdist/libMobileGL.so"
|
||||
adb_ push "$ANL_DIR/xdist/mobilegl_gbm.so" "$DEV_TOOLS/xdist/mobilegl_gbm.so"
|
||||
adb_ shell "su -c 'mkdir -p $CT_ROOT/root/xdeploy && cp $DEV_TOOLS/xdist/libMobileGL.so $DEV_TOOLS/xdist/mobilegl_gbm.so $CT_ROOT/root/xdeploy/ && rm -f $DEV_TOOLS/xdist/*.so'"
|
||||
# 3. Install inside the container.
|
||||
bash "$here/ct.sh" "${CHANNEL:-xd}" <<'IN'
|
||||
set -euo pipefail
|
||||
in=/root/xdeploy
|
||||
mkdir -p /opt/mobilegl/lib /opt/mobilegl/share/glvnd/egl_vendor.d
|
||||
cp "$in/libMobileGL.so" /opt/mobilegl/lib/libMobileGL.so.new
|
||||
mv -f /opt/mobilegl/lib/libMobileGL.so.new /opt/mobilegl/lib/libMobileGL.so
|
||||
for lib in libEGL.so libEGL.so.1 libGL.so libGL.so.1; do ln -sf libMobileGL.so "/opt/mobilegl/lib/$lib"; done
|
||||
ln -sf /opt/mobilegl/lib/libMobileGL.so /usr/lib/libGLX_mobilegl.so.0
|
||||
vendor='{"file_format_version":"1.0.0","ICD":{"library_path":"/opt/mobilegl/lib/libMobileGL.so"}}'
|
||||
# Per-process forcing (__EGL_VENDOR_LIBRARY_FILENAMES=<this file>) keeps working off the /opt copy.
|
||||
printf '%s\n' "$vendor" >/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json
|
||||
# System-wide vendor: 10_ sorts ahead of the system's 50_mesa.json, so stock libglvnd asks MobileGL
|
||||
# first and moves on to the other vendor whenever MobileGL declines (no server reachable).
|
||||
mkdir -p /usr/share/glvnd/egl_vendor.d
|
||||
printf '%s\n' "$vendor" >/usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new
|
||||
mv -f /usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new /usr/share/glvnd/egl_vendor.d/10_mobilegl.json
|
||||
# Client settings every process reads when its environment does not say otherwise (the backend
|
||||
# comes from /etc/mobilegl/backend, which switch.sh rewrites).
|
||||
mkdir -p /etc/mobilegl
|
||||
cat >/etc/mobilegl/client.conf <<'CONF'
|
||||
# MobileGL client configuration, read by every process that loads libMobileGL.so.
|
||||
# KEY=value lines; a MOBILEGL_* variable in a process's environment overrides the line here.
|
||||
# The backend is /etc/mobilegl/backend. Written by xdeploy.sh / anland-build-client.sh.
|
||||
MOBILEGL_TRANSPORT=spawn
|
||||
MOBILEGL_IPC_DATA=shm
|
||||
MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl
|
||||
CONF
|
||||
# What glvnd's libGLX and libgbm cannot infer: environment.d reaches the systemd user manager (every
|
||||
# Plasma service and what it launches, terminals included), profile.d reaches login shells.
|
||||
mkdir -p /etc/environment.d /etc/profile.d
|
||||
printf '%s\n' '__GLX_VENDOR_LIBRARY_NAME=mobilegl' 'GBM_BACKEND=mobilegl' >/etc/environment.d/10-mobilegl.conf
|
||||
printf '%s\n' 'export __GLX_VENDOR_LIBRARY_NAME=mobilegl' 'export GBM_BACKEND=mobilegl' >/etc/profile.d/mobilegl.sh
|
||||
mkdir -p /usr/lib/gbm
|
||||
cp "$in/mobilegl_gbm.so" /usr/lib/gbm/mobilegl_gbm.so.new
|
||||
mv -f /usr/lib/gbm/mobilegl_gbm.so.new /usr/lib/gbm/mobilegl_gbm.so
|
||||
rm -f "$in/libMobileGL.so" "$in/mobilegl_gbm.so"
|
||||
sha256sum /opt/mobilegl/lib/libMobileGL.so /usr/lib/gbm/mobilegl_gbm.so
|
||||
ls /usr/share/glvnd/egl_vendor.d/ /etc/mobilegl/ /etc/environment.d/
|
||||
echo DEPLOYED
|
||||
IN
|
||||
@@ -0,0 +1,47 @@
|
||||
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
|
||||
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
set(_triple aarch64-unknown-linux-gnu)
|
||||
if (DEFINED ENV{MGL_SYSROOT})
|
||||
set(_sysroot "$ENV{MGL_SYSROOT}")
|
||||
else()
|
||||
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
|
||||
endif()
|
||||
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
|
||||
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
|
||||
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
|
||||
list(GET _gcc_dirs 0 _gcc_install_dir)
|
||||
|
||||
set(CMAKE_SYSROOT "${_sysroot}")
|
||||
set(CMAKE_C_COMPILER clang)
|
||||
set(CMAKE_CXX_COMPILER clang++)
|
||||
set(CMAKE_ASM_COMPILER clang)
|
||||
set(CMAKE_C_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_AR llvm-ar)
|
||||
set(CMAKE_RANLIB llvm-ranlib)
|
||||
set(CMAKE_NM llvm-nm)
|
||||
set(CMAKE_OBJCOPY llvm-objcopy)
|
||||
set(CMAKE_STRIP llvm-strip)
|
||||
|
||||
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
|
||||
set(CMAKE_C_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_LINKER_TYPE LLD)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
|
||||
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
|
||||
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
|
||||
set(ENV{PKG_CONFIG_PATH} "")
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# xsysroot/refresh-sysroot.sh - (re)export the cross sysroot from the container into WSL
|
||||
# ~/sysroots/arch-kde-mgl and install the toolchain file as ~/sysroots/aarch64-arch.cmake.
|
||||
# Read-only on the container side (it only tars headers/libs into /root and moves the tarball out).
|
||||
# Run once on a new machine, and again after the container upgrades glibc/gcc/vulkan/libdrm/gbm;
|
||||
# then `bash xbuild.sh --reconfigure`. Env: CHANNEL (default xr), WSL_DISTRO.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/../env.sh"
|
||||
cp -f "$here/aarch64-arch.cmake" "$ANL_DIR/aarch64-arch.cmake"
|
||||
bash "$here/../ct.sh" "${CHANNEL:-xr}" <<'IN'
|
||||
set -e; cd /
|
||||
L=$(ls -d usr/lib/*crt*.o usr/lib/lib{c,m,mvec,pthread,dl,rt,util,resolv,anl,stdc++,stdc++fs,stdc++exp,supc++,gcc_s,atomic,vulkan,drm,drm_*,gbm,z}.* usr/lib/lib{c,m}_nonshared* usr/lib/ld-linux* usr/lib/pkgconfig usr/share/pkgconfig usr/share/cmake/VulkanHeaders usr/lib/cmake/Vulkan* 2>/dev/null)
|
||||
tar czf /root/xsysroot.tar.gz --exclude='usr/lib/gcc/*/*/cc1' --exclude='usr/lib/gcc/*/*/cc1plus' --exclude='usr/lib/gcc/*/*/lto1' --exclude='usr/lib/gcc/*/*/lto-wrapper' --exclude='usr/lib/gcc/*/*/collect2' --exclude='usr/lib/gcc/*/*/plugin' --exclude='usr/lib/gcc/*/*/install-tools' --exclude='usr/lib/gcc/*/*/liblto_plugin.so' lib usr/include usr/lib/gcc $L
|
||||
echo SYSROOT_TAR_DONE
|
||||
IN
|
||||
adb_ shell "su -c 'mv $CT_ROOT/root/xsysroot.tar.gz $DEV_TOOLS/xsysroot.tar.gz && chmod 644 $DEV_TOOLS/xsysroot.tar.gz'"
|
||||
adb_ pull "$DEV_TOOLS/xsysroot.tar.gz" "$ANL_DIR/xsysroot.tar.gz"
|
||||
adb_ shell "su -c 'rm -f $DEV_TOOLS/xsysroot.tar.gz'"
|
||||
wsl -d "${WSL_DISTRO:-archlinux}" -- env A="$(win2wsl "$ANL_DIR")" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
|
||||
set -e; S=$HOME/sysroots/arch-kde-mgl; rm -rf "$S.new"; mkdir -p "$S.new"
|
||||
tar xzf "$A/xsysroot.tar.gz" -C "$S.new"
|
||||
rm -rf "$S"; mv "$S.new" "$S"; cp -f "$A/aarch64-arch.cmake" "$HOME/sysroots/aarch64-arch.cmake"
|
||||
du -sh "$S"; ls "$S/usr/lib/gcc/aarch64-unknown-linux-gnu/"
|
||||
IN
|
||||
rm -f "$ANL_DIR/xsysroot.tar.gz"
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Configure Test runner
|
||||
description: Select the installed compiler and graphics stack without modifying the host
|
||||
inputs:
|
||||
gpu:
|
||||
description: Require the native GPU stack when running on 8845
|
||||
default: "false"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- shell: bash
|
||||
env:
|
||||
NEED_GPU: ${{ inputs.gpu }}
|
||||
run: |
|
||||
if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ] && [ "$NEED_GPU" = true ]; then
|
||||
for tool in clang clang++ cmake ninja ccache; do command -v "$tool"; done
|
||||
test -r /usr/share/vulkan/icd.d/dzn_icd.json
|
||||
test -e /dev/dxg
|
||||
{
|
||||
echo 'MOBILEGL_CI_NATIVE_GPU=1'
|
||||
echo 'MOBILEGL_CI_CC=clang'
|
||||
echo 'MOBILEGL_CI_CXX=clang++'
|
||||
echo 'MOBILEGL_CI_LLD=ld.lld'
|
||||
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=OFF'
|
||||
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/dzn_icd.json'
|
||||
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json'
|
||||
echo 'EGL_PLATFORM=surfaceless'
|
||||
echo 'LIBGL_ALWAYS_SOFTWARE=0'
|
||||
echo 'GALLIUM_DRIVER=d3d12'
|
||||
echo 'MESA_LOADER_DRIVER_OVERRIDE=d3d12'
|
||||
} >> "$GITHUB_ENV"
|
||||
else
|
||||
{
|
||||
echo 'MOBILEGL_CI_NATIVE_GPU=0'
|
||||
echo 'MOBILEGL_CI_CC=clang-20'
|
||||
echo 'MOBILEGL_CI_CXX=clang++-20'
|
||||
echo 'MOBILEGL_CI_LLD=ld.lld-20'
|
||||
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=ON'
|
||||
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json'
|
||||
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json'
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
@@ -44,9 +44,9 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
|
||||
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
|
||||
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_SUBGROUP_EMULATION' "$plugin_resource_text" 'V2 subgroup emulation setting'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_FRAMES_IN_FLIGHT' "$plugin_resource_text" 'V2 frames-in-flight setting'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
@@ -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.'
|
||||
|
||||
+61
-16
@@ -11,6 +11,7 @@ on:
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
- codex/fix-github-ci
|
||||
- cleanup
|
||||
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
|
||||
# workflow keeps running for docs: it hosts the doc citation lint.
|
||||
paths-ignore:
|
||||
@@ -18,6 +19,10 @@ on:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
|
||||
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
|
||||
|
||||
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
|
||||
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
|
||||
concurrency:
|
||||
@@ -37,7 +42,7 @@ jobs:
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -72,12 +77,25 @@ jobs:
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
||||
|
||||
- name: Install ccache
|
||||
timeout-minutes: 45
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ccache
|
||||
ccache --version
|
||||
|
||||
- name: Setup Android SDK
|
||||
id: netsetup84
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Setup Android SDK (retry)
|
||||
timeout-minutes: 15
|
||||
if: ${{ steps.netsetup84.outcome == 'failure' }}
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
@@ -88,8 +106,13 @@ jobs:
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
|
||||
- name: Install Android NDK
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
sdkmanager "ndk;27.3.13750724"
|
||||
for attempt in 1 2 3; do
|
||||
sdkmanager "ndk;27.3.13750724" && break
|
||||
[ "$attempt" = 3 ] && exit 1
|
||||
echo "sdkmanager attempt $attempt failed; retrying"; sleep 20
|
||||
done
|
||||
echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties
|
||||
|
||||
- name: Update glslang external sources
|
||||
@@ -102,7 +125,6 @@ jobs:
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
@@ -151,7 +173,6 @@ jobs:
|
||||
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
@@ -235,7 +256,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
|
||||
outputs:
|
||||
android: ${{ steps.trace-cases.outputs.android }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
@@ -332,6 +353,18 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Android SDK
|
||||
id: netsetup336
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Setup Android SDK (retry)
|
||||
timeout-minutes: 15
|
||||
if: ${{ steps.netsetup336.outcome == 'failure' }}
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
@@ -411,6 +444,18 @@ jobs:
|
||||
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Android SDK
|
||||
id: netsetup415
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Setup Android SDK (retry)
|
||||
timeout-minutes: 15
|
||||
if: ${{ steps.netsetup415.outcome == 'failure' }}
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
@@ -468,16 +513,14 @@ jobs:
|
||||
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_ITRP_FIX: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && 'ForceOn' || 'Auto' }}
|
||||
run: |
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
extra_retrace_args=()
|
||||
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
|
||||
--require-inproc
|
||||
)
|
||||
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
|
||||
@@ -488,17 +531,18 @@ jobs:
|
||||
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
|
||||
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
|
||||
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
|
||||
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
|
||||
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
|
||||
# value would override that. --use-pbuffer is NOT optional here: a spawned
|
||||
# server cannot be handed the client's window, because an ANativeWindow* is a pointer
|
||||
# into the CLIENT's process and SetWindowHandle is refused on the way across with
|
||||
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
|
||||
# Fatal{UnmigratedSurface, "AndroidNativeWindow:use-MOBILEGL_IPC_SURFACE=server"} (Rule H, permanent;
|
||||
# a windowed client uses MOBILEGL_IPC_SURFACE=server instead).
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
|
||||
--use-pbuffer
|
||||
--require-spawn
|
||||
)
|
||||
else
|
||||
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith")
|
||||
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}")
|
||||
fi
|
||||
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
|
||||
# min-filter downgrading on ANGLE llvmpipe.
|
||||
@@ -633,7 +677,7 @@ jobs:
|
||||
needs: retrace
|
||||
if: always()
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -731,7 +775,8 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fail before deletion if either paginated inventory cannot be read.
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
|
||||
# 50 rows per page: the jobs endpoint answers HTTP 502 for 90+ rows on a re-run attempt of a run this size.
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=50" \
|
||||
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name' \
|
||||
> "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Text hygiene
|
||||
|
||||
# CONTRIBUTING.md's checks: the pushed commits' messages, the source-text rules and ratchets, and
|
||||
# clang-format on each commit's changed lines. This is its own workflow without a concurrency
|
||||
# group, so a later push never cancels it and every push's commits are checked.
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
text-hygiene:
|
||||
name: Commit messages and source text
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TEXT_HYGIENE_BASE_BRANCH: feat/disaggregated
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
python3 -m venv "${RUNNER_TEMP}/clang-format"
|
||||
"${RUNNER_TEMP}/clang-format/bin/pip" install --quiet 'clang-format==23.1.3'
|
||||
echo "CLANG_FORMAT=${RUNNER_TEMP}/clang-format/bin/clang-format" >> "${GITHUB_ENV}"
|
||||
- name: Lint self-test
|
||||
run: python3 scripts/check_text_hygiene.py self-test
|
||||
- name: Check commits, source text and formatting
|
||||
run: python3 scripts/check_text_hygiene.py ci
|
||||
+966
-991
File diff suppressed because it is too large
Load Diff
@@ -28,3 +28,4 @@ tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
/.gradle
|
||||
/.trace-work/
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: c8036190fc...1f1100e320
Vendored
+1
-1
Submodule 3rdparty/glslang updated: d89cf443bc...d3c70bb6a1
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 */
|
||||
@@ -0,0 +1,17 @@
|
||||
# Notes for coding agents
|
||||
|
||||
Read `CONTRIBUTING.md` before changing anything. Every rule in it applies to agents.
|
||||
|
||||
@CONTRIBUTING.md
|
||||
|
||||
- Before each commit:
|
||||
1. Stage exactly what you mean to commit (no `git commit -a`).
|
||||
2. Print the scope with `python3 scripts/check_text_hygiene.py scope`.
|
||||
3. Write the message as UTF-8 without a byte-order mark, to a file outside the repository.
|
||||
4. Check it with `python3 scripts/check_text_hygiene.py message <file>`.
|
||||
5. Commit with `git commit -F <file>` once the check passes.
|
||||
- On Windows, run `python` instead of `python3`.
|
||||
- Do not copy the format of older commits. Much of the history before October 2026 breaks these rules.
|
||||
- When you brief a sub-agent, give it the exact commit format and these rules, not a paraphrase.
|
||||
- Keep working papers (briefs, reviews, scratch measurements, logs) outside the repository.
|
||||
- Add no `Co-Authored-By` or other trailers.
|
||||
+384
-136
@@ -21,7 +21,7 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
|
||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||
# gates keep (section 10.3).
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
|
||||
# The CI / debugging shape (ARCHITECTURE.md §16): both roles in ONE process, talking over the
|
||||
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
|
||||
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
|
||||
# only make sense when the two roles share an address space. It is a SUPERSET, never a
|
||||
@@ -29,18 +29,27 @@ 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)
|
||||
# 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).
|
||||
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
|
||||
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
|
||||
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
|
||||
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
|
||||
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
|
||||
# the whole migration window; it retires with the pull path itself at P13.
|
||||
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
|
||||
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) is finished: since P13 every build pushes, and
|
||||
# there is neither an option nor a macro for it (W3a removed the option, W3b the `#if` guards and
|
||||
# the definition). A -DMOBILEGL_PIPE_PUSH=OFF (or the OFF that every pre-P13 build tree holds in its
|
||||
# cache, the old default) is named and dropped rather than honoured: a warning, not an error, so an
|
||||
# existing tree reconfigures. The RUNTIME MOBILEGL_PIPE_PUSH environment bitmask is unrelated.
|
||||
if (DEFINED CACHE{MOBILEGL_PIPE_PUSH} AND NOT "$CACHE{MOBILEGL_PIPE_PUSH}")
|
||||
message(WARNING "MobileGL: MOBILEGL_PIPE_PUSH=OFF asks for the pull build, which P13 retired; "
|
||||
"ignored - every build pushes now")
|
||||
endif()
|
||||
unset(MOBILEGL_PIPE_PUSH CACHE)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; never shipped" OFF)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6) - MOBILEGL_PIPE_LEGACY_MEMOS - retired with the
|
||||
# pull path in P13 W3b: the pre-handle memo arm is no longer compiled anywhere. Named and dropped,
|
||||
# like MOBILEGL_PIPE_PUSH=OFF above (every pre-P13 tree holds the old default ON in its cache).
|
||||
if (DEFINED CACHE{MOBILEGL_PIPE_LEGACY_MEMOS} AND "$CACHE{MOBILEGL_PIPE_LEGACY_MEMOS}")
|
||||
message(WARNING "MobileGL: MOBILEGL_PIPE_LEGACY_MEMOS=ON asks for the pre-handle memo arm, "
|
||||
"which P13 retired; ignored")
|
||||
endif()
|
||||
unset(MOBILEGL_PIPE_LEGACY_MEMOS CACHE)
|
||||
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
||||
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
||||
|
||||
@@ -109,12 +118,15 @@ if (ANDROID)
|
||||
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
|
||||
"check that minSdk instead of adding an override).")
|
||||
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
|
||||
# A higher level is a separate artifact of the SAME source (gradle -Pmobilegl.androidApi=29:
|
||||
# native ELF TLS instead of emulated TLS). Any post-${MOBILEGL_ANDROID_API_LEVEL} API use must
|
||||
# sit behind a compile-time __ANDROID_API__ gate and never replace the API
|
||||
# ${MOBILEGL_ANDROID_API_LEVEL} path; the minSdk-${MOBILEGL_ANDROID_API_LEVEL} build stays the
|
||||
# enforcing compile.
|
||||
message(STATUS
|
||||
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
|
||||
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
|
||||
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
|
||||
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
|
||||
"compile.")
|
||||
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}): a separate artifact; "
|
||||
"post-${MOBILEGL_ANDROID_API_LEVEL} APIs only behind a compile-time __ANDROID_API__ gate.")
|
||||
endif()
|
||||
|
||||
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
|
||||
@@ -124,13 +136,14 @@ if (ANDROID)
|
||||
unset(_mobilegl_api_triple)
|
||||
endif()
|
||||
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO and the release optimization flags" OFF)
|
||||
|
||||
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
|
||||
# Check if ThinLTO or LTO is suppported
|
||||
include(CheckIPOSupported)
|
||||
include(CheckCCompilerFlag)
|
||||
include(CheckCXXCompilerFlag)
|
||||
include(CheckLinkerFlag)
|
||||
|
||||
check_ipo_supported(RESULT LTOSupported OUTPUT LTOError)
|
||||
|
||||
@@ -155,11 +168,49 @@ if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MO
|
||||
message(STATUS "IPO not supported: ${LTOError}")
|
||||
endif()
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MATCHES "AppleClang")
|
||||
add_compile_options(-O3 -ffunction-sections -fdata-sections)
|
||||
add_link_options(-Wl,--gc-sections)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||
# add_compile_options(/O2)
|
||||
# Every flag below is probed first, so a compiler or linker without it simply goes without.
|
||||
# add_compile_options/add_link_options set directory properties, so they act from a function.
|
||||
function(mobilegl_add_compile_flag_if_supported flag)
|
||||
string(MAKE_C_IDENTIFIER "MGL_HAS_C${flag}" c_var)
|
||||
string(MAKE_C_IDENTIFIER "MGL_HAS_CXX${flag}" cxx_var)
|
||||
check_c_compiler_flag("${flag}" ${c_var})
|
||||
check_cxx_compiler_flag("${flag}" ${cxx_var})
|
||||
if (${c_var} AND ${cxx_var})
|
||||
add_compile_options("${flag}")
|
||||
endif()
|
||||
endfunction()
|
||||
function(mobilegl_add_link_flag_if_supported flag)
|
||||
string(MAKE_C_IDENTIFIER "MGL_HAS_LINK${flag}" link_var)
|
||||
check_linker_flag(CXX "${flag}" ${link_var})
|
||||
if (${link_var})
|
||||
add_link_options("${flag}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (MSVC)
|
||||
# One COMDAT per function (/Gy) and per datum (/Gw), so /OPT:REF can drop the unreferenced
|
||||
# ones and /OPT:ICF fold the identical ones - the sections/gc/icf trio below, MSVC-spelled.
|
||||
add_compile_options(/Gy /Gw)
|
||||
add_link_options(/OPT:REF /OPT:ICF)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_compile_options(-O3)
|
||||
mobilegl_add_compile_flag_if_supported(-ffunction-sections)
|
||||
mobilegl_add_compile_flag_if_supported(-fdata-sections)
|
||||
# Hidden visibility and -Bsymbolic-functions already keep the library's calls inside it;
|
||||
# this lets the optimizer rely on that and inline across what it would otherwise have to
|
||||
# treat as interposable definitions.
|
||||
mobilegl_add_compile_flag_if_supported(-fno-semantic-interposition)
|
||||
if (APPLE)
|
||||
mobilegl_add_link_flag_if_supported(LINKER:-dead_strip)
|
||||
elseif (NOT WIN32)
|
||||
mobilegl_add_link_flag_if_supported(LINKER:--gc-sections)
|
||||
# Calls into libc and the GPU driver load their target from the GOT instead of going
|
||||
# through a PLT stub. Bionic binds everything at load time anyway.
|
||||
mobilegl_add_compile_flag_if_supported(-fno-plt)
|
||||
# Identical code folding (lld, gold). Safe mode never folds a function whose address
|
||||
# is taken, so function-pointer identity holds.
|
||||
mobilegl_add_link_flag_if_supported(LINKER:--icf=safe)
|
||||
endif()
|
||||
else ()
|
||||
add_compile_options(-O2)
|
||||
endif()
|
||||
@@ -302,6 +353,7 @@ set(SOURCE_FILES
|
||||
MobileGL/ConfigLoader.cpp
|
||||
|
||||
MobileGL/MG_Util/Debug/Log.cpp
|
||||
MobileGL/MG_Util/X11/DisplayGuard.cpp
|
||||
|
||||
MobileGL/MG_Util/Async/JobNode.cpp
|
||||
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
|
||||
@@ -357,6 +409,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
@@ -364,11 +417,15 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
@@ -395,13 +452,20 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/EtcDecoder.cpp
|
||||
|
||||
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/X11Present.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp
|
||||
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
|
||||
@@ -411,6 +475,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ShaderObjectsARB.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
@@ -431,6 +496,7 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Backend/Init.cpp
|
||||
MobileGL/MG_Backend/BackendObject.cpp
|
||||
MobileGL/MG_Backend/GpuHangWatch.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||
@@ -440,6 +506,7 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/VmaImpl.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp
|
||||
@@ -457,8 +524,10 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
|
||||
|
||||
MobileGL/MG_State/GLState/Core.cpp
|
||||
MobileGL/MG_State/GLState/ShareGroupState.cpp
|
||||
MobileGL/MG_State/EGLState/Core.cpp
|
||||
MobileGL/MG_State/GLState/ErrorState/Error.cpp
|
||||
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
|
||||
@@ -531,63 +600,34 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
|
||||
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
|
||||
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
|
||||
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
|
||||
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
|
||||
# at the CMake level from a legitimate transport-only build, which is why it is stated here
|
||||
# rather than left to whoever hits the link error. Same normal-variable form as the two
|
||||
# implications above.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
|
||||
"this configure: the split path decodes into the MGPipe applier, and the "
|
||||
"applier is what MOBILEGL_PIPE_PUSH compiles")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
# P13 W5 (docs/archive/notes/p13/W5-DESIGN.md): the record arm is part of every library - the one without
|
||||
# MG_Remote (FCL's) runs it too. Its temporary guard, MOBILEGL_BUILD_RECORD_ARM, was folded away at
|
||||
# S8 (scripts/ci/retired_switches.py keeps it from coming back).
|
||||
|
||||
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
||||
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
|
||||
# cache write, for the same reason as the disaggregated fallback above.
|
||||
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
|
||||
# A normal variable, not a forced cache write, for the same reason as the two above.
|
||||
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
|
||||
"configure: with nothing pushed it is the only arm there is")
|
||||
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.cpp
|
||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
|
||||
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
|
||||
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
|
||||
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
|
||||
# the two tables are inline variables that are zero in a pull build and nothing there
|
||||
# can reach a thunk.
|
||||
MobileGL/MG_Pipe/PipeRoute.cpp
|
||||
)
|
||||
endif()
|
||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.cpp
|
||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
|
||||
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
|
||||
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
|
||||
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
|
||||
# the two tables are inline variables that are zero in a pull build and nothing there
|
||||
# can reach a thunk.
|
||||
MobileGL/MG_Pipe/PipeRoute.cpp
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
@@ -607,9 +647,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 +678,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
# Whether the configured server is there at all, asked before a display is offered so the
|
||||
# system's EGL/GLX/GBM loaders can move on to another implementation when it is not.
|
||||
MobileGL/MG_Remote/Client/ServerProbe.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
|
||||
@@ -645,11 +696,24 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||
MobileGL/MG_Remote/Client/GpuWritePending.cpp
|
||||
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
|
||||
# P13 W5: the adoption tier's verdict (the tracker itself moved to the record arm below).
|
||||
MobileGL/MG_Remote/Client/AdoptTierChoice.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
# P14 S2: the per-session runtime and the thread routing that lets one process serve
|
||||
# several sessions (the real session a singleton accessor answers with on that
|
||||
# session's own threads).
|
||||
MobileGL/MG_Remote/Server/SessionRuntime.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
|
||||
# Shared images: server-allocated AHardwareBuffers exported to clients as dma-buf descriptors
|
||||
# (docs/archive/notes/anland/plan-ahb-dmabuf.md).
|
||||
MobileGL/MG_Remote/Server/SharedImageRegistry.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).
|
||||
@@ -659,14 +723,30 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# (the display Activity's SurfaceView on Android; nobody installs one on the host, so a
|
||||
# ServerOwned request is refused by name there).
|
||||
MobileGL/MG_Remote/Server/ServerDisplay.cpp
|
||||
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
|
||||
# arms and the probe that measures the first against the second, shared by the
|
||||
# renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm -
|
||||
# so it stays out of the pull build (gate G1).
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
# Package-neutral C ABI for an APK that owns the server and its ANativeWindow.
|
||||
MobileGL/MG_Remote/Server/EmbeddedServer.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
# P13 W5: the record arm's own sources, compiled into every library (MG_Remote or not).
|
||||
list(APPEND SOURCE_FILES
|
||||
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
|
||||
# arms and the probe that measures the first against the second, shared by the
|
||||
# renderer's bring-up and the Vulkan POST.
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireE5RawCopy.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireCopySliceProbe.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp
|
||||
# P5 b1: the block-granularity persistent-map push the record arm makes mandatory (T2).
|
||||
MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp
|
||||
# P13 W5: the verb emitters and the monolith verb port, and the client GPU-write set
|
||||
# they mark before a draw or a dispatch (MG_Record; EmitTables.cpp resolves the session).
|
||||
MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp
|
||||
MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp
|
||||
# The record verbs' consumer (ServerVerbSink under a transport is this plus the session's).
|
||||
MobileGL/MG_Backend/Record/RecordVerbSink.cpp
|
||||
)
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -682,7 +762,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
|
||||
@@ -729,15 +809,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||
|
||||
@@ -764,9 +838,86 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
endif()
|
||||
# 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. Every build compiles the two GlvndVendor.cpp
|
||||
# files, so the path is every build's (P13 W5: the Android library without MG_Remote - FCL's - did not
|
||||
# find <glvnd/libeglabi.h>; a Linux host only built because its system has the header).
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
|
||||
|
||||
# ---- P13 W7 (D12): MODULE TARGETS ------------------------------------------------------------
|
||||
#
|
||||
# One flat SOURCE_FILES list used to feed MobileGL and MobileGL_s separately (every file compiled
|
||||
# twice). It is split here into OBJECT libraries along the link ratchet's partition
|
||||
# (scripts/link_ratchet.py PARTITION, which is the written form of these boundaries):
|
||||
#
|
||||
# mg_util MG_Util, Init / GlobalObjects / ConfigLoader, MG_Remote/FatalFunnel SHARED
|
||||
# mg_pipe MG_Pipe SERVER
|
||||
# mg_backend MG_Backend (both backends, the record arm's staged stores) SERVER
|
||||
# mg_frontend MG_Impl + MG_State FRONTEND
|
||||
# mg_remote_transport MG_Remote/{Transport,Protocol,Wire}, CapsCodec SERVER
|
||||
# mg_remote_client MG_Remote/Client FRONTEND
|
||||
# mg_remote_server MG_Remote/Server SERVER
|
||||
#
|
||||
# Both MobileGL and MobileGL_s link the same objects. mgl_build_iface carries every compile-side
|
||||
# usage requirement (include directories, definitions, the link libraries whose headers the
|
||||
# sources include), so a property set on it reaches every module and both libraries; a property
|
||||
# that only matters at link time may still be set on the libraries themselves.
|
||||
add_library(mgl_build_iface INTERFACE)
|
||||
target_include_directories(mgl_build_iface INTERFACE ${MOBILEGL_INCLUDE_DIR})
|
||||
target_link_libraries(mgl_build_iface INTERFACE ${MOBILEGL_LINK_LIBRARIES})
|
||||
target_compile_definitions(mgl_build_iface INTERFACE
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
||||
)
|
||||
|
||||
set(MGL_MODULES util pipe backend frontend remote_transport remote_client remote_server)
|
||||
foreach(MGL_MODULE IN LISTS MGL_MODULES)
|
||||
set(MGL_MODULE_SOURCES_${MGL_MODULE})
|
||||
endforeach()
|
||||
foreach(MGL_SOURCE IN LISTS SOURCE_FILES)
|
||||
if (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Client/")
|
||||
list(APPEND MGL_MODULE_SOURCES_remote_client ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Server/")
|
||||
list(APPEND MGL_MODULE_SOURCES_remote_server ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/(Transport|Protocol|Wire)/" OR
|
||||
MGL_SOURCE STREQUAL "MobileGL/MG_Remote/CapsCodec.cpp")
|
||||
list(APPEND MGL_MODULE_SOURCES_remote_transport ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_(Impl|State)/")
|
||||
list(APPEND MGL_MODULE_SOURCES_frontend ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Backend/")
|
||||
list(APPEND MGL_MODULE_SOURCES_backend ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Pipe/")
|
||||
list(APPEND MGL_MODULE_SOURCES_pipe ${MGL_SOURCE})
|
||||
else()
|
||||
list(APPEND MGL_MODULE_SOURCES_util ${MGL_SOURCE})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(MGL_MODULE_OBJECTS)
|
||||
set(MGL_SERVER_MODULE_OBJECTS)
|
||||
foreach(MGL_MODULE IN LISTS MGL_MODULES)
|
||||
if (NOT MGL_MODULE_SOURCES_${MGL_MODULE})
|
||||
continue() # MG_Remote's modules are empty without MOBILEGL_BUILD_DISAGGREGATED
|
||||
endif()
|
||||
add_library(mg_${MGL_MODULE} OBJECT ${MGL_MODULE_SOURCES_${MGL_MODULE}})
|
||||
target_link_libraries(mg_${MGL_MODULE} PUBLIC mgl_build_iface)
|
||||
set_target_properties(mg_${MGL_MODULE} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set_target_properties(mg_${MGL_MODULE} PROPERTIES
|
||||
C_VISIBILITY_PRESET default CXX_VISIBILITY_PRESET default VISIBILITY_INLINES_HIDDEN OFF)
|
||||
else()
|
||||
set_target_properties(mg_${MGL_MODULE} PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)
|
||||
endif()
|
||||
list(APPEND MGL_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
|
||||
if (NOT MGL_MODULE STREQUAL "frontend" AND NOT MGL_MODULE STREQUAL "remote_client")
|
||||
list(APPEND MGL_SERVER_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
${MGL_MODULE_OBJECTS}
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
@@ -791,25 +942,11 @@ else()
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
||||
)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC mgl_build_iface)
|
||||
|
||||
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
foreach(MOBILEGL_LOADER_ALIAS
|
||||
libEGL.so libEGL.so.1)
|
||||
libEGL.so libEGL.so.1 libGL.so libGL.so.1 libGLX_mobilegl.so.0)
|
||||
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
|
||||
@@ -832,8 +969,8 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
if(NOT ANDROID)
|
||||
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
||||
${SOURCE_FILES}
|
||||
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
||||
${MGL_MODULE_OBJECTS}
|
||||
)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -850,29 +987,81 @@ if(NOT ANDROID)
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s
|
||||
PUBLIC
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC mgl_build_iface)
|
||||
endif()
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC -DTRACY_ENABLE)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
|
||||
target_link_libraries(mgl_build_iface INTERFACE Tracy::TracyClient)
|
||||
target_compile_definitions(mgl_build_iface INTERFACE -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()
|
||||
|
||||
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
|
||||
# docs/archive/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
|
||||
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
|
||||
# it through dlsym. Self-contained: nothing outside this block refers to either.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE MobileGL/MG_Impl/SharedImageApi.cpp)
|
||||
add_subdirectory(MobileGL/MG_Gbm)
|
||||
endif()
|
||||
|
||||
# ---- P13 W7 (D12): THE SERVER-ONLY LINK CHECK ---------------------------------------------------
|
||||
# The server and shared modules linked WITHOUT mg_frontend / mg_remote_client, with --no-undefined:
|
||||
# the server half has to link from its own half of the tree. The frontend references it still makes
|
||||
# are the link ratchet's baseline, each a named exemption that resolves to an aborting stub through
|
||||
# a generated linker response file (scripts/ci/server_link_check.py refuses, before the link, any
|
||||
# server reference the baseline does not name). Built with the split shape on Linux hosts; never
|
||||
# installed or loaded. NOT in a verify build: the G4 comparator reads both halves by design (its
|
||||
# MGPipeVerifyReadHook and the program-artifact codec are frontend code the server half calls), the
|
||||
# baseline is the shipped split shape's, and a verify build never ships.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_VERIFY AND UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if (Python3_Interpreter_FOUND)
|
||||
set(MGL_SERVER_LINK_RSP "${CMAKE_BINARY_DIR}/mgl_server_link_exemptions.rsp")
|
||||
set(MGL_ALL_MODULE_OBJECT_DEPS)
|
||||
foreach(MGL_MODULE IN LISTS MGL_MODULES)
|
||||
if (TARGET mg_${MGL_MODULE})
|
||||
list(APPEND MGL_ALL_MODULE_OBJECT_DEPS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
|
||||
endif()
|
||||
endforeach()
|
||||
add_custom_command(
|
||||
OUTPUT "${MGL_SERVER_LINK_RSP}"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py"
|
||||
--build-dir "${CMAKE_BINARY_DIR}"
|
||||
--baseline "${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt"
|
||||
--out "${MGL_SERVER_LINK_RSP}"
|
||||
DEPENDS ${MGL_ALL_MODULE_OBJECT_DEPS}
|
||||
"${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py"
|
||||
"${CMAKE_SOURCE_DIR}/scripts/link_ratchet.py"
|
||||
"${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt"
|
||||
COMMENT "Server-only link check: naming the frontend references the baseline exempts"
|
||||
VERBATIM)
|
||||
add_custom_target(mgl_server_link_exemptions DEPENDS "${MGL_SERVER_LINK_RSP}")
|
||||
add_library(MobileGL_server_linkcheck SHARED
|
||||
${MGL_SERVER_MODULE_OBJECTS}
|
||||
tools/server_link_check/Exempt.cpp)
|
||||
target_link_libraries(MobileGL_server_linkcheck PRIVATE mgl_build_iface)
|
||||
target_link_options(MobileGL_server_linkcheck PRIVATE
|
||||
"LINKER:--no-undefined" "@${MGL_SERVER_LINK_RSP}")
|
||||
set_property(TARGET MobileGL_server_linkcheck APPEND PROPERTY LINK_DEPENDS "${MGL_SERVER_LINK_RSP}")
|
||||
add_dependencies(MobileGL_server_linkcheck mgl_server_link_exemptions)
|
||||
else()
|
||||
message(STATUS "MobileGL: no Python3 interpreter - the server-only link check target is not configured")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
android
|
||||
@@ -913,7 +1102,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
|
||||
endif()
|
||||
|
||||
if (APPLE AND MOBILEGL_IOS)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
|
||||
target_compile_definitions(mgl_build_iface INTERFACE MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
"-framework CoreGraphics"
|
||||
"-framework Foundation"
|
||||
@@ -924,7 +1113,6 @@ if (APPLE AND MOBILEGL_IOS)
|
||||
endif()
|
||||
|
||||
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
"-framework CoreGraphics"
|
||||
"-framework Foundation"
|
||||
@@ -939,13 +1127,22 @@ endif()
|
||||
if (NOT ANDROID AND NOT MOBILEGL_IOS)
|
||||
find_package(Vulkan)
|
||||
if (Vulkan_FOUND)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Vulkan::Vulkan Vulkan::Headers)
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR})
|
||||
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR})
|
||||
target_link_libraries(mgl_build_iface INTERFACE Vulkan::Vulkan Vulkan::Headers)
|
||||
target_include_directories(mgl_build_iface INTERFACE ${Vulkan_INCLUDE_DIR})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Discover gtest cases when ctest starts, one binary after another, instead of in a post-build
|
||||
# step. The post-build discovery of the CMake releases that never pass the target name down
|
||||
# writes every binary's `--gtest_output=json` result to ONE per-directory file
|
||||
# (cmake_test_discovery_<hash of an empty name>.json), so binaries linked in parallel in the same
|
||||
# directory overwrite and delete each other's list: the registered count then differs between
|
||||
# runs of the same tree and a binary ends up with none or only some of its cases. The pre-test
|
||||
# mode reads the list from stdout, serially, and also works when the binary is cross-built.
|
||||
if (NOT DEFINED CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE)
|
||||
set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID)
|
||||
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
|
||||
# at the build-tree root. This lets `ctest` be invoked from the top-level
|
||||
@@ -982,6 +1179,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.
|
||||
#
|
||||
@@ -1001,7 +1204,7 @@ endif()
|
||||
# P0 spike, which links no MobileGL library and exists only to prove the Android
|
||||
# delivery chain - this one links the library and runs mobilegl_server_main.
|
||||
#
|
||||
# ARCHITECTURE.md:496 called for a ~30-line stub that dlopen's libMobileGL.so and
|
||||
# ARCHITECTURE.md §15.2 called for a ~30-line stub that dlopen's libMobileGL.so and
|
||||
# dlsym's the entry point, so one shared library serves both roles. On a desktop
|
||||
# configure we link it directly instead: there is no APK packaging rule to satisfy
|
||||
# and a direct link is what makes an undefined symbol a BUILD error rather than a
|
||||
@@ -1090,3 +1293,48 @@ 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()
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
# Contributing to MobileGL
|
||||
|
||||
These rules keep the code and its history readable. `scripts/check_text_hygiene.py` checks the ones listed under [Checks](#checks); the rest rely on review.
|
||||
|
||||
Commands below use `python3`. On Windows, run `python` instead.
|
||||
|
||||
## Commit messages
|
||||
|
||||
```
|
||||
[Type] (Scope): content
|
||||
|
||||
- Detail 1
|
||||
- Detail 2
|
||||
```
|
||||
|
||||
- **Subject only by default.** Add `- ` detail lines only when a detail really needs to be in the commit message. Leave one blank line after the subject. A detail that wraps continues on at most two lines indented by two spaces. No paragraphs and no trailers.
|
||||
- **Content** after `): ` is at most 72 characters of plain English in the imperative mood ("flush once per frame", not "flushes" or "flushed").
|
||||
- **Type** is one or more of these, comma-separated (`[Fix, Test]`):
|
||||
- `Feat`: new behaviour.
|
||||
- `Fix`: corrects behaviour.
|
||||
- `Perf`: faster or lighter, same behaviour.
|
||||
- `Refactor`: restructures code, same behaviour.
|
||||
- `Test`: tests only.
|
||||
- `Docs`: documentation and comments only.
|
||||
- `Build`: CMake, Gradle, toolchains.
|
||||
- `CI`: workflows and CI scripts.
|
||||
- `Chore`: housekeeping such as headers, file moves and dead files.
|
||||
- `Tools`: developer tools under `tools/` and `scripts/`.
|
||||
- **Scope** is each changed path without a leading `MobileGL/`, cut to its first two parts: `MG_Backend/DirectGLES`, `MG_Test/Wire`, `scripts/ci`, `docs/Disaggregated`.
|
||||
- A shorter path is used whole: `MG_Backend` for a file directly in a subsystem, `scripts` for a file directly in `scripts/`, and the file name for a file at the repository root (`CMakeLists.txt`).
|
||||
- A file directly in `MobileGL/` uses `MobileGL`, and a submodule uses its path (`3rdparty/glslang`).
|
||||
- List every scope the commit touches, comma-separated: `(MG_Backend/DirectGLES, MG_Test/Wire)`.
|
||||
- `python3 scripts/check_text_hygiene.py scope` prints the scope for the staged changes.
|
||||
- **No** planning ids (phase or decision codes such as `P13` or `ID-54`), local paths, external project names or non-English text anywhere in the message.
|
||||
- **Merges** use `[Merge] (area): what the merge takes`. The scope is free text there. Write the subject yourself, since git's default merge message fails the check. The commits a merge brings in are not checked again.
|
||||
- **Reverts** use `[Fix] (Scope): revert <the reverted subject>`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
[Perf] (MG_Backend/DirectGLES): flush once per frame before the window pass
|
||||
```
|
||||
|
||||
## Comments, strings and documentation
|
||||
|
||||
- Code, comments, log strings and commit messages are in English. Documentation may be in Chinese or English.
|
||||
- A comment states what the code does now and why, in a few lines. Rationale longer than about 15 lines belongs in a design document under `docs/`, with a one-line pointer in the code.
|
||||
- In code and comments, cite a symbol, file or section, never a line number: `ARCHITECTURE.md §1.3`, `VulkanRenderer::BeginFrame()`. Documents may keep `file:line` citations; the documentation citation check resolves them.
|
||||
- State the rule, not its history. No "previously" or "after review", and no planning ids in comments, strings, identifiers or file names.
|
||||
- Plain wording, without ALL-CAPS emphasis.
|
||||
- In code and comments (C, C++, GLSL, CMake, shell, Python, YAML), do not name external projects such as Mesa, Zink or gallium; describe the technique instead. Naming the driver or hardware where a behaviour was observed (llvmpipe, ANGLE, Adreno, Mali) is fine, and so are file, package and extension names that contain these words (`50_mesa.json`, `GL_MESA_*`).
|
||||
|
||||
## Code
|
||||
|
||||
- Format the lines you change with `.clang-format`, using clang-format 23. Install it with `pip install 'clang-format==23.1.3'`; `git clang-format <base>` from the same package formats only the lines changed since `<base>`. `scripts/format_code.sh` reformats whole files, so do not use it on files you only touched.
|
||||
- `.inc` files are fragments included in the middle of a `.cpp` file, inside a namespace, or are generated tables. clang-format cannot format them on their own, so the check skips them. Match the indentation of the file that includes them.
|
||||
- Text files are UTF-8.
|
||||
- These source files under `MobileGL/`, `scripts/` and `tools/` start with the license header:
|
||||
- C, C++, `.inc`, GLSL, Java and JavaScript modules, with `//` comments;
|
||||
- Python, shell, CMake modules and `CMakeLists.txt`, with `#` comments.
|
||||
- Generated files and files under another license are exempt. `python3 scripts/update-source-file-header.py` adds the header.
|
||||
- Keep files to about 3,000 lines and functions to about 300 lines.
|
||||
- Platform-specific APIs stay behind an abstraction, never raw in logic code.
|
||||
- A fix that restores intended behaviour ships unconditionally. Add a switch only for a real trade-off.
|
||||
- A cache keyed on object identity uses a monotonic lifetime id, never a GL name or a pointer, because both are recycled.
|
||||
|
||||
## What goes in the repository
|
||||
|
||||
- Source, tests, build files and maintained documentation.
|
||||
- Generated code, only together with a CI step that regenerates it and fails on any difference.
|
||||
- Binary test fixtures.
|
||||
- Not raw logs, and not working papers such as agent briefs, review rounds and drafts. Keep those outside the repository and commit the conclusion: a design-document change, a `DEBTS.md` entry, a measurement under `docs/Performance`.
|
||||
- `docs/archive/` keeps the planning notes and agent contracts of the disaggregation work as they were. It is frozen: no file there is added, edited or removed.
|
||||
|
||||
## Checks
|
||||
|
||||
`python3 scripts/check_text_hygiene.py ci` runs what CI runs. It checks the commits on the branch's own line of history since the check was added. The base branch it compares with is `feat/disaggregated`, which you can change with `TEXT_HYGIENE_BASE_BRANCH`.
|
||||
|
||||
These fail on any violation:
|
||||
|
||||
- commit messages, as described above;
|
||||
- a new file under `docs/Disaggregated/notes/` or a new `.log` file (moving a file is not adding it);
|
||||
- any change under `docs/archive/`;
|
||||
- a missing or wrong license header;
|
||||
- a text file that is not UTF-8;
|
||||
- an external project name in a comment;
|
||||
- a `file:line` citation in code that points past the end of the file;
|
||||
- changed lines that clang-format would change.
|
||||
|
||||
These are ratchets: each total may only go down.
|
||||
|
||||
- planning ids in comments;
|
||||
- comment lines beyond 15 in a block;
|
||||
- ALL-CAPS comment lines;
|
||||
- non-English comment lines;
|
||||
- line-number citations in code.
|
||||
|
||||
`python3 scripts/check_text_hygiene.py tree --update-baseline` lowers `scripts/data/text_hygiene_baseline.json` after a cleanup. It never raises it, and CI rejects a raised baseline.
|
||||
|
||||
Not checked by CI: the size limits, the platform, switch and lifetime-id rules, planning ids outside comments, English outside comments, and the regeneration step for generated code.
|
||||
+260
-152
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 9, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 10, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -85,41 +85,57 @@ namespace MobileGL::MG_Config {
|
||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||
String TraceAngleVariant;
|
||||
#endif
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool MagmaDisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||
// engages when this flag is set AND the device has no native subgroup support at
|
||||
// all - a device with real subgroup operations always uses them natively,
|
||||
// whatever their width (the known iterationRP defect is patched by
|
||||
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
// bounds. The pass grows that one array to what the device's topology needs and
|
||||
// touches nothing else; it only rewrites modules positively matching the pack's
|
||||
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
||||
// so every other shader passes through byte-identical - as does iterationRP
|
||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||
// verbatim.
|
||||
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||
Bool MagmaIterationRPFixBarrier = false;
|
||||
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_SUBGROUP_EMULATION: how either backend implements GL_KHR_shader_subgroup
|
||||
// (MG_Backend/SubgroupImplementation.h). Auto (the default) uses the driver's own subgroup
|
||||
// operations natively whenever it has them, at whatever width (8/16/32/64/128 lanes), and
|
||||
// otherwise emulates the compute stage on a 32-lane VIRTUAL subgroup in workgroup-shared
|
||||
// memory (ShaderTranspiler::EmulateSubgroupsPass). ForceOn emulates even over native
|
||||
// support, so the emulation can be exercised on any device; ForceOff never emulates,
|
||||
// leaving a driver without subgroups without the extension.
|
||||
QuirkOverride SubgroupEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_ITRP_FIX: the iterationRP shaderpack's two compute-shader bugs, repaired by
|
||||
// fingerprint-gated passes that leave every other module byte-identical
|
||||
// (MG_Backend/SubgroupImplementation.h):
|
||||
// * FixIterationRPSubgroupScratchPass - the pack declares `shared vec2
|
||||
// prefixSumCache[32]` for a 512-invocation reduction indexed by gl_SubgroupID, so a
|
||||
// sub-16-lane device (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
// bounds; the array grows to what the native width needs (inert at >= 16 lanes, and
|
||||
// under the 32-lane emulation);
|
||||
// * FixIterationRPBarrierPass - Program 203's second reduction overwrites that scratch
|
||||
// without a workgroup rendezvous after the first one's readers.
|
||||
// Auto and ForceOn apply both; ForceOff replays the pack's bugs verbatim.
|
||||
QuirkOverride IterationRPFix = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_INPASS_CLEAR: a wire-arm glClear / glClearBuffer* whose targets are the
|
||||
// attachments of the draw pass for the bound framebuffer is recorded inside that pass
|
||||
// (vkCmdClearAttachments) rather than in a render pass of its own built for the clear.
|
||||
// Auto is ON; ForceOff restores the standalone clear for every surface.
|
||||
QuirkOverride MagmaInPassClear = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT: the same for the default framebuffer. Auto is OFF
|
||||
// (an unexplained openra regression on Adreno 750, perf-p15/HANDOFF.md); ForceOn enables it.
|
||||
QuirkOverride MagmaInPassClearDefault = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PASS_CACHE: wire draw passes (render pass, framebuffer, views) are kept
|
||||
// across frames while their key's image epochs hold. Auto is ON; ForceOff drops every
|
||||
// cached pass whose last use completed at each frame begin, as before P15.
|
||||
QuirkOverride MagmaPassCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE: a wire vertex stream that needs a CPU format
|
||||
// conversion is converted once per frame slot per (buffer content, layout) and reused by
|
||||
// every draw that names it, instead of once per draw. Auto is ON; ForceOff converts per draw.
|
||||
QuirkOverride MagmaVertexConversionCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_WRITE_RENAME: a glBufferSubData into a small wire store the GPU may still
|
||||
// be reading gives the store a fresh buffer filled from a host shadow, instead of a staged
|
||||
// copy that ends the render pass. Auto is ON; ForceOff keeps the staged copy.
|
||||
QuirkOverride MagmaWriteRename = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_VAO_SHADOW: DirectGLES emits a vertex attribute's enable flag, divisor and
|
||||
// pointer to the driver VAO only when they differ from what that VAO already holds (a
|
||||
// shadow on the VAO twin). Auto is ON; ForceOff re-emits the whole configuration per walk.
|
||||
QuirkOverride EsprytVaoShadow = QuirkOverride::Auto;
|
||||
// MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP: a VAO configuration change whose vertex-elements
|
||||
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
|
||||
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
|
||||
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
|
||||
// P15: the client reuses a bound framebuffer's built set_framebuffer_state while nothing it
|
||||
// reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState).
|
||||
QuirkOverride PipeFramebufferStateMemo = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
@@ -130,8 +146,39 @@ namespace MobileGL::MG_Config {
|
||||
Bool AdvertiseFp64 = false;
|
||||
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV: hand a shader module that fails SPIR-V validation to
|
||||
// the driver anyway. Off by default: Magma declines such a program (no shader module, its
|
||||
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
|
||||
// into device loss. For diagnosing a driver that copes with a specific module only.
|
||||
Bool MagmaAllowInvalidSpirv = false;
|
||||
// MOBILEGL_FRAMES_IN_FLIGHT (P15; MOBILEGL_MAGMA_FRAMESINFLIGHT is its deprecated alias):
|
||||
// how many frames may be queued ahead of the GPU, default 3, both backends.
|
||||
// - Magma: its frame contexts (frame S+1 reuses S+1-N's), clamped to the surface.
|
||||
// - Espryt: after presenting S the backend waits for S+1-N to complete (N = 1 is a full
|
||||
// CPU/GPU sync; N >= 2 is what Android's own queueBuffer throttle already gives).
|
||||
// - inproc / spawn / TCP: the present credit, how many presents the GL thread may run
|
||||
// ahead of the server, is derived from it: max(1, N - 2), so the default 3 keeps the
|
||||
// shipped credit of 1 and N = 4 is credit 2 (one more frame of latency).
|
||||
Uint32 FramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES: swapchain images Magma asks for beyond the
|
||||
// surface's minimum (the frames-in-flight count still raises the request when it is
|
||||
// larger), clamped to the surface's maximum. With more images, the one the display has
|
||||
// just released is reused later, by when its release has usually signaled, so fewer
|
||||
// frames start their window pass behind it. Each image costs a full-size colour buffer
|
||||
// plus the default framebuffer's depth/stencil buffer of the same size.
|
||||
Uint32 MagmaExtraSwapchainImages = 0;
|
||||
// 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;
|
||||
@@ -152,6 +199,31 @@ namespace MobileGL::MG_Config {
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS: DirectGLES moves a geometry shader's
|
||||
// EmitVertex()/EndPrimitive() behind its memory writes (DeferGeometryEmitsPass). The
|
||||
// Adreno ES driver silently drops storage writes a geometry invocation makes after
|
||||
// emitting; the driver POST measures that and arms this by itself, so Auto is right
|
||||
// everywhere. ForceOn exercises the rewrite on a healthy driver; ForceOff is the
|
||||
// negative control that replays the lost writes.
|
||||
QuirkOverride EsprytDeferGeometryEmits = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH: DirectGLES's subgroup emulation reads its
|
||||
// workgroup scratch back through atomic loads (EmulateSubgroupsPass). The Adreno ES
|
||||
// compiler treats ordinary shared-memory loads as pure and merges or reorders them
|
||||
// across stores and barrier(), so an emulated subgroup operation would see the lanes of
|
||||
// an earlier one; the driver POST measures that and arms this by itself, so Auto is
|
||||
// right everywhere. ForceOn exercises the atomic reads on a healthy driver; ForceOff is
|
||||
// the negative control.
|
||||
QuirkOverride EsprytAtomicSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER: DirectGLES binds an image unit GL 4.6 8.26
|
||||
// calls invalid (a level the texture does not have) to a zeroed 1x1 texture of the bind's
|
||||
// shape instead of leaving it empty. The Adreno ES driver answers a load through an EMPTY
|
||||
// unit with alpha 1 where GL says 0, and ANGLE faults on any use of an empty unit; the
|
||||
// driver POST measures the first and Auto arms this on ANGLE without probing, so Auto is
|
||||
// right everywhere. The placeholder's one texel is real, so a store
|
||||
// through such a unit can be read back through the same unit in the same pass - which is
|
||||
// why a healthy driver keeps the empty unit. ForceOn exercises the placeholder on a
|
||||
// healthy driver; ForceOff is the negative control.
|
||||
QuirkOverride EsprytInvalidImagePlaceholder = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
@@ -202,13 +274,16 @@ 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
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (llvmpipe does,
|
||||
// Adreno does not), which means the emulation is dead code on exactly the stack the
|
||||
// headless suite runs on. This forces it live so the scenarios and the CTS can
|
||||
// exercise the path, and gives the device an A/B lever over the same choice.
|
||||
@@ -303,7 +378,7 @@ namespace MobileGL::MG_Config {
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||
// them, but a Mali driver - and lavapipe - answers 0 unless a capture span is
|
||||
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||
@@ -316,6 +391,28 @@ namespace MobileGL::MG_Config {
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_E5_RAW_READS: DirectVulkan's raw read route for E5B9G9R9 textures
|
||||
// (WireE5RawCopy.h). GL moves RGB9_E5 texel blocks bit-exactly (glGetTexImage with
|
||||
// UNSIGNED_INT_5_9_9_9_REV, glCopyImageSubData), but Adreno 750's transfer reads of
|
||||
// an E5B9G9R9 image larger than ~16x16 decode and re-encode, canonicalizing every
|
||||
// non-canonical shared exponent. Auto defers to a device probe at renderer bring-up
|
||||
// (RunWireE5TransferProbe: plain transfer read vs. a shader read through an R32_UINT
|
||||
// view) and arms the route only where the plain read changed words; ForceOn arms it
|
||||
// on any device (the host lanes exercise the route with it); ForceOff is the negative
|
||||
// control that replays the canonicalization.
|
||||
QuirkOverride MagmaE5RawReads = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_SPLIT_SLICE_COPIES: DirectVulkan's per-slice split of a cross-format
|
||||
// glCopyImageSubData between a layered texture and a 3D one (WireCopySliceProbe.h).
|
||||
// Adreno 750 lands only the first slice of such a vkCmdCopyImage region. Auto defers to
|
||||
// a device probe at renderer bring-up (one region vs. per-slice regions); ForceOn splits
|
||||
// on any device; ForceOff is the negative control that replays the dropped slices.
|
||||
QuirkOverride MagmaSplitSliceCopies = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD: DirectVulkan sets the dynamic state again after
|
||||
// every rasterizer-discard draw (WireDiscardStateProbe.h). On Adreno 750 a draw with a
|
||||
// rasterizerDiscardEnable pipeline drops the dynamic viewport/scissor, and later draws that
|
||||
// rely on it draw nothing. Auto defers to a device probe at renderer bring-up; ForceOn
|
||||
// re-emits on any device; ForceOff is the negative control that replays the lost state.
|
||||
QuirkOverride MagmaReemitStateAfterDiscard = QuirkOverride::Auto;
|
||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||
@@ -350,6 +447,10 @@ namespace MobileGL::MG_Config {
|
||||
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
||||
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
||||
// never probed. The negative control the CSO design is measured against.
|
||||
// P13 W3b (ID-P13-3): bits 0-13 are FIXED ON. Their pre-handle arms are gone, so a mask
|
||||
// that clears one is refused by name at startup (Fatal{PipeSubsystemsFixedOn},
|
||||
// ConfigLoader.cpp); bit 63 is the one switch left. Everything above about "refused,
|
||||
// runs its legacy arm" describes code that is unreachable since then and goes in W6.
|
||||
Uint64 PipePush = 0;
|
||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||
// against a snapshot taken from GLContext the old way, printing the first field
|
||||
@@ -357,7 +458,6 @@ namespace MobileGL::MG_Config {
|
||||
// the semantic gate that replaces byte identity, and it catches the dangerous
|
||||
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
|
||||
Bool PipeVerify = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
|
||||
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
|
||||
@@ -376,31 +476,9 @@ namespace MobileGL::MG_Config {
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||
// block is never handed back, so that spelling collided with nothing and the control
|
||||
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||
// measurement and for what the control still leaves standing. Under
|
||||
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||
Bool PipeHandleAbaControl = false;
|
||||
#endif
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
|
||||
// alive so the first handle waves have a real old-versus-new arm to be compared
|
||||
// against. ON by default for the whole migration window, deleted with the pull
|
||||
// path itself.
|
||||
Bool PipeLegacyMemos = true;
|
||||
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
|
||||
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
|
||||
// holds a complete CPU shadow, so this cache buys latency, never correctness.
|
||||
Uint32 PipeTexelRetainMb = 0;
|
||||
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
|
||||
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
|
||||
// the server without shipping index bytes per draw. Over budget it degrades to
|
||||
@@ -408,8 +486,9 @@ namespace MobileGL::MG_Config {
|
||||
Uint32 PipeIndexMirrorMb = 64;
|
||||
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
|
||||
// steady-state cadence; the device retrace harness never reaches the teardown dump
|
||||
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
|
||||
// needs its numbers at all sets this low enough to land at least one window.
|
||||
// and a trimmed fixture (minecraft-1.21.1-neoforge-create-indirect-in-world-align1024)
|
||||
// is shorter than 120 frames, so a run that needs its numbers at all sets this low
|
||||
// enough to land at least one window.
|
||||
Uint32 PipeStatsPeriod = 120;
|
||||
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
|
||||
// Empty (the default) means no dump; the per-120-frame summary line still goes to
|
||||
@@ -424,7 +503,7 @@ namespace MobileGL::MG_Config {
|
||||
//
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
|
||||
//
|
||||
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
|
||||
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md §16 requires that with
|
||||
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
|
||||
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
|
||||
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
|
||||
@@ -448,6 +527,42 @@ namespace MobileGL::MG_Config {
|
||||
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
|
||||
};
|
||||
|
||||
// P13 W5: THE RECORD ARM'S OWN KNOBS. They kept their MOBILEGL_IPC_* names (logs, CI lanes and
|
||||
// notes spell them so), but they tune the record arm - the persistent-map push and Magma's wire
|
||||
// stores - which runs in a library without a transport too, so they are not IpcTable's.
|
||||
struct RecordArmTable {
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
|
||||
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
|
||||
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
|
||||
// Stores no command names are destroyed at once and stores whose last submission has
|
||||
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
|
||||
// exceed it after a sweep, the server flushes what it has recorded and waits for it
|
||||
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
|
||||
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
|
||||
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
|
||||
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
|
||||
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
|
||||
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
|
||||
// and MagmaWireReclaimScenario's watermark cases must go red.
|
||||
Uint32 WireDeferredMb = 64;
|
||||
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
|
||||
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
|
||||
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
|
||||
Uint32 PersistentBlockKb = 64;
|
||||
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
|
||||
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
|
||||
// range every verb. 0 restores the whole-range push (A/B control).
|
||||
Uint32 PersistentHashSuppress = 1;
|
||||
// MOBILEGL_PIPE_PERSISTENT_MPROTECT (P13 W5): 1 (default) = the persistent-map push finds
|
||||
// written pages through mprotect and a chained SIGSEGV handler where the platform has
|
||||
// one (Linux, Android); 0 = no handler is ever installed and every persistent write map
|
||||
// takes the hash scan. The switch exists for the hosts whose own SIGSEGV use the
|
||||
// chained handler has not met yet - FCL is a JVM, whose implicit null checks fault on
|
||||
// purpose - so a bring-up can rule the handler out without a rebuild.
|
||||
Uint32 PersistentMprotect = 1;
|
||||
};
|
||||
extern RecordArmTable RecordArm;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
|
||||
// transport in is not the same as using it, and every existing lane of a build-split
|
||||
@@ -475,7 +590,7 @@ namespace MobileGL::MG_Config {
|
||||
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
|
||||
//
|
||||
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
|
||||
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
|
||||
// through the integrator, and not invented at its call site - ARCHITECTURE.md appendix A holds
|
||||
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
|
||||
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
|
||||
// to P6 or later.
|
||||
@@ -485,6 +600,9 @@ namespace MobileGL::MG_Config {
|
||||
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
|
||||
};
|
||||
// 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
|
||||
@@ -503,20 +621,6 @@ namespace MobileGL::MG_Config {
|
||||
// stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with
|
||||
// no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it.
|
||||
Uint32 StageMb = 32;
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
|
||||
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
|
||||
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
|
||||
// Stores no command names are destroyed at once and stores whose last submission has
|
||||
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
|
||||
// exceed it after a sweep, the server flushes what it has recorded and waits for it
|
||||
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
|
||||
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
|
||||
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
|
||||
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
|
||||
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
|
||||
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
|
||||
// and MagmaWireReclaimScenario's watermark cases must go red.
|
||||
Uint32 WireDeferredMb = 64;
|
||||
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
|
||||
Uint32 SpinUs = 50;
|
||||
// MOBILEGL_IPC_EVENT_WAIT_MS (PH-6, ID-P7-2): the SERVER's patience, in ms, for ONE
|
||||
@@ -534,14 +638,6 @@ namespace MobileGL::MG_Config {
|
||||
// spent twice. Read by the server only (ServerSpawn.cpp passes it to a spawned child);
|
||||
// the lockstep arm (no kCapRunAheadApply, i.e. Magma) never waits and keeps P5C's Fatal.
|
||||
Uint32 EventWaitMs = 2000;
|
||||
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
|
||||
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
|
||||
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
|
||||
Uint32 PersistentBlockKb = 64;
|
||||
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
|
||||
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
|
||||
// range every verb. 0 restores the whole-range push (A/B control).
|
||||
Uint32 PersistentHashSuppress = 1;
|
||||
// MOBILEGL_IPC_BATCH_WAITS: 1 = value-class records (kCtxState / kCtxCso / kCtxObject
|
||||
// with no reply slot) are published without waiting for their own apply; the barrier
|
||||
// is taken at the next pull-reading verb (kCtxVerb syncs, queries, screen rows, and
|
||||
@@ -549,64 +645,59 @@ namespace MobileGL::MG_Config {
|
||||
// restores the per-record barrier of R-1.
|
||||
Uint32 BatchWaits = 1;
|
||||
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
|
||||
// client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N
|
||||
// creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest
|
||||
// answer is taken when the window fills.
|
||||
//
|
||||
// DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two
|
||||
// measured reasons and one structural one, in the order they were found:
|
||||
// 1. the reply buffer was a ring of the last 8 answers, and between two creates this
|
||||
// client emits on the order of a thousand records - so the answer was always gone
|
||||
// (`want=310` on every miss while the ring spanned [1175..1504]);
|
||||
// 2. a residency budget that tried to take answers back early could not help, because
|
||||
// what displaces an answer is what the SERVER sends, not what the client emits;
|
||||
// 3. and so the fix was RETENTION - a link keeps a declared answer until its reader
|
||||
// takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store).
|
||||
//
|
||||
// MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097,
|
||||
// frame-sent identical per op throughout):
|
||||
// window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped)
|
||||
// window 2 2,500 17.27 s
|
||||
// window 4 1,644 11.02 s
|
||||
// and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672
|
||||
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
|
||||
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
|
||||
//
|
||||
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
|
||||
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
|
||||
// now; the old 4 was derived from a reply pool the window does not run on). Three
|
||||
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
|
||||
//
|
||||
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
|
||||
// window 5 1,277 11.31 s (10.61-12.12)
|
||||
// window 6 1,155 10.79 s ( 9.69-10.81)
|
||||
// window 8 859 10.96 s ( 8.92-11.03)
|
||||
//
|
||||
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
|
||||
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
|
||||
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
|
||||
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
|
||||
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
|
||||
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
|
||||
// the constant ~350-400 creates that block for a reason depth does not reach, and the
|
||||
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
|
||||
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
|
||||
// caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a
|
||||
// flag keyed on wantReply would disable the very mechanism it exists to enable
|
||||
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
|
||||
// client waits for one. 1 turns the window off; N > 1 lets up to N creates go out without
|
||||
// waiting: each caller reads a provisional accept, the answers are collected in order (the
|
||||
// oldest once the window is full), and a create that comes back declined is un-published.
|
||||
// The window runs only on a link that keeps a declared answer until its reader takes it (the
|
||||
// stream link); on the shared-memory link it is always 1.
|
||||
// Default 10: on a device world-load frame the wall clock stops improving at about 6, and a
|
||||
// deeper window costs only entries in the link's declared-answer store, so 10 sits on the
|
||||
// flat part of the curve.
|
||||
// Range 1..12; a value outside it is ignored with a warning. 12 is kCreateWindowMax
|
||||
// (WireTables.cpp), half of the stream link's 24-entry answer store.
|
||||
// Measurements: docs/Performance/ipc-create-window.md.
|
||||
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, docs/archive/contracts/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, docs/archive/contracts/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_RECOVER (client): 1 (default) = after a device loss ends this client's
|
||||
// session, the application's next eglCreateContext / eglCreate*Surface / eglInitialize
|
||||
// dials the server for a FRESH session, and contexts created from then on work. Contexts
|
||||
// (and their objects) from before the loss stay lost, as EGL's power-management event
|
||||
// has them; surfaces survive and are re-created on the new session when next used.
|
||||
// 0 = the loss is final for the process (every verb DECLINED for the rest of its life).
|
||||
Uint32 RecoverAfterLoss = 1;
|
||||
// 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
|
||||
// GLContext by the client's residual fill and a free-running queue lets the server
|
||||
// read a FUTURE value of them.
|
||||
Uint32 VerbBarrier = 1;
|
||||
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
|
||||
// MOBILEGL_IPC_RUN_AHEAD (P5e, docs/archive/contracts/CONTRACT-P5E.md §1): 1 = after publishing an
|
||||
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
|
||||
// It is one half of a conjunction and never a switch on its own - the client arms
|
||||
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
|
||||
@@ -624,14 +715,19 @@ namespace MobileGL::MG_Config {
|
||||
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
|
||||
// arm that stops filling it.
|
||||
Uint32 RunAhead = 1;
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
|
||||
// have in flight before it waits for a swap to come back. 1 = the client publishes
|
||||
// frame N+1's records while the server applies and swaps frame N - one frame of
|
||||
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
|
||||
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
|
||||
// client that is already CPU-bound and costs a frame of latency, which is why the
|
||||
// default is 1 and not "as deep as the ring".
|
||||
// The present credit (P5e, ruling 4 / ID-92): how many presents the client may have in
|
||||
// flight before it waits for a swap to come back. 1 = the client publishes frame N+1's
|
||||
// records while the server applies and swaps frame N - one frame of overlap, at most one
|
||||
// frame of added latency - and the CREDIT, never the ring's bytes, is what paces a
|
||||
// run-ahead client. Derived from MOBILEGL_FRAMES_IN_FLIGHT (max(1, N - 2)); the
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT override stays for experiments. Credit 2 is a real trade,
|
||||
// not a measurement arm: on FCL inproc (cpuhunt, 2026-10-08) it brought Magma 309 -> 320
|
||||
// and Espryt 299 -> 319 fps, at one frame of added latency.
|
||||
Uint32 PresentCredit = 1;
|
||||
// MOBILEGL_IPC_WAIT_STATS (P15, diagnostic): every 600 presents, log how many times per
|
||||
// frame the run-ahead client waited, by record op (applied/reply), by quiesce reason and
|
||||
// barriered verb, for ring space and for the present credit. Off by default.
|
||||
Bool WaitStats = false;
|
||||
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
|
||||
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
|
||||
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
|
||||
@@ -660,8 +756,9 @@ namespace MobileGL::MG_Config {
|
||||
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
|
||||
// (the comparator owns the one fill block it compares against). 0 is not merely the
|
||||
// default, it is the negative control: the lane's distinctness case must go red
|
||||
// without it (P5F §6).
|
||||
Bool RoleSplitState = false;
|
||||
// without it (P5F §6). P15: the default is ON (unset = on, 0 = off); the negative control
|
||||
// sets 0 explicitly.
|
||||
Bool RoleSplitState = true;
|
||||
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
|
||||
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
|
||||
// inproc implementation did not quietly keep using a pointer past its lifetime.
|
||||
@@ -689,10 +786,21 @@ namespace MobileGL::MG_Config {
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
|
||||
#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
|
||||
// discarded statement and the pull build gains no symbol, no branch and no byte.
|
||||
inline constexpr TransportMode Transport = TransportMode::Monolith;
|
||||
#endif
|
||||
|
||||
// P13: THE DATA-ARM QUESTION, ANSWERED ONCE FOR EVERY BUILD. The backends read RECORDS (the
|
||||
// applier's twins, the staged stores, the verb's own handles), never frontend objects - over a
|
||||
// wire and on monolith alike ("monolith is the push variant"). W4 switched monolith family by
|
||||
// family behind MOBILEGL_PIPE_DATA_ARM; W5 built the record arm into every library; W6 deleted
|
||||
// the frontend arm and the knob. The predicate stays as a name for the sites that read it.
|
||||
inline constexpr Bool DataArmIsRecord() { return true; }
|
||||
// ...and the record arm runs in this process over stores that ALIAS the frontend's shadows
|
||||
// (monolith) instead of copying them (every wire).
|
||||
inline Bool RecordArmAliasesFrontend() { return Transport == TransportMode::Monolith; }
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+282
-62
@@ -6,21 +6,20 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
#include "Config.h"
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
|
||||
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||
// pull build's translation unit is unchanged.
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
|
||||
// unchanged (G1).
|
||||
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char** environ;
|
||||
@@ -40,6 +39,7 @@ namespace MobileGL::MG_Config {
|
||||
Bool SplitTransportRequestedByConfig = false;
|
||||
IpcTable Ipc;
|
||||
#endif
|
||||
RecordArmTable RecordArm;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
namespace MobileGL::MG_ConfigLoader {
|
||||
@@ -49,6 +49,137 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
|
||||
}
|
||||
|
||||
// ---- the system configuration -----------------------------------------------------------
|
||||
//
|
||||
// A library that is every process's GL implementation (installed as the system's EGL/GLX
|
||||
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
|
||||
// most are started by something that has never heard of it. So the settings a whole machine
|
||||
// shares live in files, and the environment only overrides them:
|
||||
//
|
||||
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
|
||||
//
|
||||
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
|
||||
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
|
||||
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
|
||||
// holds just the backend's name (the file the backend switch rewrites), and names it over a
|
||||
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
|
||||
//
|
||||
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
|
||||
// file off. The defaults exist only on desktop Linux: an Android app configures its process
|
||||
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
|
||||
namespace {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
|
||||
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
|
||||
#else
|
||||
constexpr const char* kDefaultConfigFile = "";
|
||||
constexpr const char* kDefaultBackendFile = "";
|
||||
#endif
|
||||
|
||||
String FilePathSetting(const char* variable, const char* fallback) {
|
||||
const char* value = std::getenv(variable);
|
||||
return value != nullptr ? String(value) : String(fallback);
|
||||
}
|
||||
|
||||
String Trimmed(const String& text) {
|
||||
const auto first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == String::npos) return String();
|
||||
const auto last = text.find_last_not_of(" \t\r\n");
|
||||
return text.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
Bool IsServerProcess() {
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
return role != nullptr && std::strcmp(role, "server") == 0;
|
||||
}
|
||||
|
||||
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
|
||||
// not an error: the defaults are the absence of one.
|
||||
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
|
||||
if (path.empty()) return;
|
||||
std::FILE* file = std::fopen(path.c_str(), "r");
|
||||
if (file == nullptr) return;
|
||||
char buffer[4096];
|
||||
Uint32 lineNumber = 0;
|
||||
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
|
||||
++lineNumber;
|
||||
const String line = Trimmed(buffer);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
const auto equals = line.find('=');
|
||||
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
|
||||
if (key.empty() || !IsAcceptedPrefix(key)) {
|
||||
if (log) {
|
||||
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
|
||||
lineNumber);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String value = Trimmed(line.substr(equals + 1));
|
||||
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
|
||||
value = value.substr(1, value.size() - 2);
|
||||
}
|
||||
out.emplace(key, value);
|
||||
}
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
|
||||
if (path.empty()) return;
|
||||
std::FILE* file = std::fopen(path.c_str(), "r");
|
||||
if (file == nullptr) return;
|
||||
char buffer[256];
|
||||
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
|
||||
std::fclose(file);
|
||||
if (!read) return;
|
||||
const String name = Trimmed(buffer);
|
||||
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
|
||||
}
|
||||
|
||||
// Everything the system files say, the backend file ahead of client.conf (first emplace
|
||||
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
|
||||
// and a file that changed between two processes' starts must not be served stale.
|
||||
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
|
||||
if (IsServerProcess()) return;
|
||||
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
|
||||
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
|
||||
#if defined(__ANDROID__)
|
||||
// An app that hosts the library (FCL) owns its process environment, so a developer
|
||||
// has no way to set a MOBILEGL_* knob in it. `adb shell setprop debug.mobilegl.env
|
||||
// "MOBILEGL_A=1;MOBILEGL_B=0"` does it, below the environment like the Linux config
|
||||
// file (PROP_VALUE_MAX bounds it to a few keys).
|
||||
char property[PROP_VALUE_MAX] = {};
|
||||
if (__system_property_get("debug.mobilegl.env", property) > 0) {
|
||||
String all(property);
|
||||
SizeT start = 0;
|
||||
while (start < all.size()) {
|
||||
SizeT end = all.find(';', start);
|
||||
if (end == String::npos) end = all.size();
|
||||
const String item = all.substr(start, end - start);
|
||||
const SizeT eq = item.find('=');
|
||||
if (eq != String::npos && eq > 0) {
|
||||
out.emplace(item.substr(0, eq), item.substr(eq + 1));
|
||||
if (log) MGLOG_I("Config: debug.mobilegl.env sets %s", item.c_str());
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool LookupSetting(const char* key, String& outValue) {
|
||||
if (const char* value = std::getenv(key)) {
|
||||
outValue = value;
|
||||
return true;
|
||||
}
|
||||
UnorderedMap<String, String> system;
|
||||
ReadSystemConfig(system, false);
|
||||
const auto it = system.find(key);
|
||||
if (it == system.end()) return false;
|
||||
outValue = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void InitializeAcceptedEnvVariables() {
|
||||
if (!acceptedEnvVariablesMap) {
|
||||
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
|
||||
@@ -79,6 +210,15 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Below the environment: a key the environment set is never replaced.
|
||||
UnorderedMap<String, String> system;
|
||||
ReadSystemConfig(system, true);
|
||||
for (const auto& [key, value] : system) {
|
||||
if (acceptedEnvVariablesMap->emplace(key, value).second) {
|
||||
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
|
||||
@@ -109,11 +249,19 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
|
||||
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
|
||||
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
|
||||
// Unset, empty or "auto" keeps Auto; "forceon"/"on" and "forceoff"/"off"/"no" name the two
|
||||
// overrides; anything else is read as a flag (1/true -> ForceOn, 0/false -> ForceOff).
|
||||
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return MG_Config::QuirkOverride::Auto;
|
||||
}
|
||||
String lowered = it->second;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowered.empty() || lowered == "auto") return MG_Config::QuirkOverride::Auto;
|
||||
if (lowered == "forceon" || lowered == "on") return MG_Config::QuirkOverride::ForceOn;
|
||||
if (lowered == "forceoff" || lowered == "off" || lowered == "no") return MG_Config::QuirkOverride::ForceOff;
|
||||
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
|
||||
: MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
@@ -221,19 +369,38 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||
features.MagmaFixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.SubgroupEmulation = QueryEnvQuirkOverride("MOBILEGL_SUBGROUP_EMULATION");
|
||||
features.IterationRPFix = QueryEnvQuirkOverride("MOBILEGL_ITRP_FIX");
|
||||
features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR");
|
||||
features.MagmaInPassClearDefault = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT");
|
||||
features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE");
|
||||
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
|
||||
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
|
||||
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
|
||||
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
|
||||
features.PipeFramebufferStateMemo = QueryEnvQuirkOverride("MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
|
||||
// P15: one backend-neutral knob; the Magma-only spelling stays as a deprecated alias.
|
||||
if (acceptedEnvVariablesMap->count("MOBILEGL_FRAMES_IN_FLIGHT") != 0) {
|
||||
features.FramesInFlight = QueryEnvUint32("MOBILEGL_FRAMES_IN_FLIGHT", 3, 1, 64);
|
||||
} else {
|
||||
features.FramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
if (acceptedEnvVariablesMap->count("MOBILEGL_MAGMA_FRAMESINFLIGHT") != 0)
|
||||
MGLOG_W("Config: MOBILEGL_MAGMA_FRAMESINFLIGHT is deprecated; use MOBILEGL_FRAMES_IN_FLIGHT");
|
||||
}
|
||||
features.MagmaExtraSwapchainImages = QueryEnvUint32("MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES", 0, 0, 16);
|
||||
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");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.EsprytDeferGeometryEmits = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS");
|
||||
features.EsprytAtomicSubgroupScratch = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH");
|
||||
features.EsprytInvalidImagePlaceholder = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
@@ -260,37 +427,40 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
features.MagmaE5RawReads = QueryEnvQuirkOverride("MOBILEGL_MAGMA_E5_RAW_READS");
|
||||
features.MagmaSplitSliceCopies = QueryEnvQuirkOverride("MOBILEGL_MAGMA_SPLIT_SLICE_COPIES");
|
||||
features.MagmaReemitStateAfterDiscard = QueryEnvQuirkOverride("MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD");
|
||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||
// subsystems off, everything P3a landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
|
||||
#else
|
||||
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||
// "pull everything" and that stays literally true.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
||||
#endif
|
||||
// P13 W3b (ID-P13-3): bits 0-13 are fixed on. The arms a cleared bit used to select
|
||||
// (the pre-handle memos, the pull build's frontend reads) are gone, so a cleared bit has
|
||||
// nothing to run; it is refused here, by name, before any backend is brought up, rather
|
||||
// than discovered as an armless subsystem at its first draw. Bit 63 (CSO content
|
||||
// addressing off) stays a switch.
|
||||
if ((features.PipePush & MG_Pipe::kMGPipeSubsystemsMigratedAtP5e) !=
|
||||
MG_Pipe::kMGPipeSubsystemsMigratedAtP5e) {
|
||||
MGLOG_F("MGPipe: Fatal{PipeSubsystemsFixedOn, \"MOBILEGL_PIPE_PUSH=0x%llx clears subsystem "
|
||||
"bit(s) 0x%llx; bits 0-13 are fixed on since P13 - only bit 63 may be set or "
|
||||
"cleared\"}",
|
||||
static_cast<unsigned long long>(features.PipePush),
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP5e &
|
||||
~features.PipePush));
|
||||
std::abort();
|
||||
}
|
||||
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
||||
features.PipeVerifyFatal =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||
#endif
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||
// truthy check: unset must keep the memos, and only an explicitly falsy value may
|
||||
// drop them.
|
||||
features.PipeLegacyMemos =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
|
||||
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
|
||||
@@ -299,6 +469,17 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitBackendType() {
|
||||
String backendTypeStr;
|
||||
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
|
||||
#if defined(__ANDROID__)
|
||||
// An Android app that hosts the library sets the environment itself, before the library
|
||||
// loads; the debug property is how a developer picks the backend for such a process
|
||||
// without rebuilding it (`adb shell setprop debug.mobilegl.backend DirectVulkan`).
|
||||
char property[PROP_VALUE_MAX] = {};
|
||||
if (__system_property_get("debug.mobilegl.backend", property) > 0) {
|
||||
MGLOG_I("Config: debug.mobilegl.backend=%s overrides MOBILEGL_BACKEND_TYPE=%s", property,
|
||||
backendTypeStr.c_str());
|
||||
backendTypeStr = property;
|
||||
}
|
||||
#endif
|
||||
#define ENTRY(backendType) \
|
||||
if (backendTypeStr == #backendType) { \
|
||||
MG_Config::ActiveBackendType = BackendType::backendType; \
|
||||
@@ -314,7 +495,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
|
||||
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
|
||||
// (ARCHITECTURE.md §16). Shaped after InitBackendType above: an exact-name table, then
|
||||
// one fallback that names what it did instead. The two prefixed forms are the only
|
||||
// reason this is not literally that function's ENTRY macro.
|
||||
//
|
||||
@@ -325,6 +506,15 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitTransport() {
|
||||
String value;
|
||||
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
|
||||
#if defined(__ANDROID__)
|
||||
// As debug.mobilegl.backend above: a developer picks the transport of an app-hosted
|
||||
// library (a launcher's game process) without rebuilding it or its launcher.
|
||||
char property[PROP_VALUE_MAX] = {};
|
||||
if (__system_property_get("debug.mobilegl.transport", property) > 0) {
|
||||
MGLOG_I("Config: debug.mobilegl.transport=%s overrides MOBILEGL_TRANSPORT=%s", property, value.c_str());
|
||||
value = property;
|
||||
}
|
||||
#endif
|
||||
String lowered = value;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
@@ -344,12 +534,12 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"ring to an apply thread");
|
||||
return;
|
||||
}
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "rejected"
|
||||
// rather than "unknown", which are different bugs on the operator's side.
|
||||
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
|
||||
// rendezvous of its own and connects to it - the two processes are
|
||||
// independent, so this is `Dial = Connect` with a launcher attached
|
||||
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
|
||||
// rather than the fork-coupled shape ARCHITECTURE.md §15.1 described.
|
||||
if (lowered == "spawn") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Spawn;
|
||||
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
|
||||
@@ -368,9 +558,10 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return;
|
||||
}
|
||||
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
|
||||
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
|
||||
// ELSE is listening on needs the server lifecycle to be somebody
|
||||
// else's too, which is P12's. Named, never a silent degrade.
|
||||
// unix: and pipe: are NOT MOBILEGL_TRANSPORT values and are rejected here. Connecting
|
||||
// to a server somebody else is listening on is `spawn` plus
|
||||
// MOBILEGL_IPC_CONTROL=unix:<path>|unix:@<name>|tcp://host:port (InitIpc); there is
|
||||
// no pipe: form. Named, never a silent degrade.
|
||||
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
|
||||
"staying on monolith. This run is NOT a split run.",
|
||||
value.c_str());
|
||||
@@ -397,40 +588,35 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// are sanity, not policy.
|
||||
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
|
||||
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
|
||||
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
|
||||
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
|
||||
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
|
||||
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
|
||||
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
|
||||
// patience at all would forfeit a healthy run-ahead client the first time its ring
|
||||
// filled between two of the client's drains, which is an ordinary backlog.
|
||||
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
|
||||
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
|
||||
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
|
||||
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
|
||||
// Whole-range push is the 0 arm; with it on (default) only blocks whose
|
||||
// xxHash64 changed since the last push are shipped.
|
||||
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
|
||||
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
|
||||
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
|
||||
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
|
||||
// was inert before, the device numbers, and the sweep that put the default at 6 rather
|
||||
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
|
||||
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
|
||||
// declared answers, and the device sweep showed the wall clock still falling at the old
|
||||
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
|
||||
// StreamLink's crossing log are the checks on this number.
|
||||
// The create window: default 10, range 1..12. 12 is WireTables.cpp's kCreateWindowMax, half
|
||||
// of the stream link's declared-answer store, and its static_assert checks that bound.
|
||||
// Config.h has the semantics and the reason for the default.
|
||||
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);
|
||||
// A lost session is replaced by a fresh one on the application's next create (Config.h).
|
||||
ipc.RecoverAfterLoss = QueryEnvUint32("MOBILEGL_IPC_RECOVER", 1, 0, 1);
|
||||
// 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
|
||||
// P5e (docs/archive/contracts/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.
|
||||
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
|
||||
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
|
||||
@@ -440,19 +626,31 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
|
||||
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
|
||||
// admitted: a credit of zero would mean "publish no present at all".
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
|
||||
// P15: derived from MOBILEGL_FRAMES_IN_FLIGHT (Config.h states the mapping); the explicit
|
||||
// knob stays as an override for experiments.
|
||||
{
|
||||
const Uint32 derived = std::max<Uint32>(1u, std::min<Uint32>(8u, MG_Config::Features.FramesInFlight > 2
|
||||
? MG_Config::Features.FramesInFlight - 2
|
||||
: 1u));
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", derived, 1, 8);
|
||||
}
|
||||
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
|
||||
// Both are floored at 100 ms: a zero bound would declare every live server silent.
|
||||
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
|
||||
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
|
||||
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
|
||||
ipc.WaitStats = QueryEnvFlag("MOBILEGL_IPC_WAIT_STATS");
|
||||
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
|
||||
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
|
||||
// verify harness: the comparator's entry compare and compare-at-read hook are built on
|
||||
// there being ONE filled block (the hook pins itself to &gPipeInputs,
|
||||
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
|
||||
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
|
||||
// P15: ON BY DEFAULT. Every split lane and the on-device retrace gate already ran with it,
|
||||
// and the dual-block census is empty (no apply reads a client-filled field), so the
|
||||
// shipped arm now matches what is tested - and with two blocks the client's fill is its
|
||||
// own and needs no applier quiesce. 0 still turns it off (the negative control does).
|
||||
ipc.RoleSplitState = QueryEnvQuirkOverride("MOBILEGL_IPC_ROLE_SPLIT_STATE") != MG_Config::QuirkOverride::ForceOff;
|
||||
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
|
||||
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
|
||||
@@ -522,11 +720,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.RingMb, ipc.StageMb, MG_Config::RecordArm.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
MG_Config::RecordArm.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(),
|
||||
@@ -536,6 +736,26 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
|
||||
"live GLContext, so an unbarriered queue lets the server read future values");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// P13 W5: the record arm's knobs (Config.h RecordArmTable), in every build that has the arm.
|
||||
inline void InitRecordArm() {
|
||||
MG_Config::RecordArmTable& ipc = MG_Config::RecordArm;
|
||||
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
|
||||
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
|
||||
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
|
||||
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
|
||||
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
|
||||
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
|
||||
// Whole-range push is the 0 arm; with it on (default) only blocks whose
|
||||
// xxHash64 changed since the last push are shipped.
|
||||
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
|
||||
ipc.PersistentMprotect = QueryEnvUint32("MOBILEGL_PIPE_PERSISTENT_MPROTECT", 1, 0, 1);
|
||||
if (ipc.PersistentMprotect == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 - no SIGSEGV handler is installed "
|
||||
"for persistent write maps; every one takes the hash scan");
|
||||
}
|
||||
if (ipc.WireDeferredMb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
|
||||
"server never forces a sync for orphaned wire buffer stores, so a long frame "
|
||||
@@ -546,7 +766,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"the persistent-map push is OFF and a coherent-map scenario must go red");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Loading configuration from environment variables...");
|
||||
@@ -554,6 +773,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
|
||||
InitBackendType();
|
||||
InitFeatures();
|
||||
InitRecordArm();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
|
||||
// configuration, and before the accepted-env map is destroyed just below.
|
||||
|
||||
@@ -34,6 +34,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
|
||||
|
||||
|
||||
+36
-2
@@ -24,6 +24,10 @@
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
|
||||
#endif
|
||||
#include <MG_Impl/Pipe/Verb/VerbPort.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/ServerProbe.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
@@ -47,7 +51,7 @@ namespace MobileGL {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
|
||||
// P5: the split roles come down FIRST - ARCHITECTURE.md §14's order puts the whole
|
||||
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
|
||||
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
|
||||
// symbol and no resized one (the first version called it unconditionally and moved
|
||||
@@ -102,6 +106,10 @@ namespace MobileGL {
|
||||
MG_State::GLState::ClearProgramTranslationCache();
|
||||
MG_Backend::gBackendFunctionsTable = {};
|
||||
g_isInitialized = false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A later re-initialization asks whether the server is there again.
|
||||
MG_Remote::Client::ResetServerProbeCache();
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
|
||||
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
|
||||
@@ -155,7 +163,7 @@ namespace MobileGL {
|
||||
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
|
||||
// thread. And "the legacy pull path runs for it" is false under split - the client has
|
||||
// no pull path to fall back to - so the records are simply lost, and only a later
|
||||
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
|
||||
// respecify's self-healing create (EmitResourceRespecify in TextureEmit.h) ever brings one back.
|
||||
//
|
||||
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
|
||||
// MG_State::Init() no handshake is in flight, and the one that will land is started
|
||||
@@ -175,6 +183,13 @@ namespace MobileGL {
|
||||
#endif
|
||||
{
|
||||
MG_Backend::Init();
|
||||
// P13 W4: monolith's record arm reads the verb's own state, which only a verb sink
|
||||
// writes; the port routes the ported verbs through one (MG_Impl/Pipe/Verb/VerbPort.cpp).
|
||||
// Installed HERE, by the library's role-neutral init, rather than by MG_Backend's: the
|
||||
// port is client code, and the server image must not need it to link.
|
||||
if (MG_Config::RecordArmAliasesFrontend() && MG_Backend::pActiveBackendObject) {
|
||||
MG_Record::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable);
|
||||
}
|
||||
}
|
||||
MGLOG_D("MG_Backend initialized");
|
||||
MG_Impl::Init();
|
||||
@@ -220,6 +235,25 @@ namespace MobileGL {
|
||||
DestroyImpl(true);
|
||||
}
|
||||
|
||||
Bool ImplementationAvailable() {
|
||||
if (g_isInitialized.load(std::memory_order_acquire)) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Initialized: under a split transport the session either came up (a backend object
|
||||
// exists) or it did not, and a display on a session that never came up cannot draw.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
return MG_Backend::pActiveBackendObject != nullptr;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Not yet: the bring-up would dial the configured server and wait for it. Ask first.
|
||||
return MG_Remote::Client::ConfiguredServerAvailable();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// MobileGL's lifecycle is owned entirely by the host-API layers
|
||||
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
|
||||
// via EnsureInitialized(), and full teardown happens deterministically
|
||||
|
||||
@@ -26,13 +26,30 @@ namespace MobileGL {
|
||||
|
||||
namespace MG_ConfigLoader {
|
||||
void Init();
|
||||
// One setting resolved the way Init() resolves it - the environment, then the system
|
||||
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
|
||||
// before MobileGL initializes. False when neither names it.
|
||||
Bool LookupSetting(const char* key, String& outValue);
|
||||
} // namespace MG_ConfigLoader
|
||||
|
||||
// Whether this process can be served right now. False when MobileGL is configured to reach a
|
||||
// server that is not there (or a split session already failed to come up), so a
|
||||
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
|
||||
// decline rather than hand out something that cannot draw. LIBGL_ALWAYS_SOFTWARE does NOT
|
||||
// make it false: that variable selects the system GL's software renderer and is not a reason
|
||||
// to disable MobileGL's entry points. Cheap: once true it stays true
|
||||
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
|
||||
Bool ImplementationAvailable();
|
||||
|
||||
namespace MG_Backend {
|
||||
void Init();
|
||||
} // namespace MG_Backend
|
||||
|
||||
namespace MG_Impl {
|
||||
void Init();
|
||||
// GL framebuffer 0, built into the calling thread's GLContext (the process default at
|
||||
// Init) and installed as the one every later context starts with. Init's step, and the
|
||||
// rebuild after a recovered device loss, whose fresh session holds none of the old one's.
|
||||
void BuildDefaultFramebuffer();
|
||||
} // namespace MG_Impl
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -97,6 +97,8 @@ namespace MobileGL::MG_Backend {
|
||||
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
|
||||
return kFormatCapabilityTargetCount;
|
||||
}
|
||||
// An external texture is a 2D one wherever a format can go.
|
||||
if (target == TextureTarget::External) return static_cast<SizeT>(TextureTarget::Texture2D);
|
||||
return static_cast<SizeT>(target);
|
||||
}
|
||||
|
||||
@@ -172,11 +174,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) {
|
||||
@@ -196,12 +202,15 @@ namespace MobileGL::MG_Backend {
|
||||
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
auto surfaceIt = m_eglSurfaces.find(surface);
|
||||
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
|
||||
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
|
||||
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
|
||||
return false;
|
||||
}
|
||||
// A pbuffer-backed window (a Wayland window presented through wl_shm) resizes too.
|
||||
surfaceIt->second.Window.Width = width;
|
||||
surfaceIt->second.Window.Height = height;
|
||||
surfaceIt->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
surfaceIt->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == surface) {
|
||||
m_windowHandle.Width = width;
|
||||
m_windowHandle.Height = height;
|
||||
@@ -279,6 +288,7 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_activatingSurface = surface;
|
||||
if (surfaceState->Kind == SurfaceKind::Window) {
|
||||
SetWindowHandle(surfaceState->Window);
|
||||
if (!InitWindowSurface()) {
|
||||
@@ -298,8 +308,14 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurface = surface;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = surfaceState->Kind;
|
||||
m_eglCurrentThreads.clear();
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
// NOT m_eglCurrentThreads.clear(): another thread's current context and surface are its
|
||||
// own (a multi-window client renders each window on its own thread), and wiping them made
|
||||
// that thread's next eglSwapBuffers fail with "no current context attached".
|
||||
auto activatedIt = m_eglSurfaces.find(surface);
|
||||
if (activatedIt != m_eglSurfaces.end() && !activatedIt->second.Activated) {
|
||||
activatedIt->second.Activated = true;
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -366,6 +382,24 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglCurrentThreads.clear();
|
||||
}
|
||||
|
||||
void BackendObject::ForgetActiveEGLSurface() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
m_eglSurfaceInitialized = false;
|
||||
m_eglSurfaceKind = SurfaceKind::None;
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
m_windowHandle = {};
|
||||
}
|
||||
|
||||
namespace {
|
||||
MG_Util::Damage::Region& PresentDamageSlot() {
|
||||
thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full();
|
||||
return damage;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const MG_Util::Damage::Region& CurrentPresentDamage() { return PresentDamageSlot(); }
|
||||
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) { PresentDamageSlot() = damage; }
|
||||
|
||||
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
|
||||
@@ -382,10 +416,16 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
|
||||
return false;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
|
||||
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
|
||||
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
|
||||
return false;
|
||||
}
|
||||
// The calling thread's surface, which another thread's make-current may have moved the
|
||||
// backend off: bring it back before presenting it.
|
||||
if ((!m_eglSurfaceInitialized || draw != m_eglSurface) && !ActivateEGLSurface(draw)) {
|
||||
MGLOG_E("SwapEGLBuffers failed: the draw surface could not be re-activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& backendFunctions = GetBackendFunctions();
|
||||
if (!backendFunctions.Present) {
|
||||
@@ -426,8 +466,9 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurfaces.erase(surfaceIt);
|
||||
if (m_eglSurface == surface) {
|
||||
OnEGLSurfaceReleased(surface);
|
||||
ResetEGLRuntimeState();
|
||||
ForgetActiveEGLSurface();
|
||||
}
|
||||
OnEGLSurfaceForgotten(surface);
|
||||
}
|
||||
|
||||
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
|
||||
@@ -458,8 +499,9 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurfaces.erase(surfaceIt);
|
||||
if (m_eglSurface == surface) {
|
||||
OnEGLSurfaceReleased(surface);
|
||||
ResetEGLRuntimeState();
|
||||
ForgetActiveEGLSurface();
|
||||
}
|
||||
OnEGLSurfaceForgotten(surface);
|
||||
}
|
||||
|
||||
void BackendObject::ReleaseEGLResources() {
|
||||
@@ -492,4 +534,8 @@ namespace MobileGL::MG_Backend {
|
||||
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
}
|
||||
|
||||
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
(void)surface;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
@@ -36,22 +35,16 @@ namespace MobileGL {
|
||||
struct CopyImageEndpoint {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> Texture;
|
||||
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MG_Pipe::MGPipeHandle RenderbufferHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
|
||||
Bool IsRenderbuffer() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Renderbuffer != nullptr;
|
||||
}
|
||||
Bool Exists() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(TextureHandle)) return true;
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Texture != nullptr || Renderbuffer != nullptr;
|
||||
}
|
||||
};
|
||||
@@ -316,6 +309,22 @@ namespace MobileGL {
|
||||
void (*SetSwapInterval)(Int interval);
|
||||
};
|
||||
|
||||
// Device features that decide whether an extension string may be advertised, beyond the
|
||||
// per-backend positional flags of BuildAdvertisedExtensions. Each is the PROBED driver
|
||||
// capability (an ES version / extension on DirectGLES, a VkPhysicalDeviceFeatures bit on
|
||||
// DirectVulkan), never a vendor guess. Defaults describe the no-device case.
|
||||
struct AdvertisedFeatureSupport {
|
||||
// ES 3.2 or GL_EXT_tessellation_shader; VkPhysicalDeviceFeatures::tessellationShader.
|
||||
Bool TessellationShader = false;
|
||||
// At least one cull distance: GL_EXT_clip_cull_distance with a non-zero
|
||||
// GL_MAX_CULL_DISTANCES; VkPhysicalDeviceFeatures::shaderCullDistance with a non-zero
|
||||
// maxCullDistances.
|
||||
Bool CullDistance = false;
|
||||
// GL_EXT_clip_control on DirectGLES. DirectVulkan applies clip control in its own
|
||||
// position fixup and does not read this.
|
||||
Bool ClipControl = false;
|
||||
};
|
||||
|
||||
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
|
||||
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
|
||||
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
|
||||
@@ -418,6 +427,8 @@ namespace MobileGL {
|
||||
Int MaxImageUnits = 8;
|
||||
Int MaxCombinedImageUniforms = 8;
|
||||
Int MaxVertexImageUniforms = 0;
|
||||
Int MaxTessControlImageUniforms = 0;
|
||||
Int MaxTessEvaluationImageUniforms = 0;
|
||||
Int MaxGeometryImageUniforms = 0;
|
||||
Int MaxFragmentImageUniforms = 8;
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
@@ -562,8 +573,70 @@ namespace MobileGL {
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
Uint32 SubgroupSupportedFeatures = 0;
|
||||
// GL_SAMPLE_POSITION (GL 4.6 core 14.3.1.1) as the device actually places the samples
|
||||
// of a 2-, 4-, 8- and 16-sample pixel: (x, y) pairs in [0, 1], the N-sample row starting
|
||||
// at pair N - 2. Bit log2(N) of SamplePositionCountMask says row N was filled in; a row
|
||||
// the backend could not determine stays unset rather than being guessed. Read and
|
||||
// written through Get/SetSamplePositions only.
|
||||
static constexpr Int kMaxSamplePositionCount = 16;
|
||||
Float SamplePositions[60] = {}; // 2 * (2 + 4 + 8 + 16)
|
||||
Uint32 SamplePositionCountMask = 0;
|
||||
Bool SubgroupQuadOperationsInAllStages = false;
|
||||
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
|
||||
// GL_EXT_fragment_shading_rate and its two companions. Each is a statement about the
|
||||
// device, so all of them default to "absent" and the extension strings are advertised
|
||||
// only from these: DirectGLES fills them from the driver's own
|
||||
// EXT_fragment_shading_rate(_primitive, _attachment), DirectVulkan from
|
||||
// VK_KHR_fragment_shading_rate's pipeline / primitive / attachment features.
|
||||
// SupportsFragmentShadingRate glShadingRateEXT and gl_ShadingRateEXT
|
||||
// SupportsPrimitiveFragmentShadingRate gl_PrimitiveShadingRateEXT, combiner op 0
|
||||
// SupportsAttachmentFragmentShadingRate the R8UI rate attachment, combiner op 1
|
||||
Bool SupportsFragmentShadingRate = false;
|
||||
Bool SupportsPrimitiveFragmentShadingRate = false;
|
||||
Bool SupportsAttachmentFragmentShadingRate = false;
|
||||
// The four GL_FRAGMENT_SHADING_RATE_*_SUPPORTED_EXT booleans, as the device answers them.
|
||||
Bool FragmentShadingRateNonTrivialCombiners = false;
|
||||
Bool FragmentShadingRateWithShaderDepthStencilWrites = false;
|
||||
Bool FragmentShadingRateWithSampleMask = false;
|
||||
Bool FragmentShadingRatePrimitiveWithMultiViewport = false;
|
||||
Bool FragmentShadingRateAttachmentWithDefaultFramebuffer = false;
|
||||
// What glGetFragmentShadingRatesEXT answers, one mask per sample count 1, 2, 4 .. 64
|
||||
// (index = log2(samples)): bit (log2(w) << 2 | log2(h)) is set when that rate is
|
||||
// honoured exactly at that sample count, i.e. gl_ShadingRateEXT reads it back
|
||||
// unclamped. Zero masks mean no rate at all, which is what a sample count the device
|
||||
// cannot shade coarsely answers.
|
||||
Uint16 FragmentShadingRateMasks[7] = {};
|
||||
// The GL_{MIN,MAX}_FRAGMENT_SHADING_RATE_ATTACHMENT_TEXEL_{WIDTH,HEIGHT}_EXT,
|
||||
// _ASPECT_RATIO_EXT and _LAYERS_EXT limits. Meaningful only with the attachment.
|
||||
Int MinFragmentShadingRateAttachmentTexelWidth = 0;
|
||||
Int MinFragmentShadingRateAttachmentTexelHeight = 0;
|
||||
Int MaxFragmentShadingRateAttachmentTexelWidth = 0;
|
||||
Int MaxFragmentShadingRateAttachmentTexelHeight = 0;
|
||||
Int MaxFragmentShadingRateAttachmentTexelAspectRatio = 0;
|
||||
Int MaxFragmentShadingRateAttachmentLayers = 0;
|
||||
|
||||
|
||||
static constexpr Bool IsSamplePositionCount(Int samples) {
|
||||
return samples >= 2 && samples <= kMaxSamplePositionCount && (samples & (samples - 1)) == 0;
|
||||
}
|
||||
static constexpr Uint32 SamplePositionCountBit(Int samples) {
|
||||
return 1u << static_cast<Uint32>(std::countr_zero(static_cast<Uint32>(samples)));
|
||||
}
|
||||
// `xy` holds `samples` (x, y) pairs.
|
||||
void SetSamplePositions(Int samples, const Float* xy) {
|
||||
if (!IsSamplePositionCount(samples)) return;
|
||||
for (Int i = 0; i < 2 * samples; ++i) SamplePositions[2 * (samples - 2) + i] = xy[i];
|
||||
SamplePositionCountMask |= SamplePositionCountBit(samples);
|
||||
}
|
||||
Bool GetSamplePosition(Int samples, Int index, Float* outXY) const {
|
||||
if (!IsSamplePositionCount(samples) || index < 0 || index >= samples ||
|
||||
(SamplePositionCountMask & SamplePositionCountBit(samples)) == 0) {
|
||||
return false;
|
||||
}
|
||||
outXY[0] = SamplePositions[2 * (samples - 2 + index)];
|
||||
outXY[1] = SamplePositions[2 * (samples - 2 + index) + 1];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
enum class WindowBackend {
|
||||
@@ -583,26 +656,256 @@ namespace MobileGL {
|
||||
Uint32 Height = 0;
|
||||
};
|
||||
|
||||
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
|
||||
// buffers exported to clients as dma-buf descriptors.
|
||||
//
|
||||
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
|
||||
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
|
||||
// memory, GL RGBA8) or XBGR8888 (alpha ignored); ARGB8888 / XRGB8888 images are stored the
|
||||
// same way (SharedImageRegistry.h says why), so a backend binds every one as RGBA8.
|
||||
struct SharedImageView {
|
||||
Uint64 Id = 0;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Fourcc = 0;
|
||||
void* NativeBuffer = nullptr;
|
||||
};
|
||||
|
||||
// The client-side answer of an allocation: the image's id and its exported descriptor
|
||||
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
|
||||
struct SharedImageExport {
|
||||
Uint64 Id = 0;
|
||||
int Fd = -1;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Fourcc = 0;
|
||||
Uint32 Stride = 0;
|
||||
Uint32 Offset = 0;
|
||||
Uint64 Modifier = 0;
|
||||
// A YUV image's Cb Cr plane: in the same descriptor, at this offset and pitch.
|
||||
Uint32 Plane1Stride = 0;
|
||||
Uint32 Plane1Offset = 0;
|
||||
};
|
||||
|
||||
// An import of a two-plane (YUV) dma-buf: where its planes are in the one descriptor, and
|
||||
// EGL_EXT_image_dma_buf_import's colour hints (0 = not given).
|
||||
struct SharedImageImportLayout {
|
||||
Uint32 PlaneCount = 1;
|
||||
Uint32 Offset[2] = {0, 0};
|
||||
Uint32 Pitch[2] = {0, 0};
|
||||
Uint32 ColorSpace = 0;
|
||||
Uint32 Range = 0;
|
||||
Uint32 SitingX = 0;
|
||||
Uint32 SitingY = 0;
|
||||
// The DRM format modifier the import named; HasModifier false = none (the implicit one).
|
||||
Bool HasModifier = false;
|
||||
Uint64 Modifier = 0;
|
||||
};
|
||||
|
||||
// THE DAMAGE OF THE PRESENT BEING APPLIED on this thread: the server's apply thread sets it
|
||||
// around the backend's Present (from the present record), and a backend that can hand damage
|
||||
// to its native present reads it there. Full whenever nothing set it.
|
||||
const MG_Util::Damage::Region& CurrentPresentDamage();
|
||||
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage);
|
||||
|
||||
class BackendObject {
|
||||
public:
|
||||
virtual ~BackendObject() = default;
|
||||
|
||||
// ---- shared images, SERVER side (a backend implements these) ---------------------
|
||||
//
|
||||
// Copies the default framebuffer of the surface current on this (the apply) thread into
|
||||
// `image`, rows TOP FIRST - GL's top row (y = height-1) lands in the image's first memory
|
||||
// row, which is what a wl_buffer's first row is - scaled to the image's extent if the two
|
||||
// differ. The image is ORDERED THROUGH ITS SYNC STATE (SharedImageRegistry.h): the copy
|
||||
// waits for the image's pending read fences before it writes, and its own completion
|
||||
// is published with SharedImages::PublishWrite as a sync_file - the call returns once
|
||||
// the copy is submitted, never waiting for it on the CPU (a backend with no fence export
|
||||
// waits and publishes -1). A reader binding the image waits for that fence on its GPU
|
||||
// when the write generation moved, and at its frame boundary publishes its frame's fence
|
||||
// with PublishRead / ReadTracker. The application's GL state is untouched. False = not
|
||||
// supported / failed (logged).
|
||||
//
|
||||
// `region` (GL window coordinates, origin bottom-left) is the part of the frame to copy;
|
||||
// Full copies all of it. The rest of the image is left as it is - the client sends a
|
||||
// region only for an image that already holds the frame outside it - and a backend that
|
||||
// cannot be sure the image's contents survived (a target it has never written) copies
|
||||
// all of it regardless.
|
||||
virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) {
|
||||
(void)image, (void)region;
|
||||
return false;
|
||||
}
|
||||
|
||||
// EGL_BUFFER_AGE_EXT of the draw surface of the context current on this (the apply)
|
||||
// thread: how many presents ago the buffer the next frame draws into held the frame then
|
||||
// presented; 0 = unknown (the application repaints everything). Asking says the client
|
||||
// relies on the answer, so from then on the backend keeps what it presents.
|
||||
//
|
||||
// `damageRegionFollows`: the client declares the region it draws (SetCurrentDamageRegion)
|
||||
// before drawing. A driver whose only buffer age is EGL_KHR_partial_update's keeps a
|
||||
// buffer's content only outside that region, so without one coming it is not asked.
|
||||
virtual Int32 QueryCurrentBufferAge(Bool damageRegionFollows) {
|
||||
(void)damageRegionFollows;
|
||||
return 0;
|
||||
}
|
||||
// eglSetDamageRegionKHR for that draw surface: the frame draws `region` (GL window
|
||||
// coordinates; Full = all of it) and nothing outside it. A backend without the notion
|
||||
// ignores it.
|
||||
virtual Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
|
||||
(void)region;
|
||||
return true;
|
||||
}
|
||||
// IMPLICIT SYNC BY FLUSH (docs/archive/notes/anland/plan-x11-gpu.md): the session
|
||||
// on this (the apply) thread ends a frame at a glFlush. Every shared image it used since
|
||||
// its last boundary is published as written and read by one fence of everything it
|
||||
// submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first
|
||||
// use of an image in a frame waits for that image's pending reads (it may be about to
|
||||
// overwrite what another session still samples). A backend without shared images has
|
||||
// nothing to publish.
|
||||
virtual Bool PublishSharedImageAccesses() { return true; }
|
||||
// EGL_ANDROID_native_fence_sync's fence command, on the server: in `*fence`, a sync_file
|
||||
// (the caller's to close) that signals once everything the session submitted so far has
|
||||
// completed - and, for a session that writes images, what PublishSharedImageAccesses
|
||||
// does. -1 when there is none to give: the backend cannot export one, or the work has
|
||||
// already completed - in both cases it HAS completed when this returns.
|
||||
virtual Bool ExportNativeFence(int* fence) {
|
||||
*fence = -1;
|
||||
return PublishSharedImageAccesses();
|
||||
}
|
||||
|
||||
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
|
||||
//
|
||||
// A new image, allocated by the server; `out->Fd` is the caller's to close.
|
||||
virtual Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc, SharedImageExport* out) {
|
||||
(void)width, (void)height, (void)fourcc, (void)out;
|
||||
return false;
|
||||
}
|
||||
// Names the image `fd` was exported from (the caller keeps `fd`). False: not one of this
|
||||
// server's images, or the size/format disagree.
|
||||
virtual Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
|
||||
(void)fd, (void)width, (void)height, (void)fourcc, (void)outId;
|
||||
return false;
|
||||
}
|
||||
// The same for a YUV fourcc, whose planes and hints `layout` gives. A YUV buffer the
|
||||
// server did not export is taken too (its labelled CPU-copy fallback).
|
||||
virtual Bool ImportSharedImagePlanes(int fd, Uint32 width, Uint32 height, Uint32 fourcc,
|
||||
const SharedImageImportLayout& layout, Uint64* outId) {
|
||||
(void)fd, (void)width, (void)height, (void)fourcc, (void)layout, (void)outId;
|
||||
return false;
|
||||
}
|
||||
// Drops this client's reference (an allocation's or an import's).
|
||||
virtual Bool ReleaseSharedImage(Uint64 id) {
|
||||
(void)id;
|
||||
return false;
|
||||
}
|
||||
// In the GL stream: the current context's default framebuffer, copied top row first into
|
||||
// the image (BlitDefaultFramebufferToSharedImage on the server) - `region` of it, Full for
|
||||
// all. Returns when it completed.
|
||||
virtual Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) {
|
||||
(void)id, (void)region;
|
||||
return false;
|
||||
}
|
||||
// In the GL stream: EGL_BUFFER_AGE_EXT of the current draw surface (QueryCurrentBufferAge
|
||||
// on the server). False: the backend has no answer (the caller reports 0).
|
||||
virtual Bool QueryBufferAge(Bool damageRegionFollows, EGLint* age) {
|
||||
(void)damageRegionFollows, (void)age;
|
||||
return false;
|
||||
}
|
||||
// In the GL stream: eglSetDamageRegionKHR for the current draw surface
|
||||
// (SetCurrentDamageRegion on the server).
|
||||
virtual Bool SetDamageRegion(const MG_Util::Damage::Region& region) {
|
||||
(void)region;
|
||||
return false;
|
||||
}
|
||||
// In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as
|
||||
// its level-0 storage (glEGLImageTargetTexture2DOES), until its next storage definition.
|
||||
// The caller has already defined level 0 as Width x Height RGBA8 with no data.
|
||||
virtual Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
|
||||
(void)textureLifetimeId, (void)id;
|
||||
return false;
|
||||
}
|
||||
// In the GL stream, from glFlush/glFinish: PublishSharedImageAccesses on the server,
|
||||
// returning once the fence is published. Skipped (true) when nothing reached the stream
|
||||
// since the last one.
|
||||
virtual Bool FlushSharedImageAccesses() { return false; }
|
||||
// In the GL stream, from eglCreateSync(EGL_SYNC_NATIVE_FENCE_ANDROID) with no descriptor:
|
||||
// ExportNativeFence on the server. `*fence` is the sync_file it handed back (the
|
||||
// caller's to close), or -1 when it had none - the fenced work has then completed.
|
||||
virtual Bool CreateNativeFence(int* fence) {
|
||||
*fence = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void Initialize() = 0;
|
||||
virtual Bool InitCapabilities() = 0;
|
||||
virtual Bool InitWindowSurface() = 0;
|
||||
|
||||
// A served session's apply thread - the one thread that runs this backend from here on -
|
||||
// calls this first, in a process that may serve several sessions at once. A backend whose
|
||||
// native state is process-global by construction gives this object its own copy and binds
|
||||
// it to the calling thread. The default has nothing per process to separate.
|
||||
virtual void BindSessionStateToThisThread() {}
|
||||
// A served session's apply thread asks this each time it wakes, BEFORE it applies another
|
||||
// record: true when this session's own GPU work faulted or hung the GPU and the backend has
|
||||
// latched the session for it (MGPipeSessionLatch, family DeviceLost), so the next frame -
|
||||
// which would very likely stop the GPU again - is never applied. Cheap by contract: no
|
||||
// wait, at most a status read. The default knows of no fault.
|
||||
virtual Bool LatchIfGpuFaulted() { return false; }
|
||||
// A served session's ring moved to another of its client's contexts (bind_context). A
|
||||
// backend whose surface follows the native context it binds needs nothing; one that keeps
|
||||
// a single native surface binding re-binds the surface that context was last made current
|
||||
// with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given).
|
||||
virtual void OnClientContextBound(Uint64 token) { (void)token; }
|
||||
|
||||
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
|
||||
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);
|
||||
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
// eglSwapBuffersWithDamage: `damage` (GL window coordinates) is a hint for the native
|
||||
// present. A backend with nothing to hand it to swaps as SwapEGLBuffers does.
|
||||
virtual Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const MG_Util::Damage::Region& damage) {
|
||||
(void)damage;
|
||||
return SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
// Forwards the app-requested eglSwapInterval to the backend's native
|
||||
// presentation path (no-op for backends without a SetSwapInterval hook).
|
||||
virtual void SetEGLSwapInterval(Int interval);
|
||||
virtual void ReleaseEGLSurface(EGLSurface surface);
|
||||
virtual void ReleaseEGLResources();
|
||||
|
||||
// ---- the server's own display window going away and coming back -------------------
|
||||
//
|
||||
// `window` (a window surface's native window) is about to be destroyed - the display's
|
||||
// screen went off. Release ONLY what is bound to it: every window surface on it keeps its
|
||||
// client handle and draws into a placeholder from here on, and every context, object and
|
||||
// device stays. Called on the thread that runs this backend; nothing may reference the
|
||||
// window once it returns true. False: not supported (the caller ends the session instead).
|
||||
virtual Bool SuspendServerWindow(void* window) {
|
||||
(void)window;
|
||||
return false;
|
||||
}
|
||||
// The surfaces SuspendServerWindow parked are rebuilt on `window` (a new native window,
|
||||
// `width`x`height`), and their new extent is published as a creation publishes it. False:
|
||||
// they could not be (the caller ends the session).
|
||||
virtual Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
|
||||
(void)window, (void)width, (void)height;
|
||||
return false;
|
||||
}
|
||||
// `window` changed size to `width`x`height` (the display's report - the one size to publish).
|
||||
// Its surfaces publish that extent to the client now, without waiting for a present; a
|
||||
// swapchain that has to follow is rebuilt when no frame is half-recorded, else at the
|
||||
// present. False: nothing was published (no surface on it, or not supported).
|
||||
virtual Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) {
|
||||
(void)window, (void)width, (void)height;
|
||||
return false;
|
||||
}
|
||||
// CLIENT side, from eglQuerySurface(EGL_WIDTH/EGL_HEIGHT): a backend whose surface extents
|
||||
// change under the application (a remote server's window) applies what it has received.
|
||||
virtual void RefreshSurfaceExtent(EGLSurface surface) { (void)surface; }
|
||||
|
||||
void SetWindowHandle(const WindowHandle& handle);
|
||||
|
||||
virtual const RendererInfo& GetRendererInfo() const = 0;
|
||||
@@ -629,18 +932,28 @@ namespace MobileGL {
|
||||
struct EGLSurfaceState {
|
||||
SurfaceKind Kind = SurfaceKind::None;
|
||||
Bool DestroyPending = false;
|
||||
// Activated at least once: a re-activation (a multi-window client switching back)
|
||||
// is a re-bind and does not re-run the backend's capability probe.
|
||||
Bool Activated = false;
|
||||
WindowHandle Window;
|
||||
EGLint Width = 1;
|
||||
EGLint Height = 1;
|
||||
};
|
||||
|
||||
void ResetEGLRuntimeState();
|
||||
// ResetEGLRuntimeState for a released ACTIVE surface: the backend has no active surface
|
||||
// any more, but every OTHER thread's current context and surface stay as they were (a
|
||||
// multi-window client closing one popup must not unbind its other windows' threads).
|
||||
void ForgetActiveEGLSurface();
|
||||
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
|
||||
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
||||
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
|
||||
Bool ActivateEGLSurface(EGLSurface surface);
|
||||
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
virtual void OnEGLSurfaceReleased(EGLSurface surface);
|
||||
// Every registered surface the base class drops, current or not, after it is gone from
|
||||
// m_eglSurfaces. A backend that keeps a native surface per client surface frees it here.
|
||||
virtual void OnEGLSurfaceForgotten(EGLSurface surface);
|
||||
FormatCapabilityCache& MutableFormatCapabilities();
|
||||
|
||||
mutable std::recursive_mutex m_eglStateMutex;
|
||||
@@ -648,6 +961,10 @@ namespace MobileGL {
|
||||
WindowHandle m_windowHandle;
|
||||
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
|
||||
EGLSurface m_eglSurface = EGL_NO_SURFACE;
|
||||
// The client surface ActivateEGLSurface is bringing up while it calls InitWindowSurface /
|
||||
// InitPbufferSurface, which take no surface argument: a backend that maps client surfaces
|
||||
// to native ones reads it to re-bind an existing native surface instead of making another.
|
||||
EGLSurface m_activatingSurface = EGL_NO_SURFACE;
|
||||
Bool m_eglDisplayInitialized = false;
|
||||
Bool m_eglSurfaceInitialized = false;
|
||||
Bool m_backendCapabilitiesInitialized = false;
|
||||
|
||||
@@ -21,11 +21,11 @@ namespace MobileGL::MG_Backend {
|
||||
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
|
||||
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
|
||||
//
|
||||
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
|
||||
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md §14's order is
|
||||
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
|
||||
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
|
||||
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
|
||||
// to happen at its top. Two consequences are load-bearing rather than tidy:
|
||||
// the transport -> the client drains the compile pool -> MobileGL::Destroy(); DestroyImpl IS
|
||||
// MobileGL::Destroy, so everything before that last arrow has to happen at its top. Two
|
||||
// consequences are load-bearing rather than tidy:
|
||||
//
|
||||
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
|
||||
// a counter while the final line is produced;
|
||||
@@ -33,11 +33,8 @@ namespace MobileGL::MG_Backend {
|
||||
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
|
||||
// owns the context, by ServerLoop::Stop, rather than on the app thread.
|
||||
//
|
||||
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
|
||||
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
|
||||
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
|
||||
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
|
||||
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
|
||||
// The sync/query registries stay where they are: DestroyImpl drains them with
|
||||
// DestroyAllSyncObjects and DestroyAllQueryObjects before pActiveBackendObject.reset().
|
||||
void ShutdownSplitRoles();
|
||||
|
||||
// F1 (P7 wave 2). Init()'s SPLIT ARM, run BEFORE MG_State::Init() rather than after it.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
#include <MG_Backend/DirectGLES/SubgroupPolicy.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
@@ -32,7 +33,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ID-54 / ID-67 (v1, under ID-52/ID-59's grant for this file). Which VIRTUAL (dpy, draw,
|
||||
// read, ctx) the process's one native ES context is currently bound FOR, on the apply
|
||||
// thread. DirectGLES has one native context and one native surface (g_Context, g_Surface)
|
||||
@@ -78,7 +78,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
|
||||
if (!gl.glGetError) return;
|
||||
@@ -455,6 +454,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return supported;
|
||||
}
|
||||
|
||||
// GL_SAMPLE_POSITION is a property of the multisample framebuffer the driver builds, so it
|
||||
// is asked once per power-of-two sample count, of a throwaway RGBA8 renderbuffer framebuffer,
|
||||
// through the driver's own glGetMultisamplefv (ES 3.1 core). A count the driver rounds up
|
||||
// reports the first positions of the pattern it really uses - the samples gl_SampleID
|
||||
// 0..count-1 address in a shader. Counts that cannot be built are left unset.
|
||||
void ProbeSamplePositions(const MG_External::GLESFunctionsTable& gl,
|
||||
const MG_External::GLESCapabilities& capabilities,
|
||||
DynamicBackendParameters& dynamicParameters) {
|
||||
const auto& version = capabilities.GLESVersion;
|
||||
if (version.Major < 3 || (version.Major == 3 && version.Minor < 1)) return;
|
||||
if (!gl.glGetMultisamplefv || !gl.glGenFramebuffers || !gl.glBindFramebuffer ||
|
||||
!gl.glFramebufferRenderbuffer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers ||
|
||||
!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glRenderbufferStorageMultisample ||
|
||||
!gl.glDeleteRenderbuffers || !gl.glGetIntegerv) {
|
||||
return;
|
||||
}
|
||||
|
||||
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
|
||||
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
|
||||
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
||||
for (Int samples = 2; samples <= DynamicBackendParameters::kMaxSamplePositionCount &&
|
||||
samples <= capabilities.MaxSamples;
|
||||
samples *= 2) {
|
||||
GLuint framebuffer = 0, renderbuffer = 0;
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
gl.glGenRenderbuffers(1, &renderbuffer);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
gl.glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, 4, 4);
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
GLint actualSamples = 0;
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||
gl.glGetIntegerv(GL_SAMPLES, &actualSamples);
|
||||
}
|
||||
if (actualSamples >= samples) {
|
||||
Float positions[2 * DynamicBackendParameters::kMaxSamplePositionCount] = {};
|
||||
for (Int index = 0; index < samples; ++index) {
|
||||
gl.glGetMultisamplefv(GL_SAMPLE_POSITION, static_cast<GLuint>(index), &positions[2 * index]);
|
||||
}
|
||||
dynamicParameters.SetSamplePositions(samples, positions);
|
||||
}
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
gl.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
}
|
||||
// A count the driver refused leaves its error behind; it must not reach the application.
|
||||
if (gl.glGetError) {
|
||||
for (Int drained = 0; drained < 16 && gl.glGetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLuint renderbuffer,
|
||||
TextureInternalFormat format) {
|
||||
GLuint framebuffer = 0;
|
||||
@@ -843,7 +895,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
|
||||
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
|
||||
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray,
|
||||
AdvertisedFeatureSupport{.TessellationShader = capabilities.SupportsTessellationShader,
|
||||
.CullDistance = capabilities.MaxCullDistances > 0,
|
||||
.ClipControl = capabilities.SupportsClipControl});
|
||||
// Exactly what InitCapabilities forwards into the dynamic parameters: the base
|
||||
// extension and _primitive when the driver has them, _attachment not yet.
|
||||
auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions;
|
||||
if (capabilities.SupportsFragmentShadingRate) {
|
||||
extensions.push_back(E_GL_EXT_fragment_shading_rate);
|
||||
if (capabilities.SupportsFragmentShadingRatePrimitive) {
|
||||
extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive);
|
||||
}
|
||||
}
|
||||
// Native where the ES driver has subgroups, emulated in the compute stage where it
|
||||
// has none (SubgroupPolicy.h); the dynamic parameters below say which.
|
||||
if (ResolveEsprytSubgroupImplementation(capabilities) != SubgroupImplementation::None) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -860,7 +929,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Int maxSamples = 0;
|
||||
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// C6 / ID-52: read the ROLE's own cache. Under split that is the server's private backend
|
||||
// (ActiveBackendFormatCaps), not pActiveBackendObject, which holds the client's mirror.
|
||||
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
|
||||
@@ -872,17 +940,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
maxSamples = probedCounts.front();
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
|
||||
formatIndex < kFormatCapabilityFormatCount) {
|
||||
// Descending, so the head is the largest count this device actually allocated.
|
||||
const Vector<Int>& probedCounts =
|
||||
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
||||
if (!probedCounts.empty()) {
|
||||
maxSamples = probedCounts.front();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (maxSamples <= 0) {
|
||||
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
|
||||
}
|
||||
@@ -906,16 +963,69 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::RebindNativeSurfaceFor(EGLSurface surface) {
|
||||
const auto it = m_nativeSurfaces.find(surface);
|
||||
if (it == m_nativeSurfaces.end()) return false;
|
||||
if (DirectGLES::BindSessionSurface(it->second)) return true;
|
||||
// The session no longer owns it (destroyed with a window loss): forget and recreate.
|
||||
m_nativeSurfaces.erase(it);
|
||||
return false;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw) {
|
||||
// The ACTIVE CONTEXT's own surface, exactly: the creation's bind attached the new surface to
|
||||
// whichever context the session had bound (often another window's thread), and putting back
|
||||
// "the surface activated last" instead re-attached THAT context to a third window's surface -
|
||||
// a desktop drawing into and reading back from the panel's pbuffer.
|
||||
if (previousNativeDraw == EGL_NO_SURFACE) return;
|
||||
if (!DirectGLES::BindSessionSurface(previousNativeDraw)) return;
|
||||
for (const auto& [client, native] : m_nativeSurfaces) {
|
||||
if (native != previousNativeDraw) continue;
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
if (state == m_eglSurfaces.end()) break;
|
||||
m_eglSurface = client;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = state->second.Kind;
|
||||
if (state->second.Kind == SurfaceKind::Window) m_windowHandle = state->second.Window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitWindowSurface() {
|
||||
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
|
||||
// A suspended surface whose placeholder went: another placeholder - never the window, which
|
||||
// is gone.
|
||||
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), m_activatingSurface) !=
|
||||
m_suspendedWindowSurfaces.end()) {
|
||||
const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1);
|
||||
if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) {
|
||||
MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p",
|
||||
reinterpret_cast<void*>(m_activatingSurface));
|
||||
return false;
|
||||
}
|
||||
m_nativeSurfaces[m_activatingSurface] = placeholder;
|
||||
return true;
|
||||
}
|
||||
// Only use EGL for now
|
||||
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
||||
if (!DirectGLES::InitWindowSurface(nativeWindow)) {
|
||||
MGLOG_E("Failed to initialize window surface for DirectGLES backend");
|
||||
return false;
|
||||
}
|
||||
if (m_activatingSurface != EGL_NO_SURFACE) {
|
||||
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::LatchIfGpuFaulted() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return LatchIfGuiltyBeforeApply();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::Initialize() {
|
||||
MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions);
|
||||
MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress);
|
||||
@@ -942,6 +1052,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
// The colour-attachment cap the framebuffer family checks is known from here on.
|
||||
DirectGLES::ResolveRecordArmFamilies();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
return true;
|
||||
@@ -976,8 +1088,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
|
||||
//
|
||||
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
|
||||
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
|
||||
// the native context - and with it every buffer, texture and program the application had
|
||||
// built. The native context now belongs to the (session, context token) tuple the backend
|
||||
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
|
||||
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
|
||||
// bookkeeping (which surface is active, which threads made it current) does describe the
|
||||
// surface that is being replaced.
|
||||
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
|
||||
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
|
||||
if (m_eglSurfaceInitialized) {
|
||||
DestroyEGLContext();
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
|
||||
@@ -986,7 +1109,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the first virtual tuple adopts that bind. On failure nothing is known to be bound. The
|
||||
// pull arm below is the original statement, byte for byte (G1).
|
||||
const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||
if (created) {
|
||||
if (created && previous != EGL_NO_SURFACE && previous != surface) {
|
||||
// Another surface was current: the creation must not leave the context that held it
|
||||
// drawing to the new one (P14 multi-window clients).
|
||||
RestoreSurfaceAfterCreation(previousNativeDraw);
|
||||
NoteNativeContextGone();
|
||||
} else if (created) {
|
||||
NoteNativeContextFreshFromSurfaceCreation();
|
||||
} else {
|
||||
NoteNativeContextGone();
|
||||
@@ -1008,28 +1136,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// P14 S4: as for the window surface above - the native context is the (session, context
|
||||
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
|
||||
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
|
||||
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
|
||||
if (m_eglSurfaceInitialized) {
|
||||
DestroyEGLContext();
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull
|
||||
// arm below is the original statement, byte for byte (G1).
|
||||
const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
if (created) {
|
||||
if (created && previous != EGL_NO_SURFACE && previous != surface) {
|
||||
RestoreSurfaceAfterCreation(previousNativeDraw);
|
||||
NoteNativeContextGone();
|
||||
} else if (created) {
|
||||
NoteNativeContextFreshFromSurfaceCreation();
|
||||
} else {
|
||||
NoteNativeContextGone();
|
||||
}
|
||||
return created;
|
||||
#else
|
||||
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
const auto state = m_eglSurfaces.find(surface);
|
||||
const auto native = m_nativeSurfaces.find(surface);
|
||||
if (state == m_eglSurfaces.end() || state->second.Kind != SurfaceKind::Pbuffer || native == m_nativeSurfaces.end()) {
|
||||
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||
}
|
||||
// A pbuffer has no resize: the client surface gets a new native pbuffer at the new size,
|
||||
// and every context that drew to the old one draws to the new one.
|
||||
const EGLint w = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
const EGLint h = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
const EGLSurface replacement = DirectGLES::CreateSessionPbuffer(w, h);
|
||||
if (replacement == EGL_NO_SURFACE) {
|
||||
MGLOG_E("ResizeEGLWindowSurface: no %dx%d pbuffer could replace the old drawable", w, h);
|
||||
return false;
|
||||
}
|
||||
DirectGLES::ReplaceSessionSurface(native->second, replacement);
|
||||
native->second = replacement;
|
||||
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
|
||||
return DirectGLES::InitPbufferSurface(width, height);
|
||||
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
|
||||
if (!DirectGLES::InitPbufferSurface(width, height)) return false;
|
||||
if (m_activatingSurface != EGL_NO_SURFACE) {
|
||||
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
@@ -1038,9 +1194,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!DirectGLES::ReleaseCurrent()) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
}
|
||||
|
||||
@@ -1089,7 +1243,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
DirectGLES::IsBackendContextCurrentOnThisThread() &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
|
||||
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
|
||||
// The draw surface's own native surface (P14 multi-window): a known one is re-bound, so the
|
||||
// bind goes to it and not to whichever surface the session drew to last.
|
||||
// The real bind still runs whenever the skip above says so (ID-67: a different tuple onto
|
||||
// the same surface needs MakeCurrent's invalidations even though the driver triple is equal).
|
||||
const auto nativeDraw = m_nativeSurfaces.find(draw);
|
||||
if (!nativelyCurrentAlready && nativeDraw != m_nativeSurfaces.end()) {
|
||||
(void)DirectGLES::SelectSessionSurface(nativeDraw->second);
|
||||
}
|
||||
const Bool bound = nativelyCurrentAlready || DirectGLES::MakeCurrent();
|
||||
if (!bound) {
|
||||
NoteNativeContextGone();
|
||||
return false;
|
||||
}
|
||||
@@ -1102,14 +1265,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
(void)DirectGLES::ReleaseCurrent();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image, region);
|
||||
#else
|
||||
(void)image, (void)region;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Int32 BackendObject_DirectGLES::QueryCurrentBufferAge(Bool damageRegionFollows) {
|
||||
return DirectGLES::CurrentDrawBufferAge(damageRegionFollows);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
|
||||
return DirectGLES::SetCurrentDrawDamageRegion(region);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::PublishSharedImageAccesses() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return DirectGLES::SharedImageImpl::PublishPendingAccesses();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ExportNativeFence(int* fence) {
|
||||
*fence = -1;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return DirectGLES::SharedImageImpl::ExportNativeFence(fence);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
return BackendObject::SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
@@ -1122,18 +1318,111 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BackendObject_DirectGLES::ReleaseEGLResources() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
DestroyEGLContext();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
m_suspendedWindowSurfaces.clear();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
DestroyEGLContext();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr) return false;
|
||||
Vector<EGLSurface> onWindow;
|
||||
for (const auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
|
||||
m_suspendedWindowSurfaces.end())
|
||||
continue;
|
||||
onWindow.push_back(client);
|
||||
}
|
||||
for (const EGLSurface client : onWindow) {
|
||||
// A surface never activated has no native surface: only its record names the window.
|
||||
const auto native = m_nativeSurfaces.find(client);
|
||||
if (native != m_nativeSurfaces.end()) {
|
||||
const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second);
|
||||
if (placeholder == EGL_NO_SURFACE) return false;
|
||||
native->second = placeholder;
|
||||
}
|
||||
m_suspendedWindowSurfaces.push_back(client);
|
||||
}
|
||||
// The native binding moved onto the placeholder: the next make-current binds for real.
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the "
|
||||
"contexts and their objects stay",
|
||||
onWindow.size(), window);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr) return false;
|
||||
Vector<EGLSurface> suspended;
|
||||
suspended.swap(m_suspendedWindowSurfaces);
|
||||
for (const EGLSurface client : suspended) {
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
if (state == m_eglSurfaces.end()) continue; // released while suspended
|
||||
state->second.Window.Handle = window;
|
||||
state->second.Window.Width = width;
|
||||
state->second.Window.Height = height;
|
||||
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state->second.Window;
|
||||
// Never activated: its first activation creates it on the new window.
|
||||
const auto native = m_nativeSurfaces.find(client);
|
||||
if (native == m_nativeSurfaces.end()) continue;
|
||||
const EGLSurface surface =
|
||||
DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast<NativeWindowType>(window));
|
||||
if (surface == EGL_NO_SURFACE) return false;
|
||||
native->second = surface;
|
||||
}
|
||||
NoteNativeContextGone();
|
||||
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
|
||||
window, width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr) return false;
|
||||
Bool published = false;
|
||||
for (auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
|
||||
m_suspendedWindowSurfaces.end())
|
||||
continue;
|
||||
state.Window.Width = width;
|
||||
state.Window.Height = height;
|
||||
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state.Window;
|
||||
// The window surface's buffers follow the window by themselves (from the next one the
|
||||
// driver dequeues); only the client has to be told.
|
||||
const auto native = m_nativeSurfaces.find(client);
|
||||
if (native == m_nativeSurfaces.end()) continue;
|
||||
published = DirectGLES::RepublishWindowSurfaceShape(native->second,
|
||||
reinterpret_cast<NativeWindowType>(window)) ||
|
||||
published;
|
||||
}
|
||||
return published;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
// P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the
|
||||
// contexts. This used to be DestroyEGLContext() - every native context of the session, with
|
||||
// every object the client had built - which a multi-window client hit on its first closed
|
||||
// popup.
|
||||
(void)surface;
|
||||
NoteNativeContextGone();
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
m_suspendedWindowSurfaces.erase(
|
||||
std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface),
|
||||
m_suspendedWindowSurfaces.end());
|
||||
const auto it = m_nativeSurfaces.find(surface);
|
||||
if (it == m_nativeSurfaces.end()) return;
|
||||
const EGLSurface native = it->second;
|
||||
m_nativeSurfaces.erase(it);
|
||||
DirectGLES::DestroySessionSurface(native);
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
|
||||
@@ -1156,7 +1445,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported) {
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported,
|
||||
const AdvertisedFeatureSupport& features) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
@@ -1165,6 +1455,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
|
||||
// trivially true for every context this backend claims (4.6). KWin 6's
|
||||
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
|
||||
// strings, so withholding them fails its renderer bring-up on an otherwise capable
|
||||
// implementation.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||
@@ -1260,10 +1557,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Core in the 4.x versions this backend already claims, and named so a context of an
|
||||
// older requested version reaches the same, already-implemented surface (the conformance
|
||||
// suite gates its GL 3.x runs of these on the string alone):
|
||||
// - transform feedback objects, pause/resume and glDrawTransformFeedback (4.0);
|
||||
E_GL_ARB_transform_feedback2,
|
||||
// - multiple capture buffers per stream, gl_SkipComponents / gl_NextBuffer, the
|
||||
// indexed queries and glDrawTransformFeedbackStream (4.0);
|
||||
E_GL_ARB_transform_feedback3,
|
||||
// - GL_RGB10_A2UI (3.3), GL_RGB9_E5 (3.0) and the integer colour formats (3.0);
|
||||
E_GL_ARB_texture_rgb10_a2ui, E_GL_EXT_texture_shared_exponent, E_GL_EXT_texture_integer,
|
||||
// - the eleven pipeline-statistics query targets (4.6). Accepted by glBeginQuery and
|
||||
// answered with GL_QUERY_COUNTER_BITS = 0, the spec's own "this counter is not
|
||||
// instrumented" signal (see IsPipelineStatisticsQueryTarget in GL_Query.cpp);
|
||||
E_GL_ARB_pipeline_statistics_query,
|
||||
// - mix() over integer and boolean vectors in GLSL below 4.50. The frontend compiles it
|
||||
// to OpSelect, which the backend prints back as a core boolean mix.
|
||||
E_GL_EXT_shader_integer_mix,
|
||||
// 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};
|
||||
// Tessellation is core from GL 4.0, but on the host it is ES 3.2 or
|
||||
// GL_EXT_tessellation_shader, so the string follows the driver. GL_Query also reads it to
|
||||
// open the two tessellation pipeline-statistics targets.
|
||||
if (features.TessellationShader) {
|
||||
extensions.push_back(E_GL_ARB_tessellation_shader);
|
||||
}
|
||||
// gl_CullDistance goes through GL_EXT_clip_cull_distance, whose cull half the loader only
|
||||
// reports where the driver answers a non-zero GL_MAX_CULL_DISTANCES.
|
||||
if (features.CullDistance) {
|
||||
extensions.push_back(E_GL_ARB_cull_distance);
|
||||
}
|
||||
// glClipControl is forwarded to the driver's glClipControlEXT.
|
||||
if (features.ClipControl) {
|
||||
extensions.push_back(E_GL_ARB_clip_control);
|
||||
}
|
||||
// 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.
|
||||
@@ -1380,7 +1709,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.DrawElements = DrawElements;
|
||||
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
|
||||
funcsTable.GL.MultiDrawElements = MultiDrawElements;
|
||||
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||
@@ -1408,18 +1736,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.ClearBufferfv = ClearBufferfv;
|
||||
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
||||
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
||||
funcsTable.GL.CopyImageSubData = CopyImageSubData;
|
||||
funcsTable.GL.GenerateMipmap = GenerateMipmap;
|
||||
funcsTable.GL.ReadPixels = ReadPixels;
|
||||
funcsTable.GL.GetTexImage = GetTexImage;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
funcsTable.GL.WaitSync = WaitSync;
|
||||
@@ -1593,6 +1915,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.MaxCombinedImageUniforms});
|
||||
};
|
||||
m_dynamicParameters.MaxVertexImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
|
||||
m_dynamicParameters.MaxTessControlImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxTessControlImageUniforms);
|
||||
m_dynamicParameters.MaxTessEvaluationImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxTessEvaluationImageUniforms);
|
||||
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
|
||||
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||
@@ -1601,6 +1927,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
|
||||
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
|
||||
ProbeSamplePositions(DirectGLES::g_GLESFuncs, m_GLESCapabilities, m_dynamicParameters);
|
||||
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
|
||||
// attachment's layer passed through, so this backend really does render to the layer it was
|
||||
// given - provided the driver resolved the entry point at all.
|
||||
@@ -1628,6 +1955,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// mathematically mandatory here, on every device, forever - which is why this stays false
|
||||
// regardless of what the driver underneath happens to support.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = false;
|
||||
{
|
||||
const EsprytSubgroupParameters subgroups = ResolveEsprytSubgroupParameters(m_GLESCapabilities);
|
||||
m_dynamicParameters.SubgroupSize = subgroups.size;
|
||||
m_dynamicParameters.SubgroupSupportedStages = subgroups.stages;
|
||||
m_dynamicParameters.SubgroupSupportedFeatures = subgroups.features;
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = subgroups.quadInAllStages;
|
||||
MGLOG_I("DirectGLES: GL_KHR_shader_subgroup %s (size %u, stages 0x%x, features 0x%x)",
|
||||
SubgroupImplementationName(ResolveEsprytSubgroupImplementation(m_GLESCapabilities)),
|
||||
subgroups.size, subgroups.stages, subgroups.features);
|
||||
}
|
||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
@@ -1677,6 +2014,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// describe behaviour this backend does not implement.
|
||||
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
|
||||
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
|
||||
// GL_EXT_fragment_shading_rate is a straight pass-through: the pipeline rate and the
|
||||
// combiner ops are forwarded per draw (SyncRenderState), gl_ShadingRateEXT and
|
||||
// gl_PrimitiveShadingRateEXT are emitted into the ESSL as the driver's own built-ins, and
|
||||
// the device answers every query. The attachment is not forwarded yet, so it is not
|
||||
// claimed, whatever the driver offers.
|
||||
m_dynamicParameters.SupportsFragmentShadingRate = m_GLESCapabilities.SupportsFragmentShadingRate;
|
||||
m_dynamicParameters.SupportsPrimitiveFragmentShadingRate =
|
||||
m_GLESCapabilities.SupportsFragmentShadingRatePrimitive;
|
||||
m_dynamicParameters.SupportsAttachmentFragmentShadingRate = false;
|
||||
m_dynamicParameters.FragmentShadingRateNonTrivialCombiners =
|
||||
m_GLESCapabilities.FragmentShadingRateNonTrivialCombiners;
|
||||
m_dynamicParameters.FragmentShadingRateWithShaderDepthStencilWrites =
|
||||
m_GLESCapabilities.FragmentShadingRateWithShaderDepthStencilWrites;
|
||||
m_dynamicParameters.FragmentShadingRateWithSampleMask = m_GLESCapabilities.FragmentShadingRateWithSampleMask;
|
||||
m_dynamicParameters.FragmentShadingRatePrimitiveWithMultiViewport =
|
||||
m_GLESCapabilities.FragmentShadingRatePrimitiveWithMultiViewport;
|
||||
for (SizeT i = 0; i < std::size(m_dynamicParameters.FragmentShadingRateMasks); ++i) {
|
||||
m_dynamicParameters.FragmentShadingRateMasks[i] = m_GLESCapabilities.FragmentShadingRateMasks[i];
|
||||
}
|
||||
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
||||
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||
|
||||
@@ -32,16 +32,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
public:
|
||||
~BackendObject_DirectGLES() override;
|
||||
|
||||
// The driver's verdict on this session's context (DirectGLES::LatchIfDeviceLost).
|
||||
Bool LatchIfGpuFaulted() override;
|
||||
void Initialize() override;
|
||||
Bool InitCapabilities() override;
|
||||
Bool InitWindowSurface() override;
|
||||
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
|
||||
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
|
||||
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
|
||||
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
|
||||
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
|
||||
// Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is
|
||||
// the image's EGLImage, ordered after the image's pending reads and published as a native
|
||||
// fence without a CPU wait (DirectGLES.h, SharedImageImpl).
|
||||
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) override;
|
||||
// A window surface's age is the driver's (EGL_EXT_buffer_age); a pbuffer keeps its content,
|
||||
// so it is 1 once it has been presented (DirectGLES::CurrentDrawBufferAge).
|
||||
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
|
||||
// eglSetDamageRegionKHR on a window surface whose driver has EGL_KHR_partial_update.
|
||||
Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override;
|
||||
// A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses).
|
||||
Bool PublishSharedImageAccesses() override;
|
||||
// An EGL native fence of the session's work (SharedImageImpl::ExportNativeFence).
|
||||
Bool ExportNativeFence(int* fence) override;
|
||||
void ReleaseEGLSurface(EGLSurface surface) override;
|
||||
void ReleaseEGLResources() override;
|
||||
// The server window going and coming back: each window surface on it swaps its native EGL
|
||||
// window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface).
|
||||
Bool SuspendServerWindow(void* window) override;
|
||||
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
|
||||
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
|
||||
|
||||
const RendererInfo& GetRendererInfo() const override;
|
||||
String GetBackendAPIVersionString() const override;
|
||||
@@ -57,8 +79,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void UpdateDynamicBackendParameters();
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
void OnEGLSurfaceForgotten(EGLSurface surface) override;
|
||||
// An existing native surface for the client surface being activated, re-bound; false when
|
||||
// there is none yet (the caller creates it).
|
||||
Bool RebindNativeSurfaceFor(EGLSurface surface);
|
||||
// EGL: creating a surface does not change what is current. The creation bound the new
|
||||
// surface natively; put the previous one back for the context that held it.
|
||||
void RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw);
|
||||
|
||||
Bool m_initialized = false;
|
||||
// P14: ONE NATIVE SURFACE PER CLIENT SURFACE, created on its first activation and re-bound
|
||||
// on every later one. A multi-window client switches surfaces every frame; making a new
|
||||
// native surface each time leaked them and lost what the window had drawn.
|
||||
UnorderedMap<EGLSurface, EGLSurface> m_nativeSurfaces;
|
||||
// Window surfaces whose window is gone (SuspendServerWindow): their native surface is a
|
||||
// placeholder pbuffer until ResumeServerWindow.
|
||||
Vector<EGLSurface> m_suspendedWindowSurfaces;
|
||||
MG_External::EGLFunctionsTable m_EGLFunctions;
|
||||
MG_External::GLESFunctionsTable m_GLESFunctions;
|
||||
MG_External::GLESCapabilities m_GLESCapabilities;
|
||||
@@ -78,12 +114,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable,
|
||||
// plus the feature set in `features`. The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied
|
||||
// inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported);
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported,
|
||||
const AdvertisedFeatureSupport& features = {});
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/ContextEpoch.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
|
||||
|
||||
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
|
||||
#pragma once
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,16 +23,20 @@ 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);
|
||||
#endif
|
||||
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);
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
@@ -42,8 +46,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count);
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount);
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
@@ -67,21 +69,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLuint baseinstance);
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect);
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter);
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
@@ -94,7 +83,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void GenerateMipmap(GLenum target);
|
||||
const GLubyte* GetString(GLenum name);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
@@ -105,11 +93,72 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
Bool InitWindowSurface(NativeWindowType window);
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
// The calling thread's session's current native draw surface (the one the last Init*Surface
|
||||
// created, or the last BindSessionSurface named).
|
||||
EGLSurface CurrentSessionDrawSurface();
|
||||
// The surface the calling thread's ACTIVE context last drew to (EGL_NO_SURFACE for none).
|
||||
EGLSurface CurrentContextDrawSurface();
|
||||
// EGL_BUFFER_AGE_EXT of the session's current draw surface for the next frame: the driver's
|
||||
// answer for a window surface, 1 for a pbuffer that has been presented (a pbuffer's content stays
|
||||
// across swaps), 0 otherwise. A driver whose buffer age is only EGL_KHR_partial_update's keeps
|
||||
// nothing inside the frame's damage region - the whole surface unless one is set - so it is asked
|
||||
// only when `damageRegionFollows` (SetCurrentDrawDamageRegion before the first draw).
|
||||
Int32 CurrentDrawBufferAge(Bool damageRegionFollows);
|
||||
// eglSetDamageRegionKHR(region) on the session's current window surface (a pbuffer, or a driver
|
||||
// without EGL_KHR_partial_update, ignores it). The driver wants the age asked first in the frame;
|
||||
// when the client was answered without asking it, it is asked here.
|
||||
Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region);
|
||||
// Makes an EXISTING native surface of the calling thread's session the one it draws to, and
|
||||
// binds it to the active context. False for a surface the session does not own.
|
||||
Bool BindSessionSurface(EGLSurface surface);
|
||||
// The same selection without the bind (the caller binds - MakeCurrent - unconditionally).
|
||||
Bool SelectSessionSurface(EGLSurface surface);
|
||||
// Destroys one native surface of the calling thread's session - and only it; the session's
|
||||
// contexts and other surfaces are untouched.
|
||||
void DestroySessionSurface(EGLSurface surface);
|
||||
// A new pbuffer of the calling thread's session, created with the active context's config and
|
||||
// bound to nothing (EGL: creating a surface changes no binding).
|
||||
EGLSurface CreateSessionPbuffer(EGLint width, EGLint height);
|
||||
// Every context of the session that drew to `previous` draws to `replacement` instead, and
|
||||
// `previous` is destroyed (a pbuffer-backed window's resize).
|
||||
void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement);
|
||||
// The server window going away (screen off): every context of the session that drew to the
|
||||
// window surface `windowSurface` draws to a new 1x1 placeholder pbuffer instead, and the window
|
||||
// surface is destroyed - no longer current anywhere once this returns. The contexts and their
|
||||
// objects stay. Returns the placeholder, or EGL_NO_SURFACE (nothing changed) when none could be
|
||||
// made.
|
||||
EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface);
|
||||
// The way back: a window surface on `window` replaces `placeholder` (EGL_NO_SURFACE: none to
|
||||
// replace) for every context that drew to it, is bound, and its extent is published as a
|
||||
// creation publishes it. Returns the new surface, or EGL_NO_SURFACE.
|
||||
EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window);
|
||||
// The window under `surface` changed size: its extent (the display's) is published again, at once.
|
||||
Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window);
|
||||
Bool MakeCurrent();
|
||||
Bool ReleaseCurrent();
|
||||
// True when the backend ES context is current on the calling thread, i.e.
|
||||
// immediate buffer ops may issue GL calls right now.
|
||||
Bool IsBackendContextCurrentOnThisThread();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// DEVICE LOSS, Espryt's half (Magma's is VulkanRenderer::LatchWireDeviceLoss). Asks the
|
||||
// driver whether the calling thread's context was reset (glGetGraphicsResetStatus); a reset -
|
||||
// or the debug knob MGPipeDebugDeviceLossDue - latches THIS session through
|
||||
// MGPipeSessionLatch and answers true, and the caller returns from its verb without issuing
|
||||
// more GL. Asked at the frame boundaries (Present, a shared-image present), before a readback,
|
||||
// and by the apply loop each time it wakes (LatchIfGpuFaulted), never per draw. The contexts ask
|
||||
// for reset notification (NativeContextsNotifyResets) so the driver can name the GUILTY one;
|
||||
// only GUILTY (or a reset of unknown cause) ends the session - an INNOCENT report, a context
|
||||
// whose work merely waited behind another's hang (the compositor's among them), is only noted.
|
||||
// `debugKnob` false: the knob is not consulted here (the on-screen Present, so an injected loss
|
||||
// lands on an offscreen client's shared-image present or readback, never on the compositor's).
|
||||
Bool LatchIfDeviceLost(const char* site, Bool debugKnob = true);
|
||||
// The apply loop's form of it (BackendObject::LatchIfGpuFaulted), asked each time the apply thread
|
||||
// wakes: the driver's report only, no debug knob, and no EGL ground-truth verification spent.
|
||||
Bool LatchIfGuiltyBeforeApply();
|
||||
#endif
|
||||
// True once the process's native contexts were created with EGL_LOSE_CONTEXT_ON_RESET (decided
|
||||
// by the first one; the driver must offer EGL_EXT_create_context_robustness).
|
||||
Bool NativeContextsNotifyResets();
|
||||
// GL fence sync objects, backed by native ES fences. FenceSync returns null
|
||||
// (the frontend then falls back to an always-signaled sync) when the calling
|
||||
// thread does not own the ES context. Waits/queries degrade to "signaled" in
|
||||
@@ -198,6 +247,213 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
||||
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// The display is process state and stays one object (eglInitialize once per process, terminated
|
||||
// when the last native context leaves). The native EGLContext, the config it was created with
|
||||
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
|
||||
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
|
||||
// the first one's native context down, which is what lets two contexts (and two share groups)
|
||||
// be live at once in one process.
|
||||
|
||||
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
|
||||
// that session has bound. Installed once by the server layer; with no resolver installed -
|
||||
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
|
||||
// exactly the process singleton this replaces. `false` also means {0,0}.
|
||||
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
|
||||
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLES' KEY, AND ITS
|
||||
// SHAPE IS S4's, ONE LEVEL DOWN. A twin is a DRIVER object, so it lives in one native
|
||||
// context's object namespace; the identity a twin table must be keyed on is therefore
|
||||
// {session, share group} - the SAME two words the applier's OBJECT RECORDS are filed under
|
||||
// (PipeApply.h's MGPipeApplierKey, derived by the same probe and the same `0 means its own
|
||||
// group` rule, so the two agree by construction rather than by two conventions).
|
||||
//
|
||||
// Answers, for the CALLING THREAD, which session it belongs to and which share group that
|
||||
// session's current context is in. Installed once by the server layer beside the native and
|
||||
// applier probes; with no probe installed - or on a thread that is no session's own, which
|
||||
// is the client process, the in-process client+server shape and every unit case - the
|
||||
// answer is the single key {0, 0}, exactly the process-wide table this replaces.
|
||||
struct TwinKey {
|
||||
Uint64 SessionKey = 0;
|
||||
Uint64 ShareGroupKey = 0;
|
||||
Bool operator==(const TwinKey& other) const {
|
||||
return SessionKey == other.SessionKey && ShareGroupKey == other.ShareGroupKey;
|
||||
}
|
||||
Bool operator!=(const TwinKey& other) const { return !(*this == other); }
|
||||
};
|
||||
struct TwinKeyHash {
|
||||
SizeT operator()(const TwinKey& key) const {
|
||||
// The 64-bit mix SlotAllocator and the applier's registry already use for their own
|
||||
// pair keys, so every keyed table in this tree hashes a session the same way.
|
||||
Uint64 mixed = key.SessionKey ^ (key.ShareGroupKey + 0x9E3779B97F4A7C15ull +
|
||||
(key.SessionKey << 6) + (key.SessionKey >> 2));
|
||||
mixed ^= mixed >> 33;
|
||||
mixed *= 0xFF51AFD7ED558CCDull;
|
||||
mixed ^= mixed >> 33;
|
||||
return static_cast<SizeT>(mixed);
|
||||
}
|
||||
};
|
||||
using TwinKeyResolver = Bool (*)(TwinKey* outKey);
|
||||
void SetTwinKeyResolver(TwinKeyResolver resolver);
|
||||
// The calling thread's key. Answers false for {0, 0} - the process-wide group - which is
|
||||
// also the answer when no probe has ever been installed.
|
||||
Bool CurrentTwinKey(TwinKey* outKey);
|
||||
|
||||
// Builds the native context for `contextToken` under the calling thread's session. A
|
||||
// `shareGroupToken` whose group already has a live native context in this session hands that
|
||||
// context to eglCreateContext as the share argument - which is what makes two contexts see one
|
||||
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
|
||||
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
|
||||
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
|
||||
// contexts are untouched; the display is terminated only when the last context in the process
|
||||
// has gone.
|
||||
void DestroyNativeContextFor(Uint64 contextToken);
|
||||
|
||||
// ---- objects that belong to ONE native context ----
|
||||
//
|
||||
// Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never
|
||||
// shares them, not even between contexts of one share group, and every context numbers its own
|
||||
// from 1. A process-wide scratch FBO id minted in one context and bound in another therefore
|
||||
// names THAT context's own framebuffer - often an application's - and the scratch path's
|
||||
// attach/scrub then rewires it onto staging textures. With one process serving several
|
||||
// sessions (each with its own native contexts) that silently corrupted other clients' frames.
|
||||
//
|
||||
// The calling thread's current native context as a lifetime serial: assigned when the context
|
||||
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
|
||||
// made current through MakeCurrent.
|
||||
//
|
||||
// P15: it lives in one constant-initialized thread_local with the PerNativeContext lookup
|
||||
// cache below, so the draw path's "this context's entry" question is one TLS address
|
||||
// computation, not two calls each paying their own.
|
||||
struct NativeContextThreadState {
|
||||
struct CacheEntry {
|
||||
const void* Owner = nullptr;
|
||||
Uint64 Serial = 0;
|
||||
Uint64 Epoch = 0;
|
||||
void* Entry = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kCacheEntries = 8;
|
||||
Uint64 BoundSerial = 0;
|
||||
// One entry per PerNativeContext instance slot (instances sharing a slot evict each other).
|
||||
CacheEntry Cache[kCacheEntries]{};
|
||||
};
|
||||
inline constinit thread_local NativeContextThreadState t_nativeContextThread;
|
||||
inline Uint64 CurrentNativeContextSerial() { return t_nativeContextThread.BoundSerial; }
|
||||
|
||||
class PerNativeContextBase {
|
||||
public:
|
||||
PerNativeContextBase();
|
||||
PerNativeContextBase(const PerNativeContextBase&) = delete;
|
||||
PerNativeContextBase& operator=(const PerNativeContextBase&) = delete;
|
||||
// The context died, and its names with it: forget them (no GL call - there is no context
|
||||
// left to make one in).
|
||||
virtual void ForgetContext(Uint64 serial) = 0;
|
||||
|
||||
protected:
|
||||
~PerNativeContextBase();
|
||||
// This instance's entry in NativeContextThreadState::Cache.
|
||||
const Uint32 m_cacheSlot;
|
||||
};
|
||||
|
||||
// Called where a native context is destroyed.
|
||||
void ForgetNativeContextObjects(Uint64 serial);
|
||||
// Moves whenever any per-context entry is dropped; a thread's cached entry is valid while
|
||||
// it has not moved.
|
||||
Uint64 PerNativeContextEpoch();
|
||||
void BumpPerNativeContextEpoch();
|
||||
|
||||
// One T per native context, created on first use from that context.
|
||||
template <typename T>
|
||||
class PerNativeContext final : public PerNativeContextBase {
|
||||
public:
|
||||
T& Current() {
|
||||
NativeContextThreadState& thread = t_nativeContextThread;
|
||||
const Uint64 serial = thread.BoundSerial;
|
||||
// The draw path asks for the same entry over and over from one thread: a one-entry
|
||||
// thread-local cache per instance, invalidated by any drop anywhere (rare) and by an
|
||||
// instance's destruction (so a later instance at the same address cannot hit).
|
||||
NativeContextThreadState::CacheEntry& cache = thread.Cache[m_cacheSlot];
|
||||
const Uint64 epoch = PerNativeContextEpoch();
|
||||
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) {
|
||||
return *static_cast<T*>(cache.Entry);
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
|
||||
cache = {this, serial, epoch, &entry};
|
||||
return entry;
|
||||
}
|
||||
template <typename F>
|
||||
void ForEach(F&& visit) {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto& entry : m_bySerial) visit(entry.second);
|
||||
}
|
||||
void Clear() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
BumpPerNativeContextEpoch();
|
||||
m_bySerial.clear();
|
||||
}
|
||||
void ForgetContext(Uint64 serial) override {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
BumpPerNativeContextEpoch();
|
||||
m_bySerial.erase(serial);
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex m_mutex;
|
||||
std::unordered_map<Uint64, T> m_bySerial;
|
||||
};
|
||||
|
||||
// The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads
|
||||
// as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it.
|
||||
class NativeContextName {
|
||||
public:
|
||||
operator GLuint() const { return m_names.Current(); }
|
||||
NativeContextName& operator=(GLuint name) {
|
||||
m_names.Current() = name;
|
||||
return *this;
|
||||
}
|
||||
GLuint* operator&() { return &m_names.Current(); }
|
||||
|
||||
private:
|
||||
mutable PerNativeContext<GLuint> m_names;
|
||||
};
|
||||
|
||||
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
|
||||
// records belong to; this makes the calling thread's native tuple follow it, binding the
|
||||
// session's current surface to the newly named context's native EGLContext. A no-op when the
|
||||
// named context has no native context yet (the session never sent CreateContext) or when the
|
||||
// native context already current on this thread IS that one - so the single-context world pays
|
||||
// nothing for it. Called from the bind_context path on the apply thread.
|
||||
Bool MakeNativeContextCurrentForBoundToken();
|
||||
// P14: the server's backend turn moved to the calling apply thread (ServerLoop.cpp's
|
||||
// BackendTurnLock). Takes the backend's GL ownership for this thread and drops every
|
||||
// binding shadow, which still describes the previous holder's native context.
|
||||
void OnBackendTurnHandoff();
|
||||
// How many native contexts this process holds, and how many of them belong to one session. The
|
||||
// test observables for "a second context did not tear the first one down".
|
||||
Uint64 NativeContextCount();
|
||||
Uint64 NativeContextCountForSession(Uint64 sessionKey);
|
||||
|
||||
// The calling thread's session, as the resolver above reports it (0 with no resolver).
|
||||
Uint64 CurrentNativeSessionKey();
|
||||
|
||||
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
|
||||
// hold" query and no way to ask a context for its share group, so a case that must tell "the
|
||||
// same native context still serves" from "a fresh one was built", or a sharing pair from two
|
||||
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
|
||||
// a production path and neither changes behaviour.
|
||||
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
|
||||
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
|
||||
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
|
||||
// tuple it means when no session runtime is installed.
|
||||
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
|
||||
|
||||
void DestroyEGLContext();
|
||||
|
||||
// Transform feedback capture spans, performed by the real ES driver. The
|
||||
@@ -241,8 +497,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// answers from the frontend state, which this function only reads.
|
||||
void SyncRenderState(Bool forColorClear = false);
|
||||
void InvalidateSyncedRenderState();
|
||||
|
||||
// GL_SCISSOR_TEST as the driver has it in the native context current on this thread -
|
||||
// what a guard that turns the test off around its own blit, and puts it back, needs to
|
||||
// know. The shadow answers only while it describes this context (synced since the last
|
||||
// invalidation, in this context epoch); otherwise the driver is asked. The shadow is one
|
||||
// per process: once another session's context synced into it, or a context switch
|
||||
// invalidated it, its bits are some other context's.
|
||||
Bool DriverScissorTestEnabled();
|
||||
|
||||
// TESTING: the shadow as SyncRenderState leaves it in the current context epoch, with
|
||||
// GL_SCISSOR_TEST `enabled`.
|
||||
void SetSyncedScissorTestForTesting(Bool enabled);
|
||||
// TESTING: `inside` runs within the guard the shared-image present and the readbacks
|
||||
// copy under (ScopedScissorDisable).
|
||||
void RunScissorDisabledForTesting(void (*inside)(void*), void* data);
|
||||
} // namespace RenderStateImpl
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md) ----
|
||||
//
|
||||
// A shared image reaches the driver only through the Android import path: its AHardwareBuffer
|
||||
// becomes an EGLImage (EGL_ANDROID_get_native_client_buffer + EGL_ANDROID_image_native_buffer)
|
||||
// and that EGLImage the storage of a texture or renderbuffer (GL_OES_EGL_image). On a host
|
||||
// build there is no such path and every entry below declines.
|
||||
namespace SharedImageImpl {
|
||||
// One EGLImage over one shared image, holding the image's registry reference for as long
|
||||
// as the EGLImage lives (SharedImageRegistry.h's lifetime rule). The last reference
|
||||
// destroys the EGLImage, unless the display that made it has been terminated since (which
|
||||
// destroyed it already). Opaque outside DirectGLES.cpp.
|
||||
struct EglImage;
|
||||
using EglImageRef = SharedPtr<EglImage>;
|
||||
|
||||
// An EGLImage for the live shared image `id`, or null with `why`. Needs a current context
|
||||
// (the first call resolves the extension entry points).
|
||||
EglImageRef CreateEglImage(Uint64 id, String& why);
|
||||
// glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image) on what the active unit has bound to
|
||||
// GL_TEXTURE_2D. False when the driver refused it (its GL error is consumed).
|
||||
Bool TargetBoundTexture2D(const EglImageRef& image);
|
||||
// BackendObject::BlitDefaultFramebufferToSharedImage, for the calling thread's context.
|
||||
Bool BlitDefaultFramebufferTo(const SharedImageView& image, const MG_Util::Damage::Region& region);
|
||||
|
||||
// THE READER'S HALF of the image's sync state (SharedImageRegistry.h). Called on every use
|
||||
// of a name whose level 0 is `image`, before the command that samples it: when a write
|
||||
// landed since this context last waited, the context's later commands wait for its fence
|
||||
// on the GPU; and the image is noted as read by the calling session's current frame.
|
||||
void AcquireForSampling(const EglImageRef& image);
|
||||
// YUV IMAGES (NV12/P010, MG_Remote/Server/SharedImageYuv.h) are never a name's level 0: the
|
||||
// name keeps its own RGBA8 storage `target` (width x height), into which the image is
|
||||
// converted at its first use in each of the calling session's frames - the image bound as
|
||||
// an external texture, one triangle drawn - after the registry's CPU copy for a foreign
|
||||
// buffer. Every touched binding is put back. Called after AcquireForSampling.
|
||||
Bool IsYuv(const EglImageRef& image);
|
||||
void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height);
|
||||
// The calling session's frame boundary for what it has read: hands a fence for everything
|
||||
// the current context has submitted to every image noted since the last boundary. Called
|
||||
// at Present and wherever the current native context stops being current (its reads can
|
||||
// only be fenced from it). Nothing noted = nothing done.
|
||||
void PublishPendingReads();
|
||||
// IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): everything the
|
||||
// calling session used since its last boundary is published as written and read by one
|
||||
// fence, and the session's first use of an image in each frame from now on also waits for
|
||||
// the image's pending reads.
|
||||
Bool PublishPendingAccesses();
|
||||
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): a
|
||||
// sync_file of everything the current context submitted in `*fence` (-1: none, and the work
|
||||
// is waited out here), and - for a session that writes images - the boundary
|
||||
// PublishPendingAccesses makes.
|
||||
Bool ExportNativeFence(int* fence);
|
||||
} // namespace SharedImageImpl
|
||||
#endif
|
||||
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
+2438
-3912
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -44,14 +44,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MGB_CTX->GetPrimitiveRestartIndex();
|
||||
return MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
return MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
@@ -95,7 +95,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// PrepareForDraw leave on GL_ELEMENT_ARRAY_BUFFER (and how big is its store)", and
|
||||
// "give me its bytes on the CPU" - and until now it asked all three of the FRONTEND
|
||||
// VAO, on the apply thread, once per indexed multi-draw. That was the last unguarded
|
||||
// `MGB_CTX->GetBoundVertexArray()` in the backend: package vi retired the ordinary
|
||||
// `gPipeInputs->GetBoundVertexArray()` in the backend: package vi retired the ordinary
|
||||
// draw path's copy (DirectGLES.cpp's SyncCurrentVertexAttributeValues / PrepareForDraw)
|
||||
// and this one survived only because these batches used to die earlier, on the
|
||||
// framebuffer row fb has since retired.
|
||||
@@ -125,7 +125,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint8* HostBytes = nullptr; // HostBytes: null when there is no CPU copy
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A missing record on the handle arm is a NAMED refusal and never a quiet fall-back to
|
||||
// the frontend (TASK-mv requirement 2; the shape is Managers.cpp's
|
||||
// RefuseNullFrontendTextureOffTheHandleArm). The frontend element slot is not a second
|
||||
@@ -175,80 +174,45 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (!candidate.Live || candidate.Gen != res.Gen) return false;
|
||||
return candidate.Desc.HasDefinedContent != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
BoundIndexBufferView ResolveBoundIndexBuffer(IndexBufferQuestion question, const char* entry) {
|
||||
BoundIndexBufferView view;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (BufferImpl::VertexInputReadsRecords()) {
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res;
|
||||
// A null Res is "no element array buffer bound", exactly as a null frontend slot
|
||||
// is on the arm below - the batch's indices are a client array, and every caller
|
||||
// already has an arm for that. It is NOT the arm test; the arm was decided above.
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return view;
|
||||
view.Present = true;
|
||||
if (question == IndexBufferQuestion::Presence) return view;
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res;
|
||||
// A null Res is "no element array buffer bound", exactly as a null frontend slot
|
||||
// is on the arm below - the batch's indices are a client array, and every caller
|
||||
// already has an arm for that. It is NOT the arm test; the arm was decided above.
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return view;
|
||||
view.Present = true;
|
||||
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".
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
view.HostBytes = resource->hostBytes;
|
||||
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
|
||||
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
|
||||
"multidraw_index_rebase");
|
||||
}
|
||||
// A null HostBytes is not a refusal: MappedData() on the monolith arm is
|
||||
// equally allowed to be null (an adopted coherent map keeps its bytes
|
||||
// elsewhere), and the one caller that asks declines the tier for it.
|
||||
return view;
|
||||
}
|
||||
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (question == IndexBufferQuestion::HostBytes) {
|
||||
// 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.Id = resource->id;
|
||||
if (question == IndexBufferQuestion::DriverNameAndSize) {
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
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");
|
||||
}
|
||||
// A null HostBytes is not a refusal: MappedData() on the monolith arm is
|
||||
// equally allowed to be null (an adopted coherent map keeps its bytes
|
||||
// elsewhere), and the one caller that asks declines the tier for it.
|
||||
return view;
|
||||
}
|
||||
#endif
|
||||
// MONOLITH GLUE from here down, token for token what each call site did before.
|
||||
const auto& vao = MGB_CTX->GetBoundVertexArray();
|
||||
if (!vao) return view;
|
||||
const auto& ibo = vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (!ibo) return view;
|
||||
view.Present = true;
|
||||
switch (question) {
|
||||
case IndexBufferQuestion::Presence:
|
||||
break;
|
||||
case IndexBufferQuestion::DriverName: {
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
view.Id = resource ? resource->id : 0;
|
||||
break;
|
||||
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Id = resource->id;
|
||||
if (question == IndexBufferQuestion::DriverNameAndSize) {
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
}
|
||||
case IndexBufferQuestion::DriverNameAndSize: {
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
view.Id = resource ? resource->id : 0;
|
||||
view.Size = ibo->GetSize();
|
||||
break;
|
||||
}
|
||||
case IndexBufferQuestion::HostBytes:
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||
// a shader write may have moved past it since the last sync.
|
||||
ibo->SyncPersistentMappedRange();
|
||||
ibo->SyncGpuWrites();
|
||||
view.HostBytes = ibo->MappedData();
|
||||
view.Size = ibo->GetSize();
|
||||
break;
|
||||
}
|
||||
(void)entry;
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -268,7 +232,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
//
|
||||
// This was the LAST unguarded frontend read on the multi-draw apply path, and it sat
|
||||
// twenty lines above the function that retired its neighbour. It asked
|
||||
// `MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect)` and then
|
||||
// `gPipeInputs->GetBufferBindingSlot(BufferTarget::DrawIndirect)` and then
|
||||
// `EnsureBufferResource(<frontend object>)` - a registry lookup keyed by the client's
|
||||
// identity, which is §4.4's rule and not only the allocator's. Under run-ahead that is
|
||||
// `Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"}` on every batch the
|
||||
@@ -292,23 +256,13 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// an empty slot, and not the arm test (ID-110: the arm was decided by the transport
|
||||
// above, never inferred from a null).
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) {
|
||||
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
|
||||
}
|
||||
return resource->id;
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) {
|
||||
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
|
||||
}
|
||||
#endif
|
||||
// MONOLITH GLUE from here down, token for token what this function did before.
|
||||
const auto& indirect =
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
return resource ? resource->id : 0;
|
||||
return resource->id;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -436,7 +390,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
|
||||
// ns/op, median of three:
|
||||
//
|
||||
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
|
||||
// NVIDIA ES 3.2 llvmpipe ES 3.2
|
||||
// ext n/a 19300
|
||||
// basevertex 2500 25200
|
||||
// multiindirect 5700 27600
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#endif
|
||||
// P14 S6: the key this table is filed under includes the calling thread's {session, share
|
||||
// group}, and the probe that answers it lives on the backend's public surface beside S4's
|
||||
// native-tuple key (DirectGLES.h). Included here so this header is self-contained for the two
|
||||
// translation units that include it without copying Managers.h's include order.
|
||||
#include "DirectGLES.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeSessionFail.h>
|
||||
#endif
|
||||
@@ -37,68 +40,36 @@
|
||||
// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph.
|
||||
//
|
||||
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
|
||||
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
|
||||
// one the slot-table plan lists for deletion as "GC" and the one D13 makes a precondition of the switch-
|
||||
// over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed()
|
||||
// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend
|
||||
// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY
|
||||
// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr
|
||||
// goes. So:
|
||||
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The
|
||||
// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only;
|
||||
// from their destructor carrying {kind, lifetimeId, handle}; the backend consumes it in
|
||||
// Managers.cpp and releases the twin BY THAT HANDLE in every holder of the kind
|
||||
// (ReleaseTwinByHandle below - the same release the wire's object_death runs on a server), and
|
||||
// the CLIENT returns the slot after the notice (PipeFill.cpp's NotifyAndFree). So:
|
||||
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind;
|
||||
// * a twin, and the driver storage it owns, is freed when the application lets go of the
|
||||
// object rather than up to 64 creations or 1024 draw ticks later. That is what
|
||||
// Managers.h's "dead gigabytes" note asked for.
|
||||
//
|
||||
// EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the
|
||||
// ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the
|
||||
// length of a test, and a context reset does the same in reverse - and the slot allocator
|
||||
// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and
|
||||
// resolved again by the next would find nothing to resolve. Every table therefore links itself
|
||||
// into a per-table-type list at construction and out at destruction, and one notice resolves
|
||||
// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No
|
||||
// holder can be left naming a live entry for a dead object, and there is nothing a sweep could
|
||||
// still find. (The list is per table TYPE; the kind is the type's template parameter, and each
|
||||
// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints
|
||||
// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.)
|
||||
// length of a test, and a context reset does the same in reverse. Every table therefore links
|
||||
// itself into a per-table-type list at construction and out at destruction, and one release
|
||||
// drops the twin in each holder by handle. (The list is per table TYPE; the kind is the type's
|
||||
// template parameter, and each of the six kinds has exactly one table type in this backend.)
|
||||
//
|
||||
// The weak_ptr per entry survives as the MONOLITH-GLUE half of the table's identity: the
|
||||
// minting GetOrCreate stores it, NoteStateForHandle/StateForHandle read it, and the legacy
|
||||
// death notice walks it. It is never an identity test - that is what Gen is for - and it is
|
||||
// never read to decide whether an entry is dead: a destructor that runs after exit() has begun
|
||||
// has its notice dropped by InProcessTeardown(), and that twin is then a DELIBERATE leak (the
|
||||
// process is exiting, the driver reclaims the object, and a twin destructor must not call into
|
||||
// a driver that may already be unloaded), not something to be collected later.
|
||||
// A destructor that runs after exit() has begun has its notice dropped by InProcessTeardown(),
|
||||
// and that twin is then a DELIBERATE leak (the process is exiting, the driver reclaims the
|
||||
// object, and a twin destructor must not call into a driver that may already be unloaded).
|
||||
//
|
||||
// P5e (id, CONTRACT-P5E §4.1): ForEachLive() NO LONGER HANDS IT OUT. It answers
|
||||
// fn(MGPipeHandle, const BackendPtr&), because a walk that produced a frontend SharedPtr out
|
||||
// of server memory was the last place the server could hold one across records - and a caller
|
||||
// that still needs the object (the detach walk) asks StateForHandle for it BY HANDLE, inside
|
||||
// the scope that names the debt, so the read is one greppable site rather than a property of
|
||||
// the iteration.
|
||||
//
|
||||
// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS
|
||||
// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId().
|
||||
// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and
|
||||
// under a real split neither the frontend object nor its lifetime id exists on this side of
|
||||
// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both
|
||||
// belong on the client, and the backend should receive the handle in the verb payload. It is
|
||||
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
|
||||
// so nothing catches it automatically.
|
||||
//
|
||||
// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the
|
||||
// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise
|
||||
// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the
|
||||
// allocator's own three entries carry, repeated here so the refusal names this surface), and
|
||||
// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead.
|
||||
//
|
||||
// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf,
|
||||
// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport
|
||||
// apply before touching a frontend object, with or without a barrier or named scope.
|
||||
// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef.
|
||||
// The registry guard is independent of Magma's legacy allocator debt scope.
|
||||
// P13: THE TABLE IS HANDLE-ONLY. The frontend-keyed half - a GetOrCreate that minted handles
|
||||
// off a frontend object's lifetime id from inside MG_Backend, the object-keyed Find / HandleOf
|
||||
// with their lifetime-id memo, the weak per-entry state note and the lifetime-id death walk -
|
||||
// was monolith glue, unreachable once every family ran the record arm, and is deleted. A handle
|
||||
// is minted by the CLIENT and arrives in the record; nothing on this side touches the client's
|
||||
// slot allocator (the link ratchet's P13 exit gate pins that).
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
|
||||
// Declared in Managers.h as well; repeated here because this header is included from it
|
||||
// before that declaration, and the table below is the arming site on this arm (D13: "the
|
||||
@@ -116,9 +87,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled);
|
||||
|
||||
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
|
||||
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
|
||||
|
||||
// Says, at backend bring-up, that the knobs leave no arm - and does NOT stop.
|
||||
//
|
||||
// The stop cannot live here, and that is the whole point of the split. Backend context
|
||||
@@ -126,7 +94,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// sequence in a FORKED CHILD (MG_IntegrationTest/Harness/HeadlessGL.cpp): a child that dies
|
||||
// on a signal is reported as "no usable GPU/display/ICD" and every scenario in the lane is
|
||||
// SKIPPED - i.e. the lane goes green having run nothing, on the very pair of env vars the
|
||||
// D14/D18 A/B is driven with, which is what ROADMAP.md:7 forbids. So bring-up only
|
||||
// D14/D18 A/B is driven with, which is what ARCHITECTURE.md §13.2 forbids. So bring-up only
|
||||
// DIAGNOSES; the stop is raised by ResolveEsprytSlotTablesArm() at the first twin lookup,
|
||||
// which happens in the test body where the harness reports it as a failure.
|
||||
//
|
||||
@@ -134,6 +102,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// DirectGLESSlotTable.EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs
|
||||
// the real bring-up entry point under the pair in a forked child: edit that site back to
|
||||
// ResolveEsprytSlotTablesArm() and the case fails naming both knobs.
|
||||
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
|
||||
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
|
||||
|
||||
void DiagnoseEsprytSlotArm();
|
||||
|
||||
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
|
||||
@@ -145,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// True when this process runs the {slot, gen} arm. Fixed for the life of the process: the
|
||||
// two arms hold their twins in different containers, so flipping mid-run would strand them.
|
||||
//
|
||||
// INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables
|
||||
// INLINE on purpose. Every FindByHandle / GetOrCreate / ForEachLive on the twin tables
|
||||
// consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line
|
||||
// function in Managers.cpp (no LTO in any shipped configuration) that was a call through
|
||||
// the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted
|
||||
@@ -155,11 +126,245 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLE IS PER
|
||||
// {session, share group}, NOT PER PROCESS.
|
||||
// =====================================================================================
|
||||
//
|
||||
// WHAT WAS WRONG, in one sentence: a twin is a DRIVER object - a GL buffer id, a program, a
|
||||
// texture, the storage behind them - so it belongs to one native context's object namespace,
|
||||
// and a table keyed by {slot, gen} ALONE resolves the same handle value to the same twin
|
||||
// whichever context asked. The frontend gives two contexts that do NOT share a group two
|
||||
// independent name spaces, so the same {slot, gen} in both is legal and means two different
|
||||
// objects; one process-wide table made the second context adopt the first's twin (including
|
||||
// its driver ids, its persistent map and its pooled store). At the same time two contexts of
|
||||
// ONE group - which must see one set of objects - were served by whichever of them twinned
|
||||
// first, i.e. by luck rather than by the group the two agree on.
|
||||
//
|
||||
// THE KEY IS the same pair the applier files its OBJECT RECORDS under: {session, share
|
||||
// group}. Same probe, same `0 means this context started its own group` rule (see
|
||||
// DirectGLES.h's TwinKey and SessionRuntime.cpp's ResolveThreadTwinKey), so a record and the
|
||||
// twin it describes are always in the same bucket. Session is in the key because a twin
|
||||
// names a driver id and no driver id crosses a connection - two connections in one process
|
||||
// are two clients, and nothing may be shared with either from the other.
|
||||
//
|
||||
// THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an
|
||||
// OBJECT, not about a bucket: ReleaseTwinByHandle must reach
|
||||
// the by-value copies a fixture or a context reset is holding, whichever bucket they sit in,
|
||||
// and "which bucket holds it" is exactly the question that produced the two-holder leak the
|
||||
// list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers
|
||||
// every bucket of every table.
|
||||
//
|
||||
// THE SAME TABLE OBJECT IS REUSED, NOT REPLACED, and that is what keeps ~200 call sites
|
||||
// alive: `g_backendTextureObjects.Find(...)` still answers from the bucket the calling thread
|
||||
// is in, because the object the global names multiplexes on the key at each lookup. A caller
|
||||
// that must address a bucket OTHER than the calling thread's - the death walks, the
|
||||
// session-teardown drop - takes Shared() or calls the static entries, which walk buckets;
|
||||
// and a caller that wants the whole process's view (the teardown, the tests) takes Snapshot().
|
||||
// FORWARD-DECLARED, because the registry below is DEFINED before the table it holds; the
|
||||
// members that name the table are defined out of line at the bottom of this header.
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class BackendSlotTable;
|
||||
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class SlotTableRegistry {
|
||||
public:
|
||||
using Table = BackendSlotTable<StateObject, BackendObject, kKind>;
|
||||
using Key = TwinKey;
|
||||
using StatePtr = SharedPtr<StateObject>;
|
||||
using BackendPtr = SharedPtr<BackendObject>;
|
||||
// The bound the two tables that ARE a bucket registry have always spelled by name; the
|
||||
// six registries reach it through their own `SlotTable` alias.
|
||||
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
|
||||
|
||||
// One bucket. SHARED, so a caller may keep it across other lookups: the map owns the
|
||||
// pointee and never moves it.
|
||||
SharedPtr<Table> Shared(const Key& key) const {
|
||||
// The common case on the hot path: the same key as last time. One compare, no probe.
|
||||
if (m_memoTable != nullptr && m_memoKey == key) return m_memoTable;
|
||||
const auto found = m_buckets.find(key);
|
||||
if (found != m_buckets.end()) {
|
||||
m_memoKey = key;
|
||||
m_memoTable = found->second;
|
||||
return found->second;
|
||||
}
|
||||
SharedPtr<Table> fresh = MakeShared<Table>();
|
||||
fresh->SetOwnerSession(key.SessionKey);
|
||||
m_buckets.emplace(key, fresh);
|
||||
m_memoKey = key;
|
||||
m_memoTable = fresh;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
// The calling thread's bucket - the ONE entry every in-place call site below resolves
|
||||
// through, and the process-wide answer on a thread that belongs to no session.
|
||||
//
|
||||
// CONST, AND IT ANSWERS A MUTABLE BUCKET: the index is `mutable` because a LOOKUP may
|
||||
// have to create the bucket it is about to miss in (a handle with no twin yet is the
|
||||
// ordinary case), and the bucket's own entries are the table's business rather than the
|
||||
// registry's. The alternative - a const overload returning "no table" - would make every
|
||||
// read-only caller handle a case that cannot arise.
|
||||
Table& ForCallingThread() const {
|
||||
Key key;
|
||||
CurrentTwinKey(&key);
|
||||
// The memo hit without the SharedPtr copy Shared() hands out: two atomic reference
|
||||
// count operations per twin lookup, several lookups per draw (P14). The map keeps the
|
||||
// table alive; only Drop/DropSession/Reset release it, and they clear the memo.
|
||||
if (m_memoTable != nullptr && m_memoKey == key) return *m_memoTable;
|
||||
return *Shared(key);
|
||||
}
|
||||
|
||||
// Every bucket, in no particular order.
|
||||
Vector<SharedPtr<Table>> Snapshot() const {
|
||||
Vector<SharedPtr<Table>> all;
|
||||
all.reserve(m_buckets.size());
|
||||
for (const auto& bucket : m_buckets) all.push_back(bucket.second);
|
||||
return all;
|
||||
}
|
||||
|
||||
SizeT BucketCount() const { return m_buckets.size(); }
|
||||
|
||||
// THE PER-BUCKET ONE-SHOT, because a bucket held by SharedPtr is never dropped by an
|
||||
// assignment: a caller that means "this context/group is gone" has to be able to say so,
|
||||
// and the key is how it says which one. The bucket LEAVES the index before its twins are
|
||||
// released, so a twin destructor that re-entered Shared() cannot find a half-erased one.
|
||||
Bool Drop(const Key& key) {
|
||||
const auto found = m_buckets.find(key);
|
||||
if (found == m_buckets.end()) return false;
|
||||
SharedPtr<Table> dropped = found->second;
|
||||
m_buckets.erase(found);
|
||||
if (m_memoTable == dropped) {
|
||||
m_memoKey = Key{};
|
||||
m_memoTable = nullptr;
|
||||
}
|
||||
dropped->Reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
// EVERY bucket of one session, for the teardown of a session that owned several groups.
|
||||
// The session is the key's high half, so this is one pass and no per-group bookkeeping -
|
||||
// and it is the difference between "this session's twins go" and "the process's do".
|
||||
Uint32 DropSession(Uint64 sessionKey) {
|
||||
Uint32 dropped = 0;
|
||||
for (auto bucket = m_buckets.begin(); bucket != m_buckets.end();) {
|
||||
if (bucket->first.SessionKey != sessionKey) {
|
||||
++bucket;
|
||||
continue;
|
||||
}
|
||||
SharedPtr<Table> table = bucket->second;
|
||||
if (m_memoTable == table) {
|
||||
m_memoKey = Key{};
|
||||
m_memoTable = nullptr;
|
||||
}
|
||||
bucket = m_buckets.erase(bucket);
|
||||
table->Reset();
|
||||
++dropped;
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
// Replace this registry with an empty one, releasing every bucket's twins. This is what
|
||||
// `registry = {}` meant when the registry WAS one table; a caller that must keep the
|
||||
// tables alive past the call takes a Snapshot() first.
|
||||
void Reset() {
|
||||
const Vector<SharedPtr<Table>> all = Snapshot();
|
||||
m_buckets.clear();
|
||||
m_memoKey = Key{};
|
||||
m_memoTable = nullptr;
|
||||
for (const SharedPtr<Table>& table : all) table->Reset();
|
||||
}
|
||||
|
||||
// ---- the calling thread's bucket, member for member ---------------------------------
|
||||
//
|
||||
// THIS IS WHY THE SHAPE COST THE CALL SITES NOTHING. Every handle-keyed entry the table
|
||||
// has is repeated here and forwarded to the bucket the calling thread resolves to, so
|
||||
// `g_backendTextureObjects.FindByHandle(h)` and `g_backendBufferResources.GetOrCreate(h)`
|
||||
// - unchanged source - answer from the right context's object namespace. A caller that
|
||||
// must name a bucket OTHER than its own (a death walk, a case) takes Shared()/Snapshot()
|
||||
// and calls the table directly.
|
||||
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
|
||||
return ForCallingThread().GetOrCreate(handle);
|
||||
}
|
||||
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return ForCallingThread().FindByHandle(handle);
|
||||
}
|
||||
const BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
return ForCallingThread().FindByHandle(handle);
|
||||
}
|
||||
// Resolve-or-create in a bucket the caller NAMES rather than the one it is in. The
|
||||
// monolith-glue frontend-keyed entry (StateBackendObjectRegistry::GetOrCreate below)
|
||||
// needs it: a state object handed over on the client side of a sync is described by a
|
||||
// record in the CALLING thread's group, and a twin built for it must land in the same
|
||||
// bucket as that record.
|
||||
BackendPtr& GetOrCreateIn(const Key& key, MG_Pipe::MGPipeHandle handle) {
|
||||
return Shared(key)->GetOrCreate(handle);
|
||||
}
|
||||
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
|
||||
template <typename Fn>
|
||||
void ForEachLive(Fn&& fn) const {
|
||||
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
|
||||
// rather than a shortcut: a caller walking twins is asking "which of the objects I
|
||||
// can see need this", and what it can see is its own share group. Every bucket in
|
||||
// one process would walk a neighbour session's objects.
|
||||
ForCallingThread().ForEachLive(fn);
|
||||
}
|
||||
// Every bucket, in one pass. The caller receives the key with the handle so it can tell
|
||||
// which context a twin belongs to - the only walks that need the whole process's view
|
||||
// are a diagnostic and a teardown.
|
||||
template <typename Fn>
|
||||
void ForEachBucket(Fn&& fn) const {
|
||||
for (const auto& bucket : m_buckets) {
|
||||
bucket.second->ForEachLive(
|
||||
[&](MG_Pipe::MGPipeHandle handle, const BackendPtr& twin) { fn(bucket.first, handle, twin); });
|
||||
}
|
||||
}
|
||||
// The process-wide answer for the two statics: a notice is about an OBJECT, so it must
|
||||
// reach the twin in EVERY bucket of EVERY holder. They forward to the table's own
|
||||
// statics, which walk the holder list - each SHARED holder of it is one bucket.
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return Table::ReleaseTwinByHandle(handle);
|
||||
}
|
||||
// The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two
|
||||
// tables that ARE a bucket registry (buffers, sampler views) have always been called as
|
||||
// `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide
|
||||
// what happens to the driver id the twin owns. The bucket is the CALLING THREAD'S, which
|
||||
// is the bucket that twin was created in.
|
||||
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return ForCallingThread().ReleaseByHandle(handle);
|
||||
}
|
||||
static Uint32 HolderCount() { return Table::HolderCount(); }
|
||||
Uint32 LiveCount() const { return ForCallingThread().LiveCount(); }
|
||||
Uint32 CompositeLiveCount() const { return ForCallingThread().CompositeLiveCount(); }
|
||||
SizeT OrdinaryCapacityForTest() const { return ForCallingThread().OrdinaryCapacityForTest(); }
|
||||
SizeT CompositeCapacityForTest() const { return ForCallingThread().CompositeCapacityForTest(); }
|
||||
|
||||
// ---- the bucket index's own controls, for the two tables that ARE a registry --------
|
||||
//
|
||||
// The six TwinRegistry registries carry the same three under the names in Managers.h
|
||||
// (DropBucket / DropBucketsOfSession / ResetBuckets); they are repeated here because
|
||||
// SlotTableRegistry IS the public type of the buffer and sampler-view tables, so a caller
|
||||
// of those has no registry object in between.
|
||||
Bool DropBucket(const Key& key) { return Drop(key); }
|
||||
// Drop every bucket of one session; answers how many groups that took with it.
|
||||
Uint32 DropBucketsOfSession(Uint64 sessionKey) { return DropSession(sessionKey); }
|
||||
// Empty every bucket (the process-shaped world's teardown, key {0, 0}).
|
||||
void ResetBuckets() { Reset(); }
|
||||
|
||||
private:
|
||||
// ONE BUCKET PER LIVE {session, share group}, held by SHARED pointer so a caller may
|
||||
// snapshot one across other lookups and so `Drop` can take a bucket's twins away
|
||||
// explicitly - an embedded table could do neither. `std::unordered_map` rather than the
|
||||
// tree's own flat Vector: this map is probed once per twin lookup on the draw path.
|
||||
mutable std::unordered_map<Key, SharedPtr<Table>, TwinKeyHash> m_buckets;
|
||||
// Hot-path memo. A registry is per KIND and each kind is touched several times per draw;
|
||||
// a thread only ever moves between buckets when its session binds another context.
|
||||
mutable SharedPtr<Table> m_memoTable;
|
||||
mutable Key m_memoKey;
|
||||
};
|
||||
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class BackendSlotTable {
|
||||
public:
|
||||
using StatePtr = SharedPtr<StateObject>;
|
||||
using StateWeakPtr = std::weak_ptr<StateObject>;
|
||||
using BackendPtr = SharedPtr<BackendObject>;
|
||||
|
||||
// The largest slot index this table will grow to for a handle that ARRIVED in a call's
|
||||
@@ -189,14 +394,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
struct Entry {
|
||||
BackendPtr backend;
|
||||
// THE MONOLITH-GLUE HALF of the entry (P5e, CONTRACT-P5E §4.1): written by the
|
||||
// minting GetOrCreate and by NoteStateForHandle, read only by StateForHandle and
|
||||
// the legacy death notice. Never compared against another object to decide identity
|
||||
// - that is what Gen is for - never dereferenced for its address, and never read to
|
||||
// decide whether the slot is dead: death is announced, not discovered. ForEachLive
|
||||
// stopped reading it at P5e: a walk that handed a frontend SharedPtr out of server
|
||||
// memory on every step was the last place the server could hold one across records.
|
||||
StateWeakPtr stateRef;
|
||||
// The generation this entry's twin was built for. An entry whose Gen no longer
|
||||
// matches the allocator's is a twin of the slot's PREVIOUS owner.
|
||||
Uint32 Gen = 0;
|
||||
@@ -206,25 +403,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Every constructor links the table into the per-type holder list and the destructor
|
||||
// unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved
|
||||
// registry) is a holder for exactly as long as it exists. Copy and move carry the
|
||||
// ENTRIES and the memo; the links are the table's own and are never copied.
|
||||
// ENTRIES; the links are the table's own and are never copied.
|
||||
BackendSlotTable() { LinkHolder(); }
|
||||
BackendSlotTable(const BackendSlotTable& other):
|
||||
m_ownerSession(other.m_ownerSession),
|
||||
m_slots(other.m_slots),
|
||||
m_band(other.m_band),
|
||||
m_nullTwin(other.m_nullTwin),
|
||||
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||
m_memoHandle(other.m_memoHandle) {
|
||||
m_nullTwin(other.m_nullTwin) {
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable(BackendSlotTable&& other) noexcept:
|
||||
m_ownerSession(other.m_ownerSession),
|
||||
m_slots(std::move(other.m_slots)),
|
||||
m_band(std::move(other.m_band)),
|
||||
m_nullTwin(std::move(other.m_nullTwin)),
|
||||
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||
m_memoHandle(other.m_memoHandle) {
|
||||
m_nullTwin(std::move(other.m_nullTwin)) {
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable& operator=(const BackendSlotTable& other) {
|
||||
@@ -232,8 +426,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_slots = other.m_slots;
|
||||
m_band = other.m_band;
|
||||
m_nullTwin = other.m_nullTwin;
|
||||
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||
m_memoHandle = other.m_memoHandle;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -242,98 +434,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_slots = std::move(other.m_slots);
|
||||
m_band = std::move(other.m_band);
|
||||
m_nullTwin = std::move(other.m_nullTwin);
|
||||
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||
m_memoHandle = other.m_memoHandle;
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~BackendSlotTable() { UnlinkHolder(); }
|
||||
|
||||
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
|
||||
// object's lifetime id, so two calls for the same live object always land on the same
|
||||
// slot, and a successor object at the same heap address never does.
|
||||
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
|
||||
// must not trap where the release build quietly does the documented thing.
|
||||
if (stateObj == nullptr) {
|
||||
// The registry this replaces inserted a null KEY and handed back that entry's
|
||||
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
|
||||
// exactly that tolerance), so a release build never dereferenced null here.
|
||||
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
|
||||
// entry, so a second null call was handed the same twin the first one got.
|
||||
// Resetting here instead would have destroyed it - an arm difference in the one
|
||||
// path that documents relying on this. One per-table parking slot, never live,
|
||||
// never handed a handle, because a null object has no identity and
|
||||
// therefore cannot have a {slot, gen}.
|
||||
return m_nullTwin;
|
||||
}
|
||||
// The served session whose bucket this table is (SlotTableRegistry::Shared), 0 for a
|
||||
// table that belongs to none (the process-wide registry on a sessionless thread, a
|
||||
// fixture's copy of one).
|
||||
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
|
||||
Uint64 OwnerSession() const { return m_ownerSession; }
|
||||
|
||||
// D13: the teardown sentinel is armed by the slot table's first insertion. Twin
|
||||
// creation is the moment a driver-owned id starts needing a guarded destructor;
|
||||
// this is the cold path, so the once-guard costs nothing per draw. On the legacy
|
||||
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
|
||||
EnsureProcessTeardownSentinel();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// This overload mints from frontend identity and is monolith-only. Refuse
|
||||
// before either the client object or allocator is touched, even when a
|
||||
// legacy named scope is open and the current record is barriered.
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)");
|
||||
#endif
|
||||
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"MGPipe slot space of kind %u is exhausted",
|
||||
static_cast<Uint32>(kKind));
|
||||
Entry& entry = EntryAt(handle.Slot);
|
||||
if (entry.Live && entry.Gen != handle.Gen) {
|
||||
// The slot was reclaimed and handed to a new object: the twin at it describes
|
||||
// driver ids the new state object never made.
|
||||
entry.backend.reset();
|
||||
}
|
||||
entry.Gen = handle.Gen;
|
||||
entry.Live = true;
|
||||
entry.stateRef = stateObj;
|
||||
// No creation tick and no sweep here. The registry this replaces needed both,
|
||||
// because nothing told it a texture or a renderbuffer had been DELETED and object
|
||||
// CHURN rather than draw count is what made that urgent. Every one of the six kinds
|
||||
// now announces its own death from its destructor, so a dead twin's slot is already
|
||||
// back before the next creation asks for one.
|
||||
RememberHandle(stateObj->GetLifetimeId(), handle);
|
||||
return entry.backend;
|
||||
}
|
||||
|
||||
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
|
||||
// header records against itself at the top of the file.
|
||||
// P3a: resolve-or-create BY HANDLE - since P13 the only resolve-or-create there is. The
|
||||
// handle ARRIVED, in the call's payload, already minted by the client; this never
|
||||
// touches the allocator. It indexes the slot, notices a generation that no longer
|
||||
// matches (the slot was recycled, so the twin at it describes driver ids the new
|
||||
// resource never made) and hands back the twin pointer.
|
||||
//
|
||||
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
|
||||
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
|
||||
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
|
||||
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
|
||||
// the call's payload, already minted by the side that owns minting; all this does is
|
||||
// index the slot, notice a generation that no longer matches (the slot was recycled,
|
||||
// so the twin at it describes driver ids the new resource never made) and hand back
|
||||
// the twin pointer. FindByHandle beside it is the same shape and already existed.
|
||||
// Death stays ANNOUNCED: the family's own destroy call or the shared death notice
|
||||
// releases the twin by handle, and the slot is freed by the CLIENT after that returns.
|
||||
//
|
||||
// No StatePtr, therefore no Entry::stateRef unless a caller that holds the object notes
|
||||
// it (NoteStateForHandle): a handle-keyed entry has no frontend object to weakly hold
|
||||
// by itself. P5e (§4.1) makes that stop mattering for the iteration - ForEachLive keys
|
||||
// on Live && backend, so such an entry is VISIBLE to the walk and it is StateForHandle,
|
||||
// asked by the one caller that needs the object, that answers null for it. Which is the
|
||||
// same set the old stateRef-locking walk produced, decided at one site instead of in
|
||||
// the loop.
|
||||
//
|
||||
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
|
||||
// announcement is the family's own destroy call, not the shared death notice, and the
|
||||
// slot is freed by the CLIENT after that call returns.
|
||||
//
|
||||
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
|
||||
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
|
||||
// it its first caller.
|
||||
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"GetOrCreate(handle) named the reserved null handle");
|
||||
@@ -363,15 +485,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Same arming as the minting overload, and for the same reason: twin creation is
|
||||
// the moment a driver-owned id starts needing a guarded destructor.
|
||||
// Twin creation is the moment a driver-owned id starts needing a guarded destructor.
|
||||
EnsureProcessTeardownSentinel();
|
||||
|
||||
// THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload.
|
||||
// There the handle comes straight out of MGPipeSlots().Acquire and can never be
|
||||
// BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward".
|
||||
// Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable
|
||||
// input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
|
||||
// THE TWO DIRECTIONS ARE NOT SYMMETRIC. The handle arrived in a payload, so
|
||||
// `handle.Gen < entry.Gen` is a reachable input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
|
||||
// a persistent map, a pooled store, released by a defaulted destructor that issues
|
||||
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
|
||||
// dead resource's generation, after which the incumbent's own FindByHandle refuses
|
||||
@@ -413,31 +531,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return entry->Live ? entry->Gen : 0;
|
||||
}
|
||||
|
||||
// Remember/read the frontend object last used by the monolith sync path.
|
||||
// Handles supplied to the server never need this weak state. Neither method
|
||||
// touches the allocator, so each carries its own unconditional apply guard.
|
||||
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle");
|
||||
#endif
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return;
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return;
|
||||
entry->stateRef = stateObj;
|
||||
}
|
||||
|
||||
// The frontend object noted for this handle, or null. The handle answers identity;
|
||||
// this answers only "which object was this twin last synced from".
|
||||
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle");
|
||||
#endif
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
const Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
|
||||
return entry->stateRef.lock();
|
||||
}
|
||||
|
||||
// P3a: the death half of the overload above, for a kind whose announcement is its own
|
||||
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
|
||||
// than destroying it in place, because the caller may still have to decide what
|
||||
@@ -447,35 +540,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GetOrCreate from a twin destructor cannot resurrect it.
|
||||
//
|
||||
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
|
||||
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
|
||||
// kind the CLIENT frees it after the destroy call returns (MGPipeSlotAllocator::Free - the
|
||||
// Gen bump rides the next handout, so a double free cannot skip a generation). An
|
||||
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
|
||||
// left alone: the successor's own GetOrCreate resets it.
|
||||
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
|
||||
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return BackendPtr{};
|
||||
BackendPtr dead = std::move(entry->backend);
|
||||
entry->backend.reset();
|
||||
entry->stateRef.reset();
|
||||
entry->Live = false;
|
||||
return dead;
|
||||
}
|
||||
|
||||
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
|
||||
// mutates the table, so the returned pointer survives any later Find on it; only a
|
||||
// GetOrCreate that grows the vector can move it, and callers that hold one across a
|
||||
// possible insertion still copy the BackendPtr out.
|
||||
BackendPtr* Find(StateObject* stateObj) {
|
||||
if (stateObj == nullptr) return nullptr;
|
||||
return FindByHandle(HandleOf(stateObj));
|
||||
}
|
||||
|
||||
const BackendPtr* Find(StateObject* stateObj) const {
|
||||
return const_cast<BackendSlotTable*>(this)->Find(stateObj);
|
||||
}
|
||||
|
||||
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
@@ -483,65 +561,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return &entry->backend;
|
||||
}
|
||||
|
||||
// The handle this object's twin is keyed on, or the null handle. This is what a backend
|
||||
// memo stores instead of a raw pointer, a GL name or a bare lifetime id.
|
||||
//
|
||||
// A NULL answer is never memoised. The memo is per table and the allocator is per
|
||||
// kind, so with two holders of one kind the OTHER table can be the one that acquires;
|
||||
// a cached "no handle" here would then outlive the twin's creation over there, and
|
||||
// nothing on this table's own acquire path would ever refresh it. A miss costs the
|
||||
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
|
||||
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Resolve frontend identity only in the monolith path. A cache hit is
|
||||
// subject to the same guard as a fresh allocator lookup.
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf");
|
||||
#endif
|
||||
const Uint64 lifetimeId = stateObj->GetLifetimeId();
|
||||
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
// P2 step e2's backend half. The frontend object with this lifetime id has just been
|
||||
// DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and
|
||||
// return the slot to the allocator - in that order, because the allocator forgets the
|
||||
// lifetime id on Free and a holder told second could no longer resolve it.
|
||||
//
|
||||
// The slot is returned whether or not any holder still had a twin at it: the lifetime
|
||||
// id is dead and MG_State never hands one out twice, so nothing can acquire it again,
|
||||
// and a slot minted for it that no table holds (a table reset with `= {}` drops its
|
||||
// entries without freeing) would otherwise stay allocated for the life of the process.
|
||||
//
|
||||
// STATIC, and deliberately so: a notice is about an object, not about a table, and
|
||||
// "which table holds it" is exactly the question that produced the two-holder leak.
|
||||
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
|
||||
// a second call for the same id answers false because the allocator no longer maps it.
|
||||
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd): the Find/Free pair below is monolith-only with an active transport -
|
||||
// a frontend death is announced to the server by the object_death record (ct),
|
||||
// and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}.
|
||||
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed");
|
||||
#endif
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
// The successor is read BEFORE the release: ReleaseTwinAt runs the twin's
|
||||
// destructor, which is a driver call, and nothing that outlives it may be a
|
||||
// reference into this holder.
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
holder->ReleaseTwinAt(handle);
|
||||
holder = next;
|
||||
}
|
||||
MG_Pipe::MGPipeSlots().Free(kKind, handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
|
||||
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
|
||||
// handle IS the resolution - it crossed in the record's payload - so the client's
|
||||
@@ -553,12 +572,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// double Free would be refused by generation anyway. Idempotent against the kind's
|
||||
// own delete opcode, exactly as the notice arm is: a twin the delete already released
|
||||
// fails ReleaseTwinAt's generation check and the walk moves on.
|
||||
//
|
||||
// ONE SESSION'S HOLDERS, WHEN THE DEATH IS ONE SESSION'S. A handle is numbered by its own
|
||||
// client's allocator, so with several clients served by one process the same {slot, gen}
|
||||
// names a live, unrelated object in every other session: walking every holder released
|
||||
// THOSE twins too - a client deleting a texture (or exiting) deleted the compositor's
|
||||
// driver texture with the same handle, and its windows sampled freed memory. On a thread
|
||||
// that belongs to a session only that session's buckets answer; a sessionless thread
|
||||
// (the in-library backend, a unit fixture) keeps the process-wide walk.
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
TwinKey key;
|
||||
const Bool sessionScoped = CurrentTwinKey(&key) && key.SessionKey != 0;
|
||||
Bool released = false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
released = holder->ReleaseTwinAt(handle) || released;
|
||||
if (!sessionScoped || holder->m_ownerSession == key.SessionKey) {
|
||||
released = holder->ReleaseTwinAt(handle) || released;
|
||||
}
|
||||
holder = next;
|
||||
}
|
||||
return released;
|
||||
@@ -581,14 +612,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// it must not have - and replaces P3a's own fn(StatePtr, BackendPtr), which handed a
|
||||
// frontend SharedPtr OUT OF SERVER MEMORY on every step.
|
||||
//
|
||||
// WHAT THE CALLER LOST AND HOW IT GETS IT BACK, because the two are not the same thing.
|
||||
// The walk no longer locks Entry::stateRef, so a caller that needs the frontend object
|
||||
// asks StateForHandle(handle) for it - which is the same weak reference, read at one
|
||||
// named site inside the scope that owns the debt, instead of at every step of an
|
||||
// iteration. Coverage is UNCHANGED by construction: an entry ForEachLive used to skip
|
||||
// because its stateRef did not lock is one whose StateForHandle answers null, and the
|
||||
// caller skips it there instead.
|
||||
//
|
||||
// The handle is the entry's own {slot, gen}, band-aware: a composite ShaderCso's slot
|
||||
// is reported as kMGPipeShaderCsoCompositeSlotBase + its band index, i.e. the slot the
|
||||
// client minted, never the index into m_band.
|
||||
@@ -616,6 +639,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty this table, releasing every twin it holds. THE SPACES ARE MOVED OUT FIRST and
|
||||
// that is the whole of the care taken here: a twin's destructor is a driver call that
|
||||
// could re-enter GetOrCreate on this table (SlotTables.h's ReleaseTwinAt says why), and
|
||||
// destroying the entries in place would leave that re-entry resizing the vector the
|
||||
// destructors are being walked out of. Here the table is already empty when the entries
|
||||
// die, so a re-entrant insert grows a fresh vector.
|
||||
//
|
||||
// This is what `table = {}` used to mean, spelled so a caller cannot miss the ordering.
|
||||
void Reset() {
|
||||
Vector<Entry> slots = std::move(m_slots);
|
||||
Vector<Entry> band = std::move(m_band);
|
||||
BackendPtr nullTwin = std::move(m_nullTwin);
|
||||
slots.clear();
|
||||
band.clear();
|
||||
nullTwin.reset();
|
||||
}
|
||||
|
||||
Uint32 LiveCount() const {
|
||||
Uint32 count = 0;
|
||||
for (const Entry& entry : m_slots) {
|
||||
@@ -646,13 +686,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
private:
|
||||
// Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to
|
||||
// the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after
|
||||
// every holder has let go.
|
||||
// the kind, not to the table, and the client returns it once, after the release.
|
||||
Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) {
|
||||
// Forget the memo whenever it names this slot, even if this table has no entry
|
||||
// there: a memo can be a handle learned from the allocator for an object another
|
||||
// holder twinned, and it must not survive the slot's next handout.
|
||||
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||
// The twin's destructor is a driver call and could, in principle, re-enter
|
||||
// GetOrCreate on this table and resize m_slots. So NOTHING that outlives the
|
||||
// destructor may be a reference into m_slots: the twin is moved out into a local,
|
||||
@@ -663,7 +698,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return false;
|
||||
dead = std::move(entry->backend);
|
||||
entry->backend.reset();
|
||||
entry->stateRef.reset();
|
||||
entry->Live = false;
|
||||
}
|
||||
dead.reset();
|
||||
@@ -698,8 +732,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return const_cast<BackendSlotTable*>(this)->EntryOrNull(slot);
|
||||
}
|
||||
|
||||
// Grows the right space to hold `slot`. Every caller bounds `slot` first - the minting
|
||||
// overload because the allocator produced it, the handle overload against
|
||||
// Grows the right space to hold `slot`. Every caller bounds `slot` first - against
|
||||
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
|
||||
// allocation size. A composite slot grows m_band by (slot - base) + 1, so the ordinary
|
||||
// table never learns the band exists.
|
||||
@@ -710,15 +743,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return table[index];
|
||||
}
|
||||
|
||||
void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const {
|
||||
m_memoLifetimeId = lifetimeId;
|
||||
m_memoHandle = handle;
|
||||
}
|
||||
void ForgetHandle() const {
|
||||
m_memoLifetimeId = 0;
|
||||
m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
|
||||
// The holder list: intrusive and doubly linked, so registering and unregistering are
|
||||
// two pointer writes with no allocation, and its head is a constant-initialised
|
||||
// static - which is what lets the process-lifetime registry globals in Managers.cpp
|
||||
@@ -745,6 +769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static inline BackendSlotTable* s_firstHolder = nullptr;
|
||||
BackendSlotTable* m_prevHolder = nullptr;
|
||||
BackendSlotTable* m_nextHolder = nullptr;
|
||||
Uint64 m_ownerSession = 0;
|
||||
|
||||
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
|
||||
Vector<Entry> m_slots;
|
||||
@@ -755,31 +780,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Handed back by GetOrCreate for a null state object. Never live, never handed a handle.
|
||||
BackendPtr m_nullTwin;
|
||||
|
||||
// ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every
|
||||
// resolution goes through the allocator's ByLifetimeId hash, which the deleted
|
||||
// TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot
|
||||
// indexing".
|
||||
//
|
||||
// It is one entry and therefore only helps a caller that asks for the SAME object twice
|
||||
// running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers
|
||||
// it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH
|
||||
// targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per
|
||||
// texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had
|
||||
// a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit /
|
||||
// per-target is the fix, and G11 - the device-side gate that would price it - is owed.
|
||||
//
|
||||
// It cannot serve a stale answer, by three independent arguments:
|
||||
// * the key is a lifetime id, which MG_State never hands out twice, so a recycled
|
||||
// heap address cannot hit this memo the way it could hit an address-keyed one;
|
||||
// * a null answer is never stored, so another holder's acquire cannot be hidden by
|
||||
// a "no handle" this table remembered earlier; and
|
||||
// * even a hit for a slot that has since been freed and re-handed is caught, because
|
||||
// the caller resolves the handle through FindByHandle, which compares Gen.
|
||||
// Cleared anyway when a death notice names the memoised slot. 0 is never a live
|
||||
// lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss.
|
||||
mutable Uint64 m_memoLifetimeId = 0;
|
||||
mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/SubgroupPolicy.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/SubgroupImplementation.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// How DirectGLES implements GL_KHR_shader_subgroup (MG_Backend/SubgroupImplementation.h),
|
||||
// shared by the extension list, the dynamic parameters and the ESSL transpile.
|
||||
//
|
||||
// NATIVE where the ES driver itself has GL_KHR_shader_subgroup: the stage is transpiled
|
||||
// with its GroupNonUniform operations intact and SPIRV-Cross spells them as the
|
||||
// GL_KHR_shader_subgroup_* built-ins. Without Vulkan semantics SPIRV-Cross can only print
|
||||
// the basic, vote and ballot operations (plus add/mul reductions, which do not make up the
|
||||
// arithmetic feature), so those three features are all the native path advertises.
|
||||
//
|
||||
// EMULATED where it has none (Adreno's ES driver among them): the compute stage is lowered
|
||||
// onto a 32-lane virtual subgroup in workgroup-shared memory before the transpile
|
||||
// (EmulateSubgroupsPass), which needs nothing from the driver beyond barrier() and shared
|
||||
// memory, and advertises every feature.
|
||||
inline SubgroupImplementation ResolveEsprytSubgroupImplementation(const MG_External::GLESCapabilities& caps) {
|
||||
return ResolveSubgroupImplementation(caps.SupportsShaderSubgroup);
|
||||
}
|
||||
|
||||
inline constexpr Uint32 kEsslPrintableSubgroupFeatures =
|
||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR;
|
||||
|
||||
struct EsprytSubgroupParameters {
|
||||
Uint32 size = 0;
|
||||
Uint32 stages = 0;
|
||||
Uint32 features = 0;
|
||||
Bool quadInAllStages = false;
|
||||
};
|
||||
|
||||
inline EsprytSubgroupParameters ResolveEsprytSubgroupParameters(const MG_External::GLESCapabilities& caps) {
|
||||
switch (ResolveEsprytSubgroupImplementation(caps)) {
|
||||
case SubgroupImplementation::Native:
|
||||
return {caps.SubgroupSize, caps.SubgroupSupportedStages,
|
||||
caps.SubgroupSupportedFeatures & kEsslPrintableSubgroupFeatures, false};
|
||||
case SubgroupImplementation::Emulated:
|
||||
return {kEmulatedSubgroupSize, kEmulatedSubgroupStages, kEmulatedSubgroupFeatures, false};
|
||||
case SubgroupImplementation::None:
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
@@ -20,7 +20,7 @@
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -37,21 +37,19 @@
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// read_pixels writes server reply scratch. The client refuses PACK_BUFFER before
|
||||
// emission, so no frontend pack binding can affect this destination.
|
||||
static const SharedPtr<MG_State::GLState::BufferObject>& SplitReadbackPackBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
return none;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const FormatCapabilityCache* ActiveBackendFormatCaps() {
|
||||
// Under a live split session the SERVER's private backend is what owns the context on the
|
||||
// apply thread (and these reads all run there), so its cache is the authoritative one.
|
||||
// Before the session lands, or under monolith transport in a disaggregated build, fall
|
||||
// back to the process global exactly as the monolith path always has.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) {
|
||||
return &server->GetFormatCapabilities();
|
||||
@@ -66,9 +64,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"on the apply thread");
|
||||
std::abort();
|
||||
}
|
||||
#endif
|
||||
return pActiveBackendObject ? &pActiveBackendObject->GetFormatCapabilities() : nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
|
||||
@@ -108,26 +106,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SizeT targetIndex,
|
||||
Bool caveat,
|
||||
FormatCapability capability) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
|
||||
if (activeCaps == nullptr || targetIndex >= kFormatCapabilityTargetCount) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!pActiveBackendObject || targetIndex >= kFormatCapabilityTargetCount) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
const SizeT formatIndex = static_cast<SizeT>(internalFormat);
|
||||
if (formatIndex >= kFormatCapabilityFormatCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const FormatCapabilityCache& cache = *activeCaps;
|
||||
#else
|
||||
const FormatCapabilityCache& cache = pActiveBackendObject->GetFormatCapabilities();
|
||||
#endif
|
||||
const FormatCapabilityFlags caps =
|
||||
caveat ? cache.CaveatCaps[targetIndex][formatIndex] : cache.FullCaps[targetIndex][formatIndex];
|
||||
return HasFormatCapability(caps, capability);
|
||||
@@ -171,11 +159,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
using namespace MobileGL::MG_Util::TextureFormatProcessor;
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ActiveBackendFormatCaps() == nullptr || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
#else
|
||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
#endif
|
||||
options = GetRuntimeFallbackNormalizeOptions(
|
||||
requestedInternalFormat,
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
@@ -207,7 +191,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// behind the native probe), so a driver that does render to a three-channel image keeps
|
||||
// allocating it byte for byte.
|
||||
//
|
||||
// Do NOT read that as "nothing changes off-device". Measured on Mesa 26.1.6 llvmpipe
|
||||
// Do NOT read that as "nothing changes off-device". Measured on llvmpipe 26.1.6
|
||||
// (the headless CI driver), an ES 3.2 GL_TEXTURE_2D colour attachment is COMPLETE for
|
||||
// GL_RGB8 and GL_RGB16F but INCOMPLETE_ATTACHMENT for GL_RGB8_SNORM, GL_SRGB8 and every
|
||||
// RGB integer format, and UNSUPPORTED for GL_RGB32F. Those eight formats therefore DO
|
||||
@@ -269,15 +253,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
|
||||
// forever. Once the backend exists, the first narrow-format image this process
|
||||
// creates runs the probe on a live context.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ActiveBackendFormatCaps() == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
|
||||
}
|
||||
|
||||
@@ -315,15 +293,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ActiveBackendFormatCaps() != nullptr && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
|
||||
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
@@ -356,7 +328,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A driver WITH GL_NV_image_formats can spell the narrow format - but only for the
|
||||
// formats SPIRV-Cross will actually print. It throws for its is_desktop_only_format
|
||||
// set instead of emitting a token, and the throw loses the stage whatever the driver
|
||||
// would have accepted: on Mesa, which advertises the extension, `layout(r8ui)
|
||||
// would have accepted: on llvmpipe, which advertises the extension, `layout(r8ui)
|
||||
// uimage2D` still lost its whole program until the widening ran for it too.
|
||||
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
|
||||
@@ -572,7 +544,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// The name is the marker: ShaderSourceProcessor only emits it when the source
|
||||
// wrote gl_FragColor, and such a shader can have no other output.
|
||||
// names gl_FragColor, and a shader that writes it can have no other output. One that
|
||||
// names it only in an inactive preprocessor arm keeps the declaration unwritten, and
|
||||
// the IO resolver moves that inactive output off location 0, so the match below
|
||||
// (which wants location 0) does not take it.
|
||||
static const char* const kLoweredName = "mg_FragColor";
|
||||
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
|
||||
return glslCode;
|
||||
@@ -981,6 +956,170 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String RedeclareClipCullDistancesInPerVertexBlock(String glslCode, const GLenum shaderType) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (shaderType != GL_VERTEX_SHADER && shaderType != GL_TESS_EVALUATION_SHADER &&
|
||||
shaderType != GL_GEOMETRY_SHADER) {
|
||||
return glslCode;
|
||||
}
|
||||
if (glslCode.find("gl_ClipDistance[") == String::npos &&
|
||||
glslCode.find("gl_CullDistance[") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// Whole lines only: the declarations SPIRV-Cross prints are one statement per line,
|
||||
// and anchoring on the line keeps a use of the array in the body from ever matching.
|
||||
static const std::regex standaloneRegex(
|
||||
R"(^\s*out\s+(?:highp\s+)?float\s+(gl_ClipDistance|gl_CullDistance)\s*\[\s*([0-9]+)\s*\]\s*;\s*$)");
|
||||
static const std::regex invariantPositionRegex(R"(^\s*invariant\s+gl_Position\s*;\s*$)");
|
||||
|
||||
Vector<String> lines;
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
const SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
if (lineEnd == String::npos) {
|
||||
lines.push_back(glslCode.substr(lineStart));
|
||||
break;
|
||||
}
|
||||
lines.push_back(glslCode.substr(lineStart, lineEnd - lineStart));
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
|
||||
String clipSize;
|
||||
String cullSize;
|
||||
SizeT insertAt = lines.size();
|
||||
SizeT invariantLine = lines.size();
|
||||
for (SizeT i = 0; i < lines.size(); ++i) {
|
||||
std::smatch match;
|
||||
if (std::regex_match(lines[i], match, standaloneRegex)) {
|
||||
(match[1] == "gl_ClipDistance" ? clipSize : cullSize) = match[2].str();
|
||||
if (insertAt == lines.size()) insertAt = i;
|
||||
} else if (invariantLine == lines.size() && std::regex_match(lines[i], invariantPositionRegex)) {
|
||||
invariantLine = i;
|
||||
}
|
||||
}
|
||||
if (insertAt == lines.size()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// A WRITE of gl_PointSize is a bare identifier; a read of the previous stage's value
|
||||
// is a member access on gl_in, which is not this block's business.
|
||||
const auto isIdentifierChar = [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
|
||||
};
|
||||
Bool writesPointSize = false;
|
||||
static constexpr const char* kPointSize = "gl_PointSize";
|
||||
const SizeT pointSizeLength = std::strlen(kPointSize);
|
||||
for (SizeT pos = glslCode.find(kPointSize); pos != String::npos && !writesPointSize;
|
||||
pos = glslCode.find(kPointSize, pos + 1)) {
|
||||
if (pos + pointSizeLength < glslCode.size() && isIdentifierChar(glslCode[pos + pointSizeLength])) {
|
||||
continue;
|
||||
}
|
||||
SizeT before = pos;
|
||||
while (before > 0 && std::isspace(static_cast<unsigned char>(glslCode[before - 1]))) --before;
|
||||
if (before > 0 && (glslCode[before - 1] == '.' || isIdentifierChar(glslCode[before - 1]))) {
|
||||
continue;
|
||||
}
|
||||
writesPointSize = true;
|
||||
}
|
||||
|
||||
const Bool invariantPosition = invariantLine != lines.size();
|
||||
String block = "out gl_PerVertex\n{\n";
|
||||
block += invariantPosition ? " invariant highp vec4 gl_Position;\n" : " highp vec4 gl_Position;\n";
|
||||
if (writesPointSize) block += " highp float gl_PointSize;\n";
|
||||
if (!clipSize.empty()) block += " highp float gl_ClipDistance[" + clipSize + "];\n";
|
||||
if (!cullSize.empty()) block += " highp float gl_CullDistance[" + cullSize + "];\n";
|
||||
block += "};";
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size() + block.size());
|
||||
for (SizeT i = 0; i < lines.size(); ++i) {
|
||||
if (i == insertAt) {
|
||||
result += block;
|
||||
} else if (i == invariantLine || std::regex_match(lines[i], standaloneRegex)) {
|
||||
continue;
|
||||
} else {
|
||||
result += lines[i];
|
||||
}
|
||||
if (i + 1 < lines.size()) result += '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RedeclareClipCullDistanceInputsInPerVertexBlock(String glslCode, const GLenum shaderType,
|
||||
const String& producerOutMembers) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const Bool tessellation = shaderType == GL_TESS_CONTROL_SHADER || shaderType == GL_TESS_EVALUATION_SHADER;
|
||||
if (!tessellation && shaderType != GL_GEOMETRY_SHADER) return glslCode;
|
||||
if (producerOutMembers.find("gl_ClipDistance") == String::npos &&
|
||||
producerOutMembers.find("gl_CullDistance") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
if (ExtractPerVertexBlockMembers(glslCode, /*input=*/true).has_value()) return glslCode;
|
||||
|
||||
// Every gl_in member the stage reads has to be in the redeclared block, or the stage
|
||||
// stops compiling; one the producer does not write cannot be added honestly.
|
||||
static const std::regex memberRegex(R"(gl_in\s*\[[^\]]*\]\s*\.\s*(gl_[A-Za-z]+))");
|
||||
Bool readsDistances = false;
|
||||
for (auto it = std::sregex_iterator(glslCode.begin(), glslCode.end(), memberRegex);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
const String member = (*it)[1].str();
|
||||
if (member == "gl_ClipDistance" || member == "gl_CullDistance") readsDistances = true;
|
||||
if (producerOutMembers.find(member) == String::npos) return glslCode;
|
||||
}
|
||||
if (!readsDistances) return glslCode;
|
||||
|
||||
// `invariant` qualifies outputs only.
|
||||
String members = producerOutMembers;
|
||||
for (SizeT pos = members.find("invariant "); pos != String::npos; pos = members.find("invariant ")) {
|
||||
members.erase(pos, std::strlen("invariant "));
|
||||
}
|
||||
while (!members.empty() && std::isspace(static_cast<unsigned char>(members.back()))) members.pop_back();
|
||||
while (!members.empty() && (members.front() == '\n' || members.front() == '\r')) members.erase(0, 1);
|
||||
String block = "in gl_PerVertex\n{\n" + members + "\n} gl_in[";
|
||||
block += tessellation ? "gl_MaxPatchVertices" : "";
|
||||
block += "];\n";
|
||||
|
||||
// After the header (#directives, precision statements and the stage's layout-only
|
||||
// in/out declarations, which a geometry stage's unsized gl_in[] depends on).
|
||||
static const std::regex headerRegex(
|
||||
R"(^\s*(#.*|precision\s.*|layout\s*\(.*\)\s*(in|out)\s*;\s*)?$)");
|
||||
SizeT insertAt = 0;
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart < glslCode.size()) {
|
||||
SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
if (lineEnd == String::npos) lineEnd = glslCode.size();
|
||||
const String line = glslCode.substr(lineStart, lineEnd - lineStart);
|
||||
if (!std::regex_match(line, headerRegex)) break;
|
||||
insertAt = std::min(lineEnd + 1, glslCode.size());
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
glslCode.insert(insertAt, block);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String RequestClipCullDistanceExtension(String glslCode) {
|
||||
static constexpr const char* kDirective = "#extension GL_EXT_clip_cull_distance : require\n";
|
||||
if (glslCode.find("GL_EXT_clip_cull_distance") != String::npos ||
|
||||
(glslCode.find("gl_ClipDistance") == String::npos && glslCode.find("gl_CullDistance") == String::npos)) {
|
||||
return glslCode;
|
||||
}
|
||||
// Right after the #version line, like every other directive this backend adds.
|
||||
const SizeT versionPos = glslCode.find("#version");
|
||||
if (versionPos == String::npos) {
|
||||
return kDirective + glslCode;
|
||||
}
|
||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
||||
if (lineEnd == String::npos) {
|
||||
return glslCode + "\n" + kDirective;
|
||||
}
|
||||
glslCode.insert(lineEnd + 1, kDirective);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers,
|
||||
@@ -1648,7 +1787,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// KHR-GL43.shading_language_420pack.binding_images_texture_type_* went from 6
|
||||
// image uniforms to 30 and the link failed outright with "Error: Image Image
|
||||
// location or component exceeds max allowed." on an Adreno 830, where Mali and
|
||||
// Mesa both accept the same text.
|
||||
// llvmpipe both accept the same text.
|
||||
//
|
||||
// Nothing downstream reads these names: the two passes that key on the GL uniform
|
||||
// name (RebindImageUniformsToFrontendUnits, BakeImageFormatQualifiers) both run
|
||||
@@ -1918,6 +2057,124 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
String LowerZeroGradCubeArrayShadowLookups(const String& glslCode, GLenum shaderType) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (shaderType == GL_FRAGMENT_SHADER || glslCode.find("samplerCubeArrayShadow") == String::npos ||
|
||||
glslCode.find("textureGrad") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// Every name declared with the type: uniforms, and the parameters of functions the
|
||||
// sampler is passed to.
|
||||
static const std::regex declRegex(R"(samplerCubeArrayShadow\s+([A-Za-z_][A-Za-z0-9_]*))");
|
||||
UnorderedMap<String, Bool> names;
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), declRegex), end; it != end; ++it) {
|
||||
names.emplace((*it)[1].str(), true);
|
||||
}
|
||||
if (names.empty()) return glslCode;
|
||||
|
||||
static constexpr const char* kName = "textureGrad";
|
||||
static constexpr SizeT kNameLength = 11;
|
||||
String result = glslCode;
|
||||
Bool changed = false;
|
||||
// Right to left, so an earlier call's offsets stay valid.
|
||||
for (SizeT scan = result.rfind(kName); scan != String::npos;
|
||||
scan = scan == 0 ? String::npos : result.rfind(kName, scan - 1)) {
|
||||
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||
const SizeT openParen = result.find_first_not_of(" \t", scan + kNameLength);
|
||||
if (openParen == String::npos || result[openParen] != '(') continue;
|
||||
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||
// sampler, P, compare, dPdx, dPdy
|
||||
if (marks.size() != 5) continue;
|
||||
SizeT nameStart = result.find_first_not_of(" \t", openParen + 1);
|
||||
SizeT nameEnd = nameStart;
|
||||
while (nameEnd < marks[0] && IsIdentifierChar(result[nameEnd])) ++nameEnd;
|
||||
if (names.find(result.substr(nameStart, nameEnd - nameStart)) == names.end()) continue;
|
||||
const auto argument = [&](SizeT index) {
|
||||
const SizeT start = marks[index - 1] + 1;
|
||||
String text = result.substr(start, marks[index] - start);
|
||||
text.erase(std::remove_if(text.begin(), text.end(),
|
||||
[](char c) { return c == ' ' || c == '\t'; }),
|
||||
text.end());
|
||||
return text;
|
||||
};
|
||||
if (argument(3) != "vec3(0.0)" || argument(4) != "vec3(0.0)") continue;
|
||||
result.erase(marks[2], marks[4] - marks[2]);
|
||||
result.replace(scan, kNameLength, "texture");
|
||||
changed = true;
|
||||
}
|
||||
return changed ? result : glslCode;
|
||||
}
|
||||
|
||||
String ClampUnsignedMultisampleFetches(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (glslCode.find("usampler2DMS") == String::npos || glslCode.find("texelFetch") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// The non-arrayed unsigned multisample samplers - the ones ClampMultisampleFetchPass
|
||||
// deferred, and the only ones a lookup can name directly as its first argument.
|
||||
static const std::regex declRegex(
|
||||
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?usampler2DMS(?:Array)?\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
|
||||
UnorderedMap<String, String> clampNames; // sampler name -> sample-max uniform name
|
||||
Vector<std::pair<SizeT, String>> declEnds; // where each declaration ends, and its uniform
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), declRegex), end; it != end; ++it) {
|
||||
const String name = (*it)[1].str();
|
||||
const String clampName = String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + name;
|
||||
if (clampNames.emplace(name, clampName).second) {
|
||||
declEnds.emplace_back(static_cast<SizeT>(it->position(0) + it->length(0)), clampName);
|
||||
}
|
||||
}
|
||||
if (clampNames.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Right-to-left, so an earlier call's offsets survive the insertions behind it.
|
||||
constexpr const char* kFetch = "texelFetch";
|
||||
const SizeT fetchLength = std::strlen(kFetch);
|
||||
String result = glslCode;
|
||||
Vector<String> usedClamps;
|
||||
for (SizeT scan = result.rfind(kFetch); scan != String::npos;
|
||||
scan = scan == 0 ? String::npos : result.rfind(kFetch, scan - 1)) {
|
||||
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||
const SizeT openParen = result.find_first_not_of(" \t", scan + fetchLength);
|
||||
if (openParen == String::npos || result[openParen] != '(') continue; // texelFetchOffset etc.
|
||||
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||
if (marks.size() != 3) continue; // (sampler, coord, sample) is the multisample form
|
||||
const SizeT firstStart = result.find_first_not_of(" \t", openParen + 1);
|
||||
SizeT firstEnd = marks[0];
|
||||
while (firstEnd > firstStart && (result[firstEnd - 1] == ' ' || result[firstEnd - 1] == '\t')) {
|
||||
--firstEnd;
|
||||
}
|
||||
if (firstStart == String::npos || firstEnd <= firstStart) continue;
|
||||
const auto found = clampNames.find(result.substr(firstStart, firstEnd - firstStart));
|
||||
if (found == clampNames.end()) continue;
|
||||
const SizeT sampleStart = result.find_first_not_of(" \t\n", marks[1] + 1);
|
||||
if (sampleStart == String::npos || sampleStart >= marks[2]) continue;
|
||||
const SizeT sampleEnd = marks[2];
|
||||
result.insert(sampleEnd, String(", ") + found->second + ")");
|
||||
result.insert(sampleStart, "min(");
|
||||
if (std::find(usedClamps.begin(), usedClamps.end(), found->second) == usedClamps.end()) {
|
||||
usedClamps.push_back(found->second);
|
||||
}
|
||||
}
|
||||
if (usedClamps.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Declared right after the sampler they bound, back to front so the recorded
|
||||
// offsets (taken from the unedited text) stay valid: every fetch sits after the
|
||||
// declarations in the emitted ESSL, so no insertion above has moved them.
|
||||
for (auto it = declEnds.rbegin(); it != declEnds.rend(); ++it) {
|
||||
if (std::find(usedClamps.begin(), usedClamps.end(), it->second) == usedClamps.end()) continue;
|
||||
result.insert(it->first, String("\nuniform highp int ") + it->second + ";");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
@@ -2359,15 +2616,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? SplitReadbackPackBuffer() :
|
||||
#endif
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
SplitReadbackPackBuffer();
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
const auto packParams = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// C6 / ID-52 / CONTRACT-P5 table 3's pActiveBackendObject row. The format-capability cache of
|
||||
// THIS ROLE's backend. Under an active transport the server's own private
|
||||
// BackendObject_DirectGLES owns the context on the apply thread, so a backend-internal format
|
||||
@@ -24,7 +23,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (five in Utils.cpp, ClampSamplesToBackendSupport in BackendObject_DirectGLES.cpp) go through
|
||||
// this instead of dereferencing pActiveBackendObject directly.
|
||||
const FormatCapabilityCache* ActiveBackendFormatCaps();
|
||||
#endif
|
||||
|
||||
namespace DebugImpl {
|
||||
class ErrorLopper {
|
||||
@@ -354,7 +352,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
||||
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
||||
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
||||
// indexed with non-constant expressions are forbidden in GLSL ES", llvmpipe 26.1.4 at
|
||||
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
||||
// widening the array to cover the whole span of units and routing each subscript through
|
||||
// a `const highp int` offset table - was written before that pass covered images, and
|
||||
@@ -388,6 +386,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||
// program that has no other problem.
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||
// Moves the clip/cull distance OUTPUTS of a vertex, tessellation evaluation or geometry
|
||||
// stage into a redeclared `out gl_PerVertex { ... };` block. SPIRV-Cross sizes the two
|
||||
// arrays with a standalone `out float gl_ClipDistance[N];` - legal desktop GLSL, but an
|
||||
// ES compiler may refuse it outright ("reserved built-in name"), and leaving the
|
||||
// declaration out instead compiles but fails the link on an array-size mismatch against
|
||||
// the fragment stage. A member of the redeclared block is the spelling ESSL accepts.
|
||||
// gl_Position is always a member (every pre-rasterization stage has it), gl_PointSize
|
||||
// only when the stage writes it, so the block never names a built-in the stage would
|
||||
// otherwise not need. A stage-level `invariant gl_Position;` moves onto the member,
|
||||
// because the bare re-qualification of a block member is not something ESSL allows.
|
||||
// Fragment inputs and the control stage (whose block SPIRV-Cross already emits) are
|
||||
// left alone; a stage without the standalone declarations comes back unchanged.
|
||||
String RedeclareClipCullDistancesInPerVertexBlock(String glslCode, GLenum shaderType);
|
||||
// The INPUT half of the same problem, for a tessellation control, tessellation evaluation
|
||||
// or geometry stage that reads gl_in[i].gl_ClipDistance / gl_CullDistance. SPIRV-Cross
|
||||
// never redeclares an ES input block, so the stage reads the implicitly sized built-in
|
||||
// arrays - and a driver that packs cull distances after clip distances then places the
|
||||
// cull array at the implicit clip size rather than at the size the previous stage wrote,
|
||||
// reading the wrong component. Redeclares `in gl_PerVertex { ... } gl_in[]` with exactly
|
||||
// the members of the previous stage's output block (`producerOutMembers`, from
|
||||
// ExtractPerVertexBlockMembers), which is also the shape ES interface matching wants.
|
||||
// Unchanged when the stage does not read either array, already declares its input block,
|
||||
// reads a gl_in member the producer's block does not carry, or the producer has no block.
|
||||
String RedeclareClipCullDistanceInputsInPerVertexBlock(String glslCode, GLenum shaderType,
|
||||
const String& producerOutMembers);
|
||||
// Adds `#extension GL_EXT_clip_cull_distance : require` when `glslCode` names
|
||||
// gl_ClipDistance or gl_CullDistance and does not request it yet. For the synthesized
|
||||
// pass-through control stage, whose gl_in/gl_out member lists are mirrored from stages
|
||||
// that may carry the clip/cull arrays: SPIRV-Cross requests the extension for the stages
|
||||
// it emits, but nothing does for a stage built by hand.
|
||||
String RequestClipCullDistanceExtension(String glslCode);
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
@@ -481,7 +510,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
||||
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
||||
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||
// reading back zero. Mali and a desktop GLES driver link the same text, so nothing but a device gate
|
||||
// catches this.
|
||||
//
|
||||
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
|
||||
@@ -547,6 +576,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
// A depth-compare lookup at an explicit LOD of 0 on a samplerCubeArrayShadow - what
|
||||
// texture() is outside the fragment stage - has no GLSL ES spelling: there is no
|
||||
// textureLod or textureGrad overload for that sampler. SPIRV-Cross prints it as
|
||||
// textureGrad(s, P, compare, vec3(0.0), vec3(0.0)) anyway; Adreno compiles that and
|
||||
// returns wrong comparisons (KHR-GL4x.texture_cube_map_array.sampling, DEPTH32F, vertex/
|
||||
// tessellation control/geometry/compute). Outside the fragment stage texture(s, P,
|
||||
// compare) samples level 0 exactly (GLSL ES 3.20 8.9), so those calls are rewritten to
|
||||
// it. The fragment stage is left alone: there texture() would pick its own level.
|
||||
String LowerZeroGradCubeArrayShadowLookups(const String& glslCode, GLenum shaderType);
|
||||
// Prefix of the per-sampler int uniform that carries the bound multisample texture's
|
||||
// last real sample index into the shader (see ClampUnsignedMultisampleFetches); the
|
||||
// suffix is the sampler's own name.
|
||||
constexpr const char* MS_SAMPLE_MAX_UNIFORM_PREFIX = "mg_msSampleMax_";
|
||||
// The runtime half of ClampMultisampleFetchPass's deferDirectUnsignedFetches. A
|
||||
// usampler2DMS[Array] reads integer colour textures, which the backend may allocate with
|
||||
// fewer samples than MobileGL advertises, AND the stencil aspect of multisample
|
||||
// depth/stencil textures, which it allocates under the (larger) depth ceiling - so no
|
||||
// single compile-time bound is right for both. Declares one
|
||||
// `uniform highp int mg_msSampleMax_<sampler>;` per non-arrayed unsigned multisample
|
||||
// sampler and wraps the sample argument of every texelFetch that names it in
|
||||
// min(sample, that uniform). Draws push the bound texture's real sample count - 1 into
|
||||
// it (0, the program's initial value, when nothing multisampled is bound). Returns the
|
||||
// source unchanged when there is nothing to rewrite.
|
||||
String ClampUnsignedMultisampleFetches(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// 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 +289,143 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 (docs/archive/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_Record::ServerStagedTexture();
|
||||
const Uint64 key = MG_Record::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.Holds()) {
|
||||
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,15 +439,29 @@
|
||||
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);
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -32,11 +33,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return MG_Config::Features.MagmaR11G11B10FFallback;
|
||||
}
|
||||
|
||||
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
(void)dpy;
|
||||
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
Bool IsFormatIndexValid(TextureInternalFormat format) {
|
||||
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
|
||||
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
|
||||
@@ -332,6 +328,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() {
|
||||
// Everything below reaches this session's renderer slot, whichever thread destroys it.
|
||||
MagmaSession* const ownSession = m_magmaSession.get();
|
||||
MagmaSession* const previousSession = BoundMagmaSession();
|
||||
if (ownSession != nullptr) BindMagmaSessionToThisThread(ownSession);
|
||||
// P12 review fix: A SERVER SESSION THAT ENDS WITHOUT A CLEAN TEARDOWN LEAVES NO RENDERER ON
|
||||
// THE WINDOW. The renderer - swapchain and VkSurfaceKHR on the window - is a process global
|
||||
// that only ReleaseEGLResources / a released surface drops, and a client that went away
|
||||
@@ -345,6 +345,15 @@ 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();
|
||||
}
|
||||
// The session's state goes with it, and no thread may keep answering with it.
|
||||
m_magmaSession.reset();
|
||||
if (ownSession != nullptr) {
|
||||
BindMagmaSessionToThisThread(previousSession == ownSession ? nullptr : previousSession);
|
||||
}
|
||||
}
|
||||
#else
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
||||
@@ -353,6 +362,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
|
||||
|
||||
Bool BackendObject_DirectVulkan::InitWindowSurface() {
|
||||
// A suspended surface activates its placeholder - never the window, which is gone.
|
||||
const auto suspended = m_suspendedWindowSurfaces.find(m_activatingSurface);
|
||||
if (suspended != m_suspendedWindowSurfaces.end()) {
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) {
|
||||
return ActivateSuspendedPlaceholder(m_activatingSurface, suspended->second);
|
||||
}
|
||||
return InitPbufferSurface(static_cast<EGLint>(suspended->second.Width),
|
||||
static_cast<EGLint>(suspended->second.Height));
|
||||
}
|
||||
if (!m_windowHandle.Handle) {
|
||||
MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null");
|
||||
return false;
|
||||
@@ -360,12 +378,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
||||
|
||||
VulkanRendererConfig config;
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) {
|
||||
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), nativeWindow, config);
|
||||
}
|
||||
// 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);
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -373,19 +397,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
|
||||
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) {
|
||||
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), NativeWindowType{},
|
||||
config);
|
||||
}
|
||||
// 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>(NativeWindowType{}, config);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::BindSessionStateToThisThread() {
|
||||
if (m_magmaSession == nullptr) m_magmaSession = MakeUnique<MagmaSession>();
|
||||
BindMagmaSessionToThisThread(m_magmaSession.get());
|
||||
MGLOG_I("DirectVulkan: session state %llu bound to this apply thread",
|
||||
static_cast<unsigned long long>(m_magmaSession->Id()));
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::LatchIfGpuFaulted() {
|
||||
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchIfGpuHung("apply");
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::Initialize() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager::RegisterWireResourceOps();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). BESIDE the resource ops and for the same
|
||||
// reason: this is the one place both Magma server roles pass through - the inproc
|
||||
// role and the spawn/TCP session child - and ServerLoop::CreateBackend calls it at
|
||||
@@ -393,7 +432,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// before any frontend object can die unheard. A per-transport install is how one of
|
||||
// the two roles ends up without it.
|
||||
InstallStateObjectDeathOps();
|
||||
#endif
|
||||
#endif
|
||||
m_initialized = true;
|
||||
}
|
||||
@@ -416,9 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MG_Pipe::MGPipeNotifyCompileEnvChanged();
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -457,6 +493,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
|
||||
return false;
|
||||
}
|
||||
const EGLSurfaceState* state = GetRegisteredEGLSurface(surface);
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer && state != nullptr && state->Kind == SurfaceKind::Pbuffer) {
|
||||
// A pbuffer-backed window (a Wayland window presented through wl_shm) whose target is an
|
||||
// image reader of FIXED size: a swapchain rebuild cannot follow the resize, so the target
|
||||
// is rebuilt at the new size - as DirectGLES replaces its native pbuffer. A parked one is
|
||||
// rebuilt when it is next activated, from the size registered above.
|
||||
const Bool active = m_eglSurfaceInitialized && m_eglSurface == surface;
|
||||
pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface));
|
||||
if (active) {
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = static_cast<Uint32>(state->Width);
|
||||
config.SurfaceHeight = static_cast<Uint32>(state->Height);
|
||||
(void)pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (pVulkanRenderer && m_eglSurface == surface) {
|
||||
pVulkanRenderer->RequestSwapchainResize(width, height);
|
||||
}
|
||||
@@ -474,7 +526,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
const Bool ok = BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
if (ok && UsesSurfaceTargets() && ctx != EGL_NO_CONTEXT && draw != EGL_NO_SURFACE) {
|
||||
m_contextBindings[ctx] = EGLCurrentState{.Display = dpy, .DrawSurface = draw, .ReadSurface = read,
|
||||
.Context = ctx};
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::OnClientContextBound(Uint64 token) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!UsesSurfaceTargets() || token == 0) return;
|
||||
// Each of a multi-window client's contexts draws to the surface IT was made current with
|
||||
// (Qt renders every window on its own thread and context); the ring interleaves them with
|
||||
// a bind_context between, and no make-current. Follow the context back to its surface.
|
||||
const auto found = m_contextBindings.find(reinterpret_cast<EGLContext>(token));
|
||||
if (found == m_contextBindings.end()) return;
|
||||
const EGLCurrentState binding = found->second;
|
||||
if (!GetRegisteredEGLSurface(binding.DrawSurface)) {
|
||||
m_contextBindings.erase(found);
|
||||
return;
|
||||
}
|
||||
const auto current = m_eglCurrentThreads.find(std::this_thread::get_id());
|
||||
if (current != m_eglCurrentThreads.end() && current->second.Context == binding.Context &&
|
||||
current->second.DrawSurface == binding.DrawSurface && m_eglSurface == binding.DrawSurface) {
|
||||
return;
|
||||
}
|
||||
(void)BackendObject::MakeEGLCurrent(binding.Display, binding.DrawSurface, binding.ReadSurface, binding.Context);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
@@ -486,6 +564,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return BackendObject::SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Same lock as SwapEGLBuffers: the copy reads the current surface target's image.
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!pVulkanRenderer) {
|
||||
MGLOG_E("DirectVulkan: shared image %llu: the renderer is not initialized",
|
||||
static_cast<unsigned long long>(image.Id));
|
||||
return false;
|
||||
}
|
||||
return pVulkanRenderer->BlitDefaultFramebufferToSharedImage(image, region);
|
||||
#else
|
||||
(void)image, (void)region;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::PublishSharedImageAccesses() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return pVulkanRenderer ? pVulkanRenderer->PublishSharedImageAccesses() : true;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ExportNativeFence(int* fence) {
|
||||
*fence = -1;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return pVulkanRenderer ? pVulkanRenderer->ExportNativeFence(fence) : true;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// A swapchain image keeps its content through a present whatever region is drawn next: the
|
||||
// damage region the client may declare is not needed here.
|
||||
Int32 BackendObject_DirectVulkan::QueryCurrentBufferAge(Bool damageRegionFollows) {
|
||||
(void)damageRegionFollows;
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return pVulkanRenderer ? pVulkanRenderer->CurrentDrawBufferAge() : 0;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -500,11 +628,131 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
m_suspendedWindowSurfaces.clear();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ActivateSuspendedPlaceholder(EGLSurface surface,
|
||||
const SuspendedWindowSurface& extent) {
|
||||
// A pbuffer target at the window's extent: every default-framebuffer path (render passes,
|
||||
// read-back, the shared-image blit, the acquire and the present) keeps a live swapchain to
|
||||
// work on, and the extent it publishes is the one the client already has.
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = std::max<Uint32>(extent.Width, 1);
|
||||
config.SurfaceHeight = std::max<Uint32>(extent.Height, 1);
|
||||
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::SuspendServerWindow(void* window) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
// The monolith's one renderer IS its window; only surface targets can be swapped under it.
|
||||
if (window == nullptr || !UsesSurfaceTargets()) return false;
|
||||
Vector<EGLSurface> onWindow;
|
||||
for (const auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue;
|
||||
onWindow.push_back(client);
|
||||
}
|
||||
for (const EGLSurface client : onWindow) {
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
SuspendedWindowSurface extent;
|
||||
extent.Width = static_cast<Uint32>(std::max<EGLint>(state->second.Width, 1));
|
||||
extent.Height = static_cast<Uint32>(std::max<EGLint>(state->second.Height, 1));
|
||||
if (pVulkanRenderer) {
|
||||
const Uint64 key = SurfaceTargetKey(client);
|
||||
const VkExtent2D built = pVulkanRenderer->SurfaceTargetExtent(key);
|
||||
if (built.width != 0 && built.height != 0) {
|
||||
extent.Width = built.width;
|
||||
extent.Height = built.height;
|
||||
}
|
||||
const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key);
|
||||
// Flushes, waits the device idle, purges the render passes on the target and drops its
|
||||
// swapchain and VkSurfaceKHR - nothing references the window after this.
|
||||
pVulkanRenderer->DestroySurfaceTarget(key);
|
||||
m_suspendedWindowSurfaces[client] = extent;
|
||||
// A placeholder that cannot be built is a suspend that failed: the window is already let
|
||||
// go of, and the caller ends the session by name instead (its EGL resources go).
|
||||
if (active && !ActivateSuspendedPlaceholder(client, extent)) return false;
|
||||
} else {
|
||||
m_suspendedWindowSurfaces[client] = extent;
|
||||
}
|
||||
}
|
||||
MGLOG_I("DirectVulkan: %zu window surface(s) on server window %p now draw into placeholder pbuffer targets; "
|
||||
"the renderer, the device and every object stay",
|
||||
onWindow.size(), window);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr || !UsesSurfaceTargets()) return false;
|
||||
UnorderedMap<EGLSurface, SuspendedWindowSurface> suspended;
|
||||
suspended.swap(m_suspendedWindowSurfaces);
|
||||
for (const auto& [client, extent] : suspended) {
|
||||
(void)extent;
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
if (state == m_eglSurfaces.end()) continue; // released while suspended
|
||||
state->second.Window.Handle = window;
|
||||
state->second.Window.Width = width;
|
||||
state->second.Window.Height = height;
|
||||
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state->second.Window;
|
||||
if (!pVulkanRenderer) continue;
|
||||
const Uint64 key = SurfaceTargetKey(client);
|
||||
const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key);
|
||||
pVulkanRenderer->DestroySurfaceTarget(key); // the placeholder
|
||||
// A parked surface is built on the new window when it is next activated (InitWindowSurface).
|
||||
if (!active) continue;
|
||||
// The creation's own path: a new VkSurfaceKHR and swapchain with the surface's current
|
||||
// pre-transform, a new target serial (render passes rebuilt), and the extent published.
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = std::max<Uint32>(width, 1);
|
||||
config.SurfaceHeight = std::max<Uint32>(height, 1);
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
if (!pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast<NativeWindowType>(window), config)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
MGLOG_I("DirectVulkan: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
|
||||
window, width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr || !UsesSurfaceTargets()) return false;
|
||||
Bool followed = false;
|
||||
for (auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue;
|
||||
state.Window.Width = width;
|
||||
state.Window.Height = height;
|
||||
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state.Window;
|
||||
// Magma's extent is its swapchain's, published when it is built: the active target follows
|
||||
// the window now (or at the half-recorded frame's present); a parked one at its next present.
|
||||
if (pVulkanRenderer && pVulkanRenderer->IsActiveSurfaceTarget(SurfaceTargetKey(client))) {
|
||||
followed = pVulkanRenderer->FollowActiveWindowResize() || followed;
|
||||
}
|
||||
}
|
||||
return followed;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::UsesSurfaceTargets() const {
|
||||
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
m_suspendedWindowSurfaces.erase(surface);
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface));
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
// The renderer outlives its surfaces here: only this surface's target goes (in
|
||||
// OnEGLSurfaceForgotten, which follows), and every fence stays valid.
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) return;
|
||||
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
@@ -544,7 +792,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported) {
|
||||
Bool cubeMapArraySupported,
|
||||
const AdvertisedFeatureSupport& features) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
@@ -553,6 +802,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
|
||||
// trivially true for every context this backend claims (4.6). KWin 6's
|
||||
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
|
||||
// strings, so withholding them fails its renderer bring-up. Same block as DirectGLES.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
@@ -644,10 +899,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Core in the 4.x versions this backend already claims, and named so a context of an
|
||||
// older requested version reaches the same, already-implemented surface (the conformance
|
||||
// suite gates its GL 3.x runs of these on the string alone):
|
||||
// - transform feedback objects, pause/resume and glDrawTransformFeedback (4.0);
|
||||
E_GL_ARB_transform_feedback2,
|
||||
// - multiple capture buffers per stream, gl_SkipComponents / gl_NextBuffer, the
|
||||
// indexed queries and glDrawTransformFeedbackStream (4.0);
|
||||
E_GL_ARB_transform_feedback3,
|
||||
// - GL_RGB10_A2UI (3.3), GL_RGB9_E5 (3.0) and the integer colour formats (3.0);
|
||||
E_GL_ARB_texture_rgb10_a2ui, E_GL_EXT_texture_shared_exponent, E_GL_EXT_texture_integer,
|
||||
// - the eleven pipeline-statistics query targets (4.6). Accepted by glBeginQuery and
|
||||
// answered with GL_QUERY_COUNTER_BITS = 0, the spec's own "this counter is not
|
||||
// instrumented" signal (see IsPipelineStatisticsQueryTarget in GL_Query.cpp);
|
||||
E_GL_ARB_pipeline_statistics_query,
|
||||
// - mix() over integer and boolean vectors in GLSL below 4.50. The frontend compiles it
|
||||
// to OpSelect, which SPIR-V consumes directly.
|
||||
E_GL_EXT_shader_integer_mix,
|
||||
// The origin and depth mode select the Y negation and the [-1, 1] -> [0, 1] depth
|
||||
// remap of the position fixup every program already gets, so no device feature is
|
||||
// involved (ApplyClipControlToTransformFlags).
|
||||
E_GL_ARB_clip_control,
|
||||
// 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};
|
||||
// Follows the tessellationShader device feature; GL_Query also reads the string to open
|
||||
// the two tessellation pipeline-statistics targets.
|
||||
if (features.TessellationShader) {
|
||||
extensions.push_back(E_GL_ARB_tessellation_shader);
|
||||
}
|
||||
// Follows the shaderCullDistance device feature, the same gate GL_MAX_CULL_DISTANCES uses.
|
||||
if (features.CullDistance) {
|
||||
extensions.push_back(E_GL_ARB_cull_distance);
|
||||
}
|
||||
// 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
|
||||
@@ -655,7 +940,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
|
||||
if (shaderSubgroupSupported) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||
@@ -727,11 +1012,11 @@ 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;
|
||||
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
|
||||
funcsTable.GL.MultiDrawElements = MultiDrawElements;
|
||||
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||
@@ -752,19 +1037,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.ClearBufferfv = ClearBufferfv;
|
||||
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
||||
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
||||
funcsTable.GL.CopyImageSubData = CopyImageSubData;
|
||||
funcsTable.GL.GenerateMipmap = GenerateMipmap;
|
||||
funcsTable.GL.ReadPixels = ReadPixels;
|
||||
funcsTable.GL.GetTexImage = GetTexImage;
|
||||
funcsTable.GL.GetTextureImage = GetTextureImage;
|
||||
funcsTable.GL.DispatchCompute = DispatchCompute;
|
||||
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
|
||||
funcsTable.GL.MemoryBarrier = MemoryBarrier;
|
||||
@@ -814,9 +1092,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MG_Pipe::MGPipeNotifyCompileEnvChanged();
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
@@ -827,17 +1103,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||
// the whole list keeps re-runs idempotent.
|
||||
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
||||
// extension by itself on devices with no native subgroup support at all; a
|
||||
// device with native subgroups always advertises - and uses - those.
|
||||
// The emulated compute path (SubgroupSupportPolicy.h) carries the extension by
|
||||
// itself on devices with no native subgroup support at all; a device with native
|
||||
// subgroups advertises - and uses - those unless MOBILEGL_SUBGROUP_EMULATION forces
|
||||
// the emulation.
|
||||
const Bool subgroupSupportAdvertised =
|
||||
m_vulkanCaps.SupportsShaderSubgroup ||
|
||||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
||||
ResolveVulkanSubgroupImplementation(m_vulkanCaps.SupportsShaderSubgroup) !=
|
||||
MG_Backend::SubgroupImplementation::None;
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
|
||||
m_vulkanCaps.SupportsImageCubeArray);
|
||||
m_vulkanCaps.SupportsImageCubeArray,
|
||||
AdvertisedFeatureSupport{.TessellationShader = m_vulkanCaps.SupportsTessellationShader,
|
||||
.CullDistance = m_vulkanCaps.SupportsShaderCullDistance &&
|
||||
m_vulkanCaps.MaxCullDistances > 0});
|
||||
// GL_EXT_fragment_shading_rate rides on VK_KHR_fragment_shading_rate's pipeline rate, and
|
||||
// its _primitive companion on the primitive rate; both only when the renderer enabled them.
|
||||
// _attachment is not advertised: no framebuffer carries a rate attachment to this backend.
|
||||
if (pVulkanRenderer != nullptr && pVulkanRenderer->GetFragmentShadingRateSupport().Pipeline) {
|
||||
m_rendererInfo.RendererGLInfo.Extensions.push_back(E_GL_EXT_fragment_shading_rate);
|
||||
if (pVulkanRenderer->GetFragmentShadingRateSupport().Primitive) {
|
||||
m_rendererInfo.RendererGLInfo.Extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -913,6 +1202,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
|
||||
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
|
||||
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
|
||||
// GL_SAMPLE_POSITION. A device reporting standardSampleLocations places the samples exactly
|
||||
// at the Vulkan specification's table (Multisampling, "Standard sample locations"), which
|
||||
// is also what gl_SamplePosition reads back in a shader. Vulkan's sample y grows with the
|
||||
// framebuffer row, and a framebuffer object's row 0 is GL's y = 0, so the table needs no
|
||||
// flip to be GL's answer. Without the flag the positions are unknown and stay unset.
|
||||
if (m_vulkanCaps.StandardSampleLocations) {
|
||||
static constexpr Float kStandard2[] = {0.75f, 0.75f, 0.25f, 0.25f};
|
||||
static constexpr Float kStandard4[] = {0.375f, 0.125f, 0.875f, 0.375f, 0.125f, 0.625f, 0.625f, 0.875f};
|
||||
static constexpr Float kStandard8[] = {0.5625f, 0.3125f, 0.4375f, 0.6875f, 0.8125f, 0.5625f,
|
||||
0.3125f, 0.1875f, 0.1875f, 0.8125f, 0.0625f, 0.4375f,
|
||||
0.6875f, 0.9375f, 0.9375f, 0.0625f};
|
||||
static constexpr Float kStandard16[] = {
|
||||
0.5625f, 0.5625f, 0.4375f, 0.3125f, 0.3125f, 0.625f, 0.75f, 0.4375f, 0.1875f, 0.375f, 0.625f,
|
||||
0.8125f, 0.8125f, 0.6875f, 0.6875f, 0.1875f, 0.375f, 0.875f, 0.5f, 0.0625f, 0.25f, 0.125f,
|
||||
0.125f, 0.75f, 0.0f, 0.5f, 0.9375f, 0.25f, 0.875f, 0.9375f, 0.0625f, 0.0f};
|
||||
const Float* const tables[] = {kStandard2, kStandard4, kStandard8, kStandard16};
|
||||
for (Int samples = 2, row = 0; samples <= decltype(m_dynamicParameters)::kMaxSamplePositionCount &&
|
||||
samples <= m_vulkanCaps.MaxSamples;
|
||||
samples *= 2, ++row) {
|
||||
m_dynamicParameters.SetSamplePositions(samples, tables[row]);
|
||||
}
|
||||
}
|
||||
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
|
||||
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
|
||||
@@ -1027,6 +1338,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// device reports it, so these are the exact limits the logical device can compile and run.
|
||||
m_dynamicParameters.MaxVertexImageUniforms =
|
||||
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||
// The tessellation stages are vertex-pipeline stages too, so the same feature gates them.
|
||||
m_dynamicParameters.MaxTessControlImageUniforms = m_dynamicParameters.MaxVertexImageUniforms;
|
||||
m_dynamicParameters.MaxTessEvaluationImageUniforms = m_dynamicParameters.MaxVertexImageUniforms;
|
||||
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
|
||||
? maxPerStageImageUniforms
|
||||
@@ -1167,14 +1481,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
const MG_Backend::SubgroupImplementation subgroupImplementation =
|
||||
ResolveVulkanSubgroupImplementation(m_vulkanCaps.SupportsShaderSubgroup);
|
||||
if (subgroupImplementation == MG_Backend::SubgroupImplementation::Native) {
|
||||
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
|
||||
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
|
||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
||||
} else if (subgroupImplementation == MG_Backend::SubgroupImplementation::Emulated) {
|
||||
// A device with no native subgroups (or MOBILEGL_SUBGROUP_EMULATION forcing it): the
|
||||
// advertised values describe the 32-lane virtual subgroup the compute
|
||||
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
||||
// GL requires the advertisement and the execution to agree, and on this
|
||||
@@ -1183,8 +1499,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
||||
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
||||
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
||||
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
||||
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups (%s)",
|
||||
m_vulkanCaps.SupportsShaderSubgroup ? "forced by MOBILEGL_SUBGROUP_EMULATION"
|
||||
: "no native subgroup support");
|
||||
} else {
|
||||
m_dynamicParameters.SubgroupSize = 0;
|
||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||
@@ -1214,7 +1531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case 0x1010u: // Imagination
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
|
||||
break;
|
||||
case 0x10005u: // Mesa software (lavapipe)
|
||||
case 0x10005u: // software (lavapipe)
|
||||
case 0x1AE0u: // Google (SwiftShader)
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
|
||||
break;
|
||||
@@ -1222,5 +1539,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
|
||||
break;
|
||||
}
|
||||
|
||||
// GL_EXT_fragment_shading_rate: what the renderer enabled of VK_KHR_fragment_shading_rate.
|
||||
// The attachment rate stays off (see UpdateAdvertisedExtensions), and with it every limit
|
||||
// that only describes the attachment.
|
||||
{
|
||||
const FragmentShadingRateSupport fsr =
|
||||
pVulkanRenderer != nullptr ? pVulkanRenderer->GetFragmentShadingRateSupport() : FragmentShadingRateSupport{};
|
||||
m_dynamicParameters.SupportsFragmentShadingRate = fsr.Pipeline;
|
||||
m_dynamicParameters.SupportsPrimitiveFragmentShadingRate = fsr.Pipeline && fsr.Primitive;
|
||||
m_dynamicParameters.SupportsAttachmentFragmentShadingRate = false;
|
||||
m_dynamicParameters.FragmentShadingRateNonTrivialCombiners = fsr.NonTrivialCombiners;
|
||||
m_dynamicParameters.FragmentShadingRateWithShaderDepthStencilWrites = fsr.WithShaderDepthStencilWrites;
|
||||
m_dynamicParameters.FragmentShadingRateWithSampleMask = fsr.WithSampleMask;
|
||||
m_dynamicParameters.FragmentShadingRatePrimitiveWithMultiViewport = fsr.PrimitiveWithMultipleViewports;
|
||||
m_dynamicParameters.FragmentShadingRateAttachmentWithDefaultFramebuffer = false;
|
||||
for (SizeT i = 0; i < std::size(m_dynamicParameters.FragmentShadingRateMasks); ++i) {
|
||||
m_dynamicParameters.FragmentShadingRateMasks[i] = fsr.Pipeline ? fsr.RateMasks[i] : 0;
|
||||
}
|
||||
m_dynamicParameters.MinFragmentShadingRateAttachmentTexelWidth = 0;
|
||||
m_dynamicParameters.MinFragmentShadingRateAttachmentTexelHeight = 0;
|
||||
m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelWidth = 0;
|
||||
m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelHeight = 0;
|
||||
m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelAspectRatio = 0;
|
||||
m_dynamicParameters.MaxFragmentShadingRateAttachmentLayers = 0;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "../BackendObject.h"
|
||||
#include "MagmaSession.h"
|
||||
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -26,6 +27,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
~BackendObject_DirectVulkan() override;
|
||||
|
||||
void Initialize() override;
|
||||
void BindSessionStateToThisThread() override;
|
||||
// The GPU hang watch's verdict on this session's device (VulkanRenderer::LatchIfGpuHung).
|
||||
Bool LatchIfGpuFaulted() override;
|
||||
void OnClientContextBound(Uint64 token) override;
|
||||
Bool InitWindowSurface() override;
|
||||
Bool InitCapabilities() override;
|
||||
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
|
||||
@@ -34,8 +39,22 @@ 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;
|
||||
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) override;
|
||||
// The active surface target's swapchain image ages (VulkanRenderer::CurrentDrawBufferAge).
|
||||
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
|
||||
// A glFlush that ends a shared-image producer's frame (VulkanRenderer::PublishSharedImageAccesses).
|
||||
Bool PublishSharedImageAccesses() override;
|
||||
// An EGL native fence of the session's work (VulkanRenderer::ExportNativeFence).
|
||||
Bool ExportNativeFence(int* fence) override;
|
||||
void ReleaseEGLSurface(EGLSurface surface) override;
|
||||
void ReleaseEGLResources() override;
|
||||
// The server window going and coming back: the window surface's target (VkSurfaceKHR,
|
||||
// swapchain) is destroyed and a pbuffer target of the same extent stands in for it, and back.
|
||||
Bool SuspendServerWindow(void* window) override;
|
||||
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
|
||||
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
|
||||
|
||||
const RendererInfo& GetRendererInfo() const override;
|
||||
String GetBackendAPIVersionString() const override;
|
||||
@@ -47,6 +66,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
private:
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
void OnEGLSurfaceForgotten(EGLSurface surface) override;
|
||||
// A served session keeps one renderer across all its client's surfaces and switches the
|
||||
// renderer's surface target instead (VulkanRenderer::ActivateSurfaceTarget).
|
||||
Bool UsesSurfaceTargets() const;
|
||||
static Uint64 SurfaceTargetKey(EGLSurface surface) { return reinterpret_cast<Uint64>(surface); }
|
||||
void UpdateAdvertisedExtensions();
|
||||
void UpdateDynamicBackendParameters();
|
||||
|
||||
@@ -54,6 +78,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynamicBackendParameters m_dynamicParameters;
|
||||
MG_External::VulkanCapabilities m_vulkanCaps;
|
||||
RendererInfo m_rendererInfo;
|
||||
// A served session's own copy of Magma's per-process state (MagmaSession.h), bound to its
|
||||
// apply thread. Null in the monolith and every other single-session shape.
|
||||
UniquePtr<MagmaSession> m_magmaSession;
|
||||
// The binding each client context was last made current with, for OnClientContextBound.
|
||||
UnorderedMap<EGLContext, EGLCurrentState> m_contextBindings;
|
||||
// Window surfaces whose window is gone (SuspendServerWindow), with the extent their
|
||||
// placeholder pbuffer target is built at - the window's, so the default framebuffer the
|
||||
// client knows keeps its size.
|
||||
struct SuspendedWindowSurface {
|
||||
Uint32 Width = 1;
|
||||
Uint32 Height = 1;
|
||||
};
|
||||
UnorderedMap<EGLSurface, SuspendedWindowSurface> m_suspendedWindowSurfaces;
|
||||
// The placeholder pbuffer target of a suspended surface, made active. False: it could not be built.
|
||||
Bool ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent);
|
||||
};
|
||||
|
||||
// Single-source-of-truth helpers shared with the driver POST
|
||||
@@ -70,13 +109,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
||||
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_TIMERQUERY escape hatch
|
||||
// is applied inside, so callers pass the detected device support (passing an
|
||||
// already-gated value is harmless). `shaderSubgroupSupported` is whether
|
||||
// GL_KHR_shader_subgroup is offered at all, natively or emulated.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported);
|
||||
Bool cubeMapArraySupported,
|
||||
const AdvertisedFeatureSupport& features = {});
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,32 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include "MagmaSession.h"
|
||||
#include "Renderer/VulkanRenderer.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
|
||||
// The renderer of the calling thread's Magma session (MagmaSession.h): a served session's own on
|
||||
// its apply thread, the process-wide one everywhere else. Spelled like the UniquePtr it used to
|
||||
// be so the call sites read the same.
|
||||
class RendererSlot {
|
||||
public:
|
||||
UniquePtr<VulkanRenderer>& Slot() const { return m_slot.Get(); }
|
||||
VulkanRenderer* get() const { return Slot().get(); }
|
||||
VulkanRenderer* operator->() const { return get(); }
|
||||
VulkanRenderer& operator*() const { return *get(); }
|
||||
explicit operator bool() const { return get() != nullptr; }
|
||||
RendererSlot& operator=(UniquePtr<VulkanRenderer>&& renderer) {
|
||||
Slot() = std::move(renderer);
|
||||
return *this;
|
||||
}
|
||||
void reset() { Slot().reset(); }
|
||||
friend Bool operator==(const RendererSlot& slot, std::nullptr_t) { return slot.get() == nullptr; }
|
||||
friend Bool operator!=(const RendererSlot& slot, std::nullptr_t) { return slot.get() != nullptr; }
|
||||
|
||||
private:
|
||||
SessionLocal<UniquePtr<VulkanRenderer>> m_slot;
|
||||
};
|
||||
extern RendererSlot pVulkanRenderer;
|
||||
|
||||
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
|
||||
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
||||
@@ -23,7 +45,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetRendererGeneration();
|
||||
void BumpRendererGeneration();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). Magma's StateObjectDeathOps table, installed
|
||||
// from BackendObject_DirectVulkan::Initialize() so that GetStateObjectDeathOps() is
|
||||
// non-null on every Magma role that owns a backend. See the definition for what the one
|
||||
@@ -33,7 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when the notice currently installed is this backend's. Read by MG_Test so the
|
||||
// install can be asserted without a death having to happen first.
|
||||
Bool StateObjectDeathOpsInstalled();
|
||||
#endif
|
||||
|
||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||
@@ -45,21 +65,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLfloat* value);
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLuint* value);
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void Clear(GLbitfield mask);
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count);
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount);
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
@@ -85,11 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect);
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter);
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
@@ -109,9 +114,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
|
||||
// GL fence sync objects, mapped onto the renderer's frame-serial busy
|
||||
// tracking: a fence captures the frame serial current at creation and is
|
||||
// signaled once every command recorded under that serial has completed on
|
||||
@@ -147,4 +149,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
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.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 "MagmaSession.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
struct SessionLocalEntry {
|
||||
Detail::SessionLocalConstruct construct;
|
||||
Detail::SessionLocalDestroy destroy;
|
||||
};
|
||||
|
||||
// Construct-on-first-use: SessionLocals are namespace-scope objects in several translation
|
||||
// units and register during static initialization, in no particular order.
|
||||
Vector<SessionLocalEntry>& Registry() {
|
||||
static auto* registry = new Vector<SessionLocalEntry>();
|
||||
return *registry;
|
||||
}
|
||||
|
||||
std::atomic<Uint64> g_nextSessionId{1};
|
||||
|
||||
// Leak-at-exit, like the renderer it holds (GlobalObjects.cpp): nothing may tear the
|
||||
// monolith's renderer down from a static destructor after the driver is gone.
|
||||
MagmaSession& ProcessSessionInstance() {
|
||||
static auto* session = new MagmaSession();
|
||||
return *session;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uint32 Detail::RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy) {
|
||||
auto& registry = Registry();
|
||||
registry.push_back({construct, destroy});
|
||||
return static_cast<Uint32>(registry.size() - 1);
|
||||
}
|
||||
|
||||
MagmaSession& Detail::ProcessMagmaSession() { return ProcessSessionInstance(); }
|
||||
|
||||
void* Detail::SessionLocalSlotSlow(MagmaSession& session, Uint32 index) {
|
||||
if (index >= session.m_slots.size()) session.m_slots.resize(Registry().size(), nullptr);
|
||||
void*& slot = session.m_slots[index];
|
||||
if (slot == nullptr) slot = Registry()[index].construct();
|
||||
return slot;
|
||||
}
|
||||
|
||||
MagmaSession::MagmaSession() : m_id(g_nextSessionId.fetch_add(1, std::memory_order_relaxed)) {
|
||||
// Every slot is built up front: the process-wide session is reached from whichever thread
|
||||
// the app renders on, and a lazily built slot would be a first-touch race between two.
|
||||
const auto& registry = Registry();
|
||||
m_slots.resize(registry.size(), nullptr);
|
||||
for (SizeT i = 0; i < registry.size(); ++i) m_slots[i] = registry[i].construct();
|
||||
}
|
||||
|
||||
MagmaSession::~MagmaSession() {
|
||||
MagmaSession* const previous = Detail::t_boundMagmaSession;
|
||||
Detail::t_boundMagmaSession = this;
|
||||
const auto& registry = Registry();
|
||||
for (SizeT i = m_slots.size(); i-- > 0;) {
|
||||
if (m_slots[i] != nullptr) registry[i].destroy(m_slots[i]);
|
||||
m_slots[i] = nullptr;
|
||||
}
|
||||
// Never back to this session: it is gone.
|
||||
Detail::t_boundMagmaSession = previous == this ? nullptr : previous;
|
||||
}
|
||||
|
||||
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(Detail::t_boundMagmaSession) {
|
||||
Detail::t_boundMagmaSession = session;
|
||||
}
|
||||
|
||||
MagmaSessionScope::~MagmaSessionScope() { Detail::t_boundMagmaSession = m_previous; }
|
||||
|
||||
void BindMagmaSessionToThisThread(MagmaSession* session) { Detail::t_boundMagmaSession = session; }
|
||||
|
||||
MagmaSession* BoundMagmaSession() { return Detail::t_boundMagmaSession; }
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -0,0 +1,119 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.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
|
||||
|
||||
// MAGMA'S PER-SESSION STATE. Magma was written for one renderer per process: the renderer slot,
|
||||
// the active buffer manager, the dynamic-state shadow, the render-pass manager's statics and the
|
||||
// renderer generation are all file-scope. A multi-session server (several clients served from one
|
||||
// process, each on its own apply thread with its own BackendObject_DirectVulkan) needs one copy of
|
||||
// each per session, or one session's work lands in another's tables.
|
||||
//
|
||||
// SessionLocal<T> is that copy. It resolves through the CALLING THREAD's bound MagmaSession, and to
|
||||
// the process-wide default session on every thread nothing bound - which is the monolith, the client
|
||||
// role and every unit case, i.e. exactly the old behaviour, on whatever thread the app renders from.
|
||||
// A served session's backend owns its MagmaSession and binds it on its apply thread (the only thread
|
||||
// that runs that session's backend), so the two never mix.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
class MagmaSession;
|
||||
|
||||
namespace Detail {
|
||||
using SessionLocalConstruct = void* (*)();
|
||||
using SessionLocalDestroy = void (*)(void*);
|
||||
// Registers one SessionLocal<T> and returns its slot. Called from static initialization.
|
||||
Uint32 RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy);
|
||||
// The calling thread's bound session (MagmaSessionScope, BindMagmaSessionToThisThread), null
|
||||
// when it has none. P15: visible here, and constant-initialized, so the read inlines into
|
||||
// every SessionLocal access and one function's accesses can share a single TLS address
|
||||
// computation instead of paying one out-of-line call each.
|
||||
inline constinit thread_local MagmaSession* t_boundMagmaSession = nullptr;
|
||||
MagmaSession& ProcessMagmaSession();
|
||||
// The calling thread's session, or the process-wide default.
|
||||
inline MagmaSession& CurrentMagmaSession() {
|
||||
MagmaSession* const bound = t_boundMagmaSession;
|
||||
return bound != nullptr ? *bound : ProcessMagmaSession();
|
||||
}
|
||||
inline void* SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
void* SessionLocalSlotSlow(MagmaSession& session, Uint32 index);
|
||||
} // namespace Detail
|
||||
|
||||
// One served session's copy of every SessionLocal. Slots are built lazily on first use and
|
||||
// destroyed in reverse registration order with the session still bound, so a destructor that
|
||||
// reaches another SessionLocal finds this session's copy and not the binding thread's.
|
||||
class MagmaSession {
|
||||
public:
|
||||
MagmaSession();
|
||||
~MagmaSession();
|
||||
MagmaSession(const MagmaSession&) = delete;
|
||||
MagmaSession& operator=(const MagmaSession&) = delete;
|
||||
|
||||
// A monotonic id, unique for the process lifetime. Never an address: a later session can
|
||||
// be allocated where an ended one lived.
|
||||
Uint64 Id() const { return m_id; }
|
||||
|
||||
private:
|
||||
friend void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
friend void* Detail::SessionLocalSlotSlow(MagmaSession& session, Uint32 index);
|
||||
Vector<void*> m_slots;
|
||||
Uint64 m_id = 0;
|
||||
};
|
||||
|
||||
// Makes `session` the calling thread's Magma session for the scope's lifetime, restoring the
|
||||
// previous binding (usually none) afterwards.
|
||||
// A built slot is returned in line; a missing one is built out of line.
|
||||
inline void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
|
||||
if (index < session.m_slots.size()) {
|
||||
if (void* const slot = session.m_slots[index]) return slot;
|
||||
}
|
||||
return SessionLocalSlotSlow(session, index);
|
||||
}
|
||||
|
||||
class MagmaSessionScope {
|
||||
public:
|
||||
explicit MagmaSessionScope(MagmaSession* session);
|
||||
~MagmaSessionScope();
|
||||
MagmaSessionScope(const MagmaSessionScope&) = delete;
|
||||
MagmaSessionScope& operator=(const MagmaSessionScope&) = delete;
|
||||
|
||||
private:
|
||||
MagmaSession* m_previous = nullptr;
|
||||
};
|
||||
|
||||
// Binds `session` to the calling thread until the next call (nullptr unbinds). For the apply
|
||||
// thread, which binds its backend once and runs it for the rest of its life.
|
||||
void BindMagmaSessionToThisThread(MagmaSession* session);
|
||||
// The calling thread's bound session, or nullptr (the process-wide default answers).
|
||||
MagmaSession* BoundMagmaSession();
|
||||
|
||||
template <class T>
|
||||
class SessionLocal {
|
||||
public:
|
||||
SessionLocal() : m_index(Detail::RegisterSessionLocal(&Construct, &Destroy)) {}
|
||||
SessionLocal(const SessionLocal&) = delete;
|
||||
SessionLocal& operator=(const SessionLocal&) = delete;
|
||||
|
||||
T& Get() const {
|
||||
return *static_cast<T*>(Detail::SessionLocalSlot(Detail::CurrentMagmaSession(), m_index));
|
||||
}
|
||||
T& operator*() const { return Get(); }
|
||||
T* operator->() const { return &Get(); }
|
||||
|
||||
private:
|
||||
static void* Construct() { return new T(); }
|
||||
static void Destroy(void* object) { delete static_cast<T*>(object); }
|
||||
const Uint32 m_index;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -7,9 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "BufferArena.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
|
||||
@@ -128,14 +126,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bufferDesc.usage = m_desc.usage;
|
||||
bufferDesc.memoryUsage = m_desc.memoryUsage;
|
||||
bufferDesc.allocationFlags = m_desc.allocationFlags;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Split staging may also be the destination of a GPU UBO range copy.
|
||||
// Persistent host writes must be coherent: Upload deliberately writes
|
||||
// the mapped pointer without a per-range VMA flush.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) {
|
||||
if (m_desc.persistentlyMapped) {
|
||||
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
}
|
||||
#endif
|
||||
if (!buffer.Create(bufferDesc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "FrameContext.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
|
||||
@@ -66,7 +64,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
|
||||
if (!m_spareCommandBuffers.empty())
|
||||
vkFreeCommandBuffers(device, commandPool, static_cast<Uint32>(m_spareCommandBuffers.size()),
|
||||
m_spareCommandBuffers.data());
|
||||
}
|
||||
m_spareCommandBuffers.clear();
|
||||
m_frames.clear();
|
||||
currentFrameIndex = 0;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
@@ -106,7 +108,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = flags;
|
||||
// P15: a frame recording is submitted once and then reset, so the driver may skip whatever
|
||||
// it keeps for re-submission.
|
||||
beginInfo.flags = flags | VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
beginInfo.pInheritanceInfo = pInheritanceInfo;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
|
||||
|
||||
@@ -135,6 +139,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
@@ -220,13 +225,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageMemoryBarrier presentBarrier{};
|
||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
presentBarrier.srcAccessMask = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A layout transition writes the image too. Order it after the wire
|
||||
// render pass's storeOp, clears and copies instead of relying on the old
|
||||
// per-draw queue-idle path. This applies even when contents are discarded.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
#endif
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
presentBarrier.dstAccessMask = 0;
|
||||
presentBarrier.oldLayout = oldLayout;
|
||||
presentBarrier.newLayout = presentLayout;
|
||||
@@ -239,12 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
presentBarrier.subresourceRange.baseArrayLayer = 0;
|
||||
presentBarrier.subresourceRange.layerCount = 1;
|
||||
vkCmdPipelineBarrier(commandBuffer,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#else
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#endif
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
@@ -300,6 +297,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return packet;
|
||||
}
|
||||
|
||||
VkResult FrameContext::AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain,
|
||||
Uint32& outImageIndex) {
|
||||
auto& frame = GetCurrent();
|
||||
const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore,
|
||||
VK_NULL_HANDLE, &outImageIndex);
|
||||
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) {
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
|
||||
Uint64 timeout, VkFence acquireFence) {
|
||||
auto& frame = GetCurrent();
|
||||
@@ -327,6 +336,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
// Hand the acquire's own code back so the caller can schedule a rebuild.
|
||||
return resetResult == VK_SUCCESS ? result : resetResult;
|
||||
@@ -353,21 +363,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = m_commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
||||
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
VkResult result = TakeCommandBuffer(replacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (retirePreCommandBuffer) {
|
||||
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
|
||||
result = TakeCommandBuffer(preReplacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
|
||||
RecycleCommandBuffer(replacement);
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
|
||||
@@ -402,8 +407,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SizeT completedCount = 0;
|
||||
while (completedCount < frame.retiredCommandBuffers.size() &&
|
||||
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
||||
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
RecycleCommandBuffer(frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
++completedCount;
|
||||
}
|
||||
if (completedCount > 0) {
|
||||
@@ -413,6 +417,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkResult FrameContext::TakeCommandBuffer(VkCommandBuffer& out) {
|
||||
if (!m_spareCommandBuffers.empty()) {
|
||||
out = m_spareCommandBuffers.back();
|
||||
m_spareCommandBuffers.pop_back();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = m_commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
return vkAllocateCommandBuffers(m_device, &allocInfo, &out);
|
||||
}
|
||||
|
||||
void FrameContext::RecycleCommandBuffer(VkCommandBuffer commandBuffer) {
|
||||
// A handful covers the buffers in flight between two completions; beyond that, free.
|
||||
constexpr SizeT kSpareCommandBuffers = 8;
|
||||
if (commandBuffer == VK_NULL_HANDLE) return;
|
||||
if (m_spareCommandBuffers.size() < kSpareCommandBuffers) {
|
||||
m_spareCommandBuffers.push_back(commandBuffer);
|
||||
return;
|
||||
}
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
|
||||
}
|
||||
|
||||
void FrameContext::FreeAllRetiredCommandBuffers() {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
@@ -451,6 +480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.hasCommandBufferRecorded = false;
|
||||
frame.isCommandRecording = false;
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -469,5 +499,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.isCommandRecording = false;
|
||||
frame.hasCommandBufferRecorded = false;
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -25,8 +25,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
|
||||
};
|
||||
|
||||
// The stages the acquire wait blocks. Every first access of an acquired swapchain image is
|
||||
// a layout transition out of PRESENT_SRC_KHR or UNDEFINED whose source scope includes these
|
||||
// stages (TransitionWireImage, TransitionToPresent), and that chain is what orders the
|
||||
// access after the presentation engine's release. Work at other stages, in this and later
|
||||
// submissions, does not wait for the release.
|
||||
static constexpr VkPipelineStageFlags kAcquireWaitStages =
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
|
||||
struct SubmitInfoPacket {
|
||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkPipelineStageFlags waitDstStageMask = kAcquireWaitStages;
|
||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
|
||||
// [0] = pre-pass command buffer (when recorded), then the frame
|
||||
@@ -67,6 +75,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool isPreCommandRecording = false;
|
||||
Bool hasPreCommandBufferRecorded = false;
|
||||
Bool imageAvailableSemaphoreConsumed = false;
|
||||
// The open recording references the acquired swapchain image. Only a submission that
|
||||
// does (or the present's) waits on imageAvailableSemaphore: the frame's work recorded
|
||||
// before its first use of the image goes to the queue without the wait, so it never
|
||||
// queues behind the release of an image the display still scans out.
|
||||
Bool acquiredImageReferenced = false;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||
// appended in submit order; freed once their submission is known
|
||||
// complete (fence wait or completion poll).
|
||||
@@ -101,6 +114,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout presentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
SubmitInfoPacket GetSubmitInfo(Bool shouldSubmitCommandBuffer, Uint32 swapchainImageIndex) const;
|
||||
PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const;
|
||||
// Acquires into the current slot's semaphore WITHOUT the slot-fence wait: for a surface
|
||||
// brought up mid-frame, whose slot fence this frame has already waited and reset. The
|
||||
// slot's semaphore must be unsignaled with no pending signal (consumed).
|
||||
VkResult AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex);
|
||||
// The per-swapchain-image render-finished semaphores belong to one swapchain; a renderer
|
||||
// that parks a surface parks them with it.
|
||||
void ExchangeSwapchainSemaphores(Vector<VkSemaphore>& other) { std::swap(m_swapchainImageRenderFinishedSemaphores, other); }
|
||||
VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
|
||||
Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE);
|
||||
|
||||
@@ -143,5 +163,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Stored at Initialize for retired-command-buffer management.
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
// Completed command buffers kept for the next retirement instead of being freed and
|
||||
// reallocated: the pool resets a buffer implicitly at its next vkBeginCommandBuffer.
|
||||
Vector<VkCommandBuffer> m_spareCommandBuffers;
|
||||
VkResult TakeCommandBuffer(VkCommandBuffer& out);
|
||||
void RecycleCommandBuffer(VkCommandBuffer commandBuffer);
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.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 "GpuProgressMarkers.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
namespace Watch = MG_Backend::GpuHangWatch;
|
||||
|
||||
void GpuProgressMarkers::Init(VkDevice device, Uint32 queueFamilyIndex) {
|
||||
if (m_device != VK_NULL_HANDLE || device == VK_NULL_HANDLE) return;
|
||||
if (Watch::BudgetMs() == 0) {
|
||||
MGLOG_I("Magma: GPU hang watch off (MOBILEGL_GPU_HANG_BUDGET_MS=0)");
|
||||
return;
|
||||
}
|
||||
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
poolInfo.queueFamilyIndex = queueFamilyIndex;
|
||||
if (vkCreateCommandPool(device, &poolInfo, nullptr, &m_pool) != VK_SUCCESS) {
|
||||
MGLOG_W("Magma: GPU hang watch off - no command pool for its markers");
|
||||
m_pool = VK_NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
m_device = device;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!AddSlot()) {
|
||||
MGLOG_W("Magma: GPU hang watch off - the device cannot make its progress markers");
|
||||
vkDestroyCommandPool(m_device, m_pool, nullptr);
|
||||
m_pool = VK_NULL_HANDLE;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_watch = Watch::Register(this, &Sample, &OnHung);
|
||||
MGLOG_I("Magma: GPU hang watch on - a submission the GPU started and has not finished after %u ms ends "
|
||||
"its session",
|
||||
Watch::BudgetMs());
|
||||
}
|
||||
|
||||
void GpuProgressMarkers::Shutdown() {
|
||||
// The watch first, outside m_mutex: Unregister waits out a sample in progress, which takes it.
|
||||
Watch::Unregister(m_watch);
|
||||
m_watch = 0;
|
||||
if (m_device == VK_NULL_HANDLE) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (Slot& slot : m_slots) {
|
||||
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
|
||||
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
|
||||
}
|
||||
m_slots.clear();
|
||||
if (m_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, m_pool, nullptr); // frees the buffers
|
||||
m_pool = VK_NULL_HANDLE;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Bool GpuProgressMarkers::AddSlot() {
|
||||
if (m_slots.size() >= kMaxSlots) return false;
|
||||
Slot slot;
|
||||
const VkEventCreateInfo eventInfo{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO};
|
||||
VkCommandBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
||||
VkCommandBufferAllocateInfo allocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
allocInfo.commandPool = m_pool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 2;
|
||||
const auto fail = [&] {
|
||||
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
|
||||
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
|
||||
if (buffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(m_device, m_pool, 2, buffers);
|
||||
return false;
|
||||
};
|
||||
if (vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Started) != VK_SUCCESS ||
|
||||
vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Finished) != VK_SUCCESS ||
|
||||
vkAllocateCommandBuffers(m_device, &allocInfo, buffers) != VK_SUCCESS)
|
||||
return fail();
|
||||
// Prerecorded once and resubmitted for every submission the slot brackets; SIMULTANEOUS_USE
|
||||
// because the slot is reused once its `finished` event is set, which can be a moment before
|
||||
// the submission that set it formally leaves the pending state.
|
||||
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
|
||||
if (vkBeginCommandBuffer(buffers[0], &beginInfo) != VK_SUCCESS) return fail();
|
||||
vkCmdSetEvent(buffers[0], slot.Started, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
|
||||
if (vkEndCommandBuffer(buffers[0]) != VK_SUCCESS) return fail();
|
||||
if (vkBeginCommandBuffer(buffers[1], &beginInfo) != VK_SUCCESS) return fail();
|
||||
vkCmdSetEvent(buffers[1], slot.Finished, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
|
||||
if (vkEndCommandBuffer(buffers[1]) != VK_SUCCESS) return fail();
|
||||
slot.Begin = buffers[0];
|
||||
slot.End = buffers[1];
|
||||
m_slots.push_back(slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire(Uint64 completedSubmitIndex) {
|
||||
Bracket bracket;
|
||||
if (m_device == VK_NULL_HANDLE) return bracket;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_lost) return bracket;
|
||||
Slot* free = nullptr;
|
||||
for (Slot& slot : m_slots) {
|
||||
if (slot.Claimed) continue;
|
||||
// Retire what finished since the watch last looked: frames come faster than its polls.
|
||||
if (slot.InFlight && vkGetEventStatus(m_device, slot.Finished) == VK_EVENT_SET) slot.InFlight = false;
|
||||
// Free once its submission is complete by fence, not by the event read (see the header).
|
||||
if (!slot.InFlight && slot.SubmitIndex <= completedSubmitIndex && free == nullptr) free = &slot;
|
||||
}
|
||||
if (free == nullptr) {
|
||||
if (!AddSlot()) return bracket;
|
||||
free = &m_slots.back();
|
||||
}
|
||||
if (free->Dirty) {
|
||||
if (vkResetEvent(m_device, free->Started) != VK_SUCCESS ||
|
||||
vkResetEvent(m_device, free->Finished) != VK_SUCCESS)
|
||||
return bracket;
|
||||
free->Dirty = false;
|
||||
}
|
||||
free->Claimed = true;
|
||||
bracket.Begin = free->Begin;
|
||||
bracket.End = free->End;
|
||||
bracket.Slot = static_cast<Uint32>(free - m_slots.data());
|
||||
return bracket;
|
||||
}
|
||||
|
||||
void GpuProgressMarkers::Submitted(const Bracket& bracket, Uint64 submitIndex) {
|
||||
if (!bracket.Valid()) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (bracket.Slot >= m_slots.size()) return;
|
||||
Slot& slot = m_slots[bracket.Slot];
|
||||
slot.Claimed = false;
|
||||
slot.InFlight = true;
|
||||
slot.Dirty = true;
|
||||
slot.SubmitIndex = submitIndex;
|
||||
slot.Id = m_nextId++;
|
||||
}
|
||||
|
||||
void GpuProgressMarkers::Abandon(const Bracket& bracket) {
|
||||
if (!bracket.Valid()) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (bracket.Slot >= m_slots.size()) return;
|
||||
// A failed vkQueueSubmit may still have consumed nothing or something: reset before reuse.
|
||||
m_slots[bracket.Slot].Claimed = false;
|
||||
m_slots[bracket.Slot].Dirty = true;
|
||||
}
|
||||
|
||||
Bool GpuProgressMarkers::Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out) {
|
||||
auto* markers = static_cast<GpuProgressMarkers*>(self);
|
||||
const std::lock_guard<std::mutex> lock(markers->m_mutex);
|
||||
if (markers->m_lost || markers->m_device == VK_NULL_HANDLE) return false;
|
||||
for (Slot& slot : markers->m_slots) {
|
||||
if (!slot.InFlight) continue;
|
||||
const VkResult finished = vkGetEventStatus(markers->m_device, slot.Finished);
|
||||
const VkResult started = vkGetEventStatus(markers->m_device, slot.Started);
|
||||
if ((finished != VK_EVENT_SET && finished != VK_EVENT_RESET) ||
|
||||
(started != VK_EVENT_SET && started != VK_EVENT_RESET)) {
|
||||
// VK_ERROR_DEVICE_LOST: the driver already reset the device and says so itself.
|
||||
markers->m_lost = true;
|
||||
return false;
|
||||
}
|
||||
if (finished == VK_EVENT_SET) {
|
||||
slot.InFlight = false;
|
||||
continue;
|
||||
}
|
||||
out.push_back({slot.Id, started == VK_EVENT_SET, false});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuProgressMarkers::OnHung(void* self, Uint64 submissionId, Uint64 runningMs) {
|
||||
auto* markers = static_cast<GpuProgressMarkers*>(self);
|
||||
markers->m_hungRunningMs.store(runningMs, std::memory_order_release);
|
||||
markers->m_hung.store(true, std::memory_order_release);
|
||||
MGLOG_E("Magma: submission %llu of this session's VkDevice has run %llu ms on the GPU and has not finished - "
|
||||
"it is what stops the GPU for every process; the session ends at its next record and submits "
|
||||
"nothing more (budget %u ms, MOBILEGL_GPU_HANG_BUDGET_MS)",
|
||||
static_cast<unsigned long long>(submissionId), static_cast<unsigned long long>(runningMs),
|
||||
MG_Backend::GpuHangWatch::BudgetMs());
|
||||
}
|
||||
|
||||
VkResult GpuProgressMarkers::SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence,
|
||||
const Bracket& bracket) {
|
||||
constexpr Uint32 kMaxOwn = 6;
|
||||
if (!bracket.Valid() || info.commandBufferCount == 0 || info.commandBufferCount > kMaxOwn)
|
||||
return vkQueueSubmit(queue, 1, &info, fence);
|
||||
VkCommandBuffer buffers[kMaxOwn + 2];
|
||||
Uint32 count = 0;
|
||||
buffers[count++] = bracket.Begin;
|
||||
for (Uint32 i = 0; i < info.commandBufferCount; ++i) buffers[count++] = info.pCommandBuffers[i];
|
||||
buffers[count++] = bracket.End;
|
||||
const VkCommandBuffer* own = info.pCommandBuffers;
|
||||
const Uint32 ownCount = info.commandBufferCount;
|
||||
info.pCommandBuffers = buffers;
|
||||
info.commandBufferCount = count;
|
||||
const VkResult result = vkQueueSubmit(queue, 1, &info, fence);
|
||||
info.pCommandBuffers = own;
|
||||
info.commandBufferCount = ownCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -0,0 +1,105 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.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 "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/GpuHangWatch.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// WHICH OF THIS DEVICE'S SUBMISSIONS THE GPU HAS STARTED, AND WHICH IT HAS FINISHED
|
||||
// (GpuHangWatch.h says why that is the whole attribution of a GPU hang).
|
||||
//
|
||||
// A frame's submission is bracketed by two prerecorded one-command buffers: the first sets a
|
||||
// `started` event at TOP_OF_PIPE, the last sets a `finished` event once everything before it in
|
||||
// the queue has completed. The host reads both with vkGetEventStatus from the watch thread - no
|
||||
// fence is shared with the renderer's own waits and resets. On Adreno 750 a submission whose
|
||||
// fragment shader never ends reads started=SET from its first poll until the kernel resets the
|
||||
// GPU, and a submission queued behind it reads started=RESET for the whole stall.
|
||||
//
|
||||
// The renderer's thread brackets submissions (Acquire / Submitted / Abandon) and asks Hung(); the
|
||||
// watch thread samples. Everything Vulkan the two share - the events - is touched under m_mutex.
|
||||
class GpuProgressMarkers {
|
||||
public:
|
||||
struct Bracket {
|
||||
VkCommandBuffer Begin = VK_NULL_HANDLE;
|
||||
VkCommandBuffer End = VK_NULL_HANDLE;
|
||||
Uint32 Slot = ~0u;
|
||||
Bool Valid() const { return Slot != ~0u; }
|
||||
};
|
||||
|
||||
GpuProgressMarkers() = default;
|
||||
~GpuProgressMarkers() { Shutdown(); }
|
||||
GpuProgressMarkers(const GpuProgressMarkers&) = delete;
|
||||
GpuProgressMarkers& operator=(const GpuProgressMarkers&) = delete;
|
||||
|
||||
// Off (no brackets, never hung) when the budget is 0 or the device cannot make events.
|
||||
void Init(VkDevice device, Uint32 queueFamilyIndex);
|
||||
// Before the device goes; its queue must be idle. Stops the watch first.
|
||||
void Shutdown();
|
||||
|
||||
// The two buffers to put first and last in one submission (an invalid bracket when off or
|
||||
// when every slot is in flight: the submission then goes unwatched).
|
||||
//
|
||||
// A slot is reused only once the submission it last bracketed is COMPLETE by its fence
|
||||
// (`completedSubmitIndex`, the renderer's contiguous completed-submit floor), not merely once
|
||||
// the host has read its `finished` event as set. A host read of an event gives no device-side
|
||||
// ordering: nothing orders the bracket's `started` set (TOP_OF_PIPE, first buffer) against
|
||||
// its `finished` set, so a host vkResetEvent plus a re-set in a later submission could
|
||||
// overlap a set the earlier submission has not executed yet. The synchronization validator
|
||||
// reports that as SYNC-vkCmdSetEvent-missingbarrier-set. Event reads still drive the watch.
|
||||
Bracket Acquire(Uint64 completedSubmitIndex);
|
||||
// The submission carrying `bracket` (renderer submit index `submitIndex`) was accepted by
|
||||
// vkQueueSubmit / was not submitted.
|
||||
void Submitted(const Bracket& bracket, Uint64 submitIndex);
|
||||
void Abandon(const Bracket& bracket);
|
||||
|
||||
// The watch named one of this device's submissions as hanging the GPU.
|
||||
Bool Hung() const { return m_hung.load(std::memory_order_acquire); }
|
||||
Uint64 HungRunningMs() const { return m_hungRunningMs.load(std::memory_order_acquire); }
|
||||
Bool Active() const { return m_watch != 0; }
|
||||
|
||||
// vkQueueSubmit of `info` with `bracket` wrapped around its command buffers (which it puts
|
||||
// back before returning). At most 6 buffers of the caller's.
|
||||
static VkResult SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence, const Bracket& bracket);
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
VkEvent Started = VK_NULL_HANDLE;
|
||||
VkEvent Finished = VK_NULL_HANDLE;
|
||||
VkCommandBuffer Begin = VK_NULL_HANDLE;
|
||||
VkCommandBuffer End = VK_NULL_HANDLE;
|
||||
Uint64 Id = 0;
|
||||
Bool InFlight = false; // submitted and not yet seen finished
|
||||
Bool Claimed = false; // handed out by Acquire, not yet submitted or abandoned
|
||||
Bool Dirty = false; // its events were set by a past submission: reset before reuse
|
||||
Uint64 SubmitIndex = 0; // the renderer submit index of the submission it last bracketed
|
||||
};
|
||||
static constexpr Uint32 kMaxSlots = 16;
|
||||
|
||||
Bool AddSlot();
|
||||
static Bool Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out);
|
||||
static void OnHung(void* self, Uint64 submissionId, Uint64 runningMs);
|
||||
|
||||
std::mutex m_mutex;
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkCommandPool m_pool = VK_NULL_HANDLE;
|
||||
Vector<Slot> m_slots;
|
||||
Uint64 m_nextId = 1;
|
||||
Uint64 m_watch = 0;
|
||||
Bool m_lost = false;
|
||||
std::atomic<Bool> m_hung{false};
|
||||
std::atomic<Uint64> m_hungRunningMs{0};
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -10,18 +10,17 @@
|
||||
#include <Includes.h>
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
|
||||
// header-only constant/POD declarations, and both are push-only, so the pull build's include
|
||||
// graph is unchanged (G1).
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||
// {slot, gen} mint the re-keyed sites are written against.
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14). (The {slot, gen}
|
||||
// mint that used to live here keyed the frontend arm's VAO memos and went with that arm in P13
|
||||
// W6; the record arm keys every object on the applier's own handles.)
|
||||
//
|
||||
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||
//
|
||||
@@ -44,7 +43,6 @@
|
||||
// below is behind it, and the pull build's translation units are byte-identical (G1).
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
|
||||
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
|
||||
return (MG_Config::Features.PipePush & subsystemBit) != 0;
|
||||
@@ -82,8 +80,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
|
||||
// longer does it SILENTLY: the combination is named once, at startup, right here.
|
||||
inline void MagmaPipeValidateSubsystemConfiguration() {
|
||||
if (!MG_Config::Features.PipeLegacyMemos &&
|
||||
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
|
||||
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
|
||||
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
|
||||
@@ -93,18 +90,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemRenderState));
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
||||
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
||||
if (MG_Config::Features.PipeLegacyMemos) return;
|
||||
#endif
|
||||
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
|
||||
#else
|
||||
const char* const why =
|
||||
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
|
||||
#endif
|
||||
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
|
||||
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
|
||||
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
|
||||
@@ -115,497 +103,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
||||
// site asks, so that they cannot disagree with each other or with the startup gate.
|
||||
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return MagmaPipeSubsystemOn(trackHBit);
|
||||
#else
|
||||
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
||||
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
||||
// running process can be on.
|
||||
(void)trackHBit;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
||||
// this run?" - the ONE question the control's sites ask, for the same reason
|
||||
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
||||
// and a control that defeats two of three guards proves nothing.
|
||||
//
|
||||
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
||||
// RATHER THAN "USE THE HEAP ADDRESS".
|
||||
//
|
||||
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
||||
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
||||
// the freed heap block and so reproduces the key. Measured, it does not: in
|
||||
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
||||
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
||||
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
||||
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
||||
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
||||
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
||||
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
||||
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
||||
//
|
||||
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
||||
// both arms the object identity is replaced by a constant, which is the strongest form of
|
||||
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
||||
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
||||
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
||||
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
||||
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
||||
// which is exactly the vacuity this control exists to catch.
|
||||
//
|
||||
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
||||
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
||||
//
|
||||
// * THIS MINT still has no death notification, and that is now a narrower statement than
|
||||
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
|
||||
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
|
||||
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
|
||||
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
|
||||
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
|
||||
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
|
||||
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
|
||||
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
|
||||
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
|
||||
// from that is unchanged;
|
||||
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
||||
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
||||
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
||||
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
||||
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
||||
// draw, which necessarily puts the two draws in different frames - and the only memo
|
||||
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
||||
// across frames by design. The two requirements are mutually exclusive, so the
|
||||
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
||||
//
|
||||
// The generation is covered where it IS expressible, over this mint and the claim rule
|
||||
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
||||
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
||||
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
||||
// suite; it is the only place in the tree where that deletion is caught.
|
||||
//
|
||||
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
||||
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
||||
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
||||
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
||||
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
||||
// "every safety net was switched off until something broke".
|
||||
//
|
||||
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||
//
|
||||
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||
// RETIRED says nothing about the key that phase SHIPS.
|
||||
//
|
||||
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||
// conclude the knob covers its kind:
|
||||
//
|
||||
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||
// on this tree can produce, which would be a permanently red always-on lane.
|
||||
//
|
||||
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||
return MG_Config::Features.PipeHandleAbaControl;
|
||||
}
|
||||
|
||||
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
|
||||
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
|
||||
//
|
||||
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
|
||||
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
|
||||
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
|
||||
// "covered" direction is the expensive one - a control asserting a corruption nobody can
|
||||
// produce is a permanently red always-on lane - so an undecided kind must never read true,
|
||||
// and with no `default:` there is no arm for it to read true from.
|
||||
//
|
||||
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
|
||||
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
|
||||
// asserts say so in both directions, and the file no longer compiles if the tables and this
|
||||
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
|
||||
// and could not make anything red or green.)
|
||||
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
|
||||
switch (kind) {
|
||||
// The two MagmaPipeIdentityTables really mints.
|
||||
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||
case MG_Pipe::MGPipeKind::Buffer:
|
||||
return true;
|
||||
// P4a's six object classes: still reached from their frontend objects on this
|
||||
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
|
||||
// the knob to defeat.
|
||||
case MG_Pipe::MGPipeKind::Texture:
|
||||
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||
// ...and everything else this backend does not mint a handle for.
|
||||
case MG_Pipe::MGPipeKind::None:
|
||||
case MG_Pipe::MGPipeKind::Xfb:
|
||||
case MG_Pipe::MGPipeKind::RenderStateCso:
|
||||
case MG_Pipe::MGPipeKind::Fence:
|
||||
case MG_Pipe::MGPipeKind::Query:
|
||||
case MG_Pipe::MGPipeKind::Context:
|
||||
case MG_Pipe::MGPipeKind::KindCount:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
|
||||
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
|
||||
"defeat for it");
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
|
||||
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
|
||||
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
|
||||
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
|
||||
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
|
||||
"Wiring one of them is what flips this assert, this predicate and that arm - and "
|
||||
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
|
||||
"carries the answer into the lane");
|
||||
|
||||
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||
//
|
||||
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||
|
||||
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The {slot, gen} mint
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||
// properties, and the third is the one review v2 got wrong:
|
||||
//
|
||||
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||
// never match a live object even if the allocator hands back the same heap address
|
||||
// (the ABA HandleRecycleScenario reproduces);
|
||||
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||
// no probe and no mix;
|
||||
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||
// drawn, whatever the working set size.
|
||||
//
|
||||
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
||||
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
||||
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
||||
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
||||
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
||||
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
||||
// draw. That is a worse leak than the one it was introduced to avoid.
|
||||
//
|
||||
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
||||
//
|
||||
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
||||
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
||||
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
||||
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
||||
// * OnFrameBoundary retires slots whose object has not been drawn for
|
||||
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
||||
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
||||
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
||||
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
||||
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
||||
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
||||
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
||||
// plus one map node per object EVER created, for the life of the process, on a
|
||||
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
||||
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
||||
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
||||
// price a cache miss costs - and it is charged only to objects that went idle for
|
||||
// ~1024 frames, never to a hot one.
|
||||
//
|
||||
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
||||
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
||||
// no capacity to size off a device measurement because there is no capacity; what the
|
||||
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
||||
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
||||
// draw - ROADMAP.md:7).
|
||||
//
|
||||
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
||||
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
||||
// clock, across two live contexts.
|
||||
class MagmaPipeIdentityTable {
|
||||
public:
|
||||
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||
|
||||
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
||||
// Objects currently holding a slot - the live working set this table tracks.
|
||||
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
||||
|
||||
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
||||
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
||||
// that a zero can never be minted into a slot and then indexed with.
|
||||
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
||||
// that has been handed back to the free list.
|
||||
if (lifetimeId == m_lastLifetimeId) {
|
||||
m_entries[m_lastIndex].LastUse = m_boundary;
|
||||
return m_lastHandle;
|
||||
}
|
||||
Uint32 index = 0;
|
||||
const auto it = m_index.find(lifetimeId);
|
||||
if (it != m_index.end()) {
|
||||
index = it->second;
|
||||
} else {
|
||||
index = ClaimSlot();
|
||||
m_entries[index].LifetimeId = lifetimeId;
|
||||
m_index.emplace(lifetimeId, index);
|
||||
}
|
||||
Entry& entry = m_entries[index];
|
||||
entry.LastUse = m_boundary;
|
||||
m_lastLifetimeId = lifetimeId;
|
||||
m_lastIndex = index;
|
||||
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
||||
entry.Gen};
|
||||
return m_lastHandle;
|
||||
}
|
||||
|
||||
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||
// slots' consumers use.
|
||||
void OnFrameBoundary() {
|
||||
++m_boundary;
|
||||
if ((m_boundary % kSweepInterval) != 0) return;
|
||||
SizeT retired = 0;
|
||||
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||
Entry& entry = m_entries[it->second];
|
||||
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||
entry.LifetimeId = 0;
|
||||
m_freeSlots.push_back(it->second);
|
||||
it = m_index.erase(it);
|
||||
++retired;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (retired != 0) {
|
||||
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||
// so a front memo pointing at one would still hand out a handle the consumer
|
||||
// tables would accept. Drop it.
|
||||
m_lastLifetimeId = 0;
|
||||
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||
m_kindName, retired, LiveCount(), Count());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Sweep cadence and retirement age, deliberately the same numbers
|
||||
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
||||
// retired earlier than its cache entry would mint a new handle for an object whose
|
||||
// entry is still live and still correct, which is a pure waste.
|
||||
static constexpr Uint64 kSweepInterval = 256;
|
||||
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
|
||||
struct Entry {
|
||||
Uint64 LifetimeId = 0;
|
||||
Uint64 LastUse = 0;
|
||||
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
||||
// its generation, which is what makes a memo survive a reconfiguration.
|
||||
Uint32 Gen = 0;
|
||||
};
|
||||
|
||||
Uint32 ClaimSlot() {
|
||||
while (!m_freeSlots.empty()) {
|
||||
const Uint32 index = m_freeSlots.back();
|
||||
m_freeSlots.pop_back();
|
||||
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
||||
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
||||
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
||||
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
||||
// stale handle match a live object. It costs one slot.
|
||||
if (m_entries[index].Gen == ~Uint32{0}) {
|
||||
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
||||
"retired for good; {slot, gen} stays unique",
|
||||
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||
continue;
|
||||
}
|
||||
++m_entries[index].Gen;
|
||||
return index;
|
||||
}
|
||||
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
||||
m_entries.push_back(Entry{});
|
||||
m_entries[index].Gen = 1;
|
||||
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
||||
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
||||
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
||||
//
|
||||
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
||||
// one is I, not D, because D is compiled out of every build that ships and of every
|
||||
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
||||
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
||||
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
||||
// The structure no longer has a capacity to size off it, so the number is evidence
|
||||
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
||||
// and it cannot read a line that was compiled away.
|
||||
const SizeT minted = m_entries.size();
|
||||
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||
m_kindName, minted, LiveCount());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* m_kindName = "";
|
||||
Uint64 m_boundary = 0;
|
||||
Vector<Entry> m_entries;
|
||||
Vector<Uint32> m_freeSlots;
|
||||
UnorderedMap<Uint64, Uint32> m_index;
|
||||
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
||||
// object's lifetime id is never 0.
|
||||
Uint64 m_lastLifetimeId = 0;
|
||||
Uint32 m_lastIndex = 0;
|
||||
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
||||
// table and one reclamation clock, and both consumer tables are per-instance already.
|
||||
class MagmaPipeIdentityTables {
|
||||
public:
|
||||
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
||||
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
||||
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
||||
: m_vaos.Acquire(lifetimeId);
|
||||
}
|
||||
void OnFrameBoundary() {
|
||||
m_vaos.OnFrameBoundary();
|
||||
m_buffers.OnFrameBoundary();
|
||||
}
|
||||
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
||||
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
||||
|
||||
private:
|
||||
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
||||
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
||||
};
|
||||
|
||||
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
||||
//
|
||||
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
||||
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
||||
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
||||
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||
return MG_Pipe::MGPipeHandleIsNull(handle)
|
||||
? 0u
|
||||
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||
}
|
||||
|
||||
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||
//
|
||||
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||
// it replaces gave, without the fixed capacity.
|
||||
template <typename T, Uint32 kChunkEntries = 256>
|
||||
class MagmaPipeSlotTable {
|
||||
public:
|
||||
T& operator[](Uint32 index) {
|
||||
const Uint32 chunk = index / kChunkEntries;
|
||||
while (m_chunks.size() <= chunk) {
|
||||
m_chunks.push_back(MakeUnique<Chunk>());
|
||||
}
|
||||
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
||||
}
|
||||
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
T Entries[kChunkEntries] = {};
|
||||
};
|
||||
Vector<UniquePtr<Chunk>> m_chunks;
|
||||
};
|
||||
|
||||
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
||||
// negative control that defeats it cannot drift apart between consumers - and so that the
|
||||
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
||||
// this code rather than a copy of it.
|
||||
//
|
||||
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
||||
// this object's. A slot the mint recycled for a different object comes back with a moved
|
||||
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
||||
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
||||
//
|
||||
// With negative control C on, every object collapses onto one entry and the entry is handed
|
||||
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
||||
// slot" and "the slot was reused and Gen did not move".
|
||||
//
|
||||
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
||||
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
||||
template <typename Memos, Uint32 kChunkEntries>
|
||||
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
||||
const MG_Pipe::MGPipeHandle& handle) {
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
return table[kMagmaPipeAbaControlSlotIndex];
|
||||
}
|
||||
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
||||
if (!(memos.Owner == handle)) {
|
||||
memos = Memos{};
|
||||
memos.Owner = handle;
|
||||
}
|
||||
return memos;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.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
|
||||
|
||||
// MobileGL - server-owned program input for Magma (P5f fm).
|
||||
#pragma once
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include "../DirectVulkanResourceState.h"
|
||||
@@ -12,112 +19,110 @@
|
||||
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
|
||||
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
|
||||
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A borrowed view, valid only while its frontend object or server record lives.
|
||||
// It never constructs a ProgramObject on the server. Post-link mutable values
|
||||
// come from the record tails, not from the archive's link-time defaults.
|
||||
class MagmaProgramSource {
|
||||
public:
|
||||
using Program = MG_State::GLState::ProgramObject;
|
||||
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
|
||||
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
|
||||
: m_handle(handle), m_record(&record) {
|
||||
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
|
||||
}
|
||||
|
||||
Bool IsWire() const { return m_record != nullptr; }
|
||||
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
|
||||
const Program* Frontend() const { return m_frontend; }
|
||||
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
|
||||
Uint GetExternalIndex() const { return m_handle.Slot; }
|
||||
Uint64 GetLifetimeId() const {
|
||||
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
|
||||
return (Uint64(m_handle.Gen) << 32) | m_handle.Slot;
|
||||
}
|
||||
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
|
||||
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
|
||||
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
|
||||
Bool GetLinkStatus() const { return m_record->Desc.LinkStatus != 0; }
|
||||
Bool GetSpirvStatus() const { return m_record->Desc.SpirvStatus != 0; }
|
||||
Bool PointSizeDemoted() const { return m_record->Desc.PointSizeDemoted != 0; }
|
||||
Bool GetSpirvValidationEnabled() const {
|
||||
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
|
||||
return m_record->Desc.EnableSpirvValidation != 0;
|
||||
}
|
||||
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
|
||||
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
|
||||
return m_record->Archive->Spirv.generatedSpirv;
|
||||
}
|
||||
Vector<ShaderStage> GetLinkedShaderStages() const {
|
||||
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
|
||||
Vector<ShaderStage> result;
|
||||
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
|
||||
return result;
|
||||
}
|
||||
Bool HasLinkedShaderStage(ShaderStage stage) const {
|
||||
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
|
||||
const auto& stages = m_record->Archive->LinkedStages;
|
||||
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
|
||||
}
|
||||
// The view has no lifetime of its own, so the memo lives in the server record it views
|
||||
// (MGPipeShaderCsoRecord::BackendHashMemo), never in the client's program object: an entry
|
||||
// answers only while the record's Serial and BindingsSerial are the ones it was taken at.
|
||||
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
|
||||
// A view has no lifetime in which a memo can safely persist. The server
|
||||
// factory's content cache still owns compiled programs; hash from current
|
||||
// archive + binding tails rather than borrowing the client's mutable memo.
|
||||
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
|
||||
for (const auto& entry : m_record->BackendHashMemo) {
|
||||
if (entry.Valid && entry.Key == flags && entry.Serial == m_record->Serial &&
|
||||
entry.BindingsSerial == m_record->BindingsSerial) {
|
||||
hash = entry.Hash;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
|
||||
auto& memo = m_record->BackendHashMemo;
|
||||
auto& entry = memo[m_record->BackendHashMemoNext++ % memo.size()];
|
||||
entry.Serial = m_record->Serial;
|
||||
entry.BindingsSerial = m_record->BindingsSerial;
|
||||
entry.Hash = hash;
|
||||
entry.Key = flags;
|
||||
entry.Valid = true;
|
||||
}
|
||||
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
|
||||
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
|
||||
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
|
||||
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
|
||||
Uint64 GetBackendStateVersion() const { return m_record->Serial; }
|
||||
Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; }
|
||||
Uint64 GetImageUnitVersion() const { return m_record->BindingsSerial; }
|
||||
Uint64 GetLinkVersion() const { return m_record->Serial; }
|
||||
|
||||
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
|
||||
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
|
||||
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
|
||||
const void* GetUBOData() const { return m_record->GlobalConstants.data(); }
|
||||
Uint GetUBOSize() const { return static_cast<Uint>(m_record->GlobalConstants.size()); }
|
||||
Uint32 GetUBOContentVersion() const { return m_record->GlobalConstantsVersion; }
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
|
||||
return static_cast<Int>(Link().glBlockIndexToTProgram.size());
|
||||
}
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
|
||||
auto found = Link().uniformBlockIndexByName.find(name);
|
||||
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
|
||||
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
|
||||
}
|
||||
Uint GetUniformBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
|
||||
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
|
||||
}
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
|
||||
const auto* block = Block(index);
|
||||
static const String empty;
|
||||
return block ? block->name : empty;
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
|
||||
const auto* block = Block(index);
|
||||
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
|
||||
}
|
||||
Bool IsValidUniformLocation(Int location) const {
|
||||
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
|
||||
return Program::IsValidUniformLocation(Link(), location);
|
||||
}
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
|
||||
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
|
||||
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
|
||||
}
|
||||
GLenum GetUniformType(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformType(location);
|
||||
return IsValidUniformLocation(static_cast<Int>(location))
|
||||
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
|
||||
}
|
||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
|
||||
const auto& units = m_record->SamplerUnits;
|
||||
const auto found = std::lower_bound(units.begin(), units.end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
|
||||
return found != units.end() && found->Location == location ? found->Unit : -1;
|
||||
}
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformLocation(name);
|
||||
const auto& locations = Link().uniformLocations;
|
||||
auto found = locations.find(name);
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
@@ -145,17 +150,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
|
||||
}
|
||||
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return Link().xfbVaryings.size(); }
|
||||
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
|
||||
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
|
||||
return Link().xfbVaryings;
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
|
||||
SizeT GetTransformFeedbackBufferCount() const { return Link().xfbStrides.size(); }
|
||||
Uint32 GetTransformFeedbackStride(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
|
||||
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const {
|
||||
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
|
||||
return Link().xfbBufferMode;
|
||||
}
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
|
||||
//
|
||||
@@ -167,7 +171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
|
||||
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
|
||||
Uint GetShaderStorageBlockIndex(const String& name) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
// The archive carries NORMALISED names (the array subscript stripped), because
|
||||
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
|
||||
@@ -188,7 +191,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
: static_cast<Uint>(found - blocks.begin());
|
||||
}
|
||||
Uint GetShaderStorageBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
if (index >= blocks.size()) return 0;
|
||||
const auto& block = blocks[index];
|
||||
@@ -210,7 +212,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
|
||||
? &Link().blockReflection[tIndex] : nullptr;
|
||||
}
|
||||
const Program* m_frontend = nullptr;
|
||||
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
|
||||
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
|
||||
@@ -218,7 +219,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// also why they never helped: a view lives for one draw, so the latch was cold every
|
||||
// time it was read.
|
||||
};
|
||||
#else
|
||||
using MagmaProgramSource = MG_State::GLState::ProgramObject;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.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
|
||||
|
||||
// How an offscreen (pbuffer) surface target gets its VkSurfaceKHR on Android, decided from what the
|
||||
// INSTANCE has, not from what the loader could offer. A renderer created for a window enabled
|
||||
// VK_KHR_android_surface and nothing headless, so its later offscreen targets (a second client
|
||||
// surface, or the placeholder a suspended server window draws into) must take the AImageReader
|
||||
// route; asking that instance for vkCreateHeadlessSurfaceEXT answers null. Pure, so a host case pins
|
||||
// it without a driver.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
enum class OffscreenSurfaceRoute : Uint8 {
|
||||
Headless, // vkCreateHeadlessSurfaceEXT
|
||||
AndroidImageReader, // vkCreateAndroidSurfaceKHR on an AImageReader's window nobody displays
|
||||
Unavailable, // neither: the target cannot be built (the caller refuses, by name)
|
||||
};
|
||||
|
||||
// `headlessEnabled`: the instance was created with VK_EXT_headless_surface.
|
||||
// `headlessEntryPoint`: vkGetInstanceProcAddr answered vkCreateHeadlessSurfaceEXT.
|
||||
// `androidSurfaceEnabled`: the instance was created with VK_KHR_android_surface.
|
||||
constexpr OffscreenSurfaceRoute ChooseOffscreenSurfaceRoute(Bool headlessEnabled, Bool headlessEntryPoint,
|
||||
Bool androidSurfaceEnabled) {
|
||||
if (headlessEnabled && headlessEntryPoint) return OffscreenSurfaceRoute::Headless;
|
||||
if (androidSurfaceEnabled) return OffscreenSurfaceRoute::AndroidImageReader;
|
||||
return OffscreenSurfaceRoute::Unavailable;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -194,69 +194,72 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
|
||||
XXH64_state_t* const hashState = m_hashState;
|
||||
XXHASH_VERIFY(XXH64_reset(hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
if (payload.wireRenderPassCompatibilityId != 0) {
|
||||
constexpr Uint64 wireDomain = 0x5749524552504b59ull;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId,
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &wireDomain, sizeof(wireDomain)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.wireRenderPassCompatibilityId,
|
||||
sizeof(payload.wireRenderPassCompatibilityId)));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
|
||||
XXH64_update(hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.nonLayeredStages, sizeof(payload.nonLayeredStages)));
|
||||
const Bool fullCoverageSampleMask = payload.fullCoverageFragmentModule != VK_NULL_HANDLE;
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &fullCoverageSampleMask, sizeof(fullCoverageSampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXH64_update(hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXH64_update(hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXH64_update(hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXH64_update(hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXH64_update(hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXH64_update(hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
XXH64_update(hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
if (payload.colorAttachmentCount > 0) {
|
||||
XXHASH_VERIFY(XXH64_update(
|
||||
m_hashState,
|
||||
hashState,
|
||||
payload.colorBlendAttachments.data(),
|
||||
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(hashState);
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
|
||||
@@ -286,12 +289,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Wire pipelines outlive their creator pass and
|
||||
// may serve another compatible pass in flight.
|
||||
// Never evict them by a recycled native handle.
|
||||
payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE :
|
||||
#endif
|
||||
payload.renderPass,
|
||||
m_frameCounter});
|
||||
return pipeline;
|
||||
@@ -341,34 +342,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (renderPasses.empty() || m_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
||||
// per dying pass.
|
||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||
evicted, sortedPasses.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
@@ -391,6 +364,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
||||
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
||||
// The assert compiles out of shipping builds, and a program declined for an invalid
|
||||
// module (VkProgramObject::invalidSpirvDeclined) has no stages at all: a caller that
|
||||
// forgot to refuse it must get a skipped draw, not a stageCount=0 pipeline.
|
||||
if (payload.stages == nullptr || payload.stages->empty()) {
|
||||
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with no shader stages "
|
||||
"(programHash=0x%llx); its draws are skipped",
|
||||
static_cast<unsigned long long>(payload.programHash));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
||||
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
|
||||
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
|
||||
@@ -403,6 +385,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
payload.colorAttachmentCount,
|
||||
payload.subpass);
|
||||
|
||||
// The fragment shading rate goes last so the common prefix is the eight-state list every
|
||||
// pipeline shares; it is only present when the device enabled the pipeline rate.
|
||||
static constexpr VkDynamicState kDynamicStates[] = {
|
||||
VK_DYNAMIC_STATE_VIEWPORT,
|
||||
VK_DYNAMIC_STATE_SCISSOR,
|
||||
@@ -411,12 +395,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_DYNAMIC_STATE_LINE_WIDTH,
|
||||
VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK,
|
||||
VK_DYNAMIC_STATE_STENCIL_WRITE_MASK,
|
||||
VK_DYNAMIC_STATE_STENCIL_REFERENCE
|
||||
VK_DYNAMIC_STATE_STENCIL_REFERENCE,
|
||||
VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR
|
||||
};
|
||||
|
||||
VkPipelineDynamicStateCreateInfo dynamicState{};
|
||||
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
|
||||
dynamicState.dynamicStateCount = static_cast<uint32_t>(std::size(kDynamicStates));
|
||||
dynamicState.dynamicStateCount =
|
||||
static_cast<uint32_t>(std::size(kDynamicStates)) - (payload.fragmentShadingRateDynamic ? 0u : 1u);
|
||||
dynamicState.pDynamicStates = kDynamicStates;
|
||||
|
||||
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
@@ -523,11 +509,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
|
||||
// takes: the draw is skipped, nothing is memoised, and the process survives.
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||
stagesWithPassthrough = *payload.stages;
|
||||
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
|
||||
effectiveStages = &stagesWithPassthrough;
|
||||
Vector<VkPipelineShaderStageCreateInfo> adjustedStages;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE ||
|
||||
payload.fullCoverageFragmentModule != VK_NULL_HANDLE) {
|
||||
adjustedStages = *payload.stages;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||
adjustedStages.push_back(payload.passthroughTessControlStage);
|
||||
}
|
||||
if (payload.fullCoverageFragmentModule != VK_NULL_HANDLE) {
|
||||
for (auto& stageInfo : adjustedStages) {
|
||||
if (stageInfo.stage == VK_SHADER_STAGE_FRAGMENT_BIT) {
|
||||
stageInfo.module = payload.fullCoverageFragmentModule;
|
||||
}
|
||||
}
|
||||
}
|
||||
effectiveStages = &adjustedStages;
|
||||
}
|
||||
// A static rasterizer discard leaves the pipeline without fragment shader state, and then
|
||||
// pStages may not name a fragment stage (VUID-VkGraphicsPipelineCreateInfo-pStages-06894).
|
||||
// Nothing would run it anyway.
|
||||
if (payload.rasterizerDiscardEnable) {
|
||||
if (effectiveStages != &adjustedStages) adjustedStages = *effectiveStages;
|
||||
std::erase_if(adjustedStages, [](const VkPipelineShaderStageCreateInfo& stageInfo) {
|
||||
return stageInfo.stage == VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
});
|
||||
effectiveStages = &adjustedStages;
|
||||
}
|
||||
{
|
||||
VkShaderStageFlags stagesPresent = 0;
|
||||
|
||||
@@ -35,11 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
HashType vertexInputHash = 0;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Nonzero is an exact, renderer-lifetime compatibility identity.
|
||||
// renderPass remains the valid creation handle, not the cache key.
|
||||
Uint64 wireRenderPassCompatibilityId = 0;
|
||||
#endif
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
@@ -83,6 +81,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
// `stages` is the program's nonLayeredStages: the gl_Layer-zeroed modules a draw into
|
||||
// a non-layered framebuffer uses (ZeroLayerOutputPass). Hashed - the two stage sets
|
||||
// are different pipelines; the program hash alone names both.
|
||||
Bool nonLayeredStages = false;
|
||||
// Non-null: the fragment stage in `stages` is replaced by this module, the program's
|
||||
// full-coverage gl_SampleMask variant (VkProgramObject::fullCoverageFragmentModule),
|
||||
// because this draw has no multisample fragment operations. Hashed as presence only -
|
||||
// the module is a pure function of the program, which programHash already names.
|
||||
VkShaderModule fullCoverageFragmentModule = VK_NULL_HANDLE;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
@@ -110,6 +117,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
|
||||
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// The device enabled VK_KHR_fragment_shading_rate's pipeline rate, so the pipeline takes
|
||||
// VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR and every draw sets glShadingRateEXT's rate
|
||||
// and combiner ops on the command buffer. NOT hashed: it is a device constant, the same
|
||||
// for every pipeline this renderer builds.
|
||||
Bool fragmentShadingRateDynamic = false;
|
||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||
// The tessellation control stage this renderer synthesized for a program that has
|
||||
@@ -142,15 +154,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
||||
Uint32 OnFrameBoundary();
|
||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
||||
// Destroys every cached pipeline built from the program with content hash
|
||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
||||
@@ -199,7 +202,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -1313,6 +1315,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
});
|
||||
if (!ok) return false;
|
||||
// An access chain into the builtin (gl_FragCoord.y is one) carries an Input
|
||||
// pointer type. Based on the Private copy it has to carry a Private one, and so
|
||||
// does every chain built on top of it - otherwise the module hands the driver a
|
||||
// pointer whose storage class disagrees with its base, which is invalid SPIR-V
|
||||
// and reads an undefined value. Plan every retype before touching anything, so a
|
||||
// shape this pass does not model declines the whole rewrite.
|
||||
Vector<Pair<Instruction*, Uint32>> retypes;
|
||||
Vector<Instruction*> chains;
|
||||
for (Instruction* user : users) {
|
||||
if (IsAccessChain(user->opcode())) chains.push_back(user);
|
||||
}
|
||||
while (!chains.empty()) {
|
||||
Instruction* chain = chains.back();
|
||||
chains.pop_back();
|
||||
const Uint32 privateType = PrivatePointerTypeFor(chain->type_id());
|
||||
if (privateType == 0) return false;
|
||||
retypes.emplace_back(chain, privateType);
|
||||
context()->get_def_use_mgr()->ForEachUser(chain, [&](Instruction* next) {
|
||||
if (IsAccessChain(next->opcode()) && next->NumInOperands() > 0 &&
|
||||
next->GetSingleWordInOperand(0) == chain->result_id()) {
|
||||
chains.push_back(next);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Instruction* user : users) {
|
||||
for (Uint32 i = 0; i < user->NumInOperands(); ++i) {
|
||||
auto& operand = user->GetInOperand(i);
|
||||
@@ -1323,9 +1349,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
context()->AnalyzeUses(user);
|
||||
}
|
||||
for (auto& [chain, privateType] : retypes) {
|
||||
chain->SetResultType(privateType);
|
||||
context()->AnalyzeUses(chain);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsAccessChain(spv::Op opcode) {
|
||||
return opcode == spv::Op::OpAccessChain || opcode == spv::Op::OpInBoundsAccessChain ||
|
||||
opcode == spv::Op::OpPtrAccessChain || opcode == spv::Op::OpInBoundsPtrAccessChain;
|
||||
}
|
||||
|
||||
// The Private-storage twin of an Input pointer type, or 0 when `pointerTypeId` is not
|
||||
// an Input pointer (then the chain is not the shape this pass rewrites).
|
||||
Uint32 PrivatePointerTypeFor(Uint32 pointerTypeId) {
|
||||
using namespace spvtools::opt;
|
||||
Instruction* pointerType = context()->get_def_use_mgr()->GetDef(pointerTypeId);
|
||||
if (!pointerType || pointerType->opcode() != spv::Op::OpTypePointer) return 0;
|
||||
if (static_cast<spv::StorageClass>(pointerType->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
return 0;
|
||||
}
|
||||
return context()->get_type_mgr()->FindPointerToType(pointerType->GetSingleWordInOperand(1),
|
||||
spv::StorageClass::Private);
|
||||
}
|
||||
|
||||
Uint32 m_framebufferHeight = 0;
|
||||
};
|
||||
|
||||
@@ -1861,35 +1910,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||
}
|
||||
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
Uint32 framebufferHeight) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
if (framebufferHeight == 0) {
|
||||
output = input;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(
|
||||
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
Uint32 framebufferHeight) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
if (framebufferHeight == 0) {
|
||||
output = input;
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(
|
||||
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
const MagmaProgramSource& program) {
|
||||
if (input.empty()) {
|
||||
@@ -2183,28 +2235,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureTarget ReflectImageTraitsToTextureTarget(const SpvReflectImageTraits& imageTraits) {
|
||||
switch (imageTraits.dim) {
|
||||
case SpvDim1D:
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture1DArray : TextureTarget::Texture1D;
|
||||
case SpvDim2D:
|
||||
if (imageTraits.ms != 0) {
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DMultisampleArray
|
||||
: TextureTarget::Texture2DMultisample;
|
||||
}
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DArray : TextureTarget::Texture2D;
|
||||
case SpvDim3D:
|
||||
return TextureTarget::Texture3D;
|
||||
case SpvDimCube:
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::TextureCubeMapArray : TextureTarget::TextureCubeMap;
|
||||
case SpvDimBuffer:
|
||||
return TextureTarget::TextureBuffer;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ProgramFactory: unsupported sampler image dim %d", imageTraits.dim);
|
||||
return TextureTarget::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
|
||||
switch (uniformType) {
|
||||
case GL_IMAGE_1D:
|
||||
@@ -2223,8 +2253,84 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ScreenModuleForDriver's memory. Keyed by content, never by program: the same bytes get
|
||||
// the same answer whichever program, variant or session carries them.
|
||||
struct DriverModuleVerdictCache {
|
||||
struct Entry {
|
||||
Uint64 wordCount = 0;
|
||||
Bool valid = true;
|
||||
String reason;
|
||||
};
|
||||
// Generous for any one application's distinct stage modules; past it the whole table
|
||||
// is dropped rather than tracked per entry - a re-validation is the only cost.
|
||||
static constexpr SizeT kMaxEntries = 8192;
|
||||
std::mutex mutex;
|
||||
UnorderedMap<Uint64, Entry> entries;
|
||||
std::atomic<Uint64> validatorRuns{0};
|
||||
};
|
||||
|
||||
DriverModuleVerdictCache& GetDriverModuleVerdictCache() {
|
||||
// Leaked on purpose: screened from session threads that may outlive static destruction.
|
||||
static auto* cache = new DriverModuleVerdictCache();
|
||||
return *cache;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ProgramFactory::DriverModuleVerdict ProgramFactory::ScreenModuleForDriver(const Vector<Uint>& spirv,
|
||||
Bool allowInvalid, String* outReason) {
|
||||
if (outReason != nullptr) outReason->clear();
|
||||
if (spirv.empty()) return DriverModuleVerdict::Valid;
|
||||
|
||||
auto& cache = GetDriverModuleVerdictCache();
|
||||
const Uint64 key = XXH64(spirv.data(), spirv.size() * sizeof(Uint), 0);
|
||||
Bool valid = true;
|
||||
String reason;
|
||||
Bool known = false;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
const auto it = cache.entries.find(key);
|
||||
if (it != cache.entries.end() && it->second.wordCount == spirv.size()) {
|
||||
known = true;
|
||||
valid = it->second.valid;
|
||||
reason = it->second.reason;
|
||||
}
|
||||
}
|
||||
if (!known) {
|
||||
// Outside the lock: two threads screening the same new module both validate, and
|
||||
// agree; nothing waits on another session's spirv-val.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
cache.validatorRuns.fetch_add(1, std::memory_order_relaxed);
|
||||
spv_context context = spvContextCreate(GetSpirvTargetEnv(spirv));
|
||||
spv_validator_options options = spvValidatorOptionsCreate();
|
||||
spvValidatorOptionsSetFriendlyNames(options, true);
|
||||
spv_const_binary_t binary = {spirv.data(), spirv.size()};
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
valid = result == SPV_SUCCESS;
|
||||
if (!valid) {
|
||||
reason = diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<no diagnostic>";
|
||||
// The process-wide counter the validating test lanes assert on.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
}
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
spvValidatorOptionsDestroy(options);
|
||||
spvContextDestroy(context);
|
||||
|
||||
const std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
if (cache.entries.size() >= DriverModuleVerdictCache::kMaxEntries) cache.entries.clear();
|
||||
cache.entries[key] = DriverModuleVerdictCache::Entry{spirv.size(), valid, reason};
|
||||
}
|
||||
|
||||
if (valid) return DriverModuleVerdict::Valid;
|
||||
if (outReason != nullptr) *outReason = Move(reason);
|
||||
return allowInvalid ? DriverModuleVerdict::AdmittedInvalid : DriverModuleVerdict::DeclinedInvalid;
|
||||
}
|
||||
|
||||
Uint64 ProgramFactory::DriverModuleValidatorRuns() {
|
||||
return GetDriverModuleVerdictCache().validatorRuns.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
|
||||
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
|
||||
// position math between pipelines cannot desynchronize it; the blended depth-write
|
||||
@@ -2401,6 +2507,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return SamplerNumericDomain::UnsignedInteger;
|
||||
case GL_SAMPLER_1D:
|
||||
case GL_SAMPLER_2D:
|
||||
case GL_SAMPLER_EXTERNAL_OES:
|
||||
case GL_SAMPLER_3D:
|
||||
case GL_SAMPLER_CUBE:
|
||||
case GL_SAMPLER_2D_RECT:
|
||||
@@ -2468,26 +2575,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MagmaProgramSource& program,
|
||||
CompileOptionFlags flags) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
|
||||
XXH64_state_t* const hashState = m_hashState;
|
||||
XXHASH_VERIFY(XXH64_reset(hashState, m_config.CacheVersion));
|
||||
// We expect shader stages in program object are sorted
|
||||
const auto& spirvs = program.GetGeneratedSpirv();
|
||||
for (const auto& spv : spirvs) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &flags, sizeof(CompileOptionFlags)));
|
||||
// Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would
|
||||
// re-key every program in the cache on a resize for no reason.
|
||||
if (flags & CompileOptionBit::FragCoordYFlip) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &m_defaultFramebufferHeight,
|
||||
sizeof(m_defaultFramebufferHeight)));
|
||||
}
|
||||
|
||||
// Include UBO block bindings in hash so different binding configurations produce different entries
|
||||
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &blockCount, sizeof(blockCount)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &blockCount, sizeof(blockCount)));
|
||||
for (Uint32 i = 0; i < blockCount; ++i) {
|
||||
const Uint32 binding = program.GetUniformBlockBinding(i);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &binding, sizeof(binding)));
|
||||
}
|
||||
|
||||
// The transform feedback capture layout is baked into the modules by
|
||||
@@ -2498,18 +2607,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// hashed for a capturing compile, so nothing else changes key.
|
||||
if (flags & CompileOptionBit::XfbCapture) {
|
||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
}
|
||||
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &stride, sizeof(stride)));
|
||||
}
|
||||
}
|
||||
|
||||
HashType hash = XXH64_digest(m_hashState);
|
||||
HashType hash = XXH64_digest(hashState);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -2591,6 +2700,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY:
|
||||
return TextureTarget::TextureCubeMapArray;
|
||||
case GL_SAMPLER_2D:
|
||||
case GL_SAMPLER_EXTERNAL_OES: // a 2D texture below the frontend
|
||||
case GL_SAMPLER_2D_SHADOW:
|
||||
case GL_INT_SAMPLER_2D:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D:
|
||||
@@ -3069,11 +3179,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
if (descriptorKind == DescriptorBindingKind::StorageBuffer) {
|
||||
const GLuint blockIndex =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
program.GetShaderStorageBlockIndex(uniformName);
|
||||
#else
|
||||
GetShaderStorageBlockIndex(program, uniformName);
|
||||
#endif
|
||||
if (blockIndex == GL_INVALID_INDEX) {
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u",
|
||||
uniformName.c_str(), binding);
|
||||
@@ -3365,13 +3471,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
updateAfterBindSampledImages + updateAfterBindStorageImages;
|
||||
const auto& uab = m_updateAfterBindLimits;
|
||||
entry.usesUpdateAfterBind =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Vulkan forbids ANY update-after-bind binding in a set layout that
|
||||
// contains a dynamic buffer descriptor (VUID 03001/03011). Wire draws
|
||||
// retain UniformManager's per-frame, content-versioned descriptor sets;
|
||||
// this selects the legal ordinary pool, not in-place mutation of a live set.
|
||||
(!program.IsWire() || entry.dynamicBindings.empty()) &&
|
||||
#endif
|
||||
entry.dynamicBindings.empty() &&
|
||||
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||
@@ -3571,6 +3675,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan forbids the PixelCenterInteger execution mode that
|
||||
// `layout(pixel_center_integer) in vec4 gl_FragCoord;` compiles to, so a module
|
||||
// carrying it would fail validation and every draw of the program be refused. This
|
||||
// runs before the FragCoordYFlip variant: the flip then rewrites the unshifted builtin
|
||||
// read and the -0.5 lands on the flipped value, as GL defines it.
|
||||
if (stages[i] == ShaderStage::Fragment) {
|
||||
Vector<Uint> centeredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerPixelCenterIntegerForVulkan(
|
||||
moduleSpirvs[i], centeredSpirv, enableSpirvValidation) &&
|
||||
!centeredSpirv.empty()) {
|
||||
moduleSpirvs[i] = Move(centeredSpirv);
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
|
||||
Vector<Uint> fragCoordSpirv;
|
||||
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
||||
@@ -3580,8 +3698,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
||||
// operations execute natively; module repairs keep the GL contract intact
|
||||
// around them. The opt-in emulation path replaces them only on devices with no
|
||||
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
||||
// around them. The emulation path replaces them on devices with no subgroup
|
||||
// support at all, or when MOBILEGL_SUBGROUP_EMULATION forces it.
|
||||
if (stages[i] == ShaderStage::Compute) {
|
||||
// Program 203 broadcasts the first reduction through
|
||||
// prefixSumCache[0], then lets the second reduction overwrite that
|
||||
@@ -3764,27 +3882,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
||||
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
||||
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only in
|
||||
// DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed, and for every stage BEFORE any
|
||||
// module is created. By here the descriptor bindings have been remapped and the layout
|
||||
// about to be reflected describes the remapped module, so there is no module left that
|
||||
// is both valid and consistent with it to fall back to (the recovery that exists lives
|
||||
// one step earlier, at the clip/XFB fixups). What is left is to keep the bytes away from
|
||||
// the driver: a driver is not a validating entry point, and an invalid module it accepts
|
||||
// can fault the GPU into device loss (a frontend translation once put a sampler inside
|
||||
// the default uniform block, and Adreno page-faulted reading its "descriptor" out of
|
||||
// uniform-buffer bytes). One invalid stage declines the whole program: no shader module
|
||||
// of it is created, and every draw or dispatch of it is refused by name
|
||||
// (VkProgramObject::invalidSpirvDeclined). MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV hands the
|
||||
// bytes over anyway, for diagnosing a driver that copes.
|
||||
for (SizeT i = 0; i < moduleCount; ++i) {
|
||||
String reason;
|
||||
const DriverModuleVerdict verdict = ScreenModuleForDriver(
|
||||
moduleSpirvs[i], MG_Config::Features.MagmaAllowInvalidSpirv, &reason);
|
||||
if (verdict == DriverModuleVerdict::DeclinedInvalid) {
|
||||
MGLOG_E_ONCE("ProgramFactory: declining program %u - its stage %d module fails SPIR-V validation (%s); "
|
||||
"no shader module is created and its draws are refused. "
|
||||
"MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 hands such modules to the driver anyway.",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
|
||||
entry.invalidSpirvDeclined = true;
|
||||
break;
|
||||
}
|
||||
if (verdict == DriverModuleVerdict::AdmittedInvalid) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d "
|
||||
"because MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV is set (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
for (SizeT i = 0; i < moduleCount && !entry.invalidSpirvDeclined; ++i) {
|
||||
auto& moduleSpv = moduleSpirvs[i];
|
||||
if (moduleSpv.empty()) continue;
|
||||
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only
|
||||
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
|
||||
// by here the descriptor bindings have been remapped and the layout about to be
|
||||
// reflected describes the remapped module, so there is no module left that is both
|
||||
// valid and consistent with it to fall back to. The recovery lives one step earlier,
|
||||
// at the clip/XFB fixups (see the revert there) - which is where a transform can
|
||||
// introduce a reference to a delisted interface variable, the failure this whole
|
||||
// guard exists for. Anything that reaches this line names itself in the log of a
|
||||
// shipping build instead of dying anonymously inside the driver.
|
||||
SpirvValidationFailure finalFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
|
||||
"a backend transform after the clip/XFB fixups broke it (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
finalFailure.message.c_str());
|
||||
}
|
||||
|
||||
// Does the stage the driver will treat as the last pre-rasterization one actually
|
||||
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
|
||||
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
|
||||
@@ -3865,6 +3998,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{
|
||||
static_cast<Uint32>(stage.stage), static_cast<Uint32>(moduleSpv.size()),
|
||||
XXH64(moduleSpv.data(), moduleSpv.size() * sizeof(Uint), 0)});
|
||||
|
||||
// GL ignores gl_Layer when the framebuffer is not layered; Vulkan leaves a Layer at or
|
||||
// past the framebuffer's layer count undefined. A stage that stores it gets a second
|
||||
// module storing 0 for draws into a non-layered framebuffer (ZeroLayerOutputPass).
|
||||
VkPipelineShaderStageCreateInfo nonLayeredStage = stage;
|
||||
if (shaderStage == ShaderStage::Vertex || shaderStage == ShaderStage::TessEval ||
|
||||
shaderStage == ShaderStage::Geometry) {
|
||||
Vector<Uint> zeroLayerSpv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroLayerOutputForVulkan(moduleSpv, zeroLayerSpv,
|
||||
enableSpirvValidation) &&
|
||||
!zeroLayerSpv.empty() && zeroLayerSpv != moduleSpv) {
|
||||
VkShaderModuleCreateInfo zeroLayerInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
zeroLayerInfo.codeSize = zeroLayerSpv.size() * sizeof(Uint);
|
||||
zeroLayerInfo.pCode = zeroLayerSpv.data();
|
||||
VkShaderModule zeroLayerModule = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &zeroLayerInfo, nullptr, &zeroLayerModule),
|
||||
"vkCreateShaderModule(zero gl_Layer)");
|
||||
entry.modules.push_back(zeroLayerModule);
|
||||
nonLayeredStage.module = zeroLayerModule;
|
||||
// Back-fill: every stage before this one is shared with `stages`.
|
||||
if (entry.nonLayeredStages.empty()) {
|
||||
entry.nonLayeredStages.assign(entry.stages.begin(), entry.stages.end() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!entry.nonLayeredStages.empty()) entry.nonLayeredStages.push_back(nonLayeredStage);
|
||||
|
||||
// GL ignores gl_SampleMask without multisample fragment operations; Vulkan discards
|
||||
// a fragment whose one-sample mask has bit 0 clear. A fragment stage that stores it
|
||||
// gets a second module storing all-ones for those draws (FullCoverageSampleMaskPass).
|
||||
if (shaderStage == ShaderStage::Fragment) {
|
||||
Vector<Uint> fullCoverageSpv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FullCoverageSampleMaskForVulkan(
|
||||
moduleSpv, fullCoverageSpv, enableSpirvValidation) &&
|
||||
!fullCoverageSpv.empty() && fullCoverageSpv != moduleSpv) {
|
||||
VkShaderModuleCreateInfo fullCoverageInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
fullCoverageInfo.codeSize = fullCoverageSpv.size() * sizeof(Uint);
|
||||
fullCoverageInfo.pCode = fullCoverageSpv.data();
|
||||
VkShaderModule fullCoverageModule = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &fullCoverageInfo, nullptr, &fullCoverageModule),
|
||||
"vkCreateShaderModule(full-coverage gl_SampleMask)");
|
||||
entry.modules.push_back(fullCoverageModule);
|
||||
entry.fullCoverageFragmentModule = fullCoverageModule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reflect and create layout as part of the program object
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "../MagmaSession.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
@@ -74,6 +75,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ZeroBaseVertex = 1 << 8,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
|
||||
// glClipControl folded into the position fixup's two bits. An upper-left origin negates
|
||||
// clip-space Y on top of whatever the framebuffer needs (the fixup flips before it rotates,
|
||||
// so this stays in the logical orientation), and inverts the cull sense with it, which
|
||||
// keeps facing a property of the clip-space winding as the extension requires. A
|
||||
// [0, 1] depth mode is Vulkan's own clip volume, so the remap is dropped. gl_FragCoord is
|
||||
// a window coordinate, which clip control does not move.
|
||||
static constexpr Uint32 ApplyClipControlToTransformFlags(Uint32 flags, GLenum origin, GLenum depthMode) {
|
||||
if (origin == GL_UPPER_LEFT) flags ^= static_cast<Uint32>(CompileOptionBit::PositionYFlip);
|
||||
if (depthMode == GL_ZERO_TO_ONE) flags &= ~static_cast<Uint32>(CompileOptionBit::PositionZRemap);
|
||||
return flags;
|
||||
}
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
@@ -115,6 +128,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
HashType hash = 0;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stages;
|
||||
// `stages` for a draw into a NON-LAYERED framebuffer: the same list, except that every
|
||||
// module storing gl_Layer is replaced by its ZeroLayerOutputPass variant (GL ignores
|
||||
// gl_Layer there; Vulkan offsets the primitive out of the attachment). Empty for every
|
||||
// program that never writes gl_Layer, which then uses `stages` for both. The variant
|
||||
// modules are owned by `modules` like the others.
|
||||
Vector<VkPipelineShaderStageCreateInfo> nonLayeredStages;
|
||||
// The fragment module a draw without multisample fragment operations uses (a
|
||||
// single-sample framebuffer, or MULTISAMPLE disabled): every gl_SampleMask store
|
||||
// writes all-ones, since GL ignores the shader's mask there and Vulkan would discard
|
||||
// on a clear bit 0 (FullCoverageSampleMaskPass). VK_NULL_HANDLE for every program
|
||||
// whose fragment stage never writes gl_SampleMask. Owned by `modules`.
|
||||
VkShaderModule fullCoverageFragmentModule = VK_NULL_HANDLE;
|
||||
Vector<VkShaderModule> modules;
|
||||
// Parallel to stages; identifies the exact module bytes handed to the driver when a
|
||||
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
|
||||
@@ -228,6 +253,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = false;
|
||||
// One of the program's final modules fails SPIR-V validation, so none of its modules
|
||||
// was created and every draw or dispatch of it is refused by name (the ProgramInvalidSpirv /
|
||||
// ComputeProgramInvalidSpirv wire declines) - see ProgramFactory::ScreenModuleForDriver.
|
||||
// Sticky like the flag above: it lives on the cache entry, so the one log line is honest.
|
||||
Bool invalidSpirvDeclined = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
@@ -251,7 +281,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
mutable Uint64 lastUsedFrame = 0;
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
static inline SessionLocal<VkDevice> s_device; // per Magma session (MagmaSession.h)
|
||||
|
||||
VkProgramObject() = default;
|
||||
VkProgramObject(const VkProgramObject&) = delete;
|
||||
@@ -259,6 +289,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject(VkProgramObject&& other) noexcept {
|
||||
hash = other.hash;
|
||||
stages = std::move(other.stages);
|
||||
nonLayeredStages = std::move(other.nonLayeredStages);
|
||||
fullCoverageFragmentModule = other.fullCoverageFragmentModule;
|
||||
other.fullCoverageFragmentModule = VK_NULL_HANDLE;
|
||||
modules = std::move(other.modules);
|
||||
// Must travel with `modules`: these digests name the SPIR-V those exact
|
||||
// shader modules were built from, and the pipeline-failure diagnostics
|
||||
@@ -288,6 +321,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
// The three refusal flags travel like every other field: a robin-hood insert swaps
|
||||
// entries, and a flag left behind would refuse (or admit) the wrong program.
|
||||
invalidSpirvDeclined = other.invalidSpirvDeclined;
|
||||
xfbCaptureDeclined = other.xfbCaptureDeclined;
|
||||
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -309,6 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.invalidSpirvDeclined = false;
|
||||
other.xfbCaptureDeclined = false;
|
||||
other.pointSizeCapabilityUnsupported = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -330,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Destroy();
|
||||
hash = other.hash;
|
||||
stages = std::move(other.stages);
|
||||
nonLayeredStages = std::move(other.nonLayeredStages);
|
||||
fullCoverageFragmentModule = other.fullCoverageFragmentModule;
|
||||
other.fullCoverageFragmentModule = VK_NULL_HANDLE;
|
||||
modules = std::move(other.modules);
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
@@ -352,6 +396,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
// The three refusal flags travel like every other field: a robin-hood insert swaps
|
||||
// entries, and a flag left behind would refuse (or admit) the wrong program.
|
||||
invalidSpirvDeclined = other.invalidSpirvDeclined;
|
||||
xfbCaptureDeclined = other.xfbCaptureDeclined;
|
||||
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -373,6 +422,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.invalidSpirvDeclined = false;
|
||||
other.xfbCaptureDeclined = false;
|
||||
other.pointSizeCapabilityUnsupported = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -395,23 +447,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
void Destroy() {
|
||||
if (s_device != VK_NULL_HANDLE) {
|
||||
if (s_device.Get() != VK_NULL_HANDLE) {
|
||||
if (pipelineLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyPipelineLayout(s_device, pipelineLayout, nullptr);
|
||||
vkDestroyPipelineLayout(s_device.Get(), pipelineLayout, nullptr);
|
||||
pipelineLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
if (descriptorSetLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyDescriptorSetLayout(s_device, descriptorSetLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(s_device.Get(), descriptorSetLayout, nullptr);
|
||||
descriptorSetLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
for (auto module : modules) {
|
||||
if (module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(s_device, module, nullptr);
|
||||
vkDestroyShaderModule(s_device.Get(), module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
modules.clear();
|
||||
stages.clear();
|
||||
nonLayeredStages.clear();
|
||||
fullCoverageFragmentModule = VK_NULL_HANDLE;
|
||||
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||
}
|
||||
};
|
||||
@@ -435,7 +489,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// repair passes patch modules AROUND them, and the emulation only replaces them
|
||||
// on opted-in devices with no subgroup support at all.
|
||||
struct SubgroupLoweringPolicy {
|
||||
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
||||
Bool emulateSubgroups = false; // no-native-support devices, or MOBILEGL_SUBGROUP_EMULATION
|
||||
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
||||
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
|
||||
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
||||
@@ -463,7 +517,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
VkProgramObject::s_device = device;
|
||||
VkProgramObject::s_device.Get() = device;
|
||||
}
|
||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||
// still alive for the same reason ~VkProgramObject's does: this factory outlives
|
||||
@@ -503,6 +557,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// VkRenderPassManager::OnPresent's sweep.
|
||||
void OnFrameBoundary();
|
||||
|
||||
// What the last look at a module's final bytes - the exact words vkCreateShaderModule
|
||||
// would receive - decided.
|
||||
enum class DriverModuleVerdict : Uint8 {
|
||||
Valid, // validates: handed to the driver
|
||||
DeclinedInvalid, // fails validation: never handed over, the program's work is declined
|
||||
AdmittedInvalid, // fails validation, handed over anyway (MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV)
|
||||
};
|
||||
// Validates `spirv` for the Vulkan environment its version word names and turns the
|
||||
// result into a verdict. A driver is not a validating entry point: Adreno page-faulted
|
||||
// into device loss on a module that carried a sampler inside a uniform block, Mali
|
||||
// SIGSEGVs inside pipeline creation. So an invalid module is declined unless
|
||||
// `allowInvalid` (the escape hatch) says to hand it over regardless.
|
||||
//
|
||||
// The validator's answer is remembered per module content (64-bit hash plus word count),
|
||||
// process-wide: a program variant, an evicted-then-rebuilt entry and two programs that
|
||||
// share a stage all re-screen the same bytes, and only the first pays for spirv-val.
|
||||
// `outReason` receives the validator's message for an invalid module.
|
||||
static DriverModuleVerdict ScreenModuleForDriver(const Vector<Uint>& spirv, Bool allowInvalid,
|
||||
String* outReason = nullptr);
|
||||
// How many times ScreenModuleForDriver actually ran the validator (cache misses), for
|
||||
// the tests that pin the cache.
|
||||
static Uint64 DriverModuleValidatorRuns();
|
||||
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
@@ -655,6 +732,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// and adds only a handful of values, so it does not move the cap in practice.
|
||||
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
|
||||
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
// The FragCoordYFlip variant's module transform: every read of gl_FragCoord goes through a
|
||||
// Private copy whose y is (framebufferHeight - y). Fails open (output = input, false). Exposed
|
||||
// so a unit case can validate the bytes it hands the driver.
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output, Uint32 framebufferHeight);
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/RenderPassGuard.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 "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Ends the render pass that is active on `commandBuffer`, if there is one. Called right before
|
||||
// recording a command a render pass may not contain (a barrier, a copy, a blit, a clear of an
|
||||
// image) into a command buffer that may hold an open pass: the wire draw path keeps its pass
|
||||
// open across consecutive draws to the same attachments, so the resource work that falls
|
||||
// between two draws must close it first. A pass on another command buffer is left alone.
|
||||
void EndActiveRenderPassOn(VkCommandBuffer commandBuffer);
|
||||
|
||||
// A counter that moves whenever images may have been written since it was last read: a render
|
||||
// pass ended (its attachments), resource work recorded outside a pass (copies, clears, blits,
|
||||
// dispatches all close the pass first), or a draw with storage images. A sampled image needs
|
||||
// the memory barrier that makes earlier writes visible only when this moved since the last one.
|
||||
Uint64 WireImageWriteEpoch();
|
||||
void BumpWireImageWriteEpoch();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
#include "SwapchainObject.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the surface-changed event's producer callback, installed by the server session.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#endif
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<vulkan/vk_enum_string_helper.h>)
|
||||
@@ -140,12 +140,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 +167,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) {
|
||||
Uint32 extraImageCount, VkExtent2D desiredExtent, Optional<Int> swapInterval) {
|
||||
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
|
||||
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
|
||||
"SwapchainObject::Create failed: incomplete swapchain capabilities");
|
||||
@@ -174,15 +192,21 @@ 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);
|
||||
// The extra images count from the surface's own minimum: that is how many the platform
|
||||
// keeps for itself and the application, so only images beyond it delay a buffer's reuse.
|
||||
Uint32 targetImageCount =
|
||||
std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount + extraImageCount);
|
||||
if (swapchainCaps.maxImageCount != 0) {
|
||||
targetImageCount = std::min(targetImageCount, swapchainCaps.maxImageCount);
|
||||
}
|
||||
MGLOG_I("Set minImageCount = %u", targetImageCount);
|
||||
MGLOG_I("Set minImageCount = %u (surface min %u, max %u, extra %u)", targetImageCount,
|
||||
swapchainCaps.minImageCount, swapchainCaps.maxImageCount, extraImageCount);
|
||||
MGLOG_I("Swapchain currentTransform = %s",
|
||||
string_VkSurfaceTransformFlagBitsKHR(swapchainCaps.currentTransform));
|
||||
|
||||
@@ -270,6 +294,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Fresh swapchain images hold garbage until a render pass stores into them.
|
||||
m_imageContentDefined.assign(imageCount, false);
|
||||
m_depthStencilContentDefined.assign(imageCount, false);
|
||||
// Nothing presented from this chain yet: every buffer age is unknown.
|
||||
m_ages.Reset(imageCount);
|
||||
|
||||
CreateImageViews(device);
|
||||
CreateDepthStencilResources(device, physicalDevice);
|
||||
@@ -277,6 +303,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_I("Swapchain created, extent = %dx%d, swapchain imageCount = %d", m_extent.width, m_extent.height,
|
||||
imageCount);
|
||||
|
||||
PublishDefaultFramebufferInfo();
|
||||
}
|
||||
|
||||
void SwapchainObject::PublishDefaultFramebufferInfo() const {
|
||||
const VkExtent2D defaultFramebufferExtent = m_surfaceExtent;
|
||||
// Properly initialize Default FBO here
|
||||
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
|
||||
const Int extentWidth = static_cast<Int>(defaultFramebufferExtent.width);
|
||||
@@ -320,7 +351,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
break;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the default FBO's
|
||||
// attachments are CLIENT memory and this thread may not write them - the backend
|
||||
@@ -338,7 +368,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
@@ -466,6 +495,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_imageLayouts.clear();
|
||||
m_imageContentDefined.clear();
|
||||
m_depthStencilContentDefined.clear();
|
||||
m_ages.Reset(0);
|
||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -26,13 +27,26 @@ 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, Uint32 extraImageCount,
|
||||
VkExtent2D desiredExtent, Optional<Int> swapInterval);
|
||||
void Shutdown(VkDevice device);
|
||||
// Tells the frontend (or, under a transport, the client) the default framebuffer's size
|
||||
// and formats. Create does this; a renderer switching between surfaces repeats it.
|
||||
void PublishDefaultFramebufferInfo() const;
|
||||
|
||||
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
|
||||
@@ -67,6 +81,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
|
||||
void SetAllDepthStencilContentUndefined();
|
||||
|
||||
// EGL_EXT_buffer_age over this chain's images. Ages start unknown with the chain (Create).
|
||||
// Until a client asks for an age (NoteBufferAgeAsked) a presented image's content is
|
||||
// treated as undefined, as before; once one has, a presented image keeps it - its age is
|
||||
// counted and its content stays defined, so the next frame loads it.
|
||||
void NoteBufferAgeAsked() { m_bufferAgeAsked = true; }
|
||||
Bool BufferAgeAsked() const { return m_bufferAgeAsked; }
|
||||
Int32 BufferAgeOf(Uint32 index) const { return m_ages.AgeOf(index); }
|
||||
// Image `index` was presented.
|
||||
void OnImagePresented(Uint32 index) { m_ages.OnPresented(index, m_bufferAgeAsked); }
|
||||
// A swapped frame reached no image: every age is unknown.
|
||||
void OnFrameDropped() { m_ages.OnFrameDropped(); }
|
||||
|
||||
private:
|
||||
void CreateImageViews(VkDevice device);
|
||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||
@@ -79,6 +105,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{};
|
||||
@@ -94,5 +122,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||
Vector<Bool> m_imageContentDefined;
|
||||
Vector<Bool> m_depthStencilContentDefined;
|
||||
MG_Util::Damage::SwapchainAgeTracker m_ages;
|
||||
// Kept across a rebuild: it is the application's habit, not the chain's.
|
||||
Bool m_bufferAgeAsked = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user