mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-22 15:14:40 -04:00
Compare commits
837
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26fa0981c | ||
|
|
ec24b3a472 | ||
|
|
6455b1dade | ||
|
|
542ad3e060 | ||
|
|
910fb47692 | ||
|
|
3dd14dd625 | ||
|
|
d1b0ac4df1 | ||
|
|
0e225d0c6e | ||
|
|
d9cd0d2cc8 | ||
|
|
eadebc46d5 | ||
|
|
96d7c66535 | ||
|
|
90690d1c5c | ||
|
|
abbcb8e3ad | ||
|
|
fe60b3c279 | ||
|
|
c7455b74ff | ||
|
|
c875ceda4d | ||
|
|
666abd134f | ||
|
|
c115916962 | ||
|
|
0579e688ba | ||
|
|
bc50de3b55 | ||
|
|
6489232598 | ||
|
|
c23c436502 | ||
|
|
fad52ec125 | ||
|
|
d22e458d64 | ||
|
|
1b7cb4179d | ||
|
|
c1c22dfd37 | ||
|
|
0043cefab5 | ||
|
|
41f68ef1a1 | ||
|
|
9743ac3abc | ||
|
|
ca6cfcd193 | ||
|
|
d41424170a | ||
|
|
f4678935c4 | ||
|
|
416eb41d8d | ||
|
|
6e0d456dd7 | ||
|
|
8ac555664a | ||
|
|
4bf4e53ecf | ||
|
|
3d50e6b1ae | ||
|
|
846b20f78a | ||
|
|
a229d196bb | ||
|
|
61bfc04704 | ||
|
|
9b90386e73 | ||
|
|
ddc4c18faf | ||
|
|
fb8361b197 | ||
|
|
a5fd64f9f2 | ||
|
|
e0a7ce4f88 | ||
|
|
b213ae77b4 | ||
|
|
6e5932cb68 | ||
|
|
38e3fa2a33 | ||
|
|
8e1a676f19 | ||
|
|
7c946db440 | ||
|
|
eaf26c8a20 | ||
|
|
0a96767657 | ||
|
|
32054e156e | ||
|
|
fd6d6b7c46 | ||
|
|
c101a451e3 | ||
|
|
a76ba69217 | ||
|
|
d06caf9838 | ||
|
|
134fa167fe | ||
|
|
da42a5d7a7 | ||
|
|
b6c1d3c09c | ||
|
|
a58b0ac3e0 | ||
|
|
e1700ba982 | ||
|
|
736ac7db74 | ||
|
|
6e2966be43 | ||
|
|
679948e792 | ||
|
|
e0fc8362b8 | ||
|
|
9d8ff9615f | ||
|
|
1304809240 | ||
|
|
343a516e1a | ||
|
|
7c08cb58ae | ||
|
|
7e81668de2 | ||
|
|
d231564f8e | ||
|
|
37e89c5b4d | ||
|
|
e070bd72e0 | ||
|
|
9310d19857 | ||
|
|
4454a6d39a | ||
|
|
453f6f5180 | ||
|
|
6d0484941e | ||
|
|
cf47910881 | ||
|
|
4f0f0cf5b1 | ||
|
|
a78d5cb600 | ||
|
|
206c8a8846 | ||
|
|
bf17ce1151 | ||
|
|
ef38c975b9 | ||
|
|
61953c4f7b | ||
|
|
1f13285ab8 | ||
|
|
1bc23ac169 | ||
|
|
50f50b16e0 | ||
|
|
adc86fc7df | ||
|
|
f3fd3a2307 | ||
|
|
1e877fa602 | ||
|
|
906009a33a | ||
|
|
08559a02f8 | ||
|
|
1d93626453 | ||
|
|
9d0989374b | ||
|
|
3278afaa1a | ||
|
|
bf221ffbbd | ||
|
|
c636eea355 | ||
|
|
ffaa906cd1 | ||
|
|
d61af961c1 | ||
|
|
bc01b77752 | ||
|
|
a1b01a04a8 | ||
|
|
b44235c495 | ||
|
|
b80d5db176 | ||
|
|
d3c0d258b7 | ||
|
|
9dcdbf245a | ||
|
|
369e218afb | ||
|
|
119d24bf6b | ||
|
|
2a905510d5 | ||
|
|
da29919b5a | ||
|
|
05608ac4d4 | ||
|
|
135b84a501 | ||
|
|
e7cbba37d1 | ||
|
|
d9bb5e15b0 | ||
|
|
46cdc32cf1 | ||
|
|
387572e733 | ||
|
|
9f46c7a50b | ||
|
|
0607bb0b3a | ||
|
|
b89d87cbde | ||
|
|
06e06b9673 | ||
|
|
b896e7a3b9 | ||
|
|
c91daa9147 | ||
|
|
31ea194f3d | ||
|
|
472bb28c88 | ||
|
|
eaa4a7a047 | ||
|
|
4fa467c800 | ||
|
|
beb96e6f0c | ||
|
|
26cb9e2f03 | ||
|
|
6dc74523c3 | ||
|
|
3858a69867 | ||
|
|
49a5f4aa94 | ||
|
|
4cc9f2a3f3 | ||
|
|
b0d7fc01d8 | ||
|
|
9cc40246a3 | ||
|
|
423cf11919 | ||
|
|
33f7213599 | ||
|
|
6f1fb462ed | ||
|
|
336b8851f7 | ||
|
|
5da3cf673c | ||
|
|
9e54e094fb | ||
|
|
eb9ca4ab6b | ||
|
|
e7b6876877 | ||
|
|
c535f6ef6e | ||
|
|
db2e1d7da2 | ||
|
|
f5e204eb1d | ||
|
|
705a5e7b2a | ||
|
|
e60eae3646 | ||
|
|
b04900b1b2 | ||
|
|
6357543b44 | ||
|
|
67fb338e5d | ||
|
|
ba140c4ab5 | ||
|
|
f5fdbc7941 | ||
|
|
90ae85da4d | ||
|
|
f3600fa618 | ||
|
|
2d8befe1cb | ||
|
|
f982ffcaf7 | ||
|
|
255c64551e | ||
|
|
746f28c1b2 | ||
|
|
2b481ba250 | ||
|
|
49622623de | ||
|
|
69f99ade42 | ||
|
|
c236e9711f | ||
|
|
4c2efb6688 | ||
|
|
7eb2d1cf1a | ||
|
|
b4ff83cf9f | ||
|
|
7b20b70c6b | ||
|
|
86207f9d0e | ||
|
|
eda5ece382 | ||
|
|
3404bbc128 | ||
|
|
b5a431aa3c | ||
|
|
49249f6541 | ||
|
|
5614621d6e | ||
|
|
0816562065 | ||
|
|
39618e6862 | ||
|
|
c66e4ba4b3 | ||
|
|
e4b033861f | ||
|
|
85a07a556c | ||
|
|
8dfc48d58b | ||
|
|
82ebdaa0b5 | ||
|
|
ec016f85bc | ||
|
|
c2c812deaa | ||
|
|
eec3428f78 | ||
|
|
4a5e4b76e3 | ||
|
|
43f952b2fb | ||
|
|
0d86bec34d | ||
|
|
47a6f29ad7 | ||
|
|
84834f180e | ||
|
|
bfd445618a | ||
|
|
eddfd37b93 | ||
|
|
d7341863bf | ||
|
|
2f51be7b5a | ||
|
|
fbc0014d14 | ||
|
|
46df6171fc | ||
|
|
761b62914f | ||
|
|
91b0299f77 | ||
|
|
6d08e59d10 | ||
|
|
83cc0fe585 | ||
|
|
c711c627b8 | ||
|
|
0b34690721 | ||
|
|
b640734db5 | ||
|
|
f8feb63dde | ||
|
|
25a5d3fe5d | ||
|
|
41dd93fd25 | ||
|
|
adf3c9405d | ||
|
|
abf4f57b47 | ||
|
|
c1e36975d4 | ||
|
|
0bfa0915d4 | ||
|
|
4da87672a2 | ||
|
|
3ba0d287e9 | ||
|
|
b4a86cb5bb | ||
|
|
0fa1f2f868 | ||
|
|
71610ddfdb | ||
|
|
5c9e683769 | ||
|
|
32a5e83b88 | ||
|
|
88904c08f7 | ||
|
|
9928bfe780 | ||
|
|
a491dec5c4 | ||
|
|
17ef33ee31 | ||
|
|
06caaee343 | ||
|
|
1c161a76dd | ||
|
|
aa05ab03fa | ||
|
|
43ea3f3181 | ||
|
|
3966b27948 | ||
|
|
7e24327621 | ||
|
|
d025aa3c6d | ||
|
|
325f6893c5 | ||
|
|
466419407d | ||
|
|
cbd4ce6636 | ||
|
|
d4130181e7 | ||
|
|
16a9d0af2a | ||
|
|
e51698e01f | ||
|
|
519091dd86 | ||
|
|
972088f1b3 | ||
|
|
542d31daf0 | ||
|
|
8f9a849c48 | ||
|
|
e8bfe6e520 | ||
|
|
15618f357b | ||
|
|
b74010435a | ||
|
|
0a58a7faee | ||
|
|
0d21e2f913 | ||
|
|
ebb82d2dc9 | ||
|
|
557e346810 | ||
|
|
08b2cf0894 | ||
|
|
4e90e03536 | ||
|
|
11d997dc65 | ||
|
|
454193da9b | ||
|
|
cdd5c99bc7 | ||
|
|
fb1da1c39a | ||
|
|
b29ece82c8 | ||
|
|
1330fe4d6c | ||
|
|
0e1f6f15a1 | ||
|
|
4df0e012fd | ||
|
|
48054a86ab | ||
|
|
38d658abf5 | ||
|
|
5547391cfc | ||
|
|
6782e5a717 | ||
|
|
e1a80c1751 | ||
|
|
05933a5ee3 | ||
|
|
1f29cd92a5 | ||
|
|
4f30356c9b | ||
|
|
baebf50197 | ||
|
|
01674b7c6f | ||
|
|
728bfa2043 | ||
|
|
c4de482b6a | ||
|
|
55697535a1 | ||
|
|
65733c23a1 | ||
|
|
fd51b86060 | ||
|
|
d6e4ab1824 | ||
|
|
566c7d703a | ||
|
|
d7eb3256a5 | ||
|
|
c96f2d1070 | ||
|
|
cc8b743ecb | ||
|
|
45422b739b | ||
|
|
f58bf9b23c | ||
|
|
f0e8ef1274 | ||
|
|
bb6d6fee97 | ||
|
|
d59734e375 | ||
|
|
14db12290e | ||
|
|
1e476539df | ||
|
|
3c8e370744 | ||
|
|
3fcd01f536 | ||
|
|
0e2ee35b49 | ||
|
|
27920711f0 | ||
|
|
d4c16b5b59 | ||
|
|
e392364e30 | ||
|
|
3cdb77fd65 | ||
|
|
a91be68992 | ||
|
|
e16f351540 | ||
|
|
efdab60b22 | ||
|
|
8dc24f244c | ||
|
|
b565c4a6e5 | ||
|
|
a999fbdb38 | ||
|
|
52eee568f5 | ||
|
|
05cebd4a4f | ||
|
|
8cfd54a739 | ||
|
|
8565acf12b | ||
|
|
941a6c3ef9 | ||
|
|
fe84c050ad | ||
|
|
f13c885fec | ||
|
|
8ee1cbe3eb | ||
|
|
6c5d6cda73 | ||
|
|
2725ee4512 | ||
|
|
7cc1c93775 | ||
|
|
38c9b733b5 | ||
|
|
27e5f86793 | ||
|
|
a6331e44ae | ||
|
|
f53b844add | ||
|
|
fb34098006 | ||
|
|
c729f4c8e7 | ||
|
|
9f343278c3 | ||
|
|
6beeb29486 | ||
|
|
ade2e982b6 | ||
|
|
8a61a1898b | ||
|
|
dfa3b12f48 | ||
|
|
93382ff241 | ||
|
|
24b74d8346 | ||
|
|
9ed25d8ae5 | ||
|
|
b4b31d5a08 | ||
|
|
f394906726 | ||
|
|
b50dfa76d1 | ||
|
|
3f81ddff9b | ||
|
|
d1dec8d7a4 | ||
|
|
e60097e5cb | ||
|
|
7c7bf2dcbe | ||
|
|
75593cfd29 | ||
|
|
00d0f8cd2b | ||
|
|
faf4b4e8b5 | ||
|
|
5a61d37c5d | ||
|
|
25079ab819 | ||
|
|
736b7bed5e | ||
|
|
a3f4c911e5 | ||
|
|
caa7682c6f | ||
|
|
e217cbe66d | ||
|
|
4914b2cc69 | ||
|
|
051b9a320e | ||
|
|
6ecd6fa1d0 | ||
|
|
0f4e83eefd | ||
|
|
d62525f5af | ||
|
|
71057fefe1 | ||
|
|
f4d2c37fbd | ||
|
|
ffd9f92d7b | ||
|
|
213d110766 | ||
|
|
1a8c91b49b | ||
|
|
9644a4cc7f | ||
|
|
24c250fdb4 | ||
|
|
d4636afa97 | ||
|
|
32930e704e | ||
|
|
a8b18947a1 | ||
|
|
7c422577b6 | ||
|
|
1cc1ee994d | ||
|
|
d8d375cb9a | ||
|
|
49934ce933 | ||
|
|
221644e8be | ||
|
|
5179b54745 | ||
|
|
448feb30bc | ||
|
|
9d891d4db1 | ||
|
|
240f1194c0 | ||
|
|
7b74eebce2 | ||
|
|
7592684b6f | ||
|
|
7b78e1629b | ||
|
|
30741302fb | ||
|
|
645f7d0bf1 | ||
|
|
124f162ee8 | ||
|
|
3c577fd9dd | ||
|
|
659ff4817f | ||
|
|
626adb62af | ||
|
|
71cb100a40 | ||
|
|
68002f3fa7 | ||
|
|
dae7592788 | ||
|
|
e10f5b4430 | ||
|
|
3e32b75a63 | ||
|
|
435f7a1b03 | ||
|
|
1fd8c50225 | ||
|
|
e797bc5f01 | ||
|
|
0cc3fe6bca | ||
|
|
ab8542e4e5 | ||
|
|
c2bcf20172 | ||
|
|
2341659cea | ||
|
|
b2eaf380dc | ||
|
|
197fc3f163 | ||
|
|
9530e7b394 | ||
|
|
0e0f133496 | ||
|
|
d98c211168 | ||
|
|
614b4d4de4 | ||
|
|
43a19a831a | ||
|
|
0d308af024 | ||
|
|
128f73da8f | ||
|
|
e22a55e949 | ||
|
|
67cb005e6c | ||
|
|
73f76f6799 | ||
|
|
961a060461 | ||
|
|
dadbbf841a | ||
|
|
1ce8280371 | ||
|
|
78bdc0e3b5 | ||
|
|
ce5f2f5513 | ||
|
|
0f2792c7ac | ||
|
|
7185b775d4 | ||
|
|
cda6e89c12 | ||
|
|
5f80898c27 | ||
|
|
85560d6471 | ||
|
|
be6c875277 | ||
|
|
91beced154 | ||
|
|
ebc3da08b4 | ||
|
|
41eb780495 | ||
|
|
65fc2a38a3 | ||
|
|
359d1535ac | ||
|
|
4c3a9f027c | ||
|
|
5a6cc3c76e | ||
|
|
89bd7deceb | ||
|
|
169f2c3e34 | ||
|
|
e6a797695a | ||
|
|
f1a2ecc857 | ||
|
|
36916f6bde | ||
|
|
1b96d13a51 | ||
|
|
cb1d70bb14 | ||
|
|
37af590ca2 | ||
|
|
02f356f8c0 | ||
|
|
5448177814 | ||
|
|
ed6ce0ca41 | ||
|
|
9b440686c5 | ||
|
|
e7b7e9dc43 | ||
|
|
1120040aee | ||
|
|
3506a272da | ||
|
|
b752748b7d | ||
|
|
8990e7d7a4 | ||
|
|
c92a423ba1 | ||
|
|
3b82b3e1f8 | ||
|
|
e985ce1975 | ||
|
|
8496ce3e51 | ||
|
|
47188f40ed | ||
|
|
e186c9bc4c | ||
|
|
7705f3c222 | ||
|
|
5675b4897f | ||
|
|
1c87d92ef7 | ||
|
|
031e64dfe1 | ||
|
|
f590dd9763 | ||
|
|
af7dc40538 | ||
|
|
a28aae6e4a | ||
|
|
c024470cc7 | ||
|
|
16ae5ab84a | ||
|
|
144947305a | ||
|
|
563bc6cad7 | ||
|
|
59cde93821 | ||
|
|
c104e1c276 | ||
|
|
b5da0d0db6 | ||
|
|
5defc3e38b | ||
|
|
6a9f52f932 | ||
|
|
06a8d75769 | ||
|
|
ec12ddd0a8 | ||
|
|
570902d5f3 | ||
|
|
6120069975 | ||
|
|
3e337c9bda | ||
|
|
2869e76406 | ||
|
|
61fdb80823 | ||
|
|
fd180a108a | ||
|
|
4de74c740c | ||
|
|
b7c32c92d2 | ||
|
|
8610ca0928 | ||
|
|
a6b1ad37d0 | ||
|
|
97b13f7488 | ||
|
|
5d5b45a012 | ||
|
|
5ebafaabda | ||
|
|
56627a8aaf | ||
|
|
f59ced4e98 | ||
|
|
296e848718 | ||
|
|
8c63413019 | ||
|
|
2861c6d284 | ||
|
|
69c2d8e566 | ||
|
|
414fddf7b8 | ||
|
|
b5ca6145a6 | ||
|
|
5a5948b071 | ||
|
|
e58b5088b3 | ||
|
|
ecbce4c8d3 | ||
|
|
96bf0a3bf3 | ||
|
|
da7522a3ee | ||
|
|
5605e777ca | ||
|
|
a10e2e1fe8 | ||
|
|
4fb623fa33 | ||
|
|
3b4956d088 | ||
|
|
18deaafc34 | ||
|
|
0c4eede6d2 | ||
|
|
54681f2647 | ||
|
|
5271edd972 | ||
|
|
e4bb9856ef | ||
|
|
e462bbb24a | ||
|
|
d8f3090d9b | ||
|
|
d1af8b0114 | ||
|
|
dae64bbe69 | ||
|
|
8b127bed84 | ||
|
|
67187b1340 | ||
|
|
02cac57438 | ||
|
|
277dcd174c | ||
|
|
3c23ae7310 | ||
|
|
8cb13d40b5 | ||
|
|
c950d31960 | ||
|
|
ae570cd5b7 | ||
|
|
0fd1dc4fce | ||
|
|
f0854e8518 | ||
|
|
ceffd3f759 | ||
|
|
dc1816612c | ||
|
|
1d04a7e0e3 | ||
|
|
1e523c4e93 | ||
|
|
d83f766cf3 | ||
|
|
bb5e1ae63d | ||
|
|
f27a4f6c95 | ||
|
|
d7ff92106a | ||
|
|
34a17acaba | ||
|
|
110aec0b6f | ||
|
|
06502bb49b | ||
|
|
c20a166134 | ||
|
|
162fa8e2de | ||
|
|
ae5614d212 | ||
|
|
b72d8a8bab | ||
|
|
d4cf890d8a | ||
|
|
82e494269d | ||
|
|
c982d65675 | ||
|
|
f4e606fb15 | ||
|
|
9dbdb5e771 | ||
|
|
fa12478fdd | ||
|
|
c3b0d8ef03 | ||
|
|
cbb9a168e6 | ||
|
|
73e2fc58a0 | ||
|
|
500f4c69eb | ||
|
|
911ad29004 | ||
|
|
aa843b5a31 | ||
|
|
f249acc50f | ||
|
|
8cdedbe18a | ||
|
|
0f39d71f95 | ||
|
|
1dc49b527b | ||
|
|
9422608960 | ||
|
|
cc3fec0768 | ||
|
|
de37ddcfb5 | ||
|
|
4c90196890 | ||
|
|
5270574e5a | ||
|
|
30f6ca6ffc | ||
|
|
a8af973726 | ||
|
|
ed1f8dd2b6 | ||
|
|
e8aaae1343 | ||
|
|
ecff3240a6 | ||
|
|
ddd4a0c24d | ||
|
|
019949847a | ||
|
|
4dcbf269d2 | ||
|
|
036317f371 | ||
|
|
ce0da01163 | ||
|
|
75ae619e21 | ||
|
|
3efa5acfa6 | ||
|
|
9369dc3031 | ||
|
|
5d84d3fef7 | ||
|
|
51baf75af1 | ||
|
|
20f2c98d18 | ||
|
|
e3ceae412b | ||
|
|
7e6affed12 | ||
|
|
40fc5463f3 | ||
|
|
039948f4d2 | ||
|
|
bbb95e8a60 | ||
|
|
7763d00b78 | ||
|
|
3452730725 | ||
|
|
e50f61f5d9 | ||
|
|
a7a4308e3c | ||
|
|
e811576e79 | ||
|
|
960abec0ed | ||
|
|
855ab88d6c | ||
|
|
f7ee6e78f0 | ||
|
|
293c58aeba | ||
|
|
95b8a4c3c4 | ||
|
|
b7fd52de79 | ||
|
|
53ce764548 | ||
|
|
157e74907a | ||
|
|
015ac26e07 | ||
|
|
1fe7faaa8d | ||
|
|
2e7f99897d | ||
|
|
d7fa65ad20 | ||
|
|
ae614ce476 | ||
|
|
7d6e1cce2f | ||
|
|
bbf46cad9d | ||
|
|
68c7b4b3c8 | ||
|
|
5d38a6c8c5 | ||
|
|
99a0c44f98 | ||
|
|
1812ba20e2 | ||
|
|
771d3c399c | ||
|
|
af74a4449e | ||
|
|
8c171dd2c7 | ||
|
|
3292ed6f1a | ||
|
|
96bbe0c4cc | ||
|
|
039e14de5a | ||
|
|
a419c8097f | ||
|
|
3fac2e8945 | ||
|
|
c2d6d272a3 | ||
|
|
846630e300 | ||
|
|
cc1f3d256b | ||
|
|
243a760a12 | ||
|
|
4f744fc8fe | ||
|
|
54adf9ad68 | ||
|
|
8b66adb8a5 | ||
|
|
9f0c1fc797 | ||
|
|
4a46e16702 | ||
|
|
608d011b44 | ||
|
|
0d52ba66cd | ||
|
|
431fac1fca | ||
|
|
35006d6031 | ||
|
|
7ae66337ef | ||
|
|
b75e0f9800 | ||
|
|
ee3fd4d6b8 | ||
|
|
0352f4b832 | ||
|
|
7fbfeb2edd | ||
|
|
8db69f073b | ||
|
|
226fa1c1ea | ||
|
|
6f41ae58db | ||
|
|
ae9186392f | ||
|
|
4279ffd887 | ||
|
|
ac058ea112 | ||
|
|
5d42e5e87a | ||
|
|
96ac6967e6 | ||
|
|
0c301f3cec | ||
|
|
d7cadd05d6 | ||
|
|
2f8464b7f5 | ||
|
|
96314749da | ||
|
|
33f06d744f | ||
|
|
d6ca48d80a | ||
|
|
c8b628393b | ||
|
|
e9d4eb1e1b | ||
|
|
89f78a9969 | ||
|
|
94f9935ad4 | ||
|
|
66c7db5a47 | ||
|
|
b959de3d26 | ||
|
|
b802485e31 | ||
|
|
ee20fd60c4 | ||
|
|
52174e2ab0 | ||
|
|
216f3c1d62 | ||
|
|
7e6954304a | ||
|
|
8718ae0aa8 | ||
|
|
615311ecd6 | ||
|
|
3a4ef4f6d6 | ||
|
|
403cfa77ae | ||
|
|
c7998a82d5 | ||
|
|
86bc9f4a64 | ||
|
|
2f4c4624ae | ||
|
|
b83e5f87cf | ||
|
|
b49e2753df | ||
|
|
3c09e1d736 | ||
|
|
0f07dafb36 | ||
|
|
38fdca0b97 | ||
|
|
e8efe2d290 | ||
|
|
a1ae55125a | ||
|
|
706692c1fe | ||
|
|
582f031243 | ||
|
|
9c78373c73 | ||
|
|
e9f3416438 | ||
|
|
ea44a77f8a | ||
|
|
63d7051c4b | ||
|
|
4803c9d221 | ||
|
|
6388edb706 | ||
|
|
0774ade030 | ||
|
|
e5ee766961 | ||
|
|
274f3d0c7a | ||
|
|
21be683768 | ||
|
|
695923aed7 | ||
|
|
649153257d | ||
|
|
cbcd3f8023 | ||
|
|
41c5ce8d4e | ||
|
|
78ce23258b | ||
|
|
3e864fda17 | ||
|
|
0cdb945d9c | ||
|
|
15a113f783 | ||
|
|
67f00d2123 | ||
|
|
d68dcd14e1 | ||
|
|
32994210ab | ||
|
|
cfec8ab88a | ||
|
|
87c44c3218 | ||
|
|
35446df6d5 | ||
|
|
921b38bfb4 | ||
|
|
82bb924d0b | ||
|
|
08a6769bc0 | ||
|
|
dce87061d1 | ||
|
|
f4302e4fa9 | ||
|
|
0436953451 | ||
|
|
7165907ba9 | ||
|
|
e938127c7a | ||
|
|
b5c9d6bc50 | ||
|
|
f7af5091cf | ||
|
|
7ac1786a2e | ||
|
|
19a919e096 | ||
|
|
10f5506829 | ||
|
|
d3c6d54a58 | ||
|
|
6980428f82 | ||
|
|
1a3b16f614 | ||
|
|
e0b9f462ce | ||
|
|
f6f7470385 | ||
|
|
214c708b49 | ||
|
|
d567185fc0 | ||
|
|
24188bda10 | ||
|
|
1a870c5c07 | ||
|
|
fa964802d9 | ||
|
|
793180ce12 | ||
|
|
79e7be470c | ||
|
|
921f156128 | ||
|
|
c771fc67ac | ||
|
|
7f2d2ffa4a | ||
|
|
2d9e1c6d95 | ||
|
|
66dafa6021 | ||
|
|
2df3d054e8 | ||
|
|
c9cfa2126e | ||
|
|
2220578cd1 | ||
|
|
5b18807f0e | ||
|
|
d984a4ba31 | ||
|
|
7dc00d9b23 | ||
|
|
59123f1c89 | ||
|
|
aa1df93a22 | ||
|
|
c99617427a | ||
|
|
54a4677f11 | ||
|
|
edeb388233 | ||
|
|
034d484271 | ||
|
|
ebf5da9840 | ||
|
|
f9c9084584 | ||
|
|
d160f464b6 | ||
|
|
c013252a59 | ||
|
|
7fa71dc5c5 | ||
|
|
a19dfb88a0 | ||
|
|
738cd059a2 | ||
|
|
aaa6a0452f | ||
|
|
0cb18839a2 | ||
|
|
991e1af331 | ||
|
|
c8a278d009 | ||
|
|
39f5478ea9 | ||
|
|
d0b1686a84 | ||
|
|
95bd8ec2fe | ||
|
|
85b5151e1a | ||
|
|
a0be13adba | ||
|
|
a0fb8fd73d | ||
|
|
d3e70b99c0 | ||
|
|
0a0ec6acf5 | ||
|
|
50df100569 | ||
|
|
02489e617c | ||
|
|
2f22050add | ||
|
|
0081c8f7c7 | ||
|
|
12c1036f17 | ||
|
|
7ef7129f7a | ||
|
|
5cb6346017 | ||
|
|
5e599e5445 | ||
|
|
146c07a25e | ||
|
|
44265c3fcd | ||
|
|
90c7574e28 | ||
|
|
e4fbb6f956 | ||
|
|
ad2f9f8aa6 | ||
|
|
9c470950ea | ||
|
|
0ddb0be65a | ||
|
|
cbd3bc95a1 | ||
|
|
f43795714d | ||
|
|
586c9c28b4 | ||
|
|
f79dc2da43 | ||
|
|
3bb485a437 | ||
|
|
8dd0043072 | ||
|
|
4c43fe3502 | ||
|
|
92f7709a76 | ||
|
|
04f40282d2 | ||
|
|
4b1804847f | ||
|
|
4e791d486f | ||
|
|
9a8732fba4 | ||
|
|
adddbb4e50 | ||
|
|
af4df4177d | ||
|
|
fa54c5c5a0 | ||
|
|
768e6c8511 | ||
|
|
eb1b9e0d78 | ||
|
|
2f80d97e17 | ||
|
|
d9246c333e | ||
|
|
164a6e009b | ||
|
|
69aba7d0b7 | ||
|
|
60f393f878 | ||
|
|
a14451b5e9 | ||
|
|
946bbc3023 | ||
|
|
474e75f649 | ||
|
|
3c65fd8d9e | ||
|
|
066011c5fb | ||
|
|
e14fc55030 | ||
|
|
d7b61258ce | ||
|
|
ffd544c42a | ||
|
|
cb99269a6d | ||
|
|
74b9ea9f89 | ||
|
|
aa863662b8 | ||
|
|
118e78ceb2 | ||
|
|
b14cdf50e6 | ||
|
|
39dd9b03f9 | ||
|
|
5d55258383 | ||
|
|
3256809066 | ||
|
|
dfbb1e5b98 | ||
|
|
e9915c44f9 | ||
|
|
a90ba862df | ||
|
|
b6ab624862 | ||
|
|
f66138505f | ||
|
|
dab91bdfac | ||
|
|
101d155855 | ||
|
|
10d7b4905b | ||
|
|
3afad00a84 | ||
|
|
ab8b06bacd | ||
|
|
9cfb96e358 | ||
|
|
2fb79735c3 | ||
|
|
a4e59986a8 | ||
|
|
93b4ec47d1 | ||
|
|
a4b1d12f36 | ||
|
|
217485e021 | ||
|
|
1f41d6d282 | ||
|
|
6876a8d649 | ||
|
|
02c159c5fd | ||
|
|
a1e204c117 | ||
|
|
ece9f7cd46 | ||
|
|
16dab991e0 | ||
|
|
c17975943a | ||
|
|
9d9ee98eaf | ||
|
|
b0bcac6926 | ||
|
|
3b7ad53994 | ||
|
|
0fedfae1e6 | ||
|
|
3fa2a35c12 | ||
|
|
a39859a39e | ||
|
|
d2f4a84314 | ||
|
|
0959eacf39 | ||
|
|
78c4b3b264 | ||
|
|
9f4a841eb6 | ||
|
|
9b034fcbb2 | ||
|
|
81cf964168 | ||
|
|
b5158210a9 | ||
|
|
d1602710a8 | ||
|
|
ec1453831d | ||
|
|
c63af629ac | ||
|
|
916b1d46fc | ||
|
|
7df694fe49 | ||
|
|
071289b56e | ||
|
|
eca355016c | ||
|
|
0053d12407 | ||
|
|
4d6ee985b9 | ||
|
|
94d74d35db | ||
|
|
d6a157578d | ||
|
|
0b9f3106fe | ||
|
|
17c75bf671 | ||
|
|
d6a9c2ed90 | ||
|
|
9931dc13a7 | ||
|
|
4a3d5a7d66 | ||
|
|
ac2cfc1266 |
No files matched your search
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates the contributions table in README.md, between its markers, from the repository's
|
||||
# pull requests: one row per pull request by anyone but the owner, and a line of totals per
|
||||
# contributor. Run by the Contributions ledger workflow when a pull request is opened, merged or
|
||||
# closed, and by hand with: gh auth status && .github/scripts/contributions.sh OWNER/REPO
|
||||
set -euo pipefail
|
||||
REPO=${1:-The412Banner/SteamDeck}
|
||||
OWNER=${REPO%%/*}
|
||||
ROWS=$(gh api "repos/$REPO/pulls?state=all&per_page=100" --paginate \
|
||||
--jq '.[] | select(.user.login != "'"$OWNER"'")
|
||||
| [.user.login, .number, (.title | gsub("\\|"; "\\\\|")), (.created_at[0:10]),
|
||||
(if .merged_at then "merged " + .merged_at[0:10] elif .state == "closed" then "closed" elif .draft then "draft" else "open" end),
|
||||
.html_url] | @tsv' | sort -t"$(printf '\t')" -k1,1 -k2,2n)
|
||||
{
|
||||
echo "<!-- contributions:start -->"
|
||||
echo "_From the repository's pull requests; rewritten when one is opened, merged or closed._"
|
||||
echo
|
||||
echo "| Contributor | Pull request | Opened | State |"
|
||||
echo "|---|---|---|---|"
|
||||
if [ -z "$ROWS" ]; then
|
||||
echo "| — | none yet | | |"
|
||||
else
|
||||
while IFS=$'\t' read -r login num title opened state url; do
|
||||
echo "| [@$login](https://github.com/$login) | [#$num]($url) $title | $opened | $state |"
|
||||
done <<< "$ROWS"
|
||||
echo
|
||||
printf '%s\n' "$ROWS" | awk -F'\t' 'NF { t[$1]++; if ($5 ~ /^merged/) m[$1]++ } END { for (l in t) printf("- **@%s**: %d submitted, %d merged\n", l, t[l], m[l] + 0) }' | sort
|
||||
fi
|
||||
echo "<!-- contributions:end -->"
|
||||
} > /tmp/contributions.md
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
s = open('README.md').read()
|
||||
new = open('/tmp/contributions.md').read().rstrip('\n')
|
||||
out, n = re.subn(r'<!-- contributions:start -->.*?<!-- contributions:end -->', lambda m: new, s, flags=re.S)
|
||||
if n != 1:
|
||||
raise SystemExit("README.md has no contributions markers")
|
||||
open('README.md', 'w').write(out)
|
||||
PY
|
||||
echo "README.md: contributions table rewritten"
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Build AAudio sink module
|
||||
|
||||
# A quick check of tools/aaudio-sink on its own; the APK build compiles the same module into the
|
||||
# audio bundle. Runs on pushes that touch the module.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/aaudio-sink/**'
|
||||
- '.github/workflows/build-aaudio-sink.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: PulseAudio 13.0 source
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
||||
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1
|
||||
- name: Build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/out"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: audio-sink-modules
|
||||
path: out/*.so
|
||||
@@ -0,0 +1,101 @@
|
||||
name: Build desktop package (KDE Plasma)
|
||||
|
||||
# The desktop the app installs into its Linux runtime on first use: KDE Plasma as SteamOS's desktop
|
||||
# mode has it, the closure of tools/desktop-kde/seeds.txt over Arch Linux ARM minus what the runtime
|
||||
# already carries, plus tools/desktop-kde/overlay (tools/desktop-kde/build.sh), every package
|
||||
# signature-checked. Published as a release in Droid-Deck/DroidDeck-Components with DroidDeck's
|
||||
# other components; the apk pins it in tools/desktop-kde/release.env (tag, sha256, size). A
|
||||
# published tag is never rebuilt: the packages are Arch Linux ARM's current ones, so a rebuild
|
||||
# would not match the pinned sha256. A changed package gets a new tag (desktop-kde-r2, ...) and a
|
||||
# release.env update.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/desktop-kde/**'
|
||||
- '.github/workflows/build-desktop-kde.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to publish as (e.g. desktop-kde-r1); empty = artifact only'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
kwin:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build KWin with nested desktop scaling
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv patch binutils >/dev/null
|
||||
bash tools/desktop-kde/build-kwin.sh out/kwin
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-kde-kwin
|
||||
retention-days: 7
|
||||
path: out/kwin/
|
||||
build:
|
||||
needs: kwin
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: desktop-kde-kwin
|
||||
path: out/kwin
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: components-token
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-Components
|
||||
- name: Refuse a published tag
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.components-token.outputs.token }}
|
||||
run: |
|
||||
if gh release view '${{ inputs.tag }}' -R Droid-Deck/DroidDeck-Components >/dev/null 2>&1; then
|
||||
echo "::error::${{ inputs.tag }} is already published, and published tags are never replaced: use a new tag"
|
||||
exit 1
|
||||
fi
|
||||
- name: Build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv >/dev/null
|
||||
KDE_KWIN_OVERRIDE="$PWD/out/kwin" tools/desktop-kde/build.sh out
|
||||
cp out/kwin/desktop-kde.kwin.txt out/
|
||||
echo "size $(stat -c %s out/desktop-kde.tar.zst) sha256 $(cat out/desktop-kde.sha256)"
|
||||
# The package itself too, briefly: enough to try a build on a device before publishing it.
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-kde
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
path: |
|
||||
out/desktop-kde.tar.zst
|
||||
out/desktop-kde.packages.txt
|
||||
out/desktop-kde.kwin.txt
|
||||
out/desktop-kde.sha256
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.components-token.outputs.token }}
|
||||
GH_REPO: Droid-Deck/DroidDeck-Components
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG='${{ inputs.tag }}'
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
echo "::error::$TAG is already published in DroidDeck-Components, and published tags are never replaced: use a new tag"
|
||||
exit 1
|
||||
fi
|
||||
gh release create "$TAG" --latest=false --prerelease=false --title "$TAG" \
|
||||
--notes "KDE Plasma desktop package for DroidDeck's Linux runtime ($GITHUB_REPOSITORY tools/desktop-kde, $GITHUB_SHA). size $(stat -c %s out/desktop-kde.tar.zst), sha256 $(cat out/desktop-kde.sha256). desktop-kde.packages.txt lists the signature-checked Arch Linux ARM packages it was built from." \
|
||||
out/desktop-kde.tar.zst out/desktop-kde.sha256 out/desktop-kde.packages.txt out/desktop-kde.kwin.txt
|
||||
gh release edit "$TAG" --latest=false --prerelease=false
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Build gamescope (patched)
|
||||
|
||||
# The runtime's own gamescope (Arch Linux ARM's 3.16.29 package) rebuilt with the patches under
|
||||
# tools/gamescope/patches, in an Arch Linux ARM container on an arm64 runner, then published as a
|
||||
# release asset the apk build fetches (tools/gamescope/release.env names the tag and sha). The
|
||||
# hosted runtime image is never changed: the apk stages the binary over /usr/local/bin.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/gamescope/**'
|
||||
- '.github/workflows/build-gamescope.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to publish as (e.g. gamescope-3.16.29-p1); empty = artifact only'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build in Arch Linux ARM
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/gamescope/build-in-arch.sh
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gamescope-patched
|
||||
path: |
|
||||
gamescope.tzst
|
||||
gamescope.tzst.sha256
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber
|
||||
# A component the apk fetches by tag, never something users install: it must not
|
||||
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
|
||||
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
|
||||
+523
-143
@@ -1,30 +1,14 @@
|
||||
name: Build APK
|
||||
|
||||
# One job: the two glibc preload libraries the session needs, then the app around them.
|
||||
# Nothing is built on a developer machine — this workflow is the only thing that produces an apk.
|
||||
#
|
||||
# This repo is private, so every minute here is billed. Three caches carry the cost of a rebuild
|
||||
# down to what actually changed:
|
||||
# • Gradle's own (dependencies, wrapper, build cache, configuration cache) via setup-gradle
|
||||
# • the CMake/ninja tree, so a Kotlin-only change does not recompile the compositor
|
||||
# • the two cross-compiled preloads, which also skips installing the cross toolchain
|
||||
# A run that changes only Kotlin should not touch the C compiler at all.
|
||||
run-name: ${{ github.event_name == 'pull_request' && format('building pr {0} on {1}', github.event.pull_request.number, github.event.pull_request.head.sha) || format('building {0} on {1}', github.ref_name, github.sha) }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
# Documentation changes cannot alter the apk, so they do not buy a build.
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'LICENSE'
|
||||
- '.gitignore'
|
||||
# A pull request against main builds by itself: the contributor's branch, as it would be once
|
||||
# merged onto main, comes back as an apk on the run without anyone here pressing a button.
|
||||
# Every push to the pull request rebuilds it and cancels the build it replaces; a draft waits
|
||||
# until it is marked ready. pull_request_target rather than pull_request: GitHub does not run
|
||||
# the latter at all while a pull request conflicts with main, and saying what conflicts is the
|
||||
# point. The workflow file is always main's; the pull request's own code is checked out below.
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
paths-ignore:
|
||||
@@ -35,38 +19,75 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# The pull-request report below is one comment on the pull request, kept up to date.
|
||||
pull-requests: write
|
||||
|
||||
# A newer push makes the running build pointless; stop paying for it.
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
# Replace older builds only for the same PR. Each push/dispatch gets its own group so one main
|
||||
# build cannot cancel another before it signs and publishes.
|
||||
group: build-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
# Bump to force the preload cache to miss (e.g. after changing a compiler flag below).
|
||||
PRELOAD_CACHE_VERSION: v1
|
||||
PRELOAD_CACHE_VERSION: v3
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.event_name != 'pull_request_target' || !github.event.pull_request.draft }}
|
||||
name: Build APK
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
variants: ${{ steps.variants.outputs.list }}
|
||||
steps:
|
||||
# A pull request is checked out at its own head with full history, not at GitHub's merge
|
||||
# ref: that ref does not exist while the pull request conflicts with main, and the point of
|
||||
# the next step is to say so and show where.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
# Full history: the merge below needs it (a shallow clone refuses to merge at all).
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
# The pull request as it would be once merged onto main is what gets built. When it cannot
|
||||
# be merged, the conflicting files and the conflicting hunks go into the run summary and the
|
||||
# pull-request comment, and the job stops here: there is nothing coherent to build.
|
||||
- name: Check co-authors
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
base=$(git merge-base origin/main HEAD)
|
||||
messages=$(git log --format=%B "$base..HEAD")
|
||||
messages+=$'\n'"$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH")"
|
||||
if grep -Eiq '^[[:space:]]*Co-authored-by:.*(Claude|Anthropic|OpenAI|ChatGPT|GPT|Copilot|Cursor|Gemini|Codex|Devin|Windsurf|Codeium|Amazon Q|Tabnine|Cody|\[bot\])' <<<"$messages"; then
|
||||
echo "::error::AI and bot identities cannot be co-authors; remove the Co-authored-by trailer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: The packages a signed build ships as
|
||||
id: variants
|
||||
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: No private key in the tree
|
||||
run: |
|
||||
set -euo pipefail
|
||||
keys=$(git ls-files | grep -iE '\.(jks|keystore|p12|pfx|pk8|pem|key)$' | grep -vx 'keystore/testkey.p12' || true)
|
||||
if [ -n "$keys" ]; then
|
||||
echo "::error::a private key is committed - remove it from the history and make a new one: $keys"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Version bumps are isolated
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
base=$(git merge-base origin/main HEAD)
|
||||
diff=$(git diff -U0 "$base..HEAD" -- app/build.gradle)
|
||||
grep -Eq '^[+-][[:space:]]*version(Code|Name)[[:space:]]' <<<"$diff" || exit 0
|
||||
other_files=$(git diff --name-only "$base..HEAD" | grep -v '^app/build.gradle$' || true)
|
||||
other_gradle=$(grep -E '^[+-]' <<<"$diff" | grep -Ev '^(---|\+\+\+)' | grep -Ev '^[+-][[:space:]]*version(Code|Name)[[:space:]]' || true)
|
||||
if [[ -n "$other_files" || -n "$other_gradle" ]]; then
|
||||
echo "::error::versionCode/versionName changes must be isolated in their own PR"
|
||||
echo "A version PR may only change the versionCode/versionName lines in app/build.gradle." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Merge onto main, or say what conflicts
|
||||
if: github.event_name == 'pull_request_target'
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -uo pipefail
|
||||
git config user.name "ci"
|
||||
@@ -76,7 +97,7 @@ jobs:
|
||||
HEAD=$(git rev-parse --short HEAD)
|
||||
if git merge --no-ff --no-edit origin/main >/tmp/merge.out 2>&1; then
|
||||
{
|
||||
echo "### ✅ Mergeable"
|
||||
echo "### Mergeable"
|
||||
echo
|
||||
echo "\`$HEAD\` merges cleanly onto main (\`$BASE\`). What follows was built from the merged result."
|
||||
} > /tmp/pr-merge.md
|
||||
@@ -85,9 +106,8 @@ jobs:
|
||||
fi
|
||||
FILES=$(git diff --name-only --diff-filter=U)
|
||||
if [ -z "$FILES" ]; then
|
||||
# The merge did not even start (git said why): quote it rather than list nothing.
|
||||
{
|
||||
echo "### ❌ Could not merge onto main (\`$BASE\`)"
|
||||
echo "### ERROR: Could not merge onto main (\`$BASE\`)"
|
||||
echo
|
||||
echo '```'
|
||||
cat /tmp/merge.out
|
||||
@@ -98,7 +118,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "### ❌ Not mergeable — conflicts with main (\`$BASE\`)"
|
||||
echo "### ERROR: Not mergeable - conflicts with main (\`$BASE\`)"
|
||||
echo
|
||||
echo "Files that conflict:"
|
||||
echo
|
||||
@@ -127,8 +147,6 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
# Caches ~/.gradle (dependencies, wrapper distribution, build cache, configuration cache)
|
||||
# and prunes what the run did not use, so the entry cannot grow without bound.
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-cleanup: on-success
|
||||
@@ -137,82 +155,291 @@ jobs:
|
||||
run: |
|
||||
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
VERSION=$(sed -n 's/^Pkg.Revision *= *//p' "$NDK/source.properties")
|
||||
echo "NDK $VERSION at $NDK"
|
||||
echo "NDK_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
# ── The session's preloads ───────────────────────────────────────────────────────────
|
||||
# Both are glibc aarch64 libraries that live inside the rootfs, so they are cross-compiled
|
||||
# here rather than by the NDK. They ship in assets (not in the runtime tarball) so a fix in
|
||||
# either reaches an already-installed runtime on the next launch instead of waiting on a
|
||||
# ~790 MB runtime re-host. Their inputs change rarely, so most runs restore them.
|
||||
- name: Set the APK build label
|
||||
id: build-label
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BUILD_REF: ${{ github.ref_name }}
|
||||
BUILD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
tree_state=dirty
|
||||
else
|
||||
tree_state=clean
|
||||
fi
|
||||
echo "DROIDDECK_BUILD_TREE_STATE=$tree_state" >> "$GITHUB_ENV"
|
||||
if [[ -n "$PR_NUMBER" ]]; then
|
||||
label="pr${PR_NUMBER} at ${PR_HEAD_SHA:0:7}"
|
||||
else
|
||||
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
|
||||
fi
|
||||
echo "label=$label" >> "$GITHUB_OUTPUT"
|
||||
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
|
||||
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
|
||||
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Cache the preloads
|
||||
id: preloads
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/assets/linuxfs
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/desktop/*') }}
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/linuxfs/debian-image.env') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D"
|
||||
# Statically linked C++ runtime: this is preloaded into every Steam process, and Steam
|
||||
# ships its own libstdc++ while rewriting LD_LIBRARY_PATH. Pulling the system one in
|
||||
# ahead of it is a version conflict waiting to happen. Only libc is left as a dependency.
|
||||
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
|
||||
-o "$D/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libfakeinput.so"
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
|
||||
# The session's scripts travel the same way, for the same reason.
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so"
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -nostdlib -ffreestanding -fno-stack-protector \
|
||||
-fno-tree-loop-distribute-patterns -Wl,-z,defs -o "$D/libblaudit.so" tools/proot/fastpath/audit.c
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblaudit.so"
|
||||
# Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it.
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so"
|
||||
mkdir -p "$D/usr/local/bin"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
|
||||
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
|
||||
done
|
||||
# And the desktop's launcher and labwc defaults, staged over the desktop package's copies.
|
||||
install -Dm644 tools/linuxfs/desktop/steamdeck-desktop "$D/usr/local/bin/steamdeck-desktop"
|
||||
install -Dm644 tools/linuxfs/desktop/autostart "$D/etc/xdg/labwc/autostart"
|
||||
install -Dm644 tools/linuxfs/desktop/rc.xml "$D/etc/xdg/labwc/rc.xml"
|
||||
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
|
||||
install -Dm644 tools/linuxfs/desktop/firefox-steamdeck.js "$D/usr/lib/firefox/defaults/pref/steamdeck.js"
|
||||
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
|
||||
[ -f "$f" ] && install -Dm644 "$f" "$D/${f#tools/linuxfs/overlay/}"
|
||||
done
|
||||
if [ -f tools/gamescope/release.env ]; then
|
||||
. tools/gamescope/release.env
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}" -p gamescope.tzst -O gamescope.tzst
|
||||
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
|
||||
test -f "$D/usr/local/bin/gamescope"
|
||||
rm gamescope.tzst
|
||||
fi
|
||||
. tools/linuxfs/uruntime.env
|
||||
curl -fsSL --retry 3 -o uruntime "https://github.com/VHSgunzo/uruntime/releases/download/${URUNTIME_VERSION}/${URUNTIME_ASSET}"
|
||||
echo "${URUNTIME_SHA256} uruntime" | sha256sum -c -
|
||||
install -Dm644 uruntime "$D/usr/local/lib/droiddeck/uruntime"
|
||||
install -Dm644 tools/linuxfs/licenses/uruntime-LICENSE "$D/usr/local/share/licenses/uruntime/LICENSE"
|
||||
rm uruntime
|
||||
M="$D/usr/local/lib/mangoapp"
|
||||
mkdir -p "$M" mango-pkgs
|
||||
grep -v '^#' tools/mangoapp/packages.txt | while read -r sum url; do
|
||||
[ -n "$url" ] || continue
|
||||
curl -fsSL --retry 3 -o mango-pkgs/pkg.tar.zst "$url"
|
||||
echo "$sum mango-pkgs/pkg.tar.zst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf mango-pkgs/pkg.tar.zst -C mango-pkgs
|
||||
rm mango-pkgs/pkg.tar.zst
|
||||
done
|
||||
install -m644 mango-pkgs/usr/bin/mangoapp "$M/mangoapp"
|
||||
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
|
||||
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
|
||||
done
|
||||
# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c).
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
|
||||
-o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1"
|
||||
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
|
||||
rm -rf mango-pkgs
|
||||
ls -l "$M"
|
||||
# The Windows components installer's tools (tools/msitools/packages.txt).
|
||||
T="$D/usr/local/lib/droiddeck-msitools"
|
||||
mkdir -p "$T" msi-pkgs
|
||||
grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do
|
||||
[ -n "$url" ] || continue
|
||||
curl -fsSL --retry 3 -o msi-pkgs/pkg.tar.xz "$url"
|
||||
echo "$sum msi-pkgs/pkg.tar.xz" | sha256sum -c -
|
||||
tar -xJf msi-pkgs/pkg.tar.xz -C msi-pkgs
|
||||
rm msi-pkgs/pkg.tar.xz
|
||||
done
|
||||
install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo"
|
||||
install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract"
|
||||
# 7-Zip (the full build with its format plugin) opens installer .exe wrappers.
|
||||
install -m644 msi-pkgs/usr/lib/7zip/7z "$T/7z"
|
||||
install -m644 msi-pkgs/usr/lib/7zip/7z.so "$T/7z.so"
|
||||
for so in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
|
||||
cp -L "msi-pkgs/usr/lib/$so" "$T/$so"
|
||||
done
|
||||
for f in NOTICE GPL-2 GPL-3 LGPL-2.1 7zip-License 7zip-unRarLicense; do
|
||||
install -Dm644 "tools/msitools/licenses/$f" "$D/usr/local/share/licenses/droiddeck-msitools/$f"
|
||||
done
|
||||
rm -rf msi-pkgs
|
||||
ls -l "$T"
|
||||
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$D/usr/local/bin/droiddeck-desktop"
|
||||
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$D/usr/local/bin/droiddeck-gpu"
|
||||
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$D/usr/local/bin/droiddeck-desktop-gpu"
|
||||
install -Dm644 tools/linuxfs/desktop/kwin_wayland_wrapper "$D/usr/local/bin/kwin_wayland_wrapper"
|
||||
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
|
||||
ls -lR "$D"
|
||||
|
||||
# Runs whether the preloads were built or restored: readelf reads any ELF whatever the host,
|
||||
# so these assertions cost nothing and need no cross toolchain.
|
||||
# The Debian 11 image the preloads are compiled in comes from a release on this repo
|
||||
# (published once by ci-debian-image.yml, pinned in tools/linuxfs/debian-image.env): the
|
||||
# public registry mirrors rate-limit the shared runner addresses and failed builds on a
|
||||
# busy evening. Only if that download fails is the image pulled, with retries.
|
||||
- name: Fetch the Debian image the preloads build in
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. tools/linuxfs/debian-image.env
|
||||
image=public.ecr.aws/docker/library/debian:bullseye
|
||||
if gh release download "$DEBIAN_IMAGE_TAG" -R "$GITHUB_REPOSITORY" -p "$DEBIAN_IMAGE_ASSET" -O /tmp/debian-image.tar.zst \
|
||||
&& { [ -z "$DEBIAN_IMAGE_SHA256" ] || echo "$DEBIAN_IMAGE_SHA256 /tmp/debian-image.tar.zst" | sha256sum -c -; }; then
|
||||
zstd -dc /tmp/debian-image.tar.zst | docker load
|
||||
rm -f /tmp/debian-image.tar.zst
|
||||
else
|
||||
echo "::warning::the hosted Debian image could not be fetched; pulling from the registry instead"
|
||||
for attempt in 1 2 3 4 5; do
|
||||
docker pull "$image" && break
|
||||
[ "$attempt" -lt 5 ] || { echo "::error::could not pull $image after $attempt attempts"; exit 1; }
|
||||
echo "pull attempt $attempt failed (registry busy?); retrying in $((attempt * 30)) s"
|
||||
sleep $((attempt * 30))
|
||||
done
|
||||
fi
|
||||
docker image ls "$image"
|
||||
|
||||
- name: Build the x86 preloads FEX guests load (glibc 2.31)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -v "$PWD:/src" -w /src public.ecr.aws/docker/library/debian:bullseye bash -c '
|
||||
set -euo pipefail
|
||||
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
|
||||
apt-get -o Acquire::Check-Valid-Until=false update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
|
||||
tools/linuxfs/build-x86-preloads.sh app/src/main/assets/linuxfs
|
||||
'
|
||||
sudo chown -R "$(id -u):$(id -g)" app/src/main/assets/linuxfs
|
||||
ls -l app/src/main/assets/linuxfs/x86_64 app/src/main/assets/linuxfs/i386
|
||||
|
||||
- name: Bundle the droiddeck-esync packs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f tools/droiddeck-esync/release.env ]; then
|
||||
. tools/droiddeck-esync/release.env
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p "$SYNC_BUNDLE_ASSET" -O sync-bundle.tzst
|
||||
echo "$SYNC_BUNDLE_SHA256 sync-bundle.tzst" | sha256sum -c -
|
||||
mkdir -p app/src/main/assets/droiddeck-esync
|
||||
tar --use-compress-program=unzstd -xf sync-bundle.tzst -C app/src/main/assets/droiddeck-esync
|
||||
rm sync-bundle.tzst
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p index.json -O sync-index.json
|
||||
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' sync-index.json)
|
||||
for id in $revoked; do
|
||||
if [ -e "app/src/main/assets/droiddeck-esync/packs/$id.tzst" ]; then
|
||||
echo "::warning::$SYNC_BUNDLE_ASSET carries revoked pack $id; it is left out of the APK (point tools/droiddeck-esync/release.env at a newer bundle)"
|
||||
rm -f "app/src/main/assets/droiddeck-esync/packs/$id.tzst"
|
||||
fi
|
||||
done
|
||||
rm sync-index.json
|
||||
test -f app/src/main/assets/droiddeck-esync/index.json
|
||||
test -f app/src/main/assets/droiddeck-esync/index.json.sig
|
||||
ls -lR app/src/main/assets/droiddeck-esync
|
||||
fi
|
||||
|
||||
- name: Cache PulseAudio 13.0 source
|
||||
id: pa-src
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: pa13
|
||||
key: pulseaudio-src-13.0
|
||||
- name: Fetch PulseAudio 13.0 source
|
||||
if: steps.pa-src.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
||||
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz
|
||||
# DirectAudio - the game driver (one set per Wine audio interface), the Android-side helper and
|
||||
# the Steam client's sinks - comes from a pinned release of The412Banner/directaudio, checksummed,
|
||||
# exactly like gamescope; nothing compiled for audio is kept in this repo.
|
||||
- name: Fetch DirectAudio (tools/directaudio/release.env)
|
||||
run: tools/directaudio/fetch.sh "$PWD" "$PWD/sink-out"
|
||||
- name: Build the AAudio sink into the audio bundle
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/sink-out"
|
||||
rm -rf pa-bundle && mkdir -p pa-bundle
|
||||
tar --use-compress-program=unzstd -xf app/src/main/assets/pulseaudio.tzst -C pa-bundle
|
||||
test ! -f pa-bundle/modules/arm64/module-aaudio-sink.so
|
||||
install -m755 sink-out/module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
|
||||
install -m755 sink-out/module-directaudio-native-sink.so pa-bundle/modules/arm64/module-directaudio-native-sink.so
|
||||
(cd pa-bundle && tar --use-compress-program='zstd -19' -cf ../pulseaudio.tzst .)
|
||||
mkdir -p app/build/prepared-assets
|
||||
mv pulseaudio.tzst app/build/prepared-assets/pulseaudio.tzst
|
||||
tar --use-compress-program=unzstd -tf app/build/prepared-assets/pulseaudio.tzst | grep -c '\.so$'
|
||||
rm -rf pa-bundle sink-out
|
||||
- name: Cache proot
|
||||
id: proot
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
app/src/main/jniLibs/arm64-v8a/libproot.so
|
||||
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
key: proot-${{ env.NDK_VERSION }}-${{ hashFiles('tools/proot/**') }}
|
||||
- name: Build proot
|
||||
if: steps.proot.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
tools/proot/build.sh app/src/main/jniLibs/arm64-v8a
|
||||
|
||||
- name: Check the preloads
|
||||
run: |
|
||||
set -euo pipefail
|
||||
D=app/src/main/assets/linuxfs
|
||||
NEED=$(readelf -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p')
|
||||
echo "libfakeinput NEEDED: $NEED"
|
||||
for bad in libstdc++.so.6 libgcc_s.so.1; do
|
||||
if printf '%s\n' "$NEED" | grep -qx "$bad"; then
|
||||
echo "::error::libfakeinput.so links $bad — the C++ runtime must stay static"; exit 1
|
||||
fi
|
||||
done
|
||||
syms() { readelf -Ws "$1" | awk '$4 == "FUNC" && $5 == "GLOBAL" {sub(/@.*/, "", $8); print $8}'; }
|
||||
# The interposers are the whole point: if one is missing the client sees no controller.
|
||||
FAKE=$(syms "$D/libfakeinput.so")
|
||||
for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do
|
||||
grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; }
|
||||
done
|
||||
SESSION=$(syms "$D/libblsession.so")
|
||||
for sym in socket bind getsockname setsockopt statfs statvfs; do
|
||||
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
|
||||
done
|
||||
test -f "$D/usr/local/bin/bannerlator-session"
|
||||
test -f "$D/usr/local/bin/bannerlator-proton-extra"
|
||||
echo "preloads check out"
|
||||
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
|
||||
|
||||
# The store download engines (app/src/main/rust/stores) are Rust, built by Gradle's
|
||||
# buildRustStores task through cargo-ndk. rustup and cargo are on the runner image; the Android
|
||||
# target and cargo-ndk are added here. Cache = cargo registry + cargo-ndk + the crate's target
|
||||
# tree, keyed on the rustc version and Cargo.lock, so a source-only change rebuilds this crate
|
||||
# on top of the cached dependency objects, and a lockfile change still starts from the
|
||||
# previous objects (restore-keys).
|
||||
- name: Rust toolchain for the store engines (aarch64-linux-android)
|
||||
id: rust
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rustup target add aarch64-linux-android
|
||||
echo "rustc=$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
|
||||
rustc -V; cargo -V
|
||||
|
||||
- name: Cache cargo registry and the store engines' target
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
~/.cargo/bin/cargo-ndk
|
||||
app/src/main/rust/stores/target
|
||||
key: stores-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
stores-${{ steps.rust.outputs.rustc }}-
|
||||
|
||||
- name: cargo-ndk
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v cargo-ndk >/dev/null || cargo install cargo-ndk --locked
|
||||
cargo ndk --version
|
||||
|
||||
# ── The native build ─────────────────────────────────────────────────────────────────
|
||||
# The compositor and adrenotools are the slowest part of a build and change least often.
|
||||
# The key pins the C sources and the NDK; the restore-key still hands an older tree to
|
||||
# ninja, which then rebuilds only the files that differ.
|
||||
- name: Cache the CMake build tree
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -223,55 +450,57 @@ jobs:
|
||||
restore-keys: |
|
||||
cxx-${{ env.NDK_VERSION }}-
|
||||
|
||||
# Release, not debug: AGP quietly drops v1 (JAR) signing from a debug build once minSdk is
|
||||
# 24 or higher, and the apk has to carry all three schemes. Nothing is minified either way.
|
||||
# The whole log is kept so a failed build's errors can be quoted back on a pull request.
|
||||
- name: Build
|
||||
id: build
|
||||
env:
|
||||
DROIDDECK_BUILD_LABEL: ${{ steps.build-label.outputs.label }}
|
||||
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 tools/release/runtime_inputs.py record
|
||||
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
|
||||
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
|
||||
python3 tools/release/runtime_inputs.py apk app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
- name: Describe the apk
|
||||
- name: Check the store engines' library
|
||||
# The exports Kotlin binds must be in the apk's libdroiddeckstores.so, or the stores fail on
|
||||
# the device with an UnsatisfiedLinkError instead of here. The lockfile check is the
|
||||
# reproducibility guard: cargo must not have had to re-resolve anything.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
||||
echo "apk: $APK"
|
||||
ls -l "$APK"
|
||||
sha256sum "$APK"
|
||||
unzip -l "$APK" | grep -E 'lib/arm64-v8a|assets/linuxfs' | head -30
|
||||
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
python3 tools/release/check_android_libraries.py app/build/outputs/apk/release/app-release.apk \
|
||||
--readelf "$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
|
||||
git diff --stat -- app/src/main/rust/stores/Cargo.lock
|
||||
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock \
|
||||
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
|
||||
|
||||
# Every NEEDED of every library we ship must resolve inside the apk or on the device. The
|
||||
# first build shipped libwayland-server.so without libffi.so, and the app died on
|
||||
# System.loadLibrary the moment a session started — a check this step would have caught.
|
||||
- name: Check the native dependency closure
|
||||
- name: Store engines host unit tests
|
||||
# The crate's #[test] functions are pure host-side logic (manifest parsing, chunk plans,
|
||||
# assembly from a cache, hashes), so a plain `cargo test` on the runner's own architecture
|
||||
# runs them; the Android build above compiles none of them. Informational on this first
|
||||
# run - the point is to learn whether they pass - and bounded so a hang can never hold
|
||||
# the apk. Make it blocking once it is known green.
|
||||
timeout-minutes: 10
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
||||
WORK=$(mktemp -d)
|
||||
unzip -q "$APK" 'lib/arm64-v8a/*' -d "$WORK"
|
||||
cd "$WORK/lib/arm64-v8a"
|
||||
# Provided by the platform, so never packaged.
|
||||
SYSTEM="libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so
|
||||
libGLESv2.so libEGL.so libnativewindow.so libjnigraphics.so libaaudio.so
|
||||
libOpenSLES.so libmediandk.so libcamera2ndk.so libsync.so libneuralnetworks.so"
|
||||
fail=0
|
||||
for so in *.so; do
|
||||
for need in $(readelf -d "$so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p'); do
|
||||
[ -f "$need" ] && continue
|
||||
# $SYSTEM spans lines; unquoted echo folds its newlines to spaces, or the last
|
||||
# name on each line could never match (libaaudio.so did not, for the relay helper).
|
||||
case " $(echo $SYSTEM) " in *" $need "*) continue ;; esac
|
||||
echo "::error::$so needs $need, which is neither in the apk nor a platform library"
|
||||
fail=1
|
||||
done
|
||||
done
|
||||
[ "$fail" = 0 ] && echo "every NEEDED resolves"
|
||||
exit $fail
|
||||
cd app/src/main/rust/stores
|
||||
cargo test --locked 2>&1 | tail -60
|
||||
|
||||
- name: Upload the regenerated Cargo.lock
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stores-cargo-lock
|
||||
path: app/src/main/rust/stores/Cargo.lock
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Steam import and game environment tests
|
||||
run: python3 -m unittest discover -s tools/tests -p 'test_*.py'
|
||||
|
||||
- name: Unit tests
|
||||
run: ./gradlew testReleaseUnitTest --console=plain -PndkVersion="$NDK_VERSION"
|
||||
|
||||
# AGP signs with v2+v3 and drops v1 whenever minSdk is 24 or higher, and setting
|
||||
# enableV1Signing in the DSL does not move it. Re-signing here with apksigner is the one way
|
||||
# to be sure of what the apk carries: zipalign first, then all three schemes explicitly.
|
||||
- name: Sign with the testkey (v1 + v2 + v3)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -285,18 +514,12 @@ jobs:
|
||||
--out "$APK" "$APK.aligned"
|
||||
rm -f "$APK.aligned" "$APK.idsig"
|
||||
|
||||
# The apk must stay installable over the last one, and on every Android version: that means
|
||||
# one stable key and all three signature schemes. Cheap to assert, easy to lose silently.
|
||||
- name: Check the signature
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
||||
APKSIGNER=$(find "$ANDROID_HOME/build-tools" -name apksigner | sort -V | tail -1)
|
||||
# --min-sdk-version 21: apksigner only verifies the schemes an apk's own minSdk requires,
|
||||
# so at minSdk 26 it reports v1 as false whether or not the JAR signature is there.
|
||||
# Asking it to verify from 21 makes it actually look.
|
||||
"$APKSIGNER" verify --min-sdk-version 21 --verbose --print-certs "$APK" | tee /tmp/sig.txt
|
||||
# And the files themselves, which is what an old installer reads.
|
||||
unzip -l "$APK" | grep -qE 'META-INF/.*\.(SF|RSA|DSA)$' \
|
||||
|| { echo "::error::apk carries no JAR signature files"; exit 1; }
|
||||
for scheme in "v1 scheme (JAR signing): true" "v2 scheme (APK Signature Scheme v2): true" \
|
||||
@@ -306,21 +529,18 @@ jobs:
|
||||
grep -qi "CN=Android, OU=Android, O=Android" /tmp/sig.txt \
|
||||
|| { echo "::error::apk is not signed with the AOSP testkey"; exit 1; }
|
||||
|
||||
# ── The pull-request report ──────────────────────────────────────────────────────────
|
||||
# One comment per pull request, rewritten on every push: whether it merges, and when the
|
||||
# build failed, the compiler's own errors with paths made short. Fork pull requests carry a
|
||||
# read-only token and cannot comment; the same text is in the run summary either way.
|
||||
- name: Report on the pull request
|
||||
if: always() && github.event_name == 'pull_request_target'
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
BUILD_OUTCOME: ${{ steps.build.outcome }}
|
||||
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
MARK="<!-- steamdeck-pr-build -->"
|
||||
MARK="<!-- droiddeck-pr-build -->"
|
||||
{
|
||||
echo "$MARK"
|
||||
echo "## Build of this pull request"
|
||||
@@ -329,12 +549,20 @@ jobs:
|
||||
echo
|
||||
case "$BUILD_OUTCOME" in
|
||||
success)
|
||||
echo "### ✅ Build passed"
|
||||
echo "### Build passed"
|
||||
echo
|
||||
echo "The apk is the \`steamdeck-apk\` artifact on [this run]($RUN_URL) (signed with the same test key as every build, so it installs over any other)."
|
||||
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key."
|
||||
echo
|
||||
if [ "$SAME_REPO" = true ]; then
|
||||
echo "A release-signed tester build that installs over Stable or Preview is being published now and will appear in DroidDeck's **Test builds** list. Label the PR \`no-test-build\` to skip that."
|
||||
else
|
||||
echo "**Maintainer:** add the \`publish-test\` label to publish a release-signed tester build that installs over Stable or Preview and appears in DroidDeck's **Test builds** list. The label is removed automatically after publishing."
|
||||
fi
|
||||
echo
|
||||
echo "Fallback: [open **PR Release Publish**]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/pr-release-publish.yml) on \`main\` and enter PR \`$PR\`."
|
||||
;;
|
||||
failure)
|
||||
echo "### ❌ Build failed"
|
||||
echo "### ERROR: Build failed"
|
||||
echo
|
||||
echo "The compiler's errors ([full log]($RUN_URL)):"
|
||||
echo
|
||||
@@ -352,13 +580,12 @@ jobs:
|
||||
echo "</details>"
|
||||
;;
|
||||
skipped|cancelled)
|
||||
echo "### ⏭ Not built"
|
||||
echo "### Not built"
|
||||
echo
|
||||
echo "The build did not run (see above, or [the run]($RUN_URL))."
|
||||
;;
|
||||
esac
|
||||
echo
|
||||
echo "_Updated on every push to this pull request._"
|
||||
} > /tmp/pr-report.md
|
||||
cat /tmp/pr-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq ".[] | select(.body | startswith(\"$MARK\")) | .id" | head -1 || true)
|
||||
@@ -370,9 +597,162 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: steamdeck-apk
|
||||
name: droiddeck-apk
|
||||
path: app/build/outputs/apk/release/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
# An apk is a zip already; compressing it again buys nothing and costs seconds.
|
||||
compression-level: 0
|
||||
|
||||
sign:
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: signing-main
|
||||
variants: ${{ needs.build.outputs.variants }}
|
||||
secrets: inherit
|
||||
|
||||
# Each signed main build as a prerelease of the public Droid-Deck/DroidDeck-CI (main-<sha>, the
|
||||
# last KEEP kept), where anyone can download it without a GitHub account, then a post in the
|
||||
# Discord's builds channel linking to it. The release is made with a token of the org's
|
||||
# "DroidDeck CI Publisher" app (installed on DroidDeck-CI only, Contents: write): its ID in the
|
||||
# DROIDDECK_CI_APP_ID variable, its private key in the DROIDDECK_CI_APP_KEY secret.
|
||||
# DISCORD_BUILDS_WEBHOOK is the channel's webhook URL.
|
||||
announce:
|
||||
needs: [build, sign]
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
env:
|
||||
CI_REPO: Droid-Deck/DroidDeck-CI
|
||||
KEEP: 10
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: droiddeck-signed-*
|
||||
merge-multiple: true
|
||||
path: signed
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
|
||||
# What this build is, for the top of the release notes: the merged PR's title and the first
|
||||
# paragraph of its description, or the commit's subject for a push made without a PR.
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \
|
||||
--jq '[.[] | select(.merged_at != null and .base.ref == "main")] | first // empty')
|
||||
if [ -n "$pr" ]; then
|
||||
number=$(jq -r .number <<<"$pr")
|
||||
title=$(jq -r .title <<<"$pr")
|
||||
merged=$(jq -r '.merged_at[:10]' <<<"$pr")
|
||||
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
|
||||
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
|
||||
{
|
||||
echo "**Merged to main** - [PR #$number]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$number): $title (merged $merged)"
|
||||
if [ -n "$summary" ]; then
|
||||
echo
|
||||
echo "$summary"
|
||||
fi
|
||||
} > notes-head.md
|
||||
else
|
||||
subject=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA" --jq '.commit.message | split("\n")[0]')
|
||||
echo "**Pushed to main** without a PR - $subject" > notes-head.md
|
||||
fi
|
||||
cat notes-head.md
|
||||
|
||||
- name: Publish to DroidDeck-CI
|
||||
id: publish
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag=main-${GITHUB_SHA::7}
|
||||
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
|
||||
rm signed/SHA256SUMS-*.txt
|
||||
notes="$(cat notes-head.md)
|
||||
|
||||
Signed build of $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA - in main, not yet in a DroidDeck release; for testing.
|
||||
Commit: $GITHUB_SHA
|
||||
VersionCode: $(sed -n 's/^ *versionCode \([0-9][0-9]*\)$/\1/p' app/build.gradle)
|
||||
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
|
||||
|
||||
SHA-256:
|
||||
\`\`\`
|
||||
$(cat signed/SHA256SUMS.txt)
|
||||
\`\`\`"
|
||||
gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true
|
||||
gh release create "$tag" -R "$CI_REPO" --target main --prerelease \
|
||||
--title "main ${GITHUB_SHA::7}" --notes "$notes" signed/*
|
||||
# Keep the newest main builds by publication time. GitHub App releases can share createdAt.
|
||||
gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \
|
||||
--jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' |
|
||||
while IFS= read -r old; do
|
||||
# Never remove this run's release, even if the API returns an unexpected ordering.
|
||||
if [ "$old" = "$tag" ]; then
|
||||
continue
|
||||
fi
|
||||
# Another main publish may have removed the same old release concurrently.
|
||||
if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then
|
||||
if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then
|
||||
echo "::error::Could not delete old release $old"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
published=$(gh release view "$tag" -R "$CI_REPO" --json tagName,assets)
|
||||
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<< "$published" >/dev/null
|
||||
url="$GITHUB_SERVER_URL/$CI_REPO/releases/tag/$tag"
|
||||
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
|
||||
echo "release=$url" >> "$GITHUB_OUTPUT"
|
||||
echo "apk=$GITHUB_SERVER_URL/$CI_REPO/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
|
||||
echo "Published $url"
|
||||
|
||||
- name: Post to Discord
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE: ${{ steps.publish.outputs.release }}
|
||||
APK: ${{ steps.publish.outputs.apk }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$WEBHOOK" ]; then
|
||||
echo "::notice::No DISCORD_BUILDS_WEBHOOK secret; nothing posted"
|
||||
exit 0
|
||||
fi
|
||||
commit=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA")
|
||||
subject=$(jq -r '.commit.message | split("\n")[0]' <<<"$commit")
|
||||
# A merge commit's subject is "Merge pull request #N from ..."; its body's first line is
|
||||
# the PR's title, which says more.
|
||||
title=$(jq -r '.commit.message | split("\n") | map(select(length > 0)) | .[1] // ""' <<<"$commit")
|
||||
pr=$(sed -n 's/^Merge pull request #\([0-9]*\).*/\1/p' <<<"$subject")
|
||||
if [ -n "$pr" ]; then
|
||||
what="[#$pr]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$pr): ${title:-$subject}"
|
||||
else
|
||||
what="[\`${GITHUB_SHA::7}\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA): $subject"
|
||||
fi
|
||||
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
|
||||
content=$(printf '**New signed build** `main-%s`\n%s\n%s' "${GITHUB_SHA::7}" "$what" "$get")
|
||||
jq -n --arg content "$content" '{content: $content, allowed_mentions: {parse: []}}' |
|
||||
curl -fsS -H 'Content-Type: application/json' -d @- "$WEBHOOK" -o /dev/null
|
||||
echo "Posted: $content"
|
||||
|
||||
catalog:
|
||||
needs: announce
|
||||
if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && needs.announce.result != 'skipped' }}
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Publish the CI Debian image
|
||||
|
||||
# Pulls the Debian 11 image the x86 preloads build in, once, and publishes it as a release asset
|
||||
# on this repo so build.yml never depends on a registry mirror's rate limit. Run it again (with a
|
||||
# new tag) only to refresh the image; tools/linuxfs/debian-image.env names the one builds use.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to publish to (a new -rN tag for a refresh; builds pin the one in tools/linuxfs/debian-image.env)'
|
||||
required: true
|
||||
default: 'ci-debian-bullseye-r2'
|
||||
replace:
|
||||
description: 'Overwrite the asset if the release already has one (changes its checksum: re-pin it in debian-image.env)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Pull, save and publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
REPLACE: ${{ inputs.replace }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image=public.ecr.aws/docker/library/debian:bullseye
|
||||
# A published image is pinned by checksum in the build; never replace one by accident.
|
||||
if [ "$REPLACE" != true ] && gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' 2>/dev/null | grep -qx debian-bullseye.tar.zst; then
|
||||
echo "::error::$TAG already carries debian-bullseye.tar.zst; publish to a new tag, or set replace and re-pin tools/linuxfs/debian-image.env"
|
||||
exit 1
|
||||
fi
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
docker pull "$image" && break
|
||||
[ "$attempt" -lt 6 ] || { echo "::error::could not pull $image"; exit 1; }
|
||||
echo "pull attempt $attempt failed (registry busy?); retrying in $((attempt * 30)) s"
|
||||
sleep $((attempt * 30))
|
||||
done
|
||||
docker image inspect "$image" --format 'image id {{.Id}} created {{.Created}}'
|
||||
docker save "$image" | zstd -19 -T0 -o debian-bullseye.tar.zst
|
||||
sha256sum debian-bullseye.tar.zst | tee SHA256SUMS
|
||||
ls -l debian-bullseye.tar.zst
|
||||
if ! gh release view "$TAG" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$TAG" -R "$GITHUB_REPOSITORY" --title "CI Debian 11 image ($TAG)" --latest=false \
|
||||
--notes "The debian:bullseye image build.yml compiles the x86 preloads in (glibc 2.31), saved with docker save and compressed with zstd. Pulled from public.ecr.aws/docker/library/debian:bullseye on $(date -u +%Y-%m-%d); image id $(docker image inspect "$image" --format '{{.Id}}'). Pinned in tools/linuxfs/debian-image.env."
|
||||
fi
|
||||
gh release upload "$TAG" -R "$GITHUB_REPOSITORY" debian-bullseye.tar.zst SHA256SUMS --clobber
|
||||
{ echo "Published \`$TAG\`:"; echo; echo '```'; cat SHA256SUMS; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Contributions ledger
|
||||
|
||||
# Keeps the Contributors section of the README current: every pull request by a contributor,
|
||||
# with its state, and a line of totals each. Runs when a pull request is opened, merged or
|
||||
# closed and commits the README to main as the Actions bot. The README is documentation, so
|
||||
# that commit does not buy an apk build.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, reopened, closed, edited ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: contributions
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
ledger:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- name: Rewrite the table
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: .github/scripts/contributions.sh "$GITHUB_REPOSITORY"
|
||||
- name: Commit it when it changed
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add README.md
|
||||
if git diff --cached --quiet; then echo "ledger unchanged"; exit 0; fi
|
||||
git commit -m "README: contributions ledger"
|
||||
git push origin HEAD:main
|
||||
@@ -0,0 +1,78 @@
|
||||
name: PR Release Cleanup
|
||||
run-name: remove PR #${{ github.event.pull_request.number }}'s test build
|
||||
|
||||
# A merged or closed PR's test build leaves DroidDeck-CI, so the app's Updates page stops offering it
|
||||
# (a merged fix is in the next Preview). pull_request_target for the publisher app's secrets; nothing
|
||||
# from the PR is checked out or run.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ closed ]
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: pr-release-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check the current PR state
|
||||
id: state
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .state)
|
||||
echo "closed=$([ "$state" = closed ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
||||
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
|
||||
- uses: actions/create-github-app-token@v2
|
||||
if: ${{ steps.state.outputs.closed == 'true' }}
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
- name: Delete the test build
|
||||
if: ${{ steps.state.outputs.closed == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
found=0
|
||||
for tag in "pr-$PR_NUMBER" "pr-$PR_NUMBER.0"; do
|
||||
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
|
||||
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
|
||||
echo "Deleted $tag"
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
[ "$found" = 1 ] || echo "No test build for PR #$PR_NUMBER"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ steps.state.outputs.closed == 'true' }}
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
- name: Retire the Discord PR announcement
|
||||
if: ${{ steps.state.outputs.closed == 'true' }}
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_PR_BUILDS_WEBHOOK }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: python3 tools/release/pr_discord.py retire
|
||||
|
||||
catalog:
|
||||
needs: cleanup
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,341 @@
|
||||
name: PR Release Publish
|
||||
run-name: "sign PR #${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }} with the release key"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Open pull request number
|
||||
required: true
|
||||
type: string
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
branches: [main]
|
||||
# A PR from a branch of this repo is signed on its own after each green build: only people who
|
||||
# can push here can change it. A fork's PR waits for a maintainer's publish-test label.
|
||||
workflow_run:
|
||||
workflows: [Build APK]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-release-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.label.name == 'publish-test') ||
|
||||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository &&
|
||||
github.event.workflow_run.pull_requests[0] != null)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
pr_number: ${{ steps.gate.outputs.pr_number }}
|
||||
run_id: ${{ steps.gate.outputs.run_id }}
|
||||
head_sha: ${{ steps.gate.outputs.head_sha }}
|
||||
base_sha: ${{ steps.gate.outputs.base_sha }}
|
||||
version_code: ${{ steps.gate.outputs.version_code }}
|
||||
label: ${{ steps.gate.outputs.label }}
|
||||
skip: ${{ steps.gate.outputs.skip }}
|
||||
automatic: ${{ steps.gate.outputs.automatic }}
|
||||
variants: ${{ steps.variants.outputs.list }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Every variant a main build ships, so a PR build can be tried under any of them.
|
||||
- name: The packages a signed build ships as
|
||||
id: variants
|
||||
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
|
||||
- name: Check the requester, PR, and build
|
||||
id: gate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REQUESTER: ${{ github.actor }}
|
||||
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
|
||||
BUILT_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
|
||||
# The automatic path stands down quietly, never failing: a draft, a PR labelled
|
||||
# no-test-build, or a build that a later push has already overtaken.
|
||||
skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
|
||||
if [ "$EVENT_NAME" = workflow_run ]; then
|
||||
echo "automatic=true" >> "$GITHUB_OUTPUT"
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
[ "$(jq -r .state <<<"$pr")" = open ] || skip "it is not open"
|
||||
[ "$(jq -r .draft <<<"$pr")" = false ] || skip "it is a draft"
|
||||
[ "$(jq -r .base.ref <<<"$pr")" = main ] || skip "it does not target main"
|
||||
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
|
||||
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
|
||||
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
|
||||
fi
|
||||
if [ "$EVENT_NAME" = pull_request_target ]; then
|
||||
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$REQUESTER/permission" --jq .permission)
|
||||
case "$permission" in
|
||||
admin|maintain|write) ;;
|
||||
*) echo "::error::@$REQUESTER does not have permission to publish release-signed test builds"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null ||
|
||||
{ echo "::error::PR #$PR_NUMBER must be open, ready for review, and target main"; exit 1; }
|
||||
head=$(jq -r '.head.sha' <<<"$pr")
|
||||
base=$(jq -r '.base.sha' <<<"$pr")
|
||||
|
||||
# The label is often added while the PR's build is still running, or just after a push
|
||||
# before it has started: wait for the latest build of the head commit to finish (a PR
|
||||
# build takes about 4 minutes) rather than fail and need the label added again.
|
||||
run_id=
|
||||
for attempt in $(seq 1 60); do
|
||||
latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
|
||||
--event pull_request --commit "$head" \
|
||||
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
|
||||
jq -c --arg head "$head" '[.[] | select(.headSha == $head)] |
|
||||
sort_by([.createdAt, .attempt]) | last // empty')
|
||||
if [[ -n "$latest" && $(jq -r .status <<<"$latest") == completed ]]; then
|
||||
conclusion=$(jq -r .conclusion <<<"$latest")
|
||||
[[ "$conclusion" == success ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
|
||||
run_id=$(jq -r .databaseId <<<"$latest")
|
||||
break
|
||||
fi
|
||||
status=not-started
|
||||
[[ -z "$latest" ]] || status=$(jq -r .status <<<"$latest")
|
||||
echo "Waiting for the PR build of ${head:0:7} to finish ($status)"
|
||||
sleep 30
|
||||
done
|
||||
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
|
||||
# GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is
|
||||
# tied to this PR by its head repository, branch and commit instead.
|
||||
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
|
||||
jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
|
||||
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
|
||||
.head_repository.full_name == $repo and .head_branch == $ref
|
||||
' <<<"$run" >/dev/null ||
|
||||
{ echo "::error::Build run $run_id is not a green build of PR #$PR_NUMBER at ${head:0:7}"; exit 1; }
|
||||
|
||||
sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
|
||||
tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)
|
||||
# A PR build is made from the PR merged onto main, so it must install over main's own
|
||||
# build. main is what this job checked out; no release has to exist or be marked latest.
|
||||
main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle)
|
||||
[[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; }
|
||||
mkdir -p "$RUNNER_TEMP/pr"
|
||||
gh run download "$run_id" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D "$RUNNER_TEMP/pr"
|
||||
mapfile -t apks < <(find "$RUNNER_TEMP/pr" -type f -name '*.apk')
|
||||
[[ ${#apks[@]} == 1 ]] || { echo '::error::Expected one PR APK'; exit 1; }
|
||||
package_line() {
|
||||
"$tools/aapt" dump badging "$1" | sed -n "s/^package: name='\\([^']*\\)' versionCode='\\([^']*\\)'.*/\\1 \\2/p"
|
||||
}
|
||||
candidate=$(package_line "${apks[0]}")
|
||||
[[ "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && ${candidate##* } -ge $main_code ]] || {
|
||||
echo "::error::PR package/versionCode '$candidate' cannot install over main (versionCode $main_code)"; exit 1;
|
||||
}
|
||||
|
||||
current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
jq -e --arg head "$head" --arg base "$base" '
|
||||
.state == "open" and .draft == false and .base.ref == "main" and
|
||||
.head.sha == $head and .base.sha == $base
|
||||
' <<<"$current" >/dev/null ||
|
||||
{ echo "::error::PR #$PR_NUMBER moved on while its build was checked; publish again"; exit 1; }
|
||||
{
|
||||
echo "pr_number=$PR_NUMBER"
|
||||
echo "run_id=$run_id"
|
||||
echo "head_sha=$head"
|
||||
echo "base_sha=$base"
|
||||
echo "version_code=${candidate##* }"
|
||||
echo "label=PR.$PR_NUMBER-${head:0:7}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
sign:
|
||||
needs: authorize
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: signing-main
|
||||
variants: ${{ needs.authorize.outputs.variants }}
|
||||
run_id: ${{ needs.authorize.outputs.run_id }}
|
||||
label: ${{ needs.authorize.outputs.label }}
|
||||
artifact: droiddeck-pr-signed
|
||||
secrets: inherit
|
||||
if: ${{ needs.authorize.outputs.skip != 'true' }}
|
||||
|
||||
publish:
|
||||
needs: [authorize, sign]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: droiddeck-pr-signed-*
|
||||
merge-multiple: true
|
||||
path: signed
|
||||
# What this build is, for the top of the release notes: the PR's title and the first
|
||||
# paragraph of its description.
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
title=$(jq -r .title <<<"$pr")
|
||||
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
|
||||
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
|
||||
{
|
||||
echo "**Not merged** - a test build of open [PR #$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
|
||||
if [ -n "$summary" ]; then
|
||||
echo
|
||||
echo "$summary"
|
||||
fi
|
||||
} > notes-head.md
|
||||
cat notes-head.md
|
||||
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
- name: Publish to DroidDeck-CI
|
||||
id: publish
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
||||
BASE_SHA: ${{ needs.authorize.outputs.base_sha }}
|
||||
VERSION_CODE: ${{ needs.authorize.outputs.version_code }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo=Droid-Deck/DroidDeck-CI
|
||||
tag="pr-$PR_NUMBER"
|
||||
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
|
||||
rm signed/SHA256SUMS-*.txt
|
||||
notes="$(cat notes-head.md)
|
||||
|
||||
Source: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER
|
||||
Commit: $HEAD_SHA
|
||||
Base: $BASE_SHA
|
||||
VersionCode: $VERSION_CODE
|
||||
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
|
||||
|
||||
SHA-256:
|
||||
\`\`\`
|
||||
$(cat signed/SHA256SUMS.txt)
|
||||
\`\`\`"
|
||||
first=false
|
||||
if ! gh release view "$tag" -R "$repo" >/dev/null 2>&1; then
|
||||
gh release create "$tag" -R "$repo" --target main --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
||||
first=true
|
||||
fi
|
||||
echo "first=$first" >> "$GITHUB_OUTPUT"
|
||||
gh release upload "$tag" signed/* -R "$repo" --clobber
|
||||
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
||||
# Drop the previous build's files: anything this build did not just upload.
|
||||
keep=$(cd signed && ls | jq -R . | jq -sc .)
|
||||
gh api "repos/$repo/releases/tags/$tag" |
|
||||
jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' |
|
||||
while IFS= read -r id; do
|
||||
gh api --method DELETE "repos/$repo/releases/assets/$id" >/dev/null
|
||||
done
|
||||
|
||||
published=$(gh release view "$tag" -R "$repo" --json tagName,assets)
|
||||
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<<"$published" >/dev/null
|
||||
release="$GITHUB_SERVER_URL/$repo/releases/tag/$tag"
|
||||
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
|
||||
echo "release=$release" >> "$GITHUB_OUTPUT"
|
||||
echo "apk=$GITHUB_SERVER_URL/$repo/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Whether the PR's branch is in this repo
|
||||
id: same-repo
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
head_repo=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.repo.full_name)
|
||||
echo "same=$([ "$head_repo" = "$GITHUB_REPOSITORY" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clear the publish request label
|
||||
if: ${{ github.event_name == 'pull_request_target' && steps.publish.outcome == 'success' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
gh api --method DELETE "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/publish-test"
|
||||
|
||||
- name: Report the signed build on the PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
||||
RELEASE: ${{ steps.publish.outputs.release }}
|
||||
APK: ${{ steps.publish.outputs.apk }}
|
||||
SAME_REPO: ${{ steps.same-repo.outputs.same }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
short=${HEAD_SHA:0:7}
|
||||
mark='<!-- droiddeck-pr-release -->'
|
||||
if [ "$SAME_REPO" = true ]; then
|
||||
NEXT='Each green build of a new push is signed and published here on its own. Label the PR `no-test-build` to stop that.'
|
||||
else
|
||||
NEXT='Pushes to this PR do not re-sign automatically. After a new green build, a maintainer can add the `publish-test` label again.'
|
||||
fi
|
||||
body=$(cat <<EOF
|
||||
$mark
|
||||
## Signed test build
|
||||
|
||||
A release-signed build of this PR at \`$short\` is ready. It installs over Stable or Preview and keeps app data.
|
||||
|
||||
[Download the standard APK]($APK) · [All variants and checksums]($RELEASE)
|
||||
|
||||
It is also available in **DroidDeck → Updates → Test builds → PR #$PR_NUMBER**.
|
||||
|
||||
$NEXT
|
||||
EOF
|
||||
)
|
||||
comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate --slurp | \
|
||||
jq "[.[][] | select(.user.login == \"github-actions[bot]\") | select(.body | startswith(\"$mark\"))] | first // {}")
|
||||
id=$(jq -r '.id // empty' <<<"$comment")
|
||||
if [ -n "$id" ]; then
|
||||
state=$(jq -r '.body | split("\n")[] | select(startswith("<!-- droiddeck-discord: ") and endswith(" -->"))' <<<"$comment")
|
||||
if [ -n "$state" ]; then body=$(printf '%s\n%s' "$body" "$state"); fi
|
||||
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null
|
||||
else
|
||||
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null
|
||||
fi
|
||||
|
||||
- name: Update the Discord PR announcement
|
||||
if: ${{ always() && steps.publish.outcome == 'success' }}
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_PR_BUILDS_WEBHOOK }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
||||
RELEASE: ${{ steps.publish.outputs.release }}
|
||||
APK: ${{ steps.publish.outputs.apk }}
|
||||
run: python3 tools/release/pr_discord.py publish
|
||||
|
||||
catalog:
|
||||
needs: publish
|
||||
if: ${{ always() && needs.publish.result != 'skipped' }}
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,120 @@
|
||||
name: Release APKs
|
||||
run-name: release ${{ inputs.version }} from main ${{ github.sha }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version for the file names, e.g. 0.1.7
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
find:
|
||||
name: find the main build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
run_id: ${{ steps.find.outputs.run_id }}
|
||||
variants: ${{ steps.find.outputs.variants }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- id: find
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::version '$VERSION': letters, digits, . _ - only"; exit 1; }
|
||||
name=$(sed -n 's/^ *versionName "\(.*\)"$/\1/p' app/build.gradle)
|
||||
[[ $VERSION == "$name" || $VERSION == "$name"-* ]] || {
|
||||
echo "::error::version '$VERSION' does not match versionName '$name' in app/build.gradle: bump versionName and versionCode first"; exit 1;
|
||||
}
|
||||
runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml --branch main --commit "$GITHUB_SHA" \
|
||||
--status success --json databaseId,event,headSha -L 20)
|
||||
id=$(jq -r --arg sha "$GITHUB_SHA" \
|
||||
'[.[] | select(.headSha == $sha and (.event == "push" or .event == "workflow_dispatch"))][0].databaseId // empty' <<<"$runs")
|
||||
if [ -z "$id" ]; then
|
||||
echo "::error::main ${GITHUB_SHA::7} has no successful Build APK run yet: wait for it, then release"
|
||||
exit 1
|
||||
fi
|
||||
variants=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)
|
||||
echo "run_id=$id" >> "$GITHUB_OUTPUT"
|
||||
echo "variants=$variants" >> "$GITHUB_OUTPUT"
|
||||
echo "Signing Build APK run $id (main ${GITHUB_SHA::7}) as $VERSION: $variants" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
sign:
|
||||
needs: find
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: release
|
||||
variants: ${{ needs.find.outputs.variants }}
|
||||
run_id: ${{ needs.find.outputs.run_id }}
|
||||
label: ${{ inputs.version }}
|
||||
artifact: droiddeck-release
|
||||
secrets: inherit
|
||||
|
||||
# The release is made here, from the apks this run signed, so it traces back to its Build APK run.
|
||||
publish:
|
||||
name: publish the release
|
||||
needs: [find, sign]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: droiddeck-release-*
|
||||
merge-multiple: true
|
||||
path: out
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
BUILD_RUN: ${{ needs.find.outputs.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes="docs/releases/$VERSION.md"
|
||||
[ -f "$notes" ] || { echo "::error::$notes is missing: add the release notes first"; exit 1; }
|
||||
if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "::error::release $VERSION already exists"; exit 1
|
||||
fi
|
||||
# The notes' first line is the title; media paths become raw URLs at this release's tag.
|
||||
title=$(sed -n '1s/^# *//p' "$notes")
|
||||
[ -n "$title" ] || title="DroidDeck $VERSION"
|
||||
raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/"
|
||||
{
|
||||
sed '1{/^# /d;}' "$notes" | sed -E "s#(\]\(|src=\")media/#\1$raw#g"
|
||||
echo
|
||||
echo "---"
|
||||
echo
|
||||
echo "Built by [Build APK run $BUILD_RUN]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN) from \`$GITHUB_SHA\`, signed and published by [Release APKs run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
|
||||
} > "$RUNNER_TEMP/body.md"
|
||||
( cd out && cat SHA256SUMS-*.txt | sort -k2 > SHA256SUMS.txt && rm SHA256SUMS-*.txt && cat SHA256SUMS.txt )
|
||||
gh release create "$VERSION" -R "$GITHUB_REPOSITORY" out/*.apk out/SHA256SUMS.txt \
|
||||
--target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest
|
||||
echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# A release made with this run's token fires no release event, so the in-app catalog is refreshed here.
|
||||
catalog:
|
||||
needs: publish
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Sign APKs
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
type: string
|
||||
required: true
|
||||
variants:
|
||||
description: JSON array of names from tools/release/variants.txt
|
||||
type: string
|
||||
required: true
|
||||
run_id:
|
||||
description: The Build APK run whose apk to sign; empty for the calling run's own
|
||||
type: string
|
||||
default: ''
|
||||
label:
|
||||
description: Goes in the file names (a version); empty for <branch>-<short sha>
|
||||
type: string
|
||||
default: ''
|
||||
artifact:
|
||||
type: string
|
||||
default: droiddeck-signed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
name: sign ${{ matrix.variant }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
environment: ${{ inputs.environment }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ${{ fromJSON(inputs.variants) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: The apk (this run's build)
|
||||
if: inputs.run_id == ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: droiddeck-apk
|
||||
path: in
|
||||
|
||||
- name: The apk (a main build)
|
||||
if: inputs.run_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN: ${{ inputs.run_id }}
|
||||
run: gh run download "$RUN" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D in
|
||||
|
||||
- name: Sign
|
||||
env:
|
||||
RELEASE_KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_B64 }}
|
||||
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
|
||||
RELEASE_KEY_ALIAS: droiddeck
|
||||
ENVIRONMENT: ${{ inputs.environment }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
LABEL: ${{ inputs.label }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$RELEASE_KEYSTORE_B64" ]; then
|
||||
echo "::error::environment '$ENVIRONMENT' has no signing key: run tools/release/make-release-key.sh once"
|
||||
exit 1
|
||||
fi
|
||||
export RELEASE_KEYSTORE="$RUNNER_TEMP/release.p12"
|
||||
( umask 077; base64 -d <<<"$RELEASE_KEYSTORE_B64" > "$RELEASE_KEYSTORE" )
|
||||
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -n1)
|
||||
export BUILD_TOOLS
|
||||
[ -n "$LABEL" ] || LABEL="${GITHUB_REF_NAME//\//-}-${GITHUB_SHA::7}"
|
||||
suffix=$(awk -v v="$VARIANT" '!/^#/ && $1 == v { print $3 }' tools/release/variants.txt)
|
||||
[ "$suffix" = - ] && suffix=
|
||||
apks=(in/*.apk)
|
||||
[ ${#apks[@]} = 1 ] && [ -f "${apks[0]}" ] || { echo "::error::expected one apk in the build"; exit 1; }
|
||||
mkdir -p out
|
||||
out="out/DroidDeck-$LABEL$suffix.apk"
|
||||
tools/release/sign-apk.sh "${apks[0]}" "$VARIANT" "$out"
|
||||
( cd out && sha256sum -- *.apk > "SHA256SUMS-$VARIANT.txt" && cat "SHA256SUMS-$VARIANT.txt" )
|
||||
echo "- \`$(basename "$out")\` - $(cut -c1-16 "out/SHA256SUMS-$VARIANT.txt")…" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Forget the key
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/release.p12"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.artifact }}-${{ matrix.variant }}
|
||||
path: out/*
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Update app catalog
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published, edited, deleted, unpublished]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: update-app-catalog
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
jobs:
|
||||
catalog:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Always use the current trusted generator, even when a release event points at an older tag.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
|
||||
- name: Regenerate catalog.json
|
||||
env:
|
||||
SOURCE_GITHUB_TOKEN: ${{ github.token }}
|
||||
CI_GITHUB_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 tools/release/update_catalog.py
|
||||
+30
@@ -7,6 +7,36 @@ build/
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
.kotlin/
|
||||
# Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two
|
||||
# copies cannot drift.
|
||||
app/src/main/assets/linuxfs/
|
||||
app/src/main/jniLibs/arm64-v8a/libproot.so
|
||||
app/src/main/jniLibs/arm64-v8a/.proot-inputs
|
||||
app/src/main/jniLibs/arm64-v8a/.proot-outputs
|
||||
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
|
||||
# The store download engines (app/src/main/rust/stores) are cross-compiled by Gradle into
|
||||
# app/build/rustJniLibs; cargo's own build tree stays next to the crate. Neither is source.
|
||||
app/src/main/rust/stores/target/
|
||||
app/src/main/assets/droiddeck-esync/
|
||||
|
||||
# Kotlin compiler session files (build-tool state, never source).
|
||||
.kotlin/
|
||||
|
||||
# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see
|
||||
# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey.
|
||||
*.jks
|
||||
*.keystore
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pk8
|
||||
*.pem
|
||||
!keystore/testkey.p12
|
||||
droiddeck-release-key/
|
||||
password.txt
|
||||
__pycache__/
|
||||
|
||||
# Local signing with the release key (tools/build_local.sh); holds a password.
|
||||
.signing.env
|
||||
app/src/main/assets/directaudio/
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
# SteamDeck — Progress Log
|
||||
|
||||
Running engineering log for the SteamDeck app (`com.steamdeck.launcher`). Newest state first, then
|
||||
the timeline, then lessons and backlog. Companion to the README (what the app *does*) and to
|
||||
`docs/releases/` (what each version said) — this is *how it got here and where it stands*.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-22 (late) — motion front end + run-as-a-game, branch `feat/frontend-motion` (unmerged)
|
||||
|
||||
- Front end rebuilt around a motion system (`ui/FrontEndScreen.kt`, same state/actions API): the rail's selection is one pill that springs between rows; sub-lists unfold and stagger their children; a page change sinks out and cascades in over a blurred wash of the selection's art; tiles lift/ring/shine on focus and pop a play badge; the launch button sweeps a sheen and squashes on press; the session activity rises over the front end (`res/anim/session_*`). Durations follow the system animator scale.
|
||||
- The session now runs as a game: manifest `appCategory=game` + `game_mode_config` (Performance mode welcome; the OS FPS cap and downscaling refused), sustained performance mode, the panel's fastest mode at its size, GameManager game state, and an ADPF hint session over the compositor thread fed with each presented frame's interval (`session/PerfMode.kt`, `session/PerfHints.kt`, `nativeCompositorTid`). One `perf:` line in the session log says what took on the device.
|
||||
- App libraries link 16 KB-aligned. Still 4 KB-only (prebuilt, need a rebuild): libpulse, libpulseaudio, libpulsecommon-13.0, libpulsecore-13.0, libsndfile, libltdl.
|
||||
- The session's display caps at 720p by default now, client and desktop alike (`SessionPrefs.defaultResolutionCap`); a cap the user chose still wins. The cog's list marks the default per mode.
|
||||
- Builds: r1 `7ddfb57` (motion only), r2 `546c063` (+ run-as-a-game), r3 `d1ced87` (+ client 720p), r4 `fb2a3d4` (+ desktop 720p) — all CI-green, none device-tested. Staged as `SteamDeck-motion-r1..r4.apk`; r4 has everything.
|
||||
- r5 `1ba803b` + TZ in the guest; r6 `39272cf` Setup folds, "Linux desktop environment", help link above the grid (r4 seen running on the FIT); r7 `af3f377` settings without a pop-up: the cog and Performance are pages in the pane with anchored menus under each value, frame generation / logs / offline open in place on the rail. **r7 merged fast-forward to `main` (`af3f377`) and main's auto-build staged as `SteamDeck-r52.apk`; no tag, no release.**
|
||||
- r8 `90ff23a` (branch only, unmerged): the session drawer in the front end's dress with the same rows and menus (Now / Next session / leave), FEXCore presets carried over from Bannerlator (`core/FexPreset`, Steam settings page + drawer, FEX_* into the Steam session's environment; default = FEX defaults as before), file manager and picker locked to landscape.
|
||||
- Collaborators: `maxjivi05` (push) and `xXJSONDeruloXx` (push, invited 2026-09-22) on the private repo.
|
||||
|
||||
## 🔖 Checkpoint 2026-09-22 22:xx — known-good point for the front end
|
||||
|
||||
- **`main` @ `36cde7d`**, APK `SteamDeck-r51.apk` staged (sha `adb5acf5…`, run 35761437514) =
|
||||
frontend-r6 + docs. Same versionCode 5 / 0.1.4 inside; **0.1.5 not cut yet**.
|
||||
- **Proven on the Pocket FIT this evening:** the front end renders and navigates (rail, art,
|
||||
square emulator icons, focus outline); RPCS3 lists Tomb Raider (ISO in a subfolder) and God of
|
||||
War II HD (its HDD); ▶ God of War II HD boots the game under gamescope at ~40 fps
|
||||
(`session-20260922-133044`: `run: rpcs3.AppImage --no-gui …NPUA80491/USRDIR/EBOOT.BIN`, 399
|
||||
frames on screen in 10 s).
|
||||
- **Not yet proven:** desktop→Steam hand-off after the three fixes (r47–r50); ▶ FlatOut from the
|
||||
rail (Steam session with rungameid); Running tile; volume keys; automatic SD game storage
|
||||
(Install drive drop-down); Steam desktop-UI session; Tomb Raider ISO boot.
|
||||
- **Open decision:** rename the app away from Valve's marks before it spreads (shortlist offered:
|
||||
Linuxlator / Pocketscope / Portascope); rename = label, icon text, `applicationId` (fresh
|
||||
install + runtime re-download), `Download/SteamDeck/` log folder, repo, release-tag pattern.
|
||||
- **Rollback:** `git checkout 36cde7d` (or 0.1.4 tag `8e58e8e` for the last release), reinstall
|
||||
`SteamDeck-r51.apk` / `SteamDeck-0.1.4.apk` from Downloads.
|
||||
- Loose ends: `ui/MainScreen.kt` keeps ConfirmDialog/CreditsDialog/EmulatorHelpDialog but its
|
||||
`MainScreen`/tiles are dead; worktree `~/steamdeck-frontend` still exists (branch merged).
|
||||
|
||||
## Current state (2026-09-22, evening)
|
||||
|
||||
- **`main` @ the front end merge** (`feat/frontend` fast-forwarded): the launcher main screen -
|
||||
Steam ▸ games, Desktop ▸ emulators ▸ games, Running tile, focus outline. ✅ First emulator game
|
||||
proven from the app: God of War II HD in RPCS3 under gamescope at ~40 fps on the Pocket FIT.
|
||||
- Since 0.1.4 on main, unreleased: volume keys to Android; ▶ emulators under gamescope; ROMs chip
|
||||
+ ? explainer; sysmem copy + ENOSYS hint; automatic game storage (a card = a Steam library);
|
||||
the client's games on the desktop with a `steam` shim; desktop→Steam hand-off (three causes
|
||||
fixed from FIT logs: Surface detach race, the replaced session's exit ending the new one,
|
||||
shared log folder); the front end. Next release = 0.1.5 once the hand-off is seen working.
|
||||
|
||||
## State at 0.1.4 (2026-09-22)
|
||||
|
||||
- **Latest release: 0.1.4** — private: https://github.com/The412Banner/SteamDeck/releases/tag/0.1.4
|
||||
· public: https://github.com/The412Banner/winlator-contents/releases/tag/SteamDeck-0.1.4
|
||||
`main` @ `8e58e8e`, CI run 35682844106, versionCode 5, APK sha256 `4b53ce6a…` (20,957,823 B).
|
||||
- **Runtime:** `linuxfs-r9` on winlator-contents (790 MB), the only runtime release left; desktop
|
||||
packages from `steamdeck-desktop-r1`. The app rewrites its own scripts (`bannerlator-*`), the
|
||||
driver, `bannerlator-netmanager` and the DirectAudio pieces into the installed runtime at every
|
||||
launch, so a fix in those reaches an installed runtime without a re-download.
|
||||
- **Shipped feature set:** the native ARM64 Steam client under gamescope on the in-app Wayland
|
||||
compositor; a LXQt-on-labwc desktop with Firefox and a shelf of emulators; Proton tools
|
||||
(GE / CachyOS) as downloads; graphics drivers importable per mode; DirectAudio + microphone;
|
||||
client/game core masks + four session switches; NetworkManager stand-in (Max's); overlay
|
||||
restored; ROMs folder + Storage bound into the session's home; Bannerlator's File Manager;
|
||||
a cog per launch mode (resolution, shape, HDR10, drivers, touch, OSC/audio, renderer); per-session
|
||||
log folders that survive a crash; leftover-process sweep; frame generation (Win-FG, LSFG).
|
||||
- **Verification level:** every release is CI-green and staged to the maintainer's device. Proven
|
||||
on hardware (Pocket FIT / Fold): sign-in, store, install + launch (FlatOut 144 Hz), frame
|
||||
generation, controllers + OSC, sound, desktop + Firefox, folding mid-session, driver import, the
|
||||
log folder, the ANR fix, the network page. **Not yet proven:** everything 0.1.4 added (ROMs
|
||||
folder inside an emulator, File Manager, resolution cap, HDR10, crash sweep, orphan sweep, the
|
||||
drawer's Steam menu), DirectAudio *voice*, the core masks' effect, offline start, the soft
|
||||
keyboard, any emulator with a game, Adreno 710.
|
||||
- **Open field reports (Thor Pro, 8 Gen 2):** trackpad-mode tap does not click (code sends the
|
||||
click; cause not found by reading); crash after 1–2 min in Ballionaire / Geometry Wars (no log
|
||||
survived — 0.1.4's sweep will leave `crash.log`); a Fold's 10–20 fps client menus (Chromium →
|
||||
ANGLE → Zink on an experimental A8xx Turnip); Fold crash loop (xalia ENOSYS storm — switches
|
||||
shipped, untested); FlatOut shrinking after Resume (gamescope forcing the swapchain extent).
|
||||
|
||||
## Release pipeline (how every version is cut)
|
||||
1. Work on `main`, pushed → `Build APK` runs `assembleRelease` (AOSP test key, v1+v2+v3 re-signed
|
||||
by CI with zipalign + apksigner) → artifact `steamdeck-apk`. Dev builds keep the last release's
|
||||
versionCode/versionName; a release bumps both in `app/build.gradle` in its own commit.
|
||||
2. Verify: `gh run view <id> --json conclusion` (never trust a run listing's first row — a
|
||||
`workflow_dispatch` run gets cancelled by concurrency in favour of the push-triggered one),
|
||||
download the artifact, sha256 it.
|
||||
3. Stage: `cp` to `/sdcard/Download/SteamDeck-rN.apk` (dev) or `SteamDeck-X.Y.Z.apk` (release).
|
||||
Never `pm install` — the maintainer installs.
|
||||
4. Private release `X.Y.Z` targeting the **full 40-char sha** (a short sha is refused), notes from
|
||||
the draft in the session scratchpad, `--latest`. Public release `SteamDeck-X.Y.Z` on
|
||||
winlator-contents with the README-style notes; one public release per version.
|
||||
5. README ledger, this log, memory.
|
||||
|
||||
## Timeline
|
||||
- **2026-09-19 — 0.1 groundwork.** Lifted out of Bannerlator's gamescope runtime on the user's
|
||||
ask: one screen, one button. r2 reached Big Picture sign-in on the Pocket FIT. Package renamed to
|
||||
`com.steamdeck.launcher` (r4), foreground service, test key, on-screen controls.
|
||||
- **2026-09-20 — 0.1.** The Linux Steam client and a desktop; runtime r6→r9 in a day (Proton
|
||||
self-selection, proot shipped, refresh rate, video, first run reaches a game). Public explainer
|
||||
page for non-Linux users.
|
||||
- **2026-09-21 — 0.1.1.** Catch-up with Bannerlator: driver selection (two lists, per mode),
|
||||
overlay restored + GameHub force-stop, DirectAudio + microphone, core masks, four session
|
||||
switches as toggles, per-session log folders with credentials scrubbed, Adreno 710 path via
|
||||
Banners-Turnip r3.
|
||||
- **2026-09-22 — 0.1.2.** The teardown ANR (log collection on the main thread) fixed; two
|
||||
compositor log lines (frame-size change, window rename); README ledger.
|
||||
- **2026-09-22 — 0.1.3.** Max's NetworkManager stand-in ported (the client's network page); shim
|
||||
audit vs WinNative = zero functional drift.
|
||||
- **2026-09-22 (later) — after 0.1.4, on main.** Volume keys; ▶ emulators under gamescope (the
|
||||
desktop cannot: labwc on pixman offers no dma-buf); ROMs chip; sysmem warning + ENOSYS hint;
|
||||
automatic game storage; the client's games on the desktop (`steam` shim hands off to a
|
||||
gamescope session); the hand-off's three faults found in FIT logs and fixed; the front end
|
||||
merged (branch `feat/frontend`, r1–r6); God of War II HD booted in RPCS3 from the rail.
|
||||
- **2026-09-22 — 0.1.4.** ROMs folder + Storage in the session's home; Bannerlator's File Manager
|
||||
ported whole; a cog per mode (railed settings window) with resolution cap, shape, HDR10 gated on
|
||||
the panel, drivers, touch, OSC/audio/renderer; two-column main menu; crash-safe log folders +
|
||||
Session logs toggle; OrphanReaper; drawer Steam menu. Public releases reorganised: one per
|
||||
version, old `Steamdeck` release and runtimes r1–r8 deleted.
|
||||
|
||||
## Architecture (where things live)
|
||||
- `MainActivity` / `ui/MainScreen.kt` — the main screen; `ui/ModeSettingsDialog.kt` the cogs;
|
||||
`ui/*Dialog.kt` the rest.
|
||||
- `SessionActivity` — the Surface, input (touch, touchpad, pads, keyboard), the drawer, the HDR
|
||||
decision, the compositor start (`wayland/CompositorHost`, `wayland/WaylandCompositor`).
|
||||
- `session/SessionService` — proot + gamescope/labwc + PulseAudio + DirectAudio relay + network
|
||||
link; binds (`/root/Storage`, `/root/ROMs`); env for the guest; teardown; `OrphanReaper`;
|
||||
`SessionArtifacts` + `CrashHandler` + `SessionPaths` for the log folders; `SessionPrefs`.
|
||||
- `files/` — Bannerlator's File Manager (`FileManagerScreen.kt` and its helpers), the picker
|
||||
activity and `InAppFilePicker` intent API.
|
||||
- `gpu/` — Turnip (bionic, for the compositor) and Linux Turnip (glibc, for the runtime) managers.
|
||||
- `tools/linuxfs/overlay/usr/local/bin/bannerlator-*` — the guest scripts, staged into the
|
||||
runtime by `SessionFiles` at every launch (CI packages only `bannerlator-*` names).
|
||||
- `app/src/main/cpp/waylandcomp/` — the compositor (shared lineage with Bannerlator).
|
||||
|
||||
## Lessons learned (don't repeat these)
|
||||
- Nothing in teardown may do bulk filesystem work on the main thread (0.1.1's ANR).
|
||||
- A CI closure check on a multi-line variable must flatten it first (`libaaudio.so` refusal).
|
||||
- A runtime script must be named `bannerlator-*` or CI never packages it.
|
||||
- `gh release create --target` needs the full sha.
|
||||
- A proot bind is invisible to a program's "Computer" list; put what users need under home.
|
||||
- The compositor decides its driver and its HDR gate once per app process — anything that changes
|
||||
them applies after the app is fully closed, and the UI must say so.
|
||||
- Bannerlator's File Manager ports mechanically (`port_fm.py` anchors) once the container hooks
|
||||
are cut; do not hand-edit 2,500 lines.
|
||||
- Deleting a release asset on GitHub can drop a sibling asset — re-list and restore.
|
||||
|
||||
## Backlog / next
|
||||
- Device-prove 0.1.4 on the FIT (list above).
|
||||
- Thor Pro: trackpad tap; the 1–2 min in-game crash once a `crash.log` arrives.
|
||||
- Xfce as a second desktop shell (Max's branch runs XFCE 4.20 on labwc) — a catalog package + a
|
||||
shell choice in the Desktop cog; comfort, not performance.
|
||||
- Quick Access Menu for non-Deck pads — no confirmed chord; needs research, not a guess.
|
||||
- FlatOut shrink: try `vk_wsi_force_swapchain_to_current_extent=false` via `steamdeck-env`.
|
||||
- Max's stricter `winnative-directaudio` guards; `winnative-epic-launch` (a feature).
|
||||
- Rename before anything truly public: "SteamDeck" is Valve's mark.
|
||||
@@ -1,548 +1,40 @@
|
||||
# SteamDeck
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="artwork/droiddeck-banner-dark.svg">
|
||||
<img alt="DroidDeck" src="artwork/droiddeck-banner-light.svg" width="100%">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
Valve's **native aarch64 Linux Steam client**, running on an Android phone or handheld: a glibc
|
||||
rootfs under proot, gamescope as a Wayland client of an in-app Vulkan compositor, Xwayland and
|
||||
Zink for the client's CEF, and the device's Turnip driver underneath. There is no Wine here, and
|
||||
no x86 translation for the client itself — it is ARM code running as ARM code. Windows games it
|
||||
launches go through Valve's own ARM64 Proton build, which is where FEX comes in.
|
||||
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
|
||||
|
||||
One screen, one button: install the runtime, press Play, Big Picture comes up. Or start the
|
||||
**desktop** instead — LXQt, a browser and a shelf of emulators, in the same session.
|
||||
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
|
||||
|
||||
Around that: the Vulkan driver each half of the session draws with is yours to import and change,
|
||||
audio can bypass PulseAudio and carry a microphone, the cores the client and a game get are
|
||||
separate, and every session writes a folder of logs meant to be attached to a report as it is.
|
||||
<p align="center"><img src="docs/releases/media/0.2.0/launch-into-steam.gif" width="80%" alt="Tapping DroidDeck on the Android home screen and landing in Steam Big Picture"></p>
|
||||
|
||||
## Contributors
|
||||
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
|
||||
|
||||
Building this alongside the owner. A pull request from either of them builds by itself and gets
|
||||
a comment saying whether it merges, what conflicts, or what failed to compile.
|
||||
## Requirements and install
|
||||
|
||||
- **MaxsTechReview** — [@maxjivi05](https://github.com/maxjivi05)
|
||||
- **Kurt Himebauch** — [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx)
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games, browses their catalogs and libraries, downloads games into the Games storage and adds each one to the Steam client as a non-Steam game, so it launches through Proton like everything else; the cog on its chip row sets which tab a store opens on and the download speed tier.
|
||||
|
||||
<!-- contributions:start -->
|
||||
_From the repository's pull requests; rewritten when one is opened, merged or closed._
|
||||
Before Steam launches, you must turn off **Restrict child processes** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
|
||||
|
||||
| Contributor | Pull request | Opened | State |
|
||||
|---|---|---|---|
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#1](https://github.com/The412Banner/SteamDeck/pull/1) initial decky installer stuff | 2026-09-23 | draft |
|
||||
## Community
|
||||
|
||||
- **@xXJSONDeruloXx**: 1 submitted, 0 merged
|
||||
<!-- contributions:end -->
|
||||
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the zip from **Share logs** (Setup, or the session drawer) so the logs come with it.
|
||||
|
||||
## Requirements
|
||||
## Build
|
||||
|
||||
- An **arm64 Android device with an Adreno GPU that Turnip supports** — in practice Adreno **730 or
|
||||
newer** (Snapdragon 8 Gen 1 onward) and the **8xx** series. Mali, Xclipse and PowerVR are not
|
||||
supported here, and neither is the low-end Adreno 710 (Snapdragon 6 Gen 1): Mesa has no entry
|
||||
for it at all, so the session log ends at `device (chip_id = 7010000, gpu_id = 710) is
|
||||
unsupported` before anything is drawn.
|
||||
- **Android 9 or newer**, and roughly **3 GB free** for the runtime before any games. The desktop
|
||||
and its emulator packages are another ~1.1 GB if you install them.
|
||||
- **No root required.** Root only helps if you want to copy an existing Bannerlator runtime across
|
||||
instead of downloading one.
|
||||
Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, Rust through rustup, GitHub CLI, Python 3, and `zstd` installed. It prepares the same native libraries, pinned components and audio sinks as CI, then checks the APK's assets, JNI exports and dependencies. The APK is at `app/build/outputs/apk/release/app-release.apk`. Set `DROIDDECK_PA13_SOURCE_DIR` to an existing PulseAudio 13.0 source directory to skip downloading it. Set `DROIDDECK_SIGNING_ENV` to a signing environment file to install over a release build; relative keystore paths resolve beside that file. To install on an attached device, run `tools/deploy_local.sh`.
|
||||
|
||||
## Installing
|
||||
After preparation, ordinary `./gradlew :app:assembleRelease` builds can reuse the verified inputs. If native sources, component pins or staged files change, packaging stops with instructions to rerun the helper. Generated audio stays under `app/build/`; the tracked base bundle is never modified. `./gradlew -PskipRust=true :app:testDebugUnitTest` needs no native preparation. For an APK without store engines, use `DROIDDECK_SKIP_RUST=1 tools/build_local.sh`; it omits cached engine libraries too.
|
||||
|
||||
1. Install the APK from the release page.
|
||||
2. Launch it and press **Install Linux runtime** — about 790 MB, once.
|
||||
3. Press **Play**. On the very first run the app fetches Valve's client (a minute or two with
|
||||
nothing on screen), then Big Picture comes up and you sign in.
|
||||
Run the helper and preload regression suite with `bash tools/test_local.sh`. On macOS it runs in Linux through Docker, matching the helpers' runtime and CI instead of compiling Linux preloads against macOS headers.
|
||||
|
||||
For a desktop, open **Desktop & apps**, install the *Desktop* package, then press **Desktop** on
|
||||
the main screen.
|
||||
## Limits
|
||||
|
||||
**Do this once per device if the client dies with nothing in its log**: Android 12 and later kill
|
||||
the extra processes an app starts for itself, and a session is made of dozens of them. Some phones
|
||||
have a **"restrict child processes"** switch in Developer options — turn it off. See
|
||||
[If Steam dies with nothing in the log](#if-steam-dies-with-nothing-in-the-log).
|
||||
Compatibility and performance vary by device; hardware validation is limited. Desktop compositing uses software rendering. Firefox sandboxing is reduced under proot. Logs are kept app-private (`files/logs/`, the last 30 sessions); **Share logs** packs the newest session, with the stores and tool logs, into one scrubbed zip.
|
||||
|
||||
## What works today
|
||||
## Credits and licence
|
||||
|
||||
This is version 0.1.2, and most of it has been exercised on one or two devices only. An honest
|
||||
ledger rather than a feature list — where something has not been run on hardware, it says so:
|
||||
|
||||
| | State |
|
||||
|---|---|
|
||||
| Steam sign-in, store, install, launch | ✅ proven — *FlatOut* at 144 Hz through ARM64 Proton |
|
||||
| Importing and choosing a driver | ✅ proven — an imported Mesa 26.3.0 glibc Turnip drew a whole session, and an imported AdrenoTools build ran the compositor |
|
||||
| Frame generation (LSFG 2×) | ✅ proven — 30 → 61, 60 → 118, 61 → 123 fps |
|
||||
| Performance HUD | ✅ proven |
|
||||
| Desktop, panel, file manager | ✅ proven at the panel's native resolution |
|
||||
| Firefox and the network (IPv4 + IPv6) | ✅ proven — live pages at 93 fps |
|
||||
| Controller as an Xbox 360 pad | ✅ proven |
|
||||
| Foldable, opened and closed mid-session | ✅ proven |
|
||||
| Background / foreground, wake locks | ✅ proven |
|
||||
| The session log folder | ✅ proven — all of it written, including this app's own log |
|
||||
| Session folder finished after a crash or a kill | ⚠️ built: crash handler + next-start sweep; not yet forced on a device |
|
||||
| ROMs folder and Storage in the session's home | ⚠️ built; not yet opened from an emulator on a device |
|
||||
| The File Manager (Bannerlator's) and its pick mode | ⚠️ built; not yet used on a device |
|
||||
| The cog beside Play / Desktop: resolution, shape, HDR, drivers, touch, audio, renderer | ⚠️ built; the resolution cap and HDR not yet seen on a device |
|
||||
| Leftover session processes swept at the next start | ⚠️ built; not yet forced on a device |
|
||||
| The drawer's "Steam menu" (Guide) | ⚠️ built; not yet tried |
|
||||
| Emulators launched under gamescope (▶ on the rail) | ✅ proven — God of War II HD in RPCS3, ~40 fps on the FIT |
|
||||
| The front end (rail, games, focus outline, Running tile) | ⚠️ built and used on the FIT; the Running tile and Steam-game ▶ not yet exercised |
|
||||
| The device's volume keys during a session | ⚠️ fixed (they were swallowed); not yet re-tried |
|
||||
| Game storage: a second Steam library on a card or a folder | ⚠️ built; not yet seen installing to it |
|
||||
| The client's games in the desktop menu, launching Steam's desktop UI under gamescope | ⚠️ built; not yet tried |
|
||||
| Stopping a competing Steam client | ⚠️ it is asked to stop and says so, but one that restarts itself from boot wins the race — uninstall it |
|
||||
| DirectAudio for games, and the microphone | ⚠️ wired and the modules load; voice not yet confirmed in Steam's tester |
|
||||
| Client and game core masks | ⚠️ applied and logged; no measured difference yet |
|
||||
| The four session fixes (xalia, proot seccomp, sysmem, Zink) | ⚠️ they set what they say; each is a hypothesis for a device that misbehaves |
|
||||
| GE-Proton / proton-cachyos | ⚠️ adopted and registered on device; no game launched through one yet |
|
||||
| The emulators | ⚠️ they build, publish and install — none has been run with a game |
|
||||
| Starting Steam offline | ⚠️ built, not yet tested |
|
||||
| The soft keyboard | ⚠️ built, not yet tested |
|
||||
| Adreno 710 / 720 / 722 | ⚠️ a path exists through imported drivers; **nobody has run this on one yet** |
|
||||
|
||||
## Known limits
|
||||
|
||||
- **The desktop composites in software.** wlroots allocates through gbm on a real DRM render node,
|
||||
and the Adreno stand-in is not one, so labwc falls back to pixman. Programs on the desktop still
|
||||
reach the GPU themselves through Vulkan and Turnip — it is the desktop's own compositing that
|
||||
runs on the CPU.
|
||||
- **Firefox's child-process sandboxes are off.** Seccomp and namespace sandboxes cannot be set up
|
||||
under proot, and its tabs crash otherwise. This is a genuine reduction in isolation: treat
|
||||
browsing here as less protected than on an ordinary desktop.
|
||||
- **The display shape changes at the next session**, not immediately — gamescope fixes its display
|
||||
size when it starts.
|
||||
- **No Switch emulator.** Eden's repository was taken down in February; there is nothing to mirror
|
||||
or to build from.
|
||||
- PS3 (RPCS3) and Wii U (Cemu) are demanding even on desktop hardware — expect light titles only.
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
Android app (this repo)
|
||||
├── libbannerwayland.so Wayland compositor, presents into the activity's Surface
|
||||
│ on a bionic Turnip: bundled, or one you imported
|
||||
├── PulseAudio 13 + AAudio the guest's audio server, on a socket bound into the session
|
||||
├── directaudio relay owns AAudio out and the mic for the games that ask for it
|
||||
├── fake-evdev rings a physical pad, republished as /dev/input/eventN for the client
|
||||
├── frame generation Win-FG or LSFG, on gamescope's finished output
|
||||
└── proot ── linuxfs (~790 MB, downloaded once)
|
||||
├── gamescope ── Xwayland ── Zink
|
||||
│ drawing on a glibc Turnip: the runtime's, or one you imported
|
||||
└── Steam (steamrtarm64, fetched from Valve on first run) ── Proton ARM64 ── FEX
|
||||
└── a game's audio: Proton's own, or DirectAudio straight to Android
|
||||
```
|
||||
|
||||
The two Turnips are separate copies of the same driver and cannot be swapped: the compositor's is
|
||||
built against Android's C library, the runtime's against glibc. That is why there are two import
|
||||
lists rather than one.
|
||||
|
||||
The runtime image is the one published for Bannerlator (`linuxfs.json` in
|
||||
`The412Banner/winlator-contents`); this app installs and updates it from that catalog. **No Valve
|
||||
software is redistributed** — the client is fetched from Valve's own manifest on the device at
|
||||
first use.
|
||||
|
||||
## Building
|
||||
|
||||
Nothing is built locally. Push, and the `Build APK` workflow produces `steamdeck-apk`, a
|
||||
debug-signed arm64 apk. It cross-compiles the two glibc preloads the session needs
|
||||
(`libfakeinput.so`, `libblsession.so`) from the sources in this repo before the app build, so
|
||||
those can never drift from what ships.
|
||||
|
||||
## If Bannerlator is already installed
|
||||
|
||||
The runtime image is the same one Bannerlator installs, so on a device that already has it the
|
||||
790 MB download can be skipped entirely — copy it across with root, once, before the first launch:
|
||||
|
||||
```sh
|
||||
su -c 'cp -a /data/data/com.tencent.ig/files/linuxfs /data/data/com.steamdeck.launcher/files/ \
|
||||
&& chown -R $(stat -c %u /data/data/com.steamdeck.launcher/files) \
|
||||
/data/data/com.steamdeck.launcher/files/linuxfs \
|
||||
&& chcon -R u:object_r:app_data_file:s0 /data/data/com.steamdeck.launcher/files/linuxfs'
|
||||
```
|
||||
|
||||
The two runtimes are independent after that: each app has its own Steam install, its own login and
|
||||
its own games.
|
||||
|
||||
## Controls
|
||||
|
||||
A physical controller is republished into the session as a synthetic Xbox 360 pad, which is the
|
||||
identity SDL and Steam have a mapping for. With nothing attached, an on-screen pad appears —
|
||||
d-pad, A/B/X/Y, shoulders, select/start and the Steam button — writing into the same rings, so the
|
||||
client sees one pad either way. Connect a controller and the on-screen one disappears; unplug it
|
||||
and it comes back. Anywhere the controls are not is a touchpad: touch moves the pointer and a tap
|
||||
clicks, which is how the client's own on-screen keyboard is used to sign in.
|
||||
|
||||
## Desktop
|
||||
|
||||
Besides the Steam client the app can start a **desktop**: LXQt on labwc, in the same session and
|
||||
the same compositor, with Xwayland for the X11 programs. It is a second choice on the main
|
||||
screen, not a replacement — the Steam button still hands the session to gamescope.
|
||||
|
||||
What it is made of is downloaded on request from a catalog hosted beside the runtime, so the app
|
||||
stays small and nothing ships that a given device will not use:
|
||||
|
||||
| Package | What it brings |
|
||||
|---|---|
|
||||
| **Desktop** (406 MB) | labwc, LXQt's panel, file manager, terminal, text and image viewers, Firefox, mpv |
|
||||
| **Emulators** (694 MB) | PPSSPP and RetroArch with 28 cores — PS1, PS2, N64, SNES, NES, Game Boy, GBA, DS, Genesis, Dreamcast, Saturn, PC Engine, arcade — plus ScummVM, DOSBox, Mednafen, Snes9x |
|
||||
| **RPCS3 · DuckStation · melonDS** | PS3, PS1 and DS, mirrored from the projects' own ARM64 builds |
|
||||
| **PCSX2 · Dolphin · Cemu** | PS2, GameCube/Wii and Wii U. No ARM64 Linux build exists upstream for any of the three, so they are compiled from source against the same glibc the runtime uses |
|
||||
|
||||
The desktop composites in software: wlroots allocates through gbm on a real DRM render node, and
|
||||
the Adreno stand-in is not one, so labwc runs on pixman. The programs on it still reach the GPU
|
||||
themselves through Vulkan and Turnip — it is the desktop's own compositing that is on the CPU.
|
||||
`Download/steamdeck-wlr-renderer` (`pixman`, `gles2` or `vulkan`) overrides the choice.
|
||||
|
||||
Programs whose child processes sandbox themselves with seccomp and namespaces cannot set those up
|
||||
under proot; Firefox's are turned off in the session's environment, without which its tabs crash.
|
||||
|
||||
### The front end
|
||||
|
||||
The main screen is a launcher: a rail with **Steam** and its installed games (from the client's
|
||||
own manifests, with the library-cache art), **Desktop** and the installed emulators (their own
|
||||
icons) and each emulator's games from the ROMs folder (a system folder such as `ps3/` as a hint,
|
||||
by extension, one folder deep - a dump comes as a folder named for the game) plus what RPCS3 has
|
||||
installed on its own HDD; then Files, Desktop & apps, Compatibility tools, Frame generation,
|
||||
Performance, ROMs, Session logs, Start offline, the runtime and credits. The chosen thing sits on
|
||||
the right with its ▶. A Steam game launches its session with `steam://rungameid/…` directly; a
|
||||
ROM launches its emulator under gamescope with the game on the command line (RPCS3 `--no-gui`,
|
||||
Dolphin `-e`, Cemu `-g`, the rest plain). A session alive in the background is the first thing on
|
||||
the rail; tapping it goes back to it. The controller's focus draws a thin outline. ✅ God of War
|
||||
II HD booted in RPCS3 from the rail at ~40 fps on the Pocket FIT.
|
||||
|
||||
### Game storage: internal, an SD card, or a folder
|
||||
|
||||
The client's own library is internal storage and always there. An **SD card in the phone is
|
||||
registered as a second library by itself** (through the app's own folder on it, the one place a
|
||||
card lets this app write), so the choice is made where it is anywhere else - inside Steam, on
|
||||
every Install. **Game storage** in the Steam cog is only for turning that off or pointing it at a
|
||||
folder chosen in the File Manager instead. The session binds it at
|
||||
`/mnt/bannerlator-sd` and `bannerlator-steam-library` registers it with the client under the name
|
||||
the app gives it; the client then asks where to install every game, lists both on its Storage
|
||||
page and moves games between them itself. Set to internal only, the entry is removed again. A
|
||||
card and shared storage are FUSE-backed and stream slowly - keep games that stream video or big
|
||||
assets internal. ⚠️ Built; not yet seen installing to a card.
|
||||
|
||||
### The client's games on the desktop
|
||||
|
||||
The Steam client writes a menu entry for every game it installs (Games category, the game's
|
||||
icon), and the home folder has **Steam Games** and **SD Games** links to their files. Those
|
||||
entries run `steam steam://rungameid/…`; on this desktop `steam` is a shim, because the client
|
||||
cannot run where nothing can draw with Vulkan. It - and the menu's **Steam** / **Steam Big
|
||||
Picture** entries - hand off to the app (`bannerlator-steam-launch`), which ends the desktop
|
||||
session and starts a Steam session in its place: the client's **desktop UI** under gamescope, and the game straight away when one was
|
||||
chosen. Back opens the drawer as in any session; Stop session returns to the app. The list is
|
||||
rebuilt at every desktop start. ⚠️ Built; not yet tried on a device.
|
||||
|
||||
### Running an emulator with the GPU
|
||||
|
||||
The desktop composites in software (labwc on pixman) and so offers its programs no dma-buf: a
|
||||
Vulkan program on it has no swapchain to draw into and dies at its first frame — RPCS3 with
|
||||
`VK_ERROR_SURFACE_LOST`. The GPU path that works on this hardware is gamescope's, the one Steam's
|
||||
games render through. So an emulator is started **as a session of its own**: ▶ beside an installed
|
||||
emulator under **Desktop & apps** runs it fullscreen under gamescope (the session script's `run`
|
||||
mode, an AppImage or a binary inside the runtime), with Steam's display, driver and HDR settings.
|
||||
The desktop remains for files, Firefox and everything that draws in software. ⚠️ Built; not yet
|
||||
seen with a game.
|
||||
|
||||
### The device's files, and the ROMs folder
|
||||
|
||||
Inside a session the phone's storage is at `/storage/emulated/0`, as it is outside - but no
|
||||
program's file dialog will show it: they list "Computer" from `/proc/mounts`, where a proot bind
|
||||
never appears, and open in the home folder, where nothing pointed at the phone. So the home
|
||||
folder now carries two entries every session:
|
||||
|
||||
- **`Storage`** - all of internal storage.
|
||||
- **`ROMs`** - the folder chosen with **ROMs folder** on the main screen, wherever it is on the
|
||||
device, an SD card included (it is a bind, not a link). Point any emulator's game directory at
|
||||
`/root/ROMs` once and it is the same folder in every session after.
|
||||
|
||||
Both are the live folders, not copies: a save an emulator writes next to its ROM lands on the
|
||||
phone.
|
||||
|
||||
### Files
|
||||
|
||||
**Files** on the main screen is Bannerlator's File Manager, carried over whole: grid and list,
|
||||
sort, search, multi-select, copy / cut / paste with conflict handling and a cancellable progress
|
||||
bar, rename, delete, new folder, properties (read-only and hidden), favourites, and a locations
|
||||
rail with every mounted card, Downloads, the ROMs folder and the session logs. Its pick mode is
|
||||
what chooses the ROMs folder and imports a driver. What is not here is what has no meaning without
|
||||
a Wine container: running a file, adding it to shortcuts, the C: / Z: drives and the archive
|
||||
unpacker.
|
||||
|
||||
## Pointer
|
||||
|
||||
The app draws the arrow. On the desktop, touch is a **touchpad** (Bannerlator's desktop-container
|
||||
feel): drag anywhere and the pointer moves from where it is; tap to click; a second finger tapping
|
||||
while one holds, or a two-finger tap, right-clicks; two fingers scroll; hold still then drag, or
|
||||
double-tap-and-hold, drags. In Steam, touch is **direct**: the pointer jumps under the finger. The
|
||||
drawer's *Touch* switch pins either. A USB or Bluetooth mouse works as itself.
|
||||
|
||||
## Networking
|
||||
|
||||
The desktop's programs use the phone's network directly — proot makes no network namespace, so
|
||||
IPv4 and IPv6 both pass through — and the app writes the runtime's `/etc/resolv.conf` from the
|
||||
active network's own DNS servers whenever the link changes (IPv4 first, never a link-local IPv6 one,
|
||||
public servers as fallback), which is what Firefox and everything else in the desktop resolve with.
|
||||
|
||||
## Keyboard
|
||||
|
||||
**Keyboard** in the drawer opens the soft keyboard over the session. Steam's UI and the games
|
||||
under it are X11 clients of gamescope and know nothing of Wayland text input, so what the
|
||||
keyboard produces is turned into the key presses that would have typed it — Shift included, so
|
||||
capitals and the symbol row (`@`, `!`, `_` …) arrive as themselves — and fed to the compositor as
|
||||
key events. A hardware keyboard works as it is.
|
||||
|
||||
## Starting Steam offline
|
||||
|
||||
The client runs without a connection — installed games launch, Proton and the frame generation
|
||||
are local — provided it has credentials from a sign-in that succeeded earlier. **Start offline**
|
||||
on the main screen is where that is decided, and not in the drawer, because the client reads the
|
||||
two keys that control it once, while it starts: changed during a session it would write the file
|
||||
and change nothing until the next launch. Without an earlier sign-in the button says so and does
|
||||
nothing. The client rewrites those keys when it exits, so the app writes them again at every
|
||||
session start. Games with always-online DRM will not start offline, and nothing can be installed
|
||||
or updated.
|
||||
|
||||
## Audio
|
||||
|
||||
The Steam client always plays through **PulseAudio** — it is a native Linux program with no other
|
||||
way to make a sound, and its menus, music and voice chat all go through it. **Audio** on the main
|
||||
screen adds two things on top, each applying at the next session start:
|
||||
|
||||
- **DirectAudio for games** — replaces Wine's audio driver *inside the games the client launches*
|
||||
with Bannerlator's DirectAudio, which talks straight to Android over a socket instead of through
|
||||
PulseAudio: lower latency, and no resampling in between. The client is untouched. It only pairs
|
||||
with a Wine 11 Proton (Valve's ARM64 builds, GE-Proton, proton-cachyos); on anything else the
|
||||
wrapper leaves it off and says so in the log, because a mismatch would be silence rather than an
|
||||
error. A game's first launch after turning it on still uses Proton's own audio — the driver is
|
||||
named in the game's prefix, which does not exist until that first launch — and the second uses
|
||||
DirectAudio.
|
||||
- **Microphone** — the relay helper opens Android's input stream under the app's own uid (which is
|
||||
what the recording permission is checked against) and PulseAudio exposes it to the client as a
|
||||
source named **DirectAudioMic**, so Steam voice chat works. Asks for the recording permission when
|
||||
turned on; off, nothing in the session can record and Android's indicator stays off.
|
||||
|
||||
The helper is a small Android program the app runs beside the session; the driver is a glibc build
|
||||
staged into the runtime at every launch like the session scripts, so a fix reaches an installed
|
||||
runtime without re-hosting it. Proven in Bannerlator's Linux session with voice in Steam's own
|
||||
tester; ⚠️ not yet exercised in this app.
|
||||
|
||||
## Cores
|
||||
|
||||
**Performance** on the main screen carries the two core masks Bannerlator's Linux session has, kept
|
||||
separate because they are wanted at the same time and suit different things:
|
||||
|
||||
- **Steam client cores** — Steam pins its own interface renderer to a subset of cores it chooses (on
|
||||
one device 5 of 8, leaving out both little cores and the fastest), which is reasonable while a
|
||||
game runs and makes Big Picture sluggish when the client is all there is. Turning the override
|
||||
on pins the client, its UI helper and gamescope to the cores you tick, re-applied every few
|
||||
seconds because the UI keeps spawning helpers that inherit Steam's choice. Every core ticked is
|
||||
the usual fix — and, unlike the game mask, it *is* sent when it names every core, since undoing
|
||||
Steam's pin is the whole point.
|
||||
- **Game cores** — applied by the Proton wrapper, which execs the game through `taskset` so every
|
||||
thread inherits the mask from its first instruction. Leaving every core ticked sends nothing;
|
||||
untick the small cores to keep a heavy game off them.
|
||||
|
||||
Both apply at the next session start.
|
||||
|
||||
Under the same dialog, **session fixes** for a device the runtime does not sit well on, tried one at
|
||||
a time. **Skip Steam's xalia helper** (`PROTON_USE_XALIA=0`) is the one to reach for when a session
|
||||
dies seconds after Big Picture appears: xalia is an x86 Windows program Proton starts for gamepad
|
||||
navigation, it cannot load the session's preload shim under FEX, and where the vendor's seccomp
|
||||
answers its `socket()` and `memfd` calls with ENOSYS it storms until the session collapses (89
|
||||
refusals then a broken pipe, in the log). It costs gamepad navigation in Windows programs that are
|
||||
not games. Then two for a runtime that renders slowly: **Turnip: sysmem rendering** (`TU_DEBUG=sysmem`, Banners-Turnip's advice for an Adreno 8xx that
|
||||
looks glitchy or slow; a 710/720/722 gets it on its own) and **Zink: lazy descriptors**
|
||||
(`ZINK_DESCRIPTORS=lazy`, for Steam's menus, which Chromium draws through Zink — the first thing to
|
||||
try when they are slow while games are fast). The `steamdeck-tu-debug` and `steamdeck-env` files in
|
||||
Downloads still win over both.
|
||||
|
||||
## Networking and the client's own network page
|
||||
|
||||
The session uses the phone's connection directly - proot makes no network namespace, so IPv4 and
|
||||
IPv6 both pass through - and the app writes the runtime's `/etc/resolv.conf` from the active
|
||||
network's own DNS servers whenever the link changes.
|
||||
|
||||
The client's **Settings → Internet** page and its connection indicator are a different matter:
|
||||
they are built on NetworkManager, which the client looks for on the system bus as it starts. Without
|
||||
one, that page stopped with *"StartScanningForNetworks is not a function"* under a header showing no
|
||||
connection even while downloads ran. The session now starts a small system bus and a **stand-in that
|
||||
answers for NetworkManager**, reporting what is true and nothing more - one connection that is up
|
||||
while Android has a route out - and refusing what it cannot do, since nothing in this runtime can
|
||||
scan for or join a network. The session log says `network: system bus up, NetworkManager stand-in
|
||||
running` when it is working.
|
||||
|
||||
That stand-in is **maxjivi05's**, from WinNative, by way of Bannerlator.
|
||||
|
||||
## Compatibility tools
|
||||
|
||||
**Compatibility tools** (main screen and drawer) installs GE-Proton or proton-cachyos — native
|
||||
ARM64 builds — beside the ARM64 Proton Valve ships. The runtime's own registrar downloads and
|
||||
installs a requested build when the next session starts (several hundred MB), patches it past
|
||||
pressure-vessel like Valve's, and it then appears in Steam under Properties → Compatibility for
|
||||
any game. The dialog queues or cancels a request and removes an installed build.
|
||||
|
||||
## The cog beside Play and Desktop
|
||||
|
||||
Each launch button has a cog, and everything that only matters for that one mode lives behind it,
|
||||
so the main screen keeps only what applies to both: **resolution** (up to 720p by default; 900p, 1080p, or the
|
||||
panel's own), the display **shape**, **HDR10 output**, the
|
||||
**Linux runtime driver** for that mode and the shared **display driver**, **touch**, and for Steam
|
||||
the on-screen controls and audio, for the desktop its renderer.
|
||||
|
||||
**HDR10** is a switch that is greyed out, with the reason, on a panel that does not list HDR10 —
|
||||
the compositor's HDR gate is decided from the display's own word (`android.view.Display`), once,
|
||||
when the compositor starts. On, the compositor offers `wp_color_manager_v1` with BT.2020 + PQ and
|
||||
10-bit buffers, gamescope is started with `--hdr-enabled` and speaks the same protocol to it, and
|
||||
games get `DXVK_HDR=1`; a game that renders HDR then reaches the panel as HDR on its own display
|
||||
layer, tagged BT2020_PQ. The compositor lives for the whole app process, so a change applies after
|
||||
the app is fully closed and opened again. ⚠️ Wired end to end; not yet seen on a device from this
|
||||
app (Bannerlator proved the same compositor path on a Fold).
|
||||
|
||||
## Graphics drivers
|
||||
|
||||
Two drivers, two lists, because a zip for one cannot serve the other — the same split Bannerlator's
|
||||
Contents screen makes. Both are chosen in the cog beside Play / Desktop:
|
||||
|
||||
- **Linux runtime driver** — the glibc Turnip everything *inside* the runtime renders on: the Steam
|
||||
client's UI, every game it launches, and in desktop mode every program on the desktop. Import a
|
||||
"-Linux" Turnip zip from Banners-Turnip; a plain Android or "-Wayland" Turnip is refused with the
|
||||
reason, since a Linux process cannot load a bionic library. The choice is per mode, so Steam and
|
||||
the desktop can run different builds, and it takes effect at the next session start. Nothing in
|
||||
the runtime is modified: the session is handed the imported driver's ICD manifest and points the
|
||||
Vulkan loader at it, and the runtime's own Turnip is what a removed or unreadable import falls
|
||||
back to. This is how a driver fix reaches an installed runtime without a ~790 MB re-download.
|
||||
- **Display driver (Android)** — the bionic Turnip the app's compositor puts the frame on the panel
|
||||
with, the last step of every session. *Auto* picks one of the two bundled builds by GPU; an
|
||||
imported AdrenoTools zip can be chosen instead. The compositor loads its driver once per app
|
||||
process, so this one applies after the app is fully closed and started again.
|
||||
|
||||
Both imports open the app's own file picker rather than Android's document picker: an SD card is a
|
||||
card in its drive menu, the folder last picked from is where it opens next time, and it hands back
|
||||
a plain path.
|
||||
|
||||
## Frame generation
|
||||
|
||||
Extra frames are generated between the real ones on the way to the screen — inside the app's
|
||||
compositor, on gamescope's final output — so it works for any game with nothing changed in the
|
||||
runtime. **Win-FG** is built in. **LSFG** uses the shader chain from your own copy of Lossless
|
||||
Scaling: install it from the Steam client in this app and the app reads its `Lossless.dll` from
|
||||
the runtime's Steam library (parsed as data, never executed; nothing of it is redistributed).
|
||||
Pick an engine and 2×/3×/4× on the main screen; it applies immediately, mid-game included.
|
||||
|
||||
## Background and foreground
|
||||
|
||||
A session belongs to a foreground service, not to the activity, so leaving Big Picture does not
|
||||
end it: the process stays at perceptible priority, a partial wake lock keeps the CPU from dropping
|
||||
the guest's threads, and a high-performance WiFi lock keeps a backgrounded download from being
|
||||
throttled to nothing. **Back opens the drawer**: the performance HUD switch, frame generation, on-screen controls
|
||||
(auto / always / never), the display shape (the panel's own, or a fixed 16:9 — the default on a
|
||||
foldable, so opening or closing it mid-session only changes the bars, never the picture; takes
|
||||
effect on the next session), *Send to background* and *Stop session*. Backgrounded, Steam keeps
|
||||
running and the (silent) notification brings it back. A session ends only when you say so — the
|
||||
drawer or the notification's **Stop session** — or when the app is swiped out of recents.
|
||||
|
||||
## Device switches
|
||||
|
||||
Files in `/sdcard/Download`, for a device that cannot be reached with a debugger:
|
||||
|
||||
| File | Effect |
|
||||
|---|---|
|
||||
| `steamdeck-no-pad` | Turns the controller feature off entirely (a true baseline) |
|
||||
| `steamdeck-pad-log` | Traces every interposer call into the session log |
|
||||
| `steamdeck-driver` | `a7xx`, `a8xx` or `system` — overrides the Turnip build the compositor loads |
|
||||
| `steamdeck-no-hud` | Hides the top-right performance line (fps, and `base → generated` while frame generation runs) |
|
||||
| `steamdeck-osc` | `always` or `never` — pins the on-screen controls instead of following what is attached |
|
||||
| `steamdeck-env` | `KEY=VALUE` lines added to the session's environment as written, after the app's own — Zink and Turnip tunables (`ZINK_DESCRIPTORS=lazy`, `MESA_*`), gamescope's, the client's; `#` comments allowed |
|
||||
| `steamdeck-tu-debug` | Turnip's `TU_DEBUG` for the runtime's driver, verbatim (`sysmem`, `sysmem,deck_emu`). Without it, an imported A710/A720/A722 driver gets `sysmem` on its own |
|
||||
|
||||
## Session logs
|
||||
|
||||
Every session writes a folder under `/sdcard/Download/SteamDeck/`, and it is the first thing to
|
||||
look at when something does not start:
|
||||
|
||||
```
|
||||
Download/SteamDeck/session-20260921-161256/
|
||||
device.txt what this device is, and every setting the session ran with
|
||||
session.log the guest session: proot, gamescope, the client's own output
|
||||
wayland.log the app's compositor - what it presented, at what size, and how fast
|
||||
app.log what the app itself decided and reported
|
||||
crash.log Android's crash buffer, as it stood when the session ended
|
||||
crash-app.txt the stack trace, if the app itself crashed
|
||||
audio.log the PulseAudio daemon and the DirectAudio relay helper
|
||||
network.txt the link, the DNS the runtime was given, and what it means
|
||||
steam.log the Steam client's log, scrubbed (Steam mode)
|
||||
steam/ the rest of the client's logs, scrubbed (Steam mode)
|
||||
desktop.log labwc, the panel and the programs on it (desktop mode)
|
||||
```
|
||||
|
||||
`app.log` is this app's own logcat, filtered to its process: which driver it chose and why, the
|
||||
audio line, a rival Steam client being stopped, a helper that was missing, the session's exit
|
||||
status. `crash.log` is Android's crash buffer at teardown, which is where a session the *system*
|
||||
killed leaves its only trace - proot once died before `main` over a missing library and said so
|
||||
there and nowhere else. `audio.log` is the PulseAudio daemon's own output plus the relay helper's,
|
||||
so a module that refuses to load is visible instead of being a silent absence of sound.
|
||||
`network.txt` records the transport, whether the link validated, the address families and the DNS
|
||||
servers the runtime was actually given - **never the network's name**.
|
||||
|
||||
A folder is finished **however the session ends**, not only at a clean stop. Everything written
|
||||
*during* a session is on disk as it happens; only the ending - the compositor's log, Steam's
|
||||
scrubbed logs, the crash buffer - is gathered afterwards, and three things gather it: the ordinary
|
||||
stop; the app's own crash handler, which writes `crash-app.txt` with the stack trace and finishes
|
||||
the folder before the process dies; and a sweep at the next app start, for a folder whose process
|
||||
was killed outright (Android's phantom-process killer, a native crash in the compositor, a dead
|
||||
battery) - that folder gets `ended-without-teardown.txt` saying so, and `crash.log` as the buffer
|
||||
stands then, which still holds the entry unless the device rebooted. A finished folder carries a
|
||||
`.complete` marker; one without it is what the sweep looks for.
|
||||
|
||||
**Session logs** on the main screen turns the folder off. The session still writes its logs - the
|
||||
scripts and the compositor need somewhere to - but into the app's cache, and they are deleted
|
||||
when the session ends, so nothing accumulates in Downloads.
|
||||
|
||||
A session's process tree is also swept at the next start: proot, gamescope and Steam outlive an
|
||||
app that was killed or crashed, holding the rootfs, the GPU and Steam's lock, and the next session
|
||||
then never came up until the app was force-stopped (a Thor Pro report). Anything under the app's
|
||||
uid other than the app itself is a leftover at that point, and `app.log` lists what was killed.
|
||||
|
||||
`device.txt` is written before the session starts, so a session that dies in its first second
|
||||
still says what it ran on: model, SoC, Android and kernel, cores and their ceilings, RAM, the GPU
|
||||
as KGSL names it, the panel, **which driver each half of the session chose**, the runtime version,
|
||||
and every switch that was in effect. **Nothing identifying is collected** - no serial number, no
|
||||
device or advertising id, no account name, no network names - and the Steam logs are scrubbed line
|
||||
by line on the way in: session tokens, machine-auth GUIDs, WebAPI keys, Guard codes and e-mail
|
||||
addresses are replaced, and a SteamID is masked to its last four digits so lines can still be
|
||||
correlated. `loginusers.vdf`, `config.vdf` and the `ssfn` files are never copied at all. A session
|
||||
folder is meant to be attachable to a bug report exactly as it is.
|
||||
|
||||
For more from the desktop than it normally says, create `~/.steamdeck-desktop-debug` inside the
|
||||
runtime: labwc then logs at debug level, each program's window-protocol traffic is recorded, and the
|
||||
session does a name lookup plus an IPv4 and an IPv6 fetch of its own — which is how to tell a broken
|
||||
network apart from a program that only believes it has one.
|
||||
|
||||
### If Steam dies with nothing in the log
|
||||
|
||||
That is usually not Steam. **Android 12 and later kill the extra processes an app starts for
|
||||
itself** once there are more than a few, and a session is made of dozens - proot, gamescope,
|
||||
Xwayland, the client and its helpers, Wine, FEX. Where that is left on, the OS ends the session and
|
||||
no log of ours says why, because nothing in the session did it. Some phones expose it in Developer
|
||||
options as a **"restrict child processes"** switch; turn it off. Otherwise, once per device:
|
||||
|
||||
```sh
|
||||
adb shell settings put global settings_enable_monitor_phantom_procs false
|
||||
```
|
||||
|
||||
`device.txt` records what this phone reports, and the Performance dialog says so when it is not
|
||||
disabled.
|
||||
|
||||
## Authors
|
||||
|
||||
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **maxjivi05 (Max)** — the gamescope runtime this is built on: the proot session, the session
|
||||
shim, the fake-evdev interposer and the controller work, from WinNative.
|
||||
|
||||
## Lineage and licence
|
||||
|
||||
GPL-3.0. The gamescope runtime, the session shim and the fake-evdev interposer come from
|
||||
maxjivi05's WinNative work and from Bannerlator, both GPL-3.0; the Wayland compositor, the frame
|
||||
generation engines and the runtime plumbing are Bannerlator's. Underneath all of it is Winlator
|
||||
(brunodev85, GPL-3.0): the PulseAudio-on-AAudio audio stack, the gamepad state model and the shape
|
||||
of a session's host-side components are his, and WinNative and Bannerlator are both Winlator
|
||||
lineage. Valve, Steam and Proton are Valve Corporation's; this project
|
||||
is not affiliated with Valve.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
+199
-8
@@ -4,22 +4,108 @@ plugins {
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
def gitValue = { List<String> arguments ->
|
||||
try {
|
||||
def output = providers.exec {
|
||||
commandLine(['git'] + arguments)
|
||||
workingDir rootProject.projectDir
|
||||
ignoreExitValue = true
|
||||
}
|
||||
output.result.get().exitValue == 0 ? output.standardOutput.asText.get().trim() : null
|
||||
} catch (Exception ignored) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
def buildLabel = providers.environmentVariable('DROIDDECK_BUILD_LABEL').getOrNull()
|
||||
if (buildLabel == null || buildLabel.trim().isEmpty()) {
|
||||
def eventName = providers.environmentVariable('GITHUB_EVENT_NAME').getOrNull()
|
||||
def eventPath = providers.environmentVariable('GITHUB_EVENT_PATH').getOrNull()
|
||||
if (eventName == 'pull_request_target' && eventPath) {
|
||||
try {
|
||||
def eventFile = layout.file(providers.provider { new File(eventPath) })
|
||||
def eventText = providers.fileContents(eventFile).asText.get()
|
||||
def pullRequest = new groovy.json.JsonSlurper().parseText(eventText).pull_request
|
||||
if (pullRequest?.number && pullRequest?.head?.sha) {
|
||||
buildLabel = "pr${pullRequest.number} at ${pullRequest.head.sha.take(7)}"
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
if (buildLabel == null || buildLabel.trim().isEmpty()) {
|
||||
def branch = providers.environmentVariable('GITHUB_REF_NAME').getOrNull()
|
||||
?: gitValue(['branch', '--show-current']) ?: 'local'
|
||||
def commit = providers.environmentVariable('GITHUB_SHA').getOrNull()
|
||||
?.take(7) ?: gitValue(['rev-parse', '--short=7', 'HEAD']) ?: 'unknown'
|
||||
buildLabel = "${branch} at ${commit}"
|
||||
}
|
||||
}
|
||||
def buildOrigin = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' ? 'remote' : 'local'
|
||||
def treeState = providers.environmentVariable('DROIDDECK_BUILD_TREE_STATE').getOrNull()
|
||||
if (!(treeState in ['clean', 'dirty'])) {
|
||||
def status = gitValue(['status', '--porcelain'])
|
||||
treeState = status == null ? 'unknown' : (status.isEmpty() ? 'clean' : 'dirty')
|
||||
}
|
||||
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
|
||||
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
// What the in-app updater compares against the published builds: the commit this build is of (a PR
|
||||
// build's head, not the merge CI makes of it), its PR, and whether CI made it at all.
|
||||
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
|
||||
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
|
||||
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
|
||||
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
|
||||
// The release key's certificate: a build signed with it, from CI or a computer, updates in place.
|
||||
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
|
||||
// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds.
|
||||
def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' }
|
||||
// The desktop package this apk installs (tools/desktop-kde/release.env): published once per tag in
|
||||
// DroidDeck-Components, pinned here with its checksum like gamescope and the esync packs, so the
|
||||
// desktop scripts in the apk always get the package they were made for.
|
||||
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
|
||||
.findAll { it.contains('=') && !it.startsWith('#') }
|
||||
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
|
||||
def desktopRemovalSources = new groovy.json.JsonSlurper().parse(file('src/main/assets/desktop-removal/sources.json'))
|
||||
if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DESKTOP_KDE_SHA256) {
|
||||
throw new GradleException('Desktop removal inventories do not match the pinned archive')
|
||||
}
|
||||
if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) {
|
||||
throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG)
|
||||
}
|
||||
// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as.
|
||||
def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
|
||||
.findAll { it.trim() && !it.startsWith('#') }
|
||||
.collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" }
|
||||
.join(';')
|
||||
|
||||
android {
|
||||
namespace 'com.steamdeck.launcher'
|
||||
namespace 'com.droiddeck.launcher'
|
||||
compileSdk 34
|
||||
// The NDK is pinned by property so CI can hand in whatever the runner already has rather than
|
||||
// downloading a second copy; see gradle.properties.
|
||||
ndkVersion project.findProperty('ndkVersion') ?: '27.0.12077973'
|
||||
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.steamdeck.launcher"
|
||||
applicationId "com.droiddeck.launcher"
|
||||
minSdkVersion 26
|
||||
// 28 is not a leftover. An app on targetSdk 29+ runs in a domain that may not execute a
|
||||
// file out of its own data directory, and this app's entire job is to exec a rootfs:
|
||||
// proot, gamescope, the Steam client and everything it launches all live under files/.
|
||||
targetSdkVersion 28
|
||||
versionCode 5
|
||||
versionName "0.1.4"
|
||||
versionCode 12
|
||||
versionName "0.3.2"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
|
||||
buildConfigField "int", "PR_NUMBER", "${prNumber}"
|
||||
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
|
||||
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
|
||||
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
|
||||
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
|
||||
buildConfigField "String", "SGDB_API_KEY", "\"${System.getenv("SGDB_API_KEY") ?: project.findProperty("sgdbApiKey") ?: ""}\""
|
||||
buildConfigField "String", "DESKTOP_KDE_TAG", "\"${desktopKde.DESKTOP_KDE_TAG}\""
|
||||
buildConfigField "String", "DESKTOP_KDE_URL", "\"https://github.com/${desktopKde.DESKTOP_KDE_REPO}/releases/download/${desktopKde.DESKTOP_KDE_TAG}/desktop-kde.tar.zst\""
|
||||
buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${desktopKde.DESKTOP_KDE_SHA256}\""
|
||||
buildConfigField "long", "DESKTOP_KDE_SIZE", "${desktopKde.DESKTOP_KDE_SIZE}L"
|
||||
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
@@ -40,7 +126,7 @@ android {
|
||||
|
||||
signingConfigs {
|
||||
// AOSP testkey ([email protected]), the same key Bannerlator ships with. It is publicly
|
||||
// known, so it proves nothing about who built the apk — what it buys is ONE stable
|
||||
// known, so it proves nothing about who built the apk - what it buys is ONE stable
|
||||
// signature: every build installs over the last one instead of asking the user to
|
||||
// uninstall first. v1 (JAR) + v2 + v3 so every installer and Android version accepts it.
|
||||
testkey {
|
||||
@@ -69,22 +155,36 @@ android {
|
||||
lint {
|
||||
// targetSdk 28 is deliberate and structural: an app on 29+ may not execute a file out of
|
||||
// its own data directory, and executing a rootfs is the whole of what this app does. Lint
|
||||
// fails a release build over it because Google Play will not accept an sdk that old —
|
||||
// fails a release build over it because Google Play will not accept an sdk that old -
|
||||
// true, and not something this app can trade away. Every other check stays on.
|
||||
disable 'ExpiredTargetSdkVersion'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
// Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest.
|
||||
excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
|
||||
}
|
||||
jniLibs {
|
||||
// The PulseAudio daemon is executed from the native library directory and its
|
||||
// libraries are read out of the apk by the class loader: both want the old,
|
||||
// uncompressed-and-extracted layout.
|
||||
useLegacyPackaging = true
|
||||
// Prefer the PTY helper built by this app's NDK target over Termux's prebuilt copy.
|
||||
pickFirsts += ['**/libtermux.so']
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
// Plain JVM tests: a stray android.util.Log call returns a default instead of throwing.
|
||||
unitTests.returnDefaultValues = true
|
||||
// Robolectric tests start activities whose text is now all string resources.
|
||||
unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -96,10 +196,92 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// Session scripts must follow this checkout even when the native asset bundle is cached.
|
||||
// A stale local bundle can omit a newly required helper on a fresh runtime install.
|
||||
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
|
||||
from(rootProject.file('tools/linuxfs/overlay')) {
|
||||
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
|
||||
}
|
||||
into(layout.projectDirectory.dir('src/main/assets/linuxfs'))
|
||||
}
|
||||
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
|
||||
|
||||
// Prepared audio lives under build/, so native builds never rewrite the tracked base bundle.
|
||||
// Copying into one asset root also prevents an old base bundle winning a duplicate asset merge.
|
||||
def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets')
|
||||
def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
|
||||
dependsOn(stageSessionScripts)
|
||||
from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' }
|
||||
from(layout.buildDirectory.dir('prepared-assets'))
|
||||
into(runtimeAssetsDir)
|
||||
}
|
||||
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
|
||||
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
|
||||
|
||||
// Unit tests and previews remain usable without Docker. Packaging an APK requires the complete
|
||||
// runtime payload prepared by the local helper or CI, with matching sources and output hashes.
|
||||
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
|
||||
// The session scripts are re-copied into the asset tree by stageSessionScripts; with parallel
|
||||
// task execution this check once hashed a file mid-copy and failed a main build. Hash only
|
||||
// once the staging has finished.
|
||||
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
|
||||
workingDir rootProject.projectDir
|
||||
commandLine 'python3', 'tools/release/runtime_inputs.py', 'verify'
|
||||
}
|
||||
tasks.configureEach { task ->
|
||||
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
|
||||
}
|
||||
|
||||
// The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores, ported from
|
||||
// Bannerlator's bl-steam-client crate). cargo-ndk cross-compiles them for arm64-v8a into the build
|
||||
// directory, which is one more jniLibs source below: nothing compiled is committed, the same rule
|
||||
// the preloads, proot and the DirectAudio relay follow. cargo-ndk exports the NDK clang/llvm-ar as
|
||||
// the C compiler for the one C-compiling crate (ring, under rustls) - the step Bannerlator's CI
|
||||
// does by hand - and reads the NDK from ANDROID_NDK_HOME, which is set to the NDK this build pins.
|
||||
// -PskipRust=true builds without the library, for a checkout with no Rust toolchain: the stores
|
||||
// then report themselves unavailable instead of the build failing.
|
||||
def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores')
|
||||
def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs')
|
||||
def skipRust = (project.findProperty('skipRust') ?: 'false').toString().toBoolean()
|
||||
def buildRustStores = tasks.register('buildRustStores', Exec) {
|
||||
description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.'
|
||||
onlyIf { !skipRust }
|
||||
inputs.dir(rustCrateDir.dir('src'))
|
||||
inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
|
||||
inputs.property('ndkVersion', android.ndkVersion)
|
||||
if (!skipRust) inputs.property('rustToolchain', providers.exec {
|
||||
environment 'PATH', "${System.getProperty('user.home')}/.cargo/bin${File.pathSeparator}${System.getenv('PATH')}"
|
||||
commandLine 'rustup', 'run', 'stable', 'rustc', '-Vv'
|
||||
}.standardOutput.asText)
|
||||
outputs.dir(rustJniLibsDir)
|
||||
workingDir rustCrateDir
|
||||
// The configuration cache serialises the task, so everything it needs is resolved here, not
|
||||
// in doFirst. ~/.cargo/bin is where rustup puts cargo and cargo-ndk; an IDE-launched Gradle
|
||||
// does not always have it on PATH.
|
||||
def cargoBin = "${System.getProperty('user.home')}/.cargo/bin"
|
||||
environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}"
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath
|
||||
environment 'CARGO_TERM_COLOR', 'never'
|
||||
if (!skipRust) environment 'RUSTC', providers.exec {
|
||||
commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc'
|
||||
}.standardOutput.asText.get().trim()
|
||||
// --platform 26 = minSdkVersion: the engines link against the API 26 libc, like the C++ code.
|
||||
// Homebrew's cargo/rustc can precede rustup on macOS and use a different sysroot, despite
|
||||
// rustup reporting the Android target installed. Run both through the selected toolchain.
|
||||
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
|
||||
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
|
||||
'build', '--release', '--locked'
|
||||
}
|
||||
// Skipping Rust must omit its library even if another build left a cached .so behind.
|
||||
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
|
||||
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
|
||||
|
||||
dependencies {
|
||||
// The runtime tarball is a zstd-compressed tar.
|
||||
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
|
||||
implementation 'org.apache.commons:commons-compress:1.20'
|
||||
implementation 'org.apache.commons:commons-compress:1.27.1'
|
||||
// Bannerlator's Windows components come as .tar.xz (WinComponents).
|
||||
implementation 'org.tukaani:xz:1.10'
|
||||
// The UI: every menu, dialog and overlay is Compose. The session's SurfaceView and the
|
||||
// on-screen pad are Views underneath it, which is what they are in every Compose app.
|
||||
implementation platform('androidx.compose:compose-bom:2024.02.00')
|
||||
@@ -110,5 +292,14 @@ dependencies {
|
||||
// copies on. Same versions as Bannerlator so the screen stays a straight copy.
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
implementation 'io.coil-kt:coil-compose:2.6.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
// ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from
|
||||
// Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command.
|
||||
implementation 'com.flyfishxu:kadb-android:1.2.1'
|
||||
// Android terminal view/emulator used by the in-session secondary-display Linux shell.
|
||||
implementation 'com.termux.termux-app:terminal-view:0.118.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
}
|
||||
@@ -2,16 +2,23 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Session logs go to Downloads so a failed run can be handed over as one folder. -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- Store download progress and "installed" / "failed" notices (Android 13+). -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- The session keeps running while the app is not on screen: a foreground service holds the
|
||||
process at perceptible priority, and a high-performance WiFi lock keeps a backgrounded
|
||||
download from being throttled to nothing by the radio's power-save. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<!-- Optional Wi-Fi discovery: requested only from Steam settings after an explanation. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- Ends background processes of another app that holds the Steam login. GameHub starts
|
||||
itself with the phone via boot receivers and signs this client out seconds after every
|
||||
@@ -21,12 +28,18 @@
|
||||
relay helper opens the input stream under this uid, and PulseAudio exposes it to the
|
||||
Steam client as DirectAudioMic. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<!-- Installs DroidDeck's own updates (Updates page) over itself, after the user allows it. -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<!-- A paired Steam Controller, read over Bluetooth LE out of lizard mode (SteamControllerBle).
|
||||
Granted at install below targetSdk 31; no scan, so no location. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -39,19 +52,54 @@
|
||||
|
||||
<!-- A game, to the OS: Android 12+ routes the category through GameManager and the OEM
|
||||
performance frameworks (Game Booster, Game Turbo, Game Space) key off it. Without it
|
||||
a regular flagship runs the session under its "app" power profile — a conservative
|
||||
governor and GPU DVFS — which is why the same chipset feels slower here than in a
|
||||
a regular flagship runs the session under its "app" power profile - a conservative
|
||||
governor and GPU DVFS - which is why the same chipset feels slower here than in a
|
||||
gaming phone whose mode is on for everything. The config says what the OS may do. -->
|
||||
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorLandscape">
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="keyboard|keyboardHidden|navigation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="droiddeck" android:host="game" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.droiddeck.launcher.LAUNCH_GAME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
|
||||
Home launch either back into a running Steam session or to the regular front end.
|
||||
Off until the user turns on "Use as a Home screen" (Setup): while it is disabled the
|
||||
app is not a Home app at all - not offered, not in the "choose Home" list. -->
|
||||
<activity
|
||||
android:name=".HomeActivity"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true"
|
||||
android:taskAffinity="${applicationId}.home"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
@@ -61,9 +109,17 @@
|
||||
|
||||
Resizable, deliberately: a foldable opening or closing changes the window, and the
|
||||
compositor re-fits the picture with bars. Declared non-resizable, the system keeps a
|
||||
fixed-shape window instead and STRETCHES it onto the other panel — the app never sees a
|
||||
fixed-shape window instead and STRETCHES it onto the other panel - the app never sees a
|
||||
size change, so nothing it does can help.
|
||||
-->
|
||||
<activity
|
||||
android:name=".input.ControllerEditorActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/SessionTheme"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
|
||||
|
||||
<activity
|
||||
android:name=".files.FileManagerActivity"
|
||||
android:exported="false"
|
||||
@@ -76,9 +132,26 @@
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<!-- A store's own sign-in page (GOG, Epic Games, Amazon Games) in a WebView. -->
|
||||
<activity
|
||||
android:name=".stores.StoreLoginActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".agent.AgentStartActivity"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:theme="@style/SessionTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".SessionActivity"
|
||||
android:exported="false"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/SessionTheme"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
@@ -86,7 +159,7 @@
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
|
||||
|
||||
<!--
|
||||
Owns the session — gamescope, the Steam client, audio — so it survives the activity
|
||||
Owns the session - gamescope, the Steam client, audio - so it survives the activity
|
||||
going away. Without it Android demotes the process as soon as the last visible
|
||||
activity goes, and the low-memory killer reaps the guest: leaving Big Picture to
|
||||
answer a message would come back to a dead Steam and a half-finished download.
|
||||
@@ -95,5 +168,45 @@
|
||||
android:name=".session.SessionService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false" />
|
||||
<!-- Holds the process in the foreground while the Linux runtime downloads and unpacks. -->
|
||||
<service
|
||||
android:name=".runtime.RuntimeInstallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".core.WirelessAdbPairingService"
|
||||
android:exported="false" />
|
||||
<!-- Keeps the process alive while a store (GOG, Epic, Amazon) game downloads. -->
|
||||
<service
|
||||
android:name=".stores.download.StoreDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.logs"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/log_share_paths" />
|
||||
</provider>
|
||||
<!-- The app's data folder in Android's file picker ("Open from" > DroidDeck). Only the
|
||||
system's documents UI may bind it; apps get a file only when the user picks it. -->
|
||||
<provider
|
||||
android:name=".files.AppFilesProvider"
|
||||
android:authorities="${applicationId}.documents"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".agent.AgentBridgeProvider"
|
||||
android:authorities="${applicationId}.agent"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:grantUriPermissions="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"archives": {
|
||||
"lxqt-r1": "c2bb860b3b1688287451e2495be5db313ec07f8c8af501dfac2768a9f417bc86",
|
||||
"desktop-kde-r1": "55747e75d0ec055ab7ea1e92a757f3be227cc3205bb9101f5b53cadef6318579",
|
||||
"desktop-kde-r2": "9ba832677b498654136c69d8c8330434d17e4d53cd099acac9bde3ba429b35bf",
|
||||
"emulators-r1": "a1f4806a9663d28f424b4cab1277e0ede80a92e69de5bc063cdc8b45af220164"
|
||||
},
|
||||
"desktopApplications": {
|
||||
"ark-26.08.1-1-aarch64.pkg.tar.xz": "93e2c2b75ff20cd29b4f312ed1979ef6f27255b93232b93bfcdb31239903c1d4",
|
||||
"breeze-6.7.5-1-aarch64.pkg.tar.xz": "1d7f4eb9d8d234caaea23621909f5233d19462c2f058afeeb54bed7b81bd03bd",
|
||||
"breeze-gtk-6.7.5-1-any.pkg.tar.xz": "a0f59bc4fc426879c8f8a97f6dcc707fb7d1b52913da5eb7e9db83777b6957b7",
|
||||
"breeze-icons-6.30.0-1-aarch64.pkg.tar.xz": "4bae4689032501156687a227024e56554820747644953c67fc8d2e2b6b0b045a",
|
||||
"dolphin-26.08.1-1-aarch64.pkg.tar.xz": "fef738ab8132d3eba1156b98c116699ce0a5efcb4cb462efbacdcf4c47f79a23",
|
||||
"firefox-157.0.1-1-aarch64.pkg.tar.xz": "d142fdb8a339a65eca7cea6a2bba5b29202386166abd9d59851d6757c9eb2163",
|
||||
"gwenview-26.08.1-1-aarch64.pkg.tar.xz": "a9dc7d5ab73fa677905d5a57248b1b9b404b6b16e862958cc7443d387f684a47",
|
||||
"kate-26.08.1-1-aarch64.pkg.tar.xz": "acf50710042746f243a3b219a1ae0f788e338f3dd9d75093e8bb671a618b83b6",
|
||||
"kde-cli-tools-6.7.5-1-aarch64.pkg.tar.xz": "af466398a912a994a97fadfe46aff401d88a90d7fdce7e843dc4218098fa38ac",
|
||||
"kde-gtk-config-6.7.5-1-aarch64.pkg.tar.xz": "ba6cb4cb01dbb3f2c247f2f6973ede3b64c0849f89340217b51dfbf11cb8cb5c",
|
||||
"kinfocenter-6.7.5-1-aarch64.pkg.tar.xz": "f3d5416db96b50f9a5b8a284d82ae21d834a8cce646010abdd7087491c399b0b",
|
||||
"konsole-26.08.1-1-aarch64.pkg.tar.xz": "1ecf87c0d11d620ebe4aeafb02788e0d5f4fe9019ad9c5d9f1011c43237dd5ec",
|
||||
"kscreen-6.7.5-1-aarch64.pkg.tar.xz": "8fbaa6fedf93edeba021a5416b551663654919b27c2ee237d696ec0b0ce7d5b7",
|
||||
"kwin-6.7.5-1-aarch64.pkg.tar.xz": "a7535710c74b90359f80306134b2b7581132cbac508dc84b7e07ef275cc8c3fe",
|
||||
"mpv-1:0.41.0-6-aarch64.pkg.tar.xz": "6c89d00736820210a3c7f32af4cf95d155191d463f9df8bd22137c45f30a554a",
|
||||
"plasma-desktop-6.7.5-1-aarch64.pkg.tar.xz": "b266dd42cea965b5ab1cc1ae600877e9d3bc83ec642c61fe1669c4457e641a55",
|
||||
"plasma-integration-6.7.5-1-aarch64.pkg.tar.xz": "daa96da14d9a4d12c0d9c0341e1be6836a9a437c469d61da5e9844d51e2606cf",
|
||||
"plasma-pa-6.7.5-1-aarch64.pkg.tar.xz": "02dbefe9d3e0069b2bb37cac2a6be18b8f0a1887c87704a0716e7a9268eadcd8",
|
||||
"plasma-workspace-6.7.5-1-aarch64.pkg.tar.xz": "3a1455857afaa9ccf342ab6167f0f15794ee9d248b0925398ea6f62f12730a86",
|
||||
"spectacle-1:6.7.5-1-aarch64.pkg.tar.xz": "0c9837412ea6119edef18bcb0a8fceb74634cb35be0ca78b3e1dd1ff2443866e",
|
||||
"systemsettings-6.7.5-1-aarch64.pkg.tar.xz": "b02b6a726857d5125c92d1ec95379216c7a3bc1f187a3b46e88c0d056b29ce70",
|
||||
"xdg-desktop-portal-kde-6.7.5-1-aarch64.pkg.tar.xz": "2c57d17d34d577023eb9cd7b0fca1ba9163549afe60450f6271aa148af099ef2"
|
||||
},
|
||||
"legacyApplications": {
|
||||
"featherpad-1.6.3-1-aarch64.pkg.tar.xz": "fcac55215815c0b712453f9bcc09abfe4b518e36025a111a7a7598ef04913f3d",
|
||||
"labwc-0.20.2-1-aarch64.pkg.tar.xz": "bcd4cc3945165b0fdd33c0117cd7b9dfd05cf8c327326c37f3c3a27ef23ee5ca",
|
||||
"lximage-qt-2.4.0-1-aarch64.pkg.tar.xz": "5d6bdd5472ea6bcb4b993b36df3ab7010bb2e62b36a78a649708fc9a52cb0928",
|
||||
"lxqt-config-2.4.0-1-aarch64.pkg.tar.xz": "4b601d798fc7689ee76787dbef8c8958c02429ee60248cd99d950c919dbefba5",
|
||||
"lxqt-panel-2.4.1-1-aarch64.pkg.tar.xz": "a141e0f8049e73f859d843510c6f45d6ed9087c990bcebf44f5f2430621024c4",
|
||||
"lxqt-qtplugin-2.4.0-3-aarch64.pkg.tar.xz": "61c8f828be73111ee0bd5e3163b0d64c2098dd0189710a190f42be11137695cf",
|
||||
"lxqt-runner-2.4.0-1-aarch64.pkg.tar.xz": "5afa74652a37366ea4e7774cf33379977582fbdca599cef1005ebf637cbcae4b",
|
||||
"lxqt-session-2.4.0-2-aarch64.pkg.tar.xz": "e9c7c8a0011859029622ad202e0d3826e4e5bc8d64659031f6efa211456ff6eb",
|
||||
"lxqt-themes-2.4.0-1-any.pkg.tar.xz": "7ea16c62a1fa70cdac13cb3f8fdcdaad299ae1f5bd514acfda671484755787bf",
|
||||
"lxqt-wayland-session-0.4.1-1-any.pkg.tar.xz": "cd8dc11f5035469d23b6bbd937a90593402428c43de37517412aa43fe1fc6b10",
|
||||
"papirus-icon-theme-20260801-1-any.pkg.tar.xz": "4aae3f47b800a60d6fd360e55adc581408ceaafc481c5a420a681741a3966510",
|
||||
"pcmanfm-qt-2.4.1-1-aarch64.pkg.tar.xz": "7ec1a6b39dd0e4055b39f0e2c0549ea263d83004e1b821fb9e42f1d7f5fff0f9",
|
||||
"qterminal-2.4.0-1-aarch64.pkg.tar.xz": "aa4adc9c9d47641b9452ec8d879038a9cb4762c3b6b5299045fac968fd0ed22d",
|
||||
"swaybg-1.2.2-1-aarch64.pkg.tar.xz": "a26caa16ca7d581b5ca05758677058b4b60af10dc8d83e447ad6824b762b48b8",
|
||||
"wlr-randr-0.5.0-1-aarch64.pkg.tar.xz": "58c3651c6a15dc8f95a310c7e225f759aa766be2fb3c0981a9a8b567854675b3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
desktop-kde-r1
|
||||
desktop-kde-r2
|
||||
@@ -1,115 +0,0 @@
|
||||
# DirectAudio for the Linux Steam client (relay build)
|
||||
|
||||
This is the driver for games that run under **Valve's ARM64 Proton inside a Linux
|
||||
runtime on Android** - Bannerlator's linuxfs session with the native Steam client -
|
||||
where the game process is a glibc process and cannot touch Android's audio
|
||||
libraries itself.
|
||||
|
||||
Two pieces, both required:
|
||||
|
||||
| piece | what it is | where it runs |
|
||||
|---|---|---|
|
||||
| `aarch64-unix/winedirectaudio.so` + the two PE shells | the Wine `mmdevapi` driver, glibc build, **no AAudio inside** | in the game, inside the Linux rootfs |
|
||||
| `directaudio-relay` | a small bionic program that owns the AAudio output and microphone streams | on the Android side, under the app's uid |
|
||||
|
||||
They meet over a unix socket. The driver connects, sends its launch config, and
|
||||
gets back two shared-memory rings: one it fills with mixed game audio, one the
|
||||
helper fills with microphone audio. The helper plays and records; the driver
|
||||
keeps doing everything else it always did.
|
||||
|
||||
## Which Proton
|
||||
|
||||
Built inside `ValveSoftware/wine` at `proton_11.0`. The private `mmdevapi`
|
||||
interface it implements is byte-identical across **Proton 11.0 (ARM64)**,
|
||||
**Proton Experimental (ARM64)** and our own Wine-11 layers (checked 2026-09-19),
|
||||
so this one build serves any of them. A future Proton on a Wine 12 base will
|
||||
need a rebuild - the interface is not versioned, and a mismatch is silence, not
|
||||
an error.
|
||||
|
||||
## Installing the driver next to Proton (not into it)
|
||||
|
||||
Steam verifies and updates its Proton depots, so files added under
|
||||
`steamapps/common/Proton .../files/lib/wine/` will not survive. Put the driver
|
||||
in a directory of its own and point Wine at it:
|
||||
|
||||
```
|
||||
<somewhere>/directaudio/lib/wine/aarch64-unix/winedirectaudio.so
|
||||
<somewhere>/directaudio/lib/wine/aarch64-windows/winedirectaudio.drv
|
||||
<somewhere>/directaudio/lib/wine/i386-windows/winedirectaudio.drv
|
||||
```
|
||||
|
||||
and set, for the game process:
|
||||
|
||||
```
|
||||
WINEDLLPATH=<somewhere>/directaudio/lib/wine
|
||||
```
|
||||
|
||||
Wine searches `WINEDLLPATH` entries with the per-arch subdirectories appended,
|
||||
so both the PE shell and the unixlib are found there. In Bannerlator's runtime
|
||||
the natural place to export it is the `bannerlator-proton` compatibility-tool
|
||||
wrapper, which every Proton launch goes through.
|
||||
|
||||
## Selecting the driver
|
||||
|
||||
`mmdevapi` only tries the drivers named in the registry (or its built-in
|
||||
`pulse,alsa,oss,coreaudio` list, which does not know about this one). Set, in the
|
||||
game's prefix:
|
||||
|
||||
```
|
||||
wine reg add "HKCU\Software\Wine\Drivers" /v Audio /d directaudio /f
|
||||
```
|
||||
|
||||
Under Proton the prefix is `compatdata/<appid>/pfx`; run the command with that
|
||||
prefix as `WINEPREFIX` using Proton's own `bin-arm64/wine`, or write the key from
|
||||
the wrapper before it hands off to Proton.
|
||||
|
||||
## Running the helper
|
||||
|
||||
```
|
||||
directaudio-relay --socket <path> [--log]
|
||||
```
|
||||
|
||||
- Run it **under the app's uid**, the way the app already runs its PulseAudio
|
||||
daemon. Android checks `RECORD_AUDIO` against the calling uid, so a helper
|
||||
started any other way can play but cannot record.
|
||||
- Start it **before** the game: the driver asks "is a helper there?" once, when
|
||||
Wine picks an audio driver, and falls back to unavailable if not.
|
||||
- The socket path must be reachable from inside the Linux rootfs. Bannerlator
|
||||
binds the app's files directory and its runtime directory into the session at
|
||||
their own paths, so any socket under either works unchanged on both sides.
|
||||
- The helper serves any number of game processes; each gets its own streams.
|
||||
It exits on `SIGTERM`; a game exiting tears down that game's streams only.
|
||||
|
||||
## Telling the driver where the helper is
|
||||
|
||||
```
|
||||
BANNER_AUDIO_DIRECT_RELAY=<the same socket path>
|
||||
```
|
||||
|
||||
Unset, the driver looks for `$XDG_RUNTIME_DIR/directaudio-relay`.
|
||||
|
||||
## Everything else is unchanged
|
||||
|
||||
All `BANNER_AUDIO_DIRECT_*` knobs mean what they mean in the in-process build:
|
||||
`_PERF`, `_ADAPTIVE`, `_DECAY`, `_MS`, `_MAXMS`, `_BF`, `_MBF`, `_EXCLUSIVE`,
|
||||
`_WATCHDOG`, `_STALL_MS`, `_DECAY_*`, `_LOG`, `_PERIOD_MS`, `_MINPERIOD_MS`, and
|
||||
the live-config mailbox `_RUNTIME` (the helper watches the file; it must be a
|
||||
path the helper can read on the Android side). **`BANNER_AUDIO_DIRECT_MIC=1`**
|
||||
exposes the capture endpoint exactly as before: the helper opens the input
|
||||
stream (`VOICE_COMMUNICATION` preset) when the game connects and makes it hot on
|
||||
the game's first capture start.
|
||||
|
||||
One thing is different: there is one extra hand-off, the ring. The helper's
|
||||
callback pulls a burst from it, so the driver keeps two bursts queued (about 8 ms
|
||||
on a 192-frame device) on top of the AAudio buffer the presets control, and the
|
||||
helper raises that if a callback ever finds the ring short. `get_latency` reports
|
||||
the sum, so a game that sizes its own buffers from it still sees the truth.
|
||||
|
||||
## Proving it is running
|
||||
|
||||
- Helper side: `logcat -s DA-Relay:I` shows `hello from <exe>`, then `open: buffer
|
||||
... ring target ...`, then `capture open` / `capture start` for the mic.
|
||||
- Game side: the session log carries `DirectAudio: relay: connected - ...`.
|
||||
- The game process's `/proc/<pid>/maps` shows `winedirectaudio.so` and **no**
|
||||
`libaaudio.so` (it is in the helper's maps instead). The AudioFlinger track is
|
||||
owned by the helper's pid.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
directaudio linux-wine11 relay build
|
||||
source 3548a4a730e8d01cb0cb0268d51a2fb8f23dcd33
|
||||
wine ValveSoftware/wine@proton_11.0
|
||||
unixlib.h sha256 fdbed263338d0719c4d12e02b045c23721de33219ec8878e4c2d7ebc9efcbe09
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
# The app's native side: the Wayland compositor the session presents into, the adrenotools loader
|
||||
# it opens Turnip with, and one small library for the fake-input ring's memory fence.
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(steamdeck C CXX)
|
||||
project(droiddeck C CXX)
|
||||
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -Wno-unused-function -Wimplicit-function-declaration")
|
||||
|
||||
@@ -24,12 +24,12 @@ add_subdirectory(thirdparty/dxbc)
|
||||
# rot. The compositor's frame-generation leg (waylandcomp/CMakeLists.txt) stamps both in.
|
||||
set(WINFG_UPSTREAM "v0.3.0")
|
||||
set(WINFG_CHAIN_SRC
|
||||
winlator/winfg/framegen.cpp
|
||||
winlator/winfg/framegen.hpp
|
||||
winlator/winfg/record_impl.inc
|
||||
winlator/winfg/config.hpp
|
||||
winlator/winfg/embedded_shaders.hpp
|
||||
winlator/winfg/winfg_engine.cpp
|
||||
framegen/winfg/framegen.cpp
|
||||
framegen/winfg/framegen.hpp
|
||||
framegen/winfg/record_impl.inc
|
||||
framegen/winfg/config.hpp
|
||||
framegen/winfg/embedded_shaders.hpp
|
||||
framegen/winfg/winfg_engine.cpp
|
||||
)
|
||||
set(WINFG_HASH_INPUT "")
|
||||
foreach(f ${WINFG_CHAIN_SRC})
|
||||
@@ -43,3 +43,12 @@ message(STATUS "win-fg chain: ${WINFG_UPSTREAM} (src ${WINFG_CHAIN_HASH})")
|
||||
add_subdirectory(waylandcomp)
|
||||
add_library(sdnative SHARED sdnative/sdnative.c)
|
||||
target_link_libraries(sdnative log)
|
||||
|
||||
# The device info window's Vulkan section: the phone's own driver, asked once through the system
|
||||
# loader (opened with dlopen, so a phone without Vulkan still loads the app).
|
||||
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
|
||||
target_link_libraries(deviceinfo dl)
|
||||
|
||||
# Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its
|
||||
# prebuilt libtermux.so with a redundant libstdc++.so dependency.
|
||||
add_library(termux SHARED termux/termux.c)
|
||||
@@ -0,0 +1,112 @@
|
||||
// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one
|
||||
// instance on the system loader, the first physical device's properties, a few limits and the
|
||||
// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open.
|
||||
#include <dlfcn.h>
|
||||
#include <jni.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#define LINE(...) do { if (used < sizeof(out)) used += snprintf(out + used, sizeof(out) - used, __VA_ARGS__); } while (0)
|
||||
|
||||
static const char *const EXTENSIONS[] = {
|
||||
"VK_EXT_robustness2", "VK_EXT_transform_feedback", "VK_EXT_custom_border_color",
|
||||
"VK_KHR_dynamic_rendering", "VK_EXT_extended_dynamic_state3", "VK_EXT_descriptor_indexing",
|
||||
"VK_KHR_timeline_semaphore", "VK_EXT_mesh_shader", "VK_KHR_ray_query",
|
||||
};
|
||||
|
||||
static const char *type_name(VkPhysicalDeviceType type) {
|
||||
switch (type) {
|
||||
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "integrated GPU";
|
||||
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "discrete GPU";
|
||||
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "virtual GPU";
|
||||
case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz) {
|
||||
static char out[4096];
|
||||
size_t used = 0;
|
||||
out[0] = 0;
|
||||
void *lib = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!lib) { LINE("error=no Vulkan loader\n"); return (*env)->NewStringUTF(env, out); }
|
||||
PFN_vkGetInstanceProcAddr gipa = (PFN_vkGetInstanceProcAddr) dlsym(lib, "vkGetInstanceProcAddr");
|
||||
PFN_vkCreateInstance create = gipa ? (PFN_vkCreateInstance) gipa(NULL, "vkCreateInstance") : NULL;
|
||||
PFN_vkEnumerateInstanceVersion instanceVersion = gipa ? (PFN_vkEnumerateInstanceVersion) gipa(NULL, "vkEnumerateInstanceVersion") : NULL;
|
||||
if (!create) { LINE("error=no vkCreateInstance\n"); dlclose(lib); return (*env)->NewStringUTF(env, out); }
|
||||
uint32_t loader = VK_API_VERSION_1_0;
|
||||
if (instanceVersion) instanceVersion(&loader);
|
||||
LINE("loader=%u.%u.%u\n", VK_API_VERSION_MAJOR(loader), VK_API_VERSION_MINOR(loader), VK_API_VERSION_PATCH(loader));
|
||||
|
||||
VkApplicationInfo app = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "DroidDeck device info",
|
||||
.apiVersion = loader >= VK_API_VERSION_1_1 ? VK_API_VERSION_1_1 : VK_API_VERSION_1_0 };
|
||||
VkInstanceCreateInfo info = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app };
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
if (create(&info, NULL, &instance) != VK_SUCCESS) { LINE("error=vkCreateInstance failed\n"); dlclose(lib); return (*env)->NewStringUTF(env, out); }
|
||||
|
||||
#define GET(name) PFN_##name name = (PFN_##name) gipa(instance, #name)
|
||||
GET(vkDestroyInstance); GET(vkEnumeratePhysicalDevices); GET(vkGetPhysicalDeviceProperties);
|
||||
GET(vkGetPhysicalDeviceProperties2); GET(vkGetPhysicalDeviceMemoryProperties);
|
||||
GET(vkEnumerateDeviceExtensionProperties); GET(vkGetPhysicalDeviceFeatures);
|
||||
uint32_t count = 1;
|
||||
VkPhysicalDevice device = VK_NULL_HANDLE;
|
||||
VkResult listed = vkEnumeratePhysicalDevices ? vkEnumeratePhysicalDevices(instance, &count, &device) : VK_ERROR_INITIALIZATION_FAILED;
|
||||
if ((listed != VK_SUCCESS && listed != VK_INCOMPLETE) || count == 0 || !device || !vkGetPhysicalDeviceProperties) {
|
||||
LINE("error=no Vulkan device\n");
|
||||
} else {
|
||||
VkPhysicalDeviceProperties p;
|
||||
vkGetPhysicalDeviceProperties(device, &p);
|
||||
LINE("device=%s\n", p.deviceName);
|
||||
LINE("type=%s\n", type_name(p.deviceType));
|
||||
LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion));
|
||||
// Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION.
|
||||
LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff);
|
||||
LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID);
|
||||
LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D);
|
||||
|
||||
// Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties).
|
||||
uint32_t extCount = 0;
|
||||
if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL);
|
||||
static VkExtensionProperties exts[512];
|
||||
if (extCount > 512) extCount = 512;
|
||||
if (extCount && vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, exts);
|
||||
int driverProps = p.apiVersion >= VK_API_VERSION_1_2;
|
||||
for (uint32_t i = 0; i < extCount; i++)
|
||||
if (!strcmp(exts[i].extensionName, "VK_KHR_driver_properties")) driverProps = 1;
|
||||
if (driverProps && vkGetPhysicalDeviceProperties2) {
|
||||
VkPhysicalDeviceDriverProperties driver = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES };
|
||||
VkPhysicalDeviceProperties2 p2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver };
|
||||
vkGetPhysicalDeviceProperties2(device, &p2);
|
||||
LINE("driver_name=%s\n", driver.driverName);
|
||||
LINE("driver_info=%s\n", driver.driverInfo);
|
||||
LINE("conformance=%u.%u.%u.%u\n", driver.conformanceVersion.major, driver.conformanceVersion.minor,
|
||||
driver.conformanceVersion.subminor, driver.conformanceVersion.patch);
|
||||
}
|
||||
LINE("extension_count=%u\n", extCount);
|
||||
for (size_t e = 0; e < sizeof(EXTENSIONS) / sizeof(*EXTENSIONS); e++) {
|
||||
int has = 0;
|
||||
for (uint32_t i = 0; i < extCount && !has; i++) has = !strcmp(exts[i].extensionName, EXTENSIONS[e]);
|
||||
LINE("ext.%s=%s\n", EXTENSIONS[e], has ? "yes" : "no");
|
||||
}
|
||||
if (vkGetPhysicalDeviceFeatures) {
|
||||
VkPhysicalDeviceFeatures f;
|
||||
vkGetPhysicalDeviceFeatures(device, &f);
|
||||
LINE("geometry_shader=%s\ntessellation_shader=%s\ntexture_bc=%s\n",
|
||||
f.geometryShader ? "yes" : "no", f.tessellationShader ? "yes" : "no", f.textureCompressionBC ? "yes" : "no");
|
||||
}
|
||||
if (vkGetPhysicalDeviceMemoryProperties) {
|
||||
VkPhysicalDeviceMemoryProperties m;
|
||||
vkGetPhysicalDeviceMemoryProperties(device, &m);
|
||||
VkDeviceSize largest = 0;
|
||||
for (uint32_t i = 0; i < m.memoryHeapCount; i++)
|
||||
if ((m.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) && m.memoryHeaps[i].size > largest) largest = m.memoryHeaps[i].size;
|
||||
LINE("device_heap_mb=%llu\n", (unsigned long long) (largest >> 20));
|
||||
}
|
||||
}
|
||||
if (vkDestroyInstance) vkDestroyInstance(instance, NULL);
|
||||
dlclose(lib);
|
||||
return (*env)->NewStringUTF(env, out);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+5
-13
@@ -108,10 +108,10 @@ struct VkTable {
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string>
|
||||
#define WLOG_TAG "Winlator_Renderer"
|
||||
#define WLOG_TAG "DroidDeck_Renderer"
|
||||
#define RLOG(...) if(verboseLog) __android_log_print(ANDROID_LOG_DEBUG,WLOG_TAG,__VA_ARGS__)
|
||||
#define RLOG_E(...) __android_log_print(ANDROID_LOG_ERROR,WLOG_TAG,__VA_ARGS__)
|
||||
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"Winlator_Scanout",__VA_ARGS__)
|
||||
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"DroidDeck_Scanout",__VA_ARGS__)
|
||||
|
||||
#include <vulkan/vulkan_android.h>
|
||||
#include <android/hardware_buffer.h>
|
||||
@@ -129,7 +129,7 @@ struct VkTable {
|
||||
// Included after SCANOUT_LOG is defined above; its own definition is guarded.
|
||||
#include "../scanout/ScanoutContext.h"
|
||||
|
||||
// Native (compositor-side) LSFG frame generation — capability gate.
|
||||
// Native (compositor-side) LSFG frame generation - capability gate.
|
||||
#include "lsfg/lsfg_probe.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -139,8 +139,6 @@ namespace winfg { class Engine; }
|
||||
|
||||
static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;
|
||||
|
||||
struct WindowPushConstants { float ndcX0, ndcY0, ndcX1, ndcY1; int useTexAlpha; };
|
||||
|
||||
// Push-constant layouts for the spatial-upscaler post passes. The leading vec4
|
||||
// `ndc` matches upscale.vert; the remaining members are read only by the
|
||||
// fragment stage. std430 offsets line up with these C structs.
|
||||
@@ -164,11 +162,6 @@ struct RcasPushConstants { // 40 bytes
|
||||
uint32_t con[4]; // con.x = bit-packed sharpness
|
||||
float outW, outH;
|
||||
};
|
||||
struct DownscalePushConstants { // 32 bytes
|
||||
float ndc[4];
|
||||
float srcW, srcH; // render (source) resolution
|
||||
float dstW, dstH; // display (output) resolution
|
||||
};
|
||||
// Composable post effects (AMD CAS sharpen + fake-HDR). Both lead with vec4 ndc
|
||||
// (offset 0) like the upscaler PCs and stay well under the 88-byte PC range.
|
||||
struct CasPushConstants { // 32 bytes
|
||||
@@ -493,8 +486,8 @@ private:
|
||||
// storage support on a swapchain format is not something a driver owes us.
|
||||
//
|
||||
// So when frame gen is armed the whole existing recording is redirected at
|
||||
// a composite image we own — format-identical to the swapchain, so every
|
||||
// existing pipeline stays render-pass compatible — and a copy moves it into
|
||||
// a composite image we own - format-identical to the swapchain, so every
|
||||
// existing pipeline stays render-pass compatible - and a copy moves it into
|
||||
// the acquired swapchain image at the end. With frame gen off, not one of
|
||||
// these objects is created and the direct-to-swapchain path is untouched.
|
||||
struct CompositeTarget {
|
||||
@@ -741,7 +734,6 @@ private:
|
||||
NisPushConstants nisPC{};
|
||||
EasuPushConstants easuPC{};
|
||||
RcasPushConstants rcasPC{};
|
||||
DownscalePushConstants dsPC{};
|
||||
CasPushConstants casPC{};
|
||||
HdrPushConstants hdrPC{};
|
||||
FxaaPushConstants fxaaPC{};
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_alpha.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_beta.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_chain.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_common.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_delta.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+4
-34
@@ -1,9 +1,9 @@
|
||||
// See lsfg_dll.h. PE resource walk + on-device SPIR-V cache for the Lossless
|
||||
// Scaling frame-generation chain.
|
||||
//
|
||||
// Derived from WinNative's lsfg_dll.c (GPL-3.0-or-later), LSFG port credited to
|
||||
// Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk
|
||||
// (PancakeTAS). Bannerlator is GPL-3.0.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
|
||||
// by @maxjivi05. GPL-3.0-or-later.
|
||||
|
||||
#include "lsfg_dll.h"
|
||||
#include "lsfg_dxbc.h"
|
||||
@@ -315,7 +315,7 @@ uint32_t lookupDescriptorSet(const std::vector<uint32_t>& w, uint32_t targetId)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Precompiled blobs are renumbered into set/binding order — that is the
|
||||
// Precompiled blobs are renumbered into set/binding order - that is the
|
||||
// convention they were built with. The DXBC path uses encounter order instead
|
||||
// (see lsfg_dxbc), which is what DXVK's output pairs with. Do not merge them.
|
||||
bool renumberBindingsSetOrder(std::vector<uint32_t>& w) {
|
||||
@@ -478,20 +478,6 @@ DllStatus validateDll(const std::string& dllPath) {
|
||||
return status;
|
||||
}
|
||||
|
||||
Variant dllVariant(const std::string& dllPath, bool preferFp16) {
|
||||
if (dllPath.empty()) return Variant::None;
|
||||
|
||||
PeImage img;
|
||||
if (!img.open(dllPath)) return Variant::None;
|
||||
|
||||
ResourceTable table;
|
||||
if (parseResources(img, table) != DllStatus::Ok) return Variant::None;
|
||||
|
||||
Variant v = selectVariant(table, preferFp16);
|
||||
if (v == Variant::None && hasBaseChain(table)) v = Variant::DxbcTranslated;
|
||||
return v;
|
||||
}
|
||||
|
||||
DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16) {
|
||||
if (dllPath.empty() || cachePath.empty()) return DllStatus::NotInstalled;
|
||||
|
||||
@@ -547,22 +533,6 @@ DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, b
|
||||
return DllStatus::Ok;
|
||||
}
|
||||
|
||||
DllStatus cacheMatchesSource(const std::string& cachePath, const std::string& dllPath,
|
||||
bool& outMatches) {
|
||||
outMatches = false;
|
||||
if (cachePath.empty() || dllPath.empty()) return DllStatus::CacheUnusable;
|
||||
|
||||
CacheHeader header{};
|
||||
if (!readCacheHeader(cachePath, header)) return DllStatus::CacheUnusable;
|
||||
|
||||
PeImage img;
|
||||
if (!img.open(dllPath)) return DllStatus::NotInstalled;
|
||||
|
||||
outMatches = header.sourceSize == (uint64_t)img.size()
|
||||
&& header.sourceHash == fnv1a64(img.data(), img.size());
|
||||
return DllStatus::Ok;
|
||||
}
|
||||
|
||||
DllStatus cacheVariant(const std::string& cachePath, Variant& outVariant) {
|
||||
outVariant = Variant::None;
|
||||
CacheHeader header{};
|
||||
+10
-17
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// lsfg_dll — extract the Lossless Scaling frame-generation shader chain from
|
||||
// lsfg_dll - extract the Lossless Scaling frame-generation shader chain from
|
||||
// the user's own Lossless.dll, and cache it on device as SPIR-V.
|
||||
//
|
||||
// The 25 compute shaders that make up LSFG 3.1 are NOT redistributable. They
|
||||
// ship as RCDATA resources inside Lossless.dll from the user's own paid copy
|
||||
// of Lossless Scaling (Steam app 993090). Every implementation — upstream
|
||||
// lsfg-vk, LSFG-Android, eden, WinNative — requires the user to supply their
|
||||
// own DLL and extracts the resources on device. Bannerlator does the same, and
|
||||
// of Lossless Scaling (Steam app 993090). Every implementation - upstream
|
||||
// lsfg-vk, LSFG-Android, eden, WinNative - requires the user to supply their
|
||||
// own DLL and extracts the resources on device. DroidDeck does the same, and
|
||||
// bundles nothing.
|
||||
//
|
||||
// The DLL is mmap'd READ-ONLY and parsed as data. It is never loaded as a
|
||||
@@ -18,17 +18,17 @@
|
||||
// and is DXBC. It has always been DXBC.
|
||||
// * Lossless Scaling MAY additionally carry precompiled SPIR-V copies at
|
||||
// base+49 (native fp16) and base+98 (native fp32). No build currently
|
||||
// downloadable from Steam carries them — public buildId 19655272 was
|
||||
// measured with zero SPIR-V anywhere in the 311 MB install — so the DXBC
|
||||
// downloadable from Steam carries them - public buildId 19655272 was
|
||||
// measured with zero SPIR-V anywhere in the 311 MB install - so the DXBC
|
||||
// path is the normal path and the translator is not optional. The SPIR-V
|
||||
// path is kept because it costs nothing and a future build may ship them.
|
||||
//
|
||||
// Both producers fill the same shader-id -> SPIR-V words map, so everything
|
||||
// downstream of this file is source-agnostic.
|
||||
//
|
||||
// Derived from WinNative's lsfg_dll.c (GPL-3.0-or-later), whose LSFG port is
|
||||
// credited to Camille LaVey / the Eden Emulator Project, and which follows
|
||||
// upstream lsfg-vk (PancakeTAS). Bannerlator is GPL-3.0; see THIRD-PARTY-LICENSES.md.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
|
||||
// by @maxjivi05. GPL-3.0-or-later.
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdint>
|
||||
@@ -95,21 +95,14 @@ const char* variantName(Variant v);
|
||||
|
||||
// Does this file look like a usable Lossless.dll? Checks the base chain only,
|
||||
// so a DLL without the SPIR-V variants (i.e. every build on Steam today) is
|
||||
// reported as valid — the translator handles it.
|
||||
// reported as valid - the translator handles it.
|
||||
DllStatus validateDll(const std::string& dllPath);
|
||||
|
||||
// Which producer would be used for this DLL.
|
||||
Variant dllVariant(const std::string& dllPath, bool preferFp16);
|
||||
|
||||
// Parse the DLL, produce all 25 SPIR-V modules, and write them to cachePath
|
||||
// (via temp file + rename, so a failed build cannot leave a half-written
|
||||
// cache behind). Records which producer ran in the cache header.
|
||||
DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16);
|
||||
|
||||
// Is the cache current for this DLL? Compares source size + content hash.
|
||||
DllStatus cacheMatchesSource(const std::string& cachePath, const std::string& dllPath,
|
||||
bool& outMatches);
|
||||
|
||||
// Load a previously built cache.
|
||||
DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet);
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
// See lsfg_dxbc.h. DXBC -> SPIR-V via DXVK's vendored translator.
|
||||
//
|
||||
// Follows upstream lsfg-vk's src/extract/trans.cpp, including its
|
||||
// encounter-order binding renumber. Derived from WinNative's lsfg_dxbc.cpp
|
||||
// (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed, vendored
|
||||
// under cpp/thirdparty/dxbc.
|
||||
// encounter-order binding renumber. Ported to WinNative (lsfg_dxbc.cpp) and
|
||||
// DroidDeck by @maxjivi05 from the work of Camille LaVey / the Eden Emulator
|
||||
// Project (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed,
|
||||
// vendored under cpp/thirdparty/dxbc.
|
||||
|
||||
#include "lsfg_dxbc.h"
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// lsfg_dxbc — DXBC -> SPIR-V for the Lossless Scaling shader chain.
|
||||
// lsfg_dxbc - DXBC -> SPIR-V for the Lossless Scaling shader chain.
|
||||
//
|
||||
// Why this exists at all: the base chain inside Lossless.dll is DXBC, and has
|
||||
// always been DXBC. Lossless Scaling 3.2.2's release note ("Added shaders
|
||||
// intended for use by the lsfg-vk project") describes precompiled SPIR-V
|
||||
// copies at RCDATA base+49 / base+98 — but no build downloadable from Steam
|
||||
// copies at RCDATA base+49 / base+98 - but no build downloadable from Steam
|
||||
// carries them. Measured against the current public build (buildId 19655272,
|
||||
// 2025-08-19; linux_testing byte-identical on depot 993091): the whole 311 MB
|
||||
// install contains zero occurrences of the SPIR-V magic word, and the DLL's
|
||||
@@ -16,8 +16,8 @@
|
||||
// same subset (zlib licence) under cpp/thirdparty/dxbc and follow its
|
||||
// trans.cpp step for step.
|
||||
//
|
||||
// The binding renumber here is ENCOUNTER ORDER — the order in which Binding
|
||||
// decorations appear in DXVK's output — which is what DXVK's own layout pairs
|
||||
// The binding renumber here is ENCOUNTER ORDER - the order in which Binding
|
||||
// decorations appear in DXVK's output - which is what DXVK's own layout pairs
|
||||
// with. The precompiled-SPIR-V path in lsfg_dll.cpp uses a set/binding sort
|
||||
// instead, because those blobs were built with that convention. The two must
|
||||
// not be merged.
|
||||
@@ -30,7 +30,7 @@ namespace lsfg {
|
||||
|
||||
// Translate one DXBC compute shader to SPIR-V, renumbering descriptor bindings
|
||||
// into a dense 0..n range in encounter order. Returns false (leaving outWords
|
||||
// empty) on any malformed input — DXVK's compiler throws, and everything is
|
||||
// empty) on any malformed input - DXVK's compiler throws, and everything is
|
||||
// caught here.
|
||||
// Emits DXVK's native SPIR-V 1.6; lowering for a Vulkan 1.1/1.2 device happens
|
||||
// at load time (lsfg_dll.h downgradeSpirv).
|
||||
+5
-4
@@ -1,7 +1,8 @@
|
||||
// See lsfg_engine.h.
|
||||
//
|
||||
// Ported from WinNative's vkr_lsfg.cpp (GPL-3.0-or-later), LSFG port credited
|
||||
// to Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
|
||||
// @maxjivi05, where it runs on gamescope's output in the compositor.
|
||||
|
||||
#include "lsfg_engine.h"
|
||||
|
||||
@@ -269,7 +270,7 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
|
||||
// nothing. Copy only when generating, on the one priming frame that
|
||||
// precedes generation, or while the governor is warming up towards a
|
||||
// probe - so the ring is populated by the time it is needed.
|
||||
const bool needHistory = generations > 0 || primeHistory_
|
||||
const bool needHistory = generations > 0 || primeHistory_ || followEveryFrame()
|
||||
|| (governorEnabled_ && governor_.wantsHistory());
|
||||
if (needHistory) {
|
||||
copyPresentedFrame(cmd, source, chain_->Input(count), VkExtent2D{width, height});
|
||||
@@ -284,7 +285,7 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
|
||||
// regardless of the game's own settings, and it fed straight back into the
|
||||
// governor: the chain made the source rate collapse, the governor saw the
|
||||
// collapse and refused to generate, and refusing did not stop the chain.
|
||||
if (warm_ && generations > 0) chain_->DispatchShared(cmd, count);
|
||||
if (warm_ && (generations > 0 || followEveryFrame())) chain_->DispatchShared(cmd, count);
|
||||
}
|
||||
|
||||
void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t targetIndex,
|
||||
+8
-3
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// lsfg_engine — the compositor's handle on native LSFG frame generation.
|
||||
// lsfg_engine - the compositor's handle on native LSFG frame generation.
|
||||
//
|
||||
// Owns the shader modules, the interpolation chain and the pacer, and exposes
|
||||
// the small contract the render loop needs:
|
||||
@@ -16,8 +16,9 @@
|
||||
// unavoidable cost of interpolating rather than extrapolating; no placement of
|
||||
// this code changes it.
|
||||
//
|
||||
// Ported from WinNative's vkr_lsfg.cpp (GPL-3.0-or-later), LSFG port credited
|
||||
// to Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
|
||||
// @maxjivi05, where it runs on gamescope's output in the compositor.
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdint>
|
||||
@@ -109,6 +110,10 @@ private:
|
||||
// generation last stopped.
|
||||
bool historyFresh() const { return haveCopied_ && lastCopiedCount_ + 1 == frameCount_; }
|
||||
|
||||
// With a target rate the pacer plans 0..3 generations frame by frame, so the ring and the
|
||||
// shared chain have to follow every frame it keeps warm, not just the ones that generate.
|
||||
bool followEveryFrame() const { return warm_ && pacer_.Config().target_rate != 0; }
|
||||
|
||||
Device device_{};
|
||||
std::string cachePath_;
|
||||
std::unique_ptr<LsfgShaders> shaders_;
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_gamma.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_generate.hpp"
|
||||
#include "lsfg_dll.h"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// lsfg_governor — decides whether an extra generated frame is actually WORTH
|
||||
// lsfg_governor - decides whether an extra generated frame is actually WORTH
|
||||
// it on this device, right now.
|
||||
//
|
||||
// The ported pacer answers "how many frames fit in the panel's budget". That
|
||||
+5
-21
@@ -23,35 +23,19 @@ std::string toStdString(JNIEnv* env, jstring s) {
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) {
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) {
|
||||
return (jint)lsfg::validateDll(toStdString(env, dllPath));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeDllVariant(JNIEnv* env, jclass, jstring dllPath,
|
||||
jboolean preferFp16) {
|
||||
return (jint)lsfg::dllVariant(toStdString(env, dllPath), preferFp16 == JNI_TRUE);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeBuildCache(JNIEnv* env, jclass, jstring dllPath,
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeBuildCache(JNIEnv* env, jclass, jstring dllPath,
|
||||
jstring cachePath, jboolean preferFp16) {
|
||||
return (jint)lsfg::buildCache(toStdString(env, dllPath), toStdString(env, cachePath),
|
||||
preferFp16 == JNI_TRUE);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheMatchesSource(JNIEnv* env, jclass,
|
||||
jstring cachePath,
|
||||
jstring dllPath) {
|
||||
bool matches = false;
|
||||
const lsfg::DllStatus status = lsfg::cacheMatchesSource(toStdString(env, cachePath),
|
||||
toStdString(env, dllPath), matches);
|
||||
return (jboolean)(status == lsfg::DllStatus::Ok && matches);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclass, jstring cachePath) {
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclass, jstring cachePath) {
|
||||
lsfg::Variant variant = lsfg::Variant::None;
|
||||
if (lsfg::cacheVariant(toStdString(env, cachePath), variant) != lsfg::DllStatus::Ok)
|
||||
return (jint)lsfg::Variant::None;
|
||||
@@ -59,11 +43,11 @@ Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclas
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeStatusName(JNIEnv* env, jclass, jint status) {
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeStatusName(JNIEnv* env, jclass, jint status) {
|
||||
return env->NewStringUTF(lsfg::statusName((lsfg::DllStatus)status));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeVariantName(JNIEnv* env, jclass, jint variant) {
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeVariantName(JNIEnv* env, jclass, jint variant) {
|
||||
return env->NewStringUTF(lsfg::variantName((lsfg::Variant)variant));
|
||||
}
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_mipmaps.hpp"
|
||||
#include "lsfg_dll.h"
|
||||
+3
-4
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+6
-4
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_pacer.hpp"
|
||||
|
||||
@@ -22,6 +21,8 @@ constexpr float SOURCE_SMOOTHING = 0.15f;
|
||||
constexpr float SOURCE_STALE_SECONDS = 0.5f;
|
||||
constexpr float DISCONTINUITY_SECONDS = 0.25f;
|
||||
constexpr float HEADROOM_EPSILON = 0.02f;
|
||||
// A target is aimed just under, so rounding never puts output past the panel's refresh rate.
|
||||
constexpr float TARGET_MARGIN = 0.1f;
|
||||
constexpr float CREDIT_EPSILON = 1.0e-4f;
|
||||
constexpr float SOURCE_ACCUM_FLOOR = 0.01f;
|
||||
constexpr uint32_t MIN_RATE_SAMPLES = 12;
|
||||
@@ -117,6 +118,7 @@ LsfgPlan LsfgPacer::Plan(size_t capacity, uint64_t source_frames) {
|
||||
if (target_rate > 0.0f && config.refresh_rate > 0.0f) {
|
||||
target_rate = std::min(target_rate, config.refresh_rate);
|
||||
}
|
||||
if (target_rate > 0.0f) target_rate -= TARGET_MARGIN;
|
||||
|
||||
if (target_rate == 0.0f) {
|
||||
// No explicit target rate: the multiplier is the multiplier, full stop.
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt) {
|
||||
|
||||
// The composite target is COLOR_ATTACHMENT (effect chain writes it),
|
||||
// SAMPLED (next frame's LSFG input reads it), STORAGE (generate writes it)
|
||||
// and both blit ends (it is copied — or, below panel resolution, blitted —
|
||||
// and both blit ends (it is copied - or, below panel resolution, blitted -
|
||||
// into the swapchain image).
|
||||
const VkFormatFeatureFlags need =
|
||||
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// lsfg_probe — capability gate for native (compositor-side) LSFG frame
|
||||
// lsfg_probe - capability gate for native (compositor-side) LSFG frame
|
||||
// generation.
|
||||
//
|
||||
// The Lossless Scaling chain is 25 compute shaders that DXVK's DXBC translator
|
||||
@@ -18,7 +18,7 @@
|
||||
// shaderStorageImageExtendedFormats must be ENABLED at device creation.
|
||||
// Today the renderer enables no features at all, so all three are off.
|
||||
// * `generate` writes into a storage image, and Android swapchain formats
|
||||
// are frequently not storage-capable — so the format is probed separately
|
||||
// are frequently not storage-capable - so the format is probed separately
|
||||
// once the swapchain has picked one.
|
||||
//
|
||||
// A device failing any gate reports unsupported UP FRONT, with a reason, so
|
||||
@@ -85,7 +85,7 @@ struct Caps {
|
||||
// Ask the physical device which of the required features it offers.
|
||||
// Safe on any driver: if vkGetPhysicalDeviceFeatures2 cannot be resolved, or
|
||||
// the device reports below Vulkan 1.2 and `allowVk11` is false, nothing is
|
||||
// chained and `queried` is left false — the caller then creates the device
|
||||
// chained and `queried` is left false - the caller then creates the device
|
||||
// exactly as it always has. With `allowVk11`, a 1.1 device is accepted when
|
||||
// `deviceExtensions` lists VK_KHR_spirv_1_4, VK_KHR_shader_float_controls and
|
||||
// VK_KHR_vulkan_memory_model, and the memory-model features are queried
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_shaders.hpp"
|
||||
#include "lsfg_common.hpp"
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
File renamed without changes.
+4
-4
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// lsfg_vkd — the Vulkan entry points the LSFG chain calls, as a single global.
|
||||
// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global.
|
||||
//
|
||||
// Two dispatch styles meet here. Bannerlator's compositor resolves Vulkan
|
||||
// Two dispatch styles meet here. DroidDeck's compositor resolves Vulkan
|
||||
// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver
|
||||
// lives in an isolated linker namespace and shares no global symbols with the
|
||||
// system loader. The LSFG chain, ported from eden by way of WinNative, calls
|
||||
// system loader. The LSFG chain, ported from Eden by way of WinNative, calls
|
||||
// through a file-scope `vkd`. Rather than rewrite ~2,300 lines of proven chain
|
||||
// code to thread a table through every call, this shim exposes exactly the 34
|
||||
// entry points the chain uses and is filled from the renderer's own table when
|
||||
@@ -64,7 +64,7 @@ struct LsfgVkDispatch {
|
||||
extern LsfgVkDispatch vkd;
|
||||
|
||||
// Fill `vkd` from the renderer's table. Returns false (and leaves the chain
|
||||
// unusable) if any entry point failed to resolve — better to report the engine
|
||||
// unusable) if any entry point failed to resolve - better to report the engine
|
||||
// unsupported than to call through a null pointer mid-frame.
|
||||
bool lsfgVkdInit(const VkTable& table);
|
||||
bool lsfgVkdReady();
|
||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
||||
# NOTICE — FidelityFX Optical Flow (model 3)
|
||||
# NOTICE - FidelityFX Optical Flow (model 3)
|
||||
|
||||
win-fg "model 3" (FSR3 Optical Flow) embeds compute shaders that are an
|
||||
adaptation of the **AMD FidelityFX SDK FSR3 Optical Flow** algorithm.
|
||||
+22
-22
@@ -1,4 +1,4 @@
|
||||
// win-fg — runtime config: enable gate, model, multiplier, and the synthesis
|
||||
// win-fg - runtime config: enable gate, model, multiplier, and the synthesis
|
||||
// tuning parameters. Sourced from environment variables and an optional
|
||||
// conf.toml (a config file, when present, wins over the env defaults).
|
||||
#pragma once
|
||||
@@ -17,7 +17,7 @@ struct Config {
|
||||
// emits a per-frame, step-by-step trail (logcat tag "win-fg") so a freeze/
|
||||
// crash-on-enable leaves an obvious LAST line at the exact stage that hung or
|
||||
// faulted (acquire-spare, compute-submit, present-generated/real, fence wait,
|
||||
// fallback). Zero behaviour change and effectively zero cost when off — one
|
||||
// fallback). Zero behaviour change and effectively zero cost when off - one
|
||||
// predicted-not-taken bool test per step, no logcat writes.
|
||||
// WIN_FG_DEBUG=1|0 / conf.toml debug=on|off.
|
||||
bool debug = false;
|
||||
@@ -25,11 +25,11 @@ struct Config {
|
||||
// Default ON. The 2x insert path presents the GENERATED frame then the REAL
|
||||
// frame back-to-back in one vkQueuePresentKHR hook call, so on Mailbox/
|
||||
// Immediate both land in ~one instant followed by a full frame-interval gap
|
||||
// until the next pair — uneven in time → judder ("slows down to catch up").
|
||||
// until the next pair - uneven in time → judder ("slows down to catch up").
|
||||
// With pacing on, the layer measures the real-frame interval (EMA of a
|
||||
// monotonic clock) and holds the REAL present to its scheduled beat so the
|
||||
// generated frame lands near the temporal MIDPOINT between consecutive real
|
||||
// frames (even gen,real,gen,real cadence). Pure bounded CPU sleep — no GPU
|
||||
// frames (even gen,real,gen,real cadence). Pure bounded CPU sleep - no GPU
|
||||
// wait added; if a frame is already behind schedule the wait is SKIPPED
|
||||
// (present immediately) so worst case is one un-paced pair, never a hitch.
|
||||
// FIFO self-paces so the waits collapse to ~0 there. Off ⇒ byte-identical
|
||||
@@ -49,7 +49,7 @@ struct Config {
|
||||
// slowly to tolerate the extra present with only the +1 spare and freeze on the
|
||||
// first inserted frame; the AYANEO (Adreno 750) recycles fast enough to tolerate
|
||||
// it. Default 2 (a few MB VRAM), applied on all devices. On a device that already
|
||||
// works this only grows the pool — no FPS/quality change; the pool-headroom guard
|
||||
// works this only grows the pool - no FPS/quality change; the pool-headroom guard
|
||||
// in QueuePresentKHR stays a no-op when headroom is adequate.
|
||||
// WIN_FG_EXTRA_IMAGES / conf.toml extra_images.
|
||||
int extraImages = 2;
|
||||
@@ -62,7 +62,7 @@ struct Config {
|
||||
// Out-of-range clamps to Balanced (1), so an unset/older conf.toml is byte-
|
||||
// identical to today. Changing this LIVE rebuilds the affected per-size flow
|
||||
// images (like a resize) inside the layer and resets the flow predictor, so a
|
||||
// preset change is fully hot — no FG toggle needed. See FrameGen::configure /
|
||||
// preset change is fully hot - no FG toggle needed. See FrameGen::configure /
|
||||
// FrameGen::flowFinestForPreset. WIN_FG_PERF_PRESET / conf.toml perf_preset.
|
||||
int perfPreset = 1; // 0 quality, 1 balanced (default), 2 performance
|
||||
float flowScale = 1.0f; // scales solved flow magnitude
|
||||
@@ -76,8 +76,8 @@ struct Config {
|
||||
// C2 FLOW REGULARIZATION (TV-L1 smoothness prior). After C1 removes the camera
|
||||
// motion, the dense flow (flowLvl_ at kFlowFinest, 1/4-res) is the OBJECT-only
|
||||
// residual; it still carries spurious per-block vectors. C2 runs N semi-implicit
|
||||
// TV-L1 denoising iterations on that field — an L1 data term + edge-aware Total-
|
||||
// Variation regularizer — to kill incoherent vectors while KEEPING true motion
|
||||
// TV-L1 denoising iterations on that field - an L1 data term + edge-aware Total-
|
||||
// Variation regularizer - to kill incoherent vectors while KEEPING true motion
|
||||
// discontinuities (object edges) sharp. Solved in-place; expand's median + C1
|
||||
// add-back consume the cleaned field unchanged. 0 iterations ⇒ byte-identical to
|
||||
// pre-C2. See shaders/of3_flowreg.comp.
|
||||
@@ -91,7 +91,7 @@ struct Config {
|
||||
// synthesis (wfg_synth) tuning
|
||||
float beta = 8.0f; // softmax sharpness on importance Z
|
||||
float lambda = 0.6f; // FB-consistency vs photometric weight
|
||||
// Tightened 2026-08-17 to prioritise "no ghost" over max sharpness — the
|
||||
// Tightened 2026-08-17 to prioritise "no ghost" over max sharpness - the
|
||||
// remaining single-frame ghosts came from pixels that the previous defaults
|
||||
// still trusted with warp when they should have cross-faded. Raising both
|
||||
// pushes borderline pixels toward the safe cross-fade path (softer, but no
|
||||
@@ -107,7 +107,7 @@ struct Config {
|
||||
// passed through as the real current frame (no warp, no synth) so text /
|
||||
// overlays don't ghost. Disabled when x0 >= x1 or y0 >= y1 (the default).
|
||||
// Sourced from WIN_FG_HUD_RECT="x0,y0,x1,y1" env var or conf.toml
|
||||
// (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen —
|
||||
// (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen -
|
||||
// host knows where the HUD is, tell the shader to skip it.
|
||||
float hudX0 = 0.0f;
|
||||
float hudY0 = 0.0f;
|
||||
@@ -116,11 +116,11 @@ struct Config {
|
||||
|
||||
// ── TRAINING-DATA CAPTURE MODE (Route-B VFI dataset collection) ───────────
|
||||
// A dev-only gate. When OFF (default) the present path is byte-identical to a
|
||||
// build without this feature — a single bool test and nothing else. When ON,
|
||||
// build without this feature - a single bool test and nothing else. When ON,
|
||||
// every real present is downscaled on the GPU, read back async, and written to
|
||||
// disk as lossless QOI (motion-rich 256² triplet patches by default, or full
|
||||
// downscaled frames) plus a JSONL manifest, for offline (i-1,i+1)->i training.
|
||||
// NEVER captures win-fg's generated frames — capture reads the real swapchain
|
||||
// NEVER captures win-fg's generated frames - capture reads the real swapchain
|
||||
// image at the TOP of QueuePresentKHR, before any interpolation.
|
||||
// WIN_FG_CAPTURE=on|off / conf.toml capture=on|off.
|
||||
bool capture = false;
|
||||
@@ -197,7 +197,7 @@ static inline int parse_capmode(const std::string& v, int dflt) {
|
||||
return dflt;
|
||||
}
|
||||
|
||||
// key=value TOML-lite reader (flat keys, '#' comments) — enough for our knobs.
|
||||
// key=value TOML-lite reader (flat keys, '#' comments) - enough for our knobs.
|
||||
static inline void apply_toml(Config& c, const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f) return;
|
||||
@@ -303,7 +303,7 @@ static inline Config load_config() {
|
||||
c.capShardMB = envi("WIN_FG_CAPTURE_SHARD_MB", c.capShardMB);
|
||||
if (const char* cd = std::getenv("WIN_FG_CAPTURE_DIR")) c.capDir = cd;
|
||||
if (const char* h = std::getenv("WIN_FG_HUD_RECT")) {
|
||||
// "x0,y0,x1,y1" — pixel coords; disabled when x0>=x1 or y0>=y1
|
||||
// "x0,y0,x1,y1" - pixel coords; disabled when x0>=x1 or y0>=y1
|
||||
float r[4] = {0,0,0,0}; int n = 0;
|
||||
std::string s = h; size_t start = 0;
|
||||
for (; n < 4; ++n) {
|
||||
@@ -321,17 +321,17 @@ static inline Config load_config() {
|
||||
return c;
|
||||
}
|
||||
|
||||
// UBO layout — MUST match wfg_synth.comp binding 0.
|
||||
// UBO layout - MUST match wfg_synth.comp binding 0.
|
||||
// The vec4 hudRect is 16-byte aligned in std140, sits at offset 32 (after the
|
||||
// 8 leading floats which naturally occupy 32B). Disabled when hudX0>=hudX1.
|
||||
struct SynthUBO {
|
||||
float flowScale, alpha, beta, lambda, epsilon, photoScale, pad0, pad1;
|
||||
float hudX0, hudY0, hudX1, hudY1;
|
||||
};
|
||||
// UBO layout — MUST match of3_flow / of3_expand_m4 binding 0 (Model3UBO).
|
||||
// UBO layout - MUST match of3_flow / of3_expand_m4 binding 0 (Model3UBO).
|
||||
struct FlowUBO { float flowScale; uint32_t level; float occlLo; float occlHi; };
|
||||
|
||||
// C1 global-motion UBO — MUST match binding 1 in of3_gm_reduce / of3_gm_prewarp /
|
||||
// C1 global-motion UBO - MUST match binding 1 in of3_gm_reduce / of3_gm_prewarp /
|
||||
// of3_expand(_m4). std140: four vec4 = 64 bytes. The affine is in centered-UV
|
||||
// space: P = [[p1,p3],[p2,p4]], translation (tx,ty); warp uvSrc = uv + P*(uv-0.5)
|
||||
// + t maps a prev(template) coord to the curr(image) coord it samples (prev→curr).
|
||||
@@ -345,13 +345,13 @@ struct GMUBO {
|
||||
float appT[4]; // tx,ty, pad, pad
|
||||
};
|
||||
|
||||
// C2 flow-regularization UBO — MUST match binding 0 (vec4 params) in
|
||||
// C2 flow-regularization UBO - MUST match binding 0 (vec4 params) in
|
||||
// of3_flowreg.comp. std140: one vec4 = 16 bytes. See Config::fr* fields.
|
||||
struct FlowRegUBO {
|
||||
float dt; // params.x — semi-implicit smoothing step
|
||||
float lambda; // params.y — L1 data-fidelity weight (θ = dt·lambda)
|
||||
float edgeAlpha; // params.z — luma-gradient edge sensitivity for g
|
||||
float epsTV; // params.w — Charbonnier epsilon (px) for the TV diffusivity
|
||||
float dt; // params.x - semi-implicit smoothing step
|
||||
float lambda; // params.y - L1 data-fidelity weight (θ = dt·lambda)
|
||||
float edgeAlpha; // params.z - luma-gradient edge sensitivity for g
|
||||
float epsTV; // params.w - Charbonnier epsilon (px) for the TV diffusivity
|
||||
};
|
||||
|
||||
} // namespace winfg
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// GENERATED by tools/embed_spv.py — do not edit by hand.
|
||||
// GENERATED by tools/embed_spv.py - do not edit by hand.
|
||||
// win-fg embedded SPIR-V: our optical-flow front end + synthesis only.
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
// win-fg — compute frame-generation engine implementation.
|
||||
// win-fg - compute frame-generation engine implementation.
|
||||
#include "framegen.hpp"
|
||||
#include "embedded_shaders.hpp"
|
||||
#include "log.hpp"
|
||||
@@ -51,7 +51,7 @@ void FrameGen::destroyImage(Img& i) {
|
||||
|
||||
// C1: allocate the per-slot host-visible SSBOs the LK reduce writes partials into.
|
||||
// Host-visible + coherent so the CPU can read them back after the owning frame's
|
||||
// fence signals (no staging copy needed; the buffers are tiny — kGmThreads*kGmStride
|
||||
// fence signals (no staging copy needed; the buffers are tiny - kGmThreads*kGmStride
|
||||
// floats each, ~64 KB). Persistently mapped.
|
||||
bool FrameGen::makeGmBuffers() {
|
||||
const VkDeviceSize sz = (VkDeviceSize)kGmThreads * kGmStride * sizeof(float);
|
||||
@@ -167,7 +167,7 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id,
|
||||
if (!makePipe(pGmPrewarp_, embedded::OF3_GM_PREWARP, unused)) return false;
|
||||
if (!makePipe(pFlowReg_, embedded::OF3_FLOWREG, unused)) return false;
|
||||
if (!makePipe(pSynth_, embedded::WFG_SYNTH, unused)) return false;
|
||||
if (!makeGmBuffers()) { WFG_LOGE("framegen: GM SSBO alloc failed — C1 disabled"); }
|
||||
if (!makeGmBuffers()) { WFG_LOGE("framegen: GM SSBO alloc failed - C1 disabled"); }
|
||||
WFG_LOGI("framegen init ok (queueFamily=%u, 10 pipelines, model=%d, C1 gm=%d, C2 flow_reg=%d iters=%d)",
|
||||
queueFamily_, cfg_.model, cfg_.gmMode, cfg_.frMode, cfg_.frIters);
|
||||
return true;
|
||||
@@ -192,7 +192,7 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force)
|
||||
|
||||
const VkImageUsageFlags lumaUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
|
||||
// flowLvl_ is cleared every frame (per-frame reset) and C2 copies the cleaned
|
||||
// finest level back into it — both need TRANSFER_DST. The C2 ping-pong images
|
||||
// finest level back into it - both need TRANSFER_DST. The C2 ping-pong images
|
||||
// are the copy SOURCE (TRANSFER_SRC).
|
||||
const VkImageUsageFlags flowUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
const VkImageUsageFlags regUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
@@ -203,7 +203,7 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force)
|
||||
if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
|
||||
if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
|
||||
}
|
||||
// C1: stabilized curr luma — only the levels the dense flow reads (finest..coarsest).
|
||||
// C1: stabilized curr luma - only the levels the dense flow reads (finest..coarsest).
|
||||
pyrAs_.resize(kLevels);
|
||||
for (int l = flowFinest_; l < kLevels; ++l)
|
||||
if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
// win-fg — compute frame-generation engine.
|
||||
// win-fg - compute frame-generation engine.
|
||||
// Owns the compute pipelines (our of3_* optical flow + wfg_synth) and the
|
||||
// per-resolution intermediate images, and records the flow+synth graph that
|
||||
// turns (prev, curr) into an interpolated frame. Written for win-fg; no code
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
// Applies a new config. Recomputes the active finest flow level from
|
||||
// cfg_.perfPreset; if it changed while resources are live, safely rebuilds the
|
||||
// affected per-size flow images (like a resize) and resets the flow predictor,
|
||||
// so a perf_preset change is FULLY LIVE — no FG toggle needed. Defined in
|
||||
// so a perf_preset change is FULLY LIVE - no FG toggle needed. Defined in
|
||||
// record_impl.inc (needs the scratch-state teardown). See flowFinestForPreset.
|
||||
void configure(const Config& c);
|
||||
// (Re)build per-resolution resources for a swapchain of the given size/format.
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
// gmSlot selects which C1 global-motion SSBO to reduce into / read back; the
|
||||
// caller MUST pass the ring-slot index of the FrameCtx it just fence-waited
|
||||
// (that guarantees this slot's PREVIOUS reduce has completed and is safe to
|
||||
// read on the host — see the decoupled-LK note in record_impl.inc).
|
||||
// read on the host - see the decoupled-LK note in record_impl.inc).
|
||||
void record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView,
|
||||
VkImageView outView, float alpha, uint32_t gmSlot = 0);
|
||||
void destroy();
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
// 2 Performance=3). kFlowFinestDefault is the Balanced value (today's behaviour);
|
||||
// the ACTIVE level lives in flowFinest_ and is read through flowFinest(). onResize
|
||||
// sizes the stabilized-curr pyramid + C2 scratch from it, and record_impl.inc
|
||||
// wires the flow/expand inputs to it — both must agree, so they all read
|
||||
// wires the flow/expand inputs to it - both must agree, so they all read
|
||||
// flowFinest()/flowFinest_ (never a hard-coded 2). (Was a static constexpr = 2.)
|
||||
static constexpr int kFlowFinestDefault = 2; // Balanced (perf_preset=1)
|
||||
// Map perf_preset -> finest solved level. Out-of-range -> Balanced. A higher
|
||||
@@ -1,4 +1,4 @@
|
||||
// win-fg — logging. On Android goes to logcat (tag "win-fg") so device bring-up
|
||||
// win-fg - logging. On Android goes to logcat (tag "win-fg") so device bring-up
|
||||
// can trace the layer; elsewhere falls back to stderr.
|
||||
#pragma once
|
||||
#if defined(__ANDROID__)
|
||||
@@ -12,7 +12,7 @@
|
||||
#endif
|
||||
|
||||
// Runtime-gated granular debug trace. The gate is a RUNTIME bool (Config::debug),
|
||||
// not a compile-time switch — a shipping .so carries the full trail but pays only
|
||||
// not a compile-time switch - a shipping .so carries the full trail but pays only
|
||||
// a predicted-not-taken bool test per step when debug is off (no logcat write, no
|
||||
// arg evaluation of the message). Same "win-fg" tag so the app captures it under
|
||||
// its own UID. Used to step the present path so a freeze leaves an obvious last
|
||||
+15
-15
@@ -1,4 +1,4 @@
|
||||
// win-fg — FrameGen::record() and its lazy per-size descriptor/UBO setup.
|
||||
// win-fg - FrameGen::record() and its lazy per-size descriptor/UBO setup.
|
||||
// Included from framegen.cpp (has access to FrameGen privates). Written for
|
||||
// win-fg; standard Vulkan compute wiring, no third-party code.
|
||||
//
|
||||
@@ -12,12 +12,12 @@
|
||||
// the global affine back so wfg_synth is unchanged and the final warp is correct.
|
||||
//
|
||||
// DECOUPLED (stall-free) SCHEDULING: a true in-frame CPU solve would need a
|
||||
// mid-frame GPU submit+readback stall — unacceptable on a GPU-bound title. So we
|
||||
// mid-frame GPU submit+readback stall - unacceptable on a GPU-bound title. So we
|
||||
// read back the reduce SSBO the caller ALREADY fence-waited (its ring slot from a
|
||||
// couple of presents ago) and run ONE Gauss-Newton step per frame, warm-started
|
||||
// from the running estimate. Camera motion is temporally smooth, so a ~1-2 frame
|
||||
// old affine is an excellent predictor; and because the SAME affine is removed
|
||||
// (prewarp) and added back (expand), the result is EXACT for any affine we pick —
|
||||
// (prewarp) and added back (expand), the result is EXACT for any affine we pick -
|
||||
// an approximate/stale one only changes how much residual the search must absorb,
|
||||
// never correctness. Identity affine ⇒ byte-identical to the pre-C1 pipeline, so
|
||||
// the bail-to-identity safety path can never be worse than "no pre-warp".
|
||||
@@ -31,7 +31,7 @@ struct ScratchState {
|
||||
// C2: flow-regularization UBO (shared by all iterations; refreshed per frame).
|
||||
VkBuffer flowRegUbo = VK_NULL_HANDLE; VkDeviceMemory flowRegUboMem = VK_NULL_HANDLE; void* flowRegUboPtr = nullptr;
|
||||
// C1: per-slot global-motion UBO (double-buffered by ring slot so a live GPU
|
||||
// read is never overwritten by the next frame — see the header note).
|
||||
// read is never overwritten by the next frame - see the header note).
|
||||
VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{};
|
||||
VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{};
|
||||
void* gmUboPtr[winfg::FrameGen::kGmSlots]{};
|
||||
@@ -170,7 +170,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
|
||||
// Tear down the lazily-built scratch (all UBO buffers + every descriptor set) so a
|
||||
// live perf_preset rebuild neither leaks them nor exhausts the descriptor pool.
|
||||
// The CALLER must have made the device idle first — no in-flight command buffer may
|
||||
// The CALLER must have made the device idle first - no in-flight command buffer may
|
||||
// still reference these. Resetting the pool frees every set FrameGen allocated (all
|
||||
// sets come from this pool); the mapped UBO memory is unmapped + freed explicitly.
|
||||
static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorPool pool) {
|
||||
@@ -191,8 +191,8 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP
|
||||
}
|
||||
|
||||
// ── perf_preset apply / live rebuild ─────────────────────────────────────────
|
||||
// Sets the config, resolves the perf_preset -> finest-solved-level, and — when
|
||||
// that level changed while resources are already live — safely rebuilds the
|
||||
// Sets the config, resolves the perf_preset -> finest-solved-level, and - when
|
||||
// that level changed while resources are already live - safely rebuilds the
|
||||
// affected per-size flow images (a resize) + scratch descriptor sets and resets
|
||||
// the flow predictor. This makes a perf_preset change FULLY LIVE per-present (the
|
||||
// conf.toml hot-reload path calls this), so no FG toggle / bg-fg cycle is needed.
|
||||
@@ -205,7 +205,7 @@ void FrameGen::configure(const Config& c) {
|
||||
const int maxFinest = kLevels - 2; // keep >= 2 solved levels
|
||||
if (want > maxFinest) {
|
||||
WFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist "
|
||||
"(need >=2 solved) — clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest);
|
||||
"(need >=2 solved) - clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest);
|
||||
want = maxFinest;
|
||||
}
|
||||
if (want < 1) want = 1; // never solve full-res (level 0)
|
||||
@@ -224,13 +224,13 @@ void FrameGen::configure(const Config& c) {
|
||||
dd_->DeviceWaitIdle(device_); // no in-flight cmd buf may touch these
|
||||
destroyScratch(dd_, device_, descPool_); // free old scratch (UBOs + all sets)
|
||||
if (!onResize(extent_, VK_FORMAT_UNDEFINED, /*force=*/true))
|
||||
WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u — FG disabled until next resize",
|
||||
WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize",
|
||||
extent_.width, extent_.height);
|
||||
// Reset the persistent flow predictor so stale, differently-scaled state never
|
||||
// feeds the new resolution (avoids a melt/garbage frame on switch). The per-frame
|
||||
// flow field (flowLvl_) is already cleared every frame; the cross-frame state is
|
||||
// the C1 global-motion estimate + its per-slot reduce partials (dispatched against
|
||||
// the old pyramid) — drop both back to identity.
|
||||
// the old pyramid) - drop both back to identity.
|
||||
for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0;
|
||||
for (int s = 0; s < kGmSlots; ++s) { gmSlotValid_[s] = false; for (int i = 0; i < 6; ++i) gmLinAt_[s][i] = 0.0; }
|
||||
gmHaveEst_ = false; gmEngaged_ = false; gmStableCount_ = 0; gmLastTransPx_ = 0.0f;
|
||||
@@ -387,7 +387,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
|
||||
// Reset the flow field to zero every frame. The coarsest level uses it as its
|
||||
// search predictor; carrying it across frames lets a spurious vector feed
|
||||
// forward until it diverges and melts the image (the bg/fg pause was resetting
|
||||
// it — that's why artifacts vanished on resume). Fresh zero every frame =
|
||||
// it - that's why artifacts vanished on resume). Fresh zero every frame =
|
||||
// stable, standard pyramid flow, no feedback loop.
|
||||
{
|
||||
VkClearColorValue zero{};
|
||||
@@ -436,7 +436,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
|
||||
disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l));
|
||||
bar(pyrAs_[l]);
|
||||
}
|
||||
// 3) coarse->fine flow (only down to flowFinest_ — reduced res for perf; the
|
||||
// 3) coarse->fine flow (only down to flowFinest_ - reduced res for perf; the
|
||||
// finest level is perf_preset-selectable). Curr input is now the STABILIZED
|
||||
// pyramid (pyrAs), wired in ensureScratch.
|
||||
for (int l = kLevels - 1; l >= flowFinest_; --l) {
|
||||
@@ -575,7 +575,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN);
|
||||
}
|
||||
|
||||
// C1: reduce sets (one per slot) — prev/curr raw luma at kGmLevel + SSBO + GMUBO.
|
||||
// C1: reduce sets (one per slot) - prev/curr raw luma at kGmLevel + SSBO + GMUBO.
|
||||
if (gmSsbo) {
|
||||
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
|
||||
if (!gmSsbo[s]) continue;
|
||||
@@ -587,7 +587,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
}
|
||||
}
|
||||
|
||||
// C1: prewarp sets (one per slot, per solved flow level) — raw curr in, stabilized out.
|
||||
// C1: prewarp sets (one per slot, per solved flow level) - raw curr in, stabilized out.
|
||||
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
|
||||
S.sGmPrewarp[s].assign(kLevels, VK_NULL_HANDLE);
|
||||
for (int l = fg->flowFinest(); l < kLevels; ++l) {
|
||||
@@ -637,7 +637,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
S.sSynth = allocSet(dd, dev, pool, pSynth.setLayout);
|
||||
wUbo(dd, dev, S.sSynth, 0, S.synthUbo, sizeof(SynthUBO));
|
||||
// GENERAL (not SR): these internal flow images stay in GENERAL across the whole
|
||||
// record() (written as storage by expand, sampled here) — see the discard
|
||||
// record() (written as storage by expand, sampled here) - see the discard
|
||||
// transition at the top of record().
|
||||
wImg(dd, dev, S.sSynth, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expA.view, samp, GEN);
|
||||
wImg(dd, dev, S.sSynth, 35, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expB.view, samp, GEN);
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
// win-fg — minimal Vulkan layer dispatch tables.
|
||||
// win-fg - minimal Vulkan layer dispatch tables.
|
||||
// Structure follows the public Khronos loader/layer interface and the
|
||||
// renderdoc "Vulkan layer guide" skeleton (learning references, Apache-2.0 /
|
||||
// CC-BY). No code from bionic-fg / lsfg / GameScope.
|
||||
@@ -21,10 +21,10 @@ struct InstanceDispatch {
|
||||
PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties = nullptr;
|
||||
PFN_vkGetPhysicalDeviceQueueFamilyProperties GetPhysicalDeviceQueueFamilyProperties = nullptr;
|
||||
// Device-context logging (affected-device fingerprint). Properties2 is core 1.1;
|
||||
// may be null on a 1.0 instance — always null-check before calling.
|
||||
// may be null on a 1.0 instance - always null-check before calling.
|
||||
PFN_vkGetPhysicalDeviceProperties GetPhysicalDeviceProperties = nullptr;
|
||||
PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2 = nullptr;
|
||||
// Surface caps — used to clamp the extra-image-headroom request to the surface's
|
||||
// Surface caps - used to clamp the extra-image-headroom request to the surface's
|
||||
// maxImageCount so the bump never exceeds what the driver can grant.
|
||||
PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR GetPhysicalDeviceSurfaceCapabilitiesKHR = nullptr;
|
||||
};
|
||||
@@ -38,7 +38,7 @@ struct DeviceDispatch {
|
||||
PFN_vkQueueSubmit QueueSubmit = nullptr;
|
||||
// Synchronization2 submit path (core 1.3 + KHR alias). DXVK may use either the
|
||||
// legacy QueueSubmit or QueueSubmit2/2KHR; both are hooked for the guest-submit
|
||||
// diagnostic. May be null if the device did not enable them — always null-check.
|
||||
// diagnostic. May be null if the device did not enable them - always null-check.
|
||||
PFN_vkQueueSubmit2 QueueSubmit2 = nullptr;
|
||||
PFN_vkQueueSubmit2KHR QueueSubmit2KHR = nullptr;
|
||||
PFN_vkQueueWaitIdle QueueWaitIdle = nullptr;
|
||||
@@ -50,8 +50,8 @@ struct DeviceDispatch {
|
||||
PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR = nullptr;
|
||||
PFN_vkAcquireNextImageKHR AcquireNextImageKHR = nullptr;
|
||||
// Alternate acquire entry (VK_KHR_swapchain 1.1+ / device-group). DXVK may use
|
||||
// either AcquireNextImageKHR or AcquireNextImage2KHR — both hooked for the
|
||||
// guest-acquire diagnostic. May be null — always null-check.
|
||||
// either AcquireNextImageKHR or AcquireNextImage2KHR - both hooked for the
|
||||
// guest-acquire diagnostic. May be null - always null-check.
|
||||
PFN_vkAcquireNextImage2KHR AcquireNextImage2KHR = nullptr;
|
||||
PFN_vkQueuePresentKHR QueuePresentKHR = nullptr;
|
||||
|
||||
@@ -84,7 +84,7 @@ struct DeviceDispatch {
|
||||
PFN_vkDestroyPipeline DestroyPipeline = nullptr;
|
||||
PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr;
|
||||
PFN_vkDestroyDescriptorPool DestroyDescriptorPool = nullptr;
|
||||
// Frees every set in the pool at once — used by FrameGen::configure() to reclaim
|
||||
// Frees every set in the pool at once - used by FrameGen::configure() to reclaim
|
||||
// the old scratch descriptor sets when a perf_preset change forces a live rebuild
|
||||
// (otherwise repeated preset switches would exhaust the pool). May be null in
|
||||
// theory; the rebuild path null-checks before calling.
|
||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// winfg_engine — win-fg (Bannerlator's own frame generation) running inside
|
||||
// winfg_engine - win-fg (Bannerlator's own frame generation) running inside
|
||||
// the compositor, behind the same contract the LSFG engine uses:
|
||||
//
|
||||
// prepare(w, h, format) build/rebuild the chain for this size
|
||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// ============================================================================
|
||||
// winfg_vkd — fill win-fg's own dispatch tables from the renderer's VkTable.
|
||||
// winfg_vkd - fill win-fg's own dispatch tables from the renderer's VkTable.
|
||||
//
|
||||
// win-fg's chain (framegen.cpp / record_impl.inc) was written as a Vulkan
|
||||
// LAYER and calls through winfg::DeviceDispatch / InstanceDispatch. Inside the
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
#ifndef SCANOUT_LOG
|
||||
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"Winlator_Scanout",__VA_ARGS__)
|
||||
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"DroidDeck_Scanout",__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
class ScanoutContext {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <stdatomic.h>
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_steamdeck_launcher_input_FakeInputWriter_nativeStoreFence(JNIEnv *env, jclass clazz) {
|
||||
Java_com_droiddeck_launcher_input_FakeInputWriter_nativeStoreFence(JNIEnv *env, jclass clazz) {
|
||||
(void)env;
|
||||
(void)clazz;
|
||||
atomic_thread_fence(memory_order_release);
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
// 1116.2.0
|
||||
#pragma once
|
||||
const uint32_t downscale_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x000000b5,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||
0x00000032,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,
|
||||
0x00000000,0x00000001,0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000046,
|
||||
0x000000ab,0x00030010,0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,
|
||||
0x00000004,0x6e69616d,0x00000000,0x00060005,0x0000000a,0x636e616c,0x32736f7a,0x3b316628,
|
||||
0x00000000,0x00030005,0x00000009,0x00000078,0x00030005,0x0000001d,0x00007870,0x00040005,
|
||||
0x00000031,0x6c616373,0x00000065,0x00030005,0x00000033,0x00004350,0x00040006,0x00000033,
|
||||
0x00000000,0x0063646e,0x00050006,0x00000033,0x00000001,0x53637273,0x00657a69,0x00050006,
|
||||
0x00000033,0x00000002,0x53747364,0x00657a69,0x00030005,0x00000035,0x00006370,0x00050005,
|
||||
0x00000041,0x53766e69,0x656c6163,0x00000000,0x00030005,0x00000044,0x00000063,0x00060005,
|
||||
0x00000046,0x67617266,0x43786554,0x64726f6f,0x00000000,0x00040005,0x0000004d,0x65736162,
|
||||
0x00000000,0x00030005,0x00000051,0x00007a73,0x00030005,0x00000055,0x00637273,0x00030005,
|
||||
0x0000005c,0x00636361,0x00040005,0x0000005e,0x6d757377,0x00000000,0x00030005,0x00000060,
|
||||
0x00007964,0x00030005,0x0000006a,0x00007864,0x00030005,0x00000072,0x00000069,0x00030005,
|
||||
0x00000078,0x00000074,0x00030005,0x00000081,0x00000077,0x00040005,0x00000082,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000088,0x61726170,0x0000006d,0x00030005,0x00000093,0x00006969,
|
||||
0x00050005,0x000000ab,0x4374756f,0x726f6c6f,0x00000000,0x00030047,0x00000033,0x00000002,
|
||||
0x00050048,0x00000033,0x00000000,0x00000023,0x00000000,0x00050048,0x00000033,0x00000001,
|
||||
0x00000023,0x00000010,0x00050048,0x00000033,0x00000002,0x00000023,0x00000018,0x00040047,
|
||||
0x00000046,0x0000001e,0x00000000,0x00040047,0x00000055,0x00000021,0x00000000,0x00040047,
|
||||
0x00000055,0x00000022,0x00000000,0x00040047,0x000000ab,0x0000001e,0x00000000,0x00020013,
|
||||
0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040020,
|
||||
0x00000007,0x00000007,0x00000006,0x00040021,0x00000008,0x00000006,0x00000007,0x0004002b,
|
||||
0x00000006,0x0000000f,0x3727c5ac,0x00020014,0x00000010,0x0004002b,0x00000006,0x00000014,
|
||||
0x3f800000,0x0004002b,0x00000006,0x00000017,0x40000000,0x0004002b,0x00000006,0x0000001b,
|
||||
0x00000000,0x0004002b,0x00000006,0x0000001e,0x40490fdb,0x0004002b,0x00000006,0x00000025,
|
||||
0x3f000000,0x00040017,0x0000002f,0x00000006,0x00000002,0x00040020,0x00000030,0x00000007,
|
||||
0x0000002f,0x00040017,0x00000032,0x00000006,0x00000004,0x0005001e,0x00000033,0x00000032,
|
||||
0x0000002f,0x0000002f,0x00040020,0x00000034,0x00000009,0x00000033,0x0004003b,0x00000034,
|
||||
0x00000035,0x00000009,0x00040015,0x00000036,0x00000020,0x00000001,0x0004002b,0x00000036,
|
||||
0x00000037,0x00000001,0x00040020,0x00000038,0x00000009,0x0000002f,0x0004002b,0x00000036,
|
||||
0x0000003b,0x00000002,0x0005002c,0x0000002f,0x0000003f,0x00000014,0x00000014,0x00040020,
|
||||
0x00000045,0x00000001,0x0000002f,0x0004003b,0x00000045,0x00000046,0x00000001,0x00040017,
|
||||
0x0000004b,0x00000036,0x00000002,0x00040020,0x0000004c,0x00000007,0x0000004b,0x00090019,
|
||||
0x00000052,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,0x00000000,
|
||||
0x0003001b,0x00000053,0x00000052,0x00040020,0x00000054,0x00000000,0x00000053,0x0004003b,
|
||||
0x00000054,0x00000055,0x00000000,0x0004002b,0x00000036,0x00000057,0x00000000,0x00040017,
|
||||
0x0000005a,0x00000006,0x00000003,0x00040020,0x0000005b,0x00000007,0x0000005a,0x0006002c,
|
||||
0x0000005a,0x0000005d,0x0000001b,0x0000001b,0x0000001b,0x00040020,0x0000005f,0x00000007,
|
||||
0x00000036,0x0004002b,0x00000036,0x00000061,0xfffffffc,0x0004002b,0x00000036,0x00000068,
|
||||
0x00000004,0x0005002c,0x0000002f,0x0000007b,0x00000025,0x00000025,0x00040015,0x00000083,
|
||||
0x00000020,0x00000000,0x0004002b,0x00000083,0x00000084,0x00000000,0x0004002b,0x00000083,
|
||||
0x00000089,0x00000001,0x0005002c,0x0000004b,0x00000095,0x00000057,0x00000057,0x0005002c,
|
||||
0x0000004b,0x00000097,0x00000037,0x00000037,0x00040020,0x000000aa,0x00000003,0x00000032,
|
||||
0x0004003b,0x000000aa,0x000000ab,0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,
|
||||
0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000030,0x00000031,0x00000007,0x0004003b,
|
||||
0x00000030,0x00000041,0x00000007,0x0004003b,0x00000030,0x00000044,0x00000007,0x0004003b,
|
||||
0x0000004c,0x0000004d,0x00000007,0x0004003b,0x0000004c,0x00000051,0x00000007,0x0004003b,
|
||||
0x0000005b,0x0000005c,0x00000007,0x0004003b,0x00000007,0x0000005e,0x00000007,0x0004003b,
|
||||
0x0000005f,0x00000060,0x00000007,0x0004003b,0x0000005f,0x0000006a,0x00000007,0x0004003b,
|
||||
0x0000004c,0x00000072,0x00000007,0x0004003b,0x00000030,0x00000078,0x00000007,0x0004003b,
|
||||
0x00000007,0x00000081,0x00000007,0x0004003b,0x00000007,0x00000082,0x00000007,0x0004003b,
|
||||
0x00000007,0x00000088,0x00000007,0x0004003b,0x0000004c,0x00000093,0x00000007,0x00050041,
|
||||
0x00000038,0x00000039,0x00000035,0x00000037,0x0004003d,0x0000002f,0x0000003a,0x00000039,
|
||||
0x00050041,0x00000038,0x0000003c,0x00000035,0x0000003b,0x0004003d,0x0000002f,0x0000003d,
|
||||
0x0000003c,0x00050088,0x0000002f,0x0000003e,0x0000003a,0x0000003d,0x0007000c,0x0000002f,
|
||||
0x00000040,0x00000001,0x00000028,0x0000003e,0x0000003f,0x0003003e,0x00000031,0x00000040,
|
||||
0x0004003d,0x0000002f,0x00000042,0x00000031,0x00050088,0x0000002f,0x00000043,0x0000003f,
|
||||
0x00000042,0x0003003e,0x00000041,0x00000043,0x0004003d,0x0000002f,0x00000047,0x00000046,
|
||||
0x00050041,0x00000038,0x00000048,0x00000035,0x00000037,0x0004003d,0x0000002f,0x00000049,
|
||||
0x00000048,0x00050085,0x0000002f,0x0000004a,0x00000047,0x00000049,0x0003003e,0x00000044,
|
||||
0x0000004a,0x0004003d,0x0000002f,0x0000004e,0x00000044,0x0006000c,0x0000002f,0x0000004f,
|
||||
0x00000001,0x00000008,0x0000004e,0x0004006e,0x0000004b,0x00000050,0x0000004f,0x0003003e,
|
||||
0x0000004d,0x00000050,0x0004003d,0x00000053,0x00000056,0x00000055,0x00040064,0x00000052,
|
||||
0x00000058,0x00000056,0x00050067,0x0000004b,0x00000059,0x00000058,0x00000057,0x0003003e,
|
||||
0x00000051,0x00000059,0x0003003e,0x0000005c,0x0000005d,0x0003003e,0x0000005e,0x0000001b,
|
||||
0x0003003e,0x00000060,0x00000061,0x000200f9,0x00000062,0x000200f8,0x00000062,0x000400f6,
|
||||
0x00000064,0x00000065,0x00000000,0x000200f9,0x00000066,0x000200f8,0x00000066,0x0004003d,
|
||||
0x00000036,0x00000067,0x00000060,0x000500b3,0x00000010,0x00000069,0x00000067,0x00000068,
|
||||
0x000400fa,0x00000069,0x00000063,0x00000064,0x000200f8,0x00000063,0x0003003e,0x0000006a,
|
||||
0x00000061,0x000200f9,0x0000006b,0x000200f8,0x0000006b,0x000400f6,0x0000006d,0x0000006e,
|
||||
0x00000000,0x000200f9,0x0000006f,0x000200f8,0x0000006f,0x0004003d,0x00000036,0x00000070,
|
||||
0x0000006a,0x000500b3,0x00000010,0x00000071,0x00000070,0x00000068,0x000400fa,0x00000071,
|
||||
0x0000006c,0x0000006d,0x000200f8,0x0000006c,0x0004003d,0x0000004b,0x00000073,0x0000004d,
|
||||
0x0004003d,0x00000036,0x00000074,0x0000006a,0x0004003d,0x00000036,0x00000075,0x00000060,
|
||||
0x00050050,0x0000004b,0x00000076,0x00000074,0x00000075,0x00050080,0x0000004b,0x00000077,
|
||||
0x00000073,0x00000076,0x0003003e,0x00000072,0x00000077,0x0004003d,0x0000004b,0x00000079,
|
||||
0x00000072,0x0004006f,0x0000002f,0x0000007a,0x00000079,0x00050081,0x0000002f,0x0000007c,
|
||||
0x0000007a,0x0000007b,0x0004003d,0x0000002f,0x0000007d,0x00000044,0x00050083,0x0000002f,
|
||||
0x0000007e,0x0000007c,0x0000007d,0x0004003d,0x0000002f,0x0000007f,0x00000041,0x00050085,
|
||||
0x0000002f,0x00000080,0x0000007e,0x0000007f,0x0003003e,0x00000078,0x00000080,0x00050041,
|
||||
0x00000007,0x00000085,0x00000078,0x00000084,0x0004003d,0x00000006,0x00000086,0x00000085,
|
||||
0x0003003e,0x00000082,0x00000086,0x00050039,0x00000006,0x00000087,0x0000000a,0x00000082,
|
||||
0x00050041,0x00000007,0x0000008a,0x00000078,0x00000089,0x0004003d,0x00000006,0x0000008b,
|
||||
0x0000008a,0x0003003e,0x00000088,0x0000008b,0x00050039,0x00000006,0x0000008c,0x0000000a,
|
||||
0x00000088,0x00050085,0x00000006,0x0000008d,0x00000087,0x0000008c,0x0003003e,0x00000081,
|
||||
0x0000008d,0x0004003d,0x00000006,0x0000008e,0x00000081,0x000500b4,0x00000010,0x0000008f,
|
||||
0x0000008e,0x0000001b,0x000300f7,0x00000091,0x00000000,0x000400fa,0x0000008f,0x00000090,
|
||||
0x00000091,0x000200f8,0x00000090,0x000200f9,0x0000006e,0x000200f8,0x00000091,0x0004003d,
|
||||
0x0000004b,0x00000094,0x00000072,0x0004003d,0x0000004b,0x00000096,0x00000051,0x00050082,
|
||||
0x0000004b,0x00000098,0x00000096,0x00000097,0x0008000c,0x0000004b,0x00000099,0x00000001,
|
||||
0x0000002d,0x00000094,0x00000095,0x00000098,0x0003003e,0x00000093,0x00000099,0x0004003d,
|
||||
0x00000053,0x0000009a,0x00000055,0x0004003d,0x0000004b,0x0000009b,0x00000093,0x00040064,
|
||||
0x00000052,0x0000009c,0x0000009a,0x0007005f,0x00000032,0x0000009d,0x0000009c,0x0000009b,
|
||||
0x00000002,0x00000057,0x0008004f,0x0000005a,0x0000009e,0x0000009d,0x0000009d,0x00000000,
|
||||
0x00000001,0x00000002,0x0004003d,0x00000006,0x0000009f,0x00000081,0x0005008e,0x0000005a,
|
||||
0x000000a0,0x0000009e,0x0000009f,0x0004003d,0x0000005a,0x000000a1,0x0000005c,0x00050081,
|
||||
0x0000005a,0x000000a2,0x000000a1,0x000000a0,0x0003003e,0x0000005c,0x000000a2,0x0004003d,
|
||||
0x00000006,0x000000a3,0x00000081,0x0004003d,0x00000006,0x000000a4,0x0000005e,0x00050081,
|
||||
0x00000006,0x000000a5,0x000000a4,0x000000a3,0x0003003e,0x0000005e,0x000000a5,0x000200f9,
|
||||
0x0000006e,0x000200f8,0x0000006e,0x0004003d,0x00000036,0x000000a6,0x0000006a,0x00050080,
|
||||
0x00000036,0x000000a7,0x000000a6,0x00000037,0x0003003e,0x0000006a,0x000000a7,0x000200f9,
|
||||
0x0000006b,0x000200f8,0x0000006d,0x000200f9,0x00000065,0x000200f8,0x00000065,0x0004003d,
|
||||
0x00000036,0x000000a8,0x00000060,0x00050080,0x00000036,0x000000a9,0x000000a8,0x00000037,
|
||||
0x0003003e,0x00000060,0x000000a9,0x000200f9,0x00000062,0x000200f8,0x00000064,0x0004003d,
|
||||
0x0000005a,0x000000ac,0x0000005c,0x0004003d,0x00000006,0x000000ad,0x0000005e,0x0007000c,
|
||||
0x00000006,0x000000ae,0x00000001,0x00000028,0x000000ad,0x0000000f,0x00060050,0x0000005a,
|
||||
0x000000af,0x000000ae,0x000000ae,0x000000ae,0x00050088,0x0000005a,0x000000b0,0x000000ac,
|
||||
0x000000af,0x00050051,0x00000006,0x000000b1,0x000000b0,0x00000000,0x00050051,0x00000006,
|
||||
0x000000b2,0x000000b0,0x00000001,0x00050051,0x00000006,0x000000b3,0x000000b0,0x00000002,
|
||||
0x00070050,0x00000032,0x000000b4,0x000000b1,0x000000b2,0x000000b3,0x00000014,0x0003003e,
|
||||
0x000000ab,0x000000b4,0x000100fd,0x00010038,0x00050036,0x00000006,0x0000000a,0x00000000,
|
||||
0x00000008,0x00030037,0x00000007,0x00000009,0x000200f8,0x0000000b,0x0004003b,0x00000007,
|
||||
0x0000001d,0x00000007,0x0004003d,0x00000006,0x0000000c,0x00000009,0x0006000c,0x00000006,
|
||||
0x0000000d,0x00000001,0x00000004,0x0000000c,0x0003003e,0x00000009,0x0000000d,0x0004003d,
|
||||
0x00000006,0x0000000e,0x00000009,0x000500b8,0x00000010,0x00000011,0x0000000e,0x0000000f,
|
||||
0x000300f7,0x00000013,0x00000000,0x000400fa,0x00000011,0x00000012,0x00000013,0x000200f8,
|
||||
0x00000012,0x000200fe,0x00000014,0x000200f8,0x00000013,0x0004003d,0x00000006,0x00000016,
|
||||
0x00000009,0x000500be,0x00000010,0x00000018,0x00000016,0x00000017,0x000300f7,0x0000001a,
|
||||
0x00000000,0x000400fa,0x00000018,0x00000019,0x0000001a,0x000200f8,0x00000019,0x000200fe,
|
||||
0x0000001b,0x000200f8,0x0000001a,0x0004003d,0x00000006,0x0000001f,0x00000009,0x00050085,
|
||||
0x00000006,0x00000020,0x0000001e,0x0000001f,0x0003003e,0x0000001d,0x00000020,0x0004003d,
|
||||
0x00000006,0x00000021,0x0000001d,0x0006000c,0x00000006,0x00000022,0x00000001,0x0000000d,
|
||||
0x00000021,0x00050085,0x00000006,0x00000023,0x00000017,0x00000022,0x0004003d,0x00000006,
|
||||
0x00000024,0x0000001d,0x00050085,0x00000006,0x00000026,0x00000024,0x00000025,0x0006000c,
|
||||
0x00000006,0x00000027,0x00000001,0x0000000d,0x00000026,0x00050085,0x00000006,0x00000028,
|
||||
0x00000023,0x00000027,0x0004003d,0x00000006,0x00000029,0x0000001d,0x0004003d,0x00000006,
|
||||
0x0000002a,0x0000001d,0x00050085,0x00000006,0x0000002b,0x00000029,0x0000002a,0x00050088,
|
||||
0x00000006,0x0000002c,0x00000028,0x0000002b,0x000200fe,0x0000002c,0x00010038
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
// 1116.3.0
|
||||
#pragma once
|
||||
const uint32_t window_frag_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x00000033,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000011,0x00000015,0x00030010,
|
||||
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
|
||||
0x00000000,0x00030005,0x00000009,0x00000063,0x00050005,0x0000000d,0x53786574,0x6c706d61,
|
||||
0x00007265,0x00060005,0x00000011,0x67617266,0x43786554,0x64726f6f,0x00000000,0x00050005,
|
||||
0x00000015,0x4374756f,0x726f6c6f,0x00000000,0x00030005,0x0000001a,0x00004350,0x00050006,
|
||||
0x0000001a,0x00000000,0x5863646e,0x00000030,0x00050006,0x0000001a,0x00000001,0x5963646e,
|
||||
0x00000030,0x00050006,0x0000001a,0x00000002,0x5863646e,0x00000031,0x00050006,0x0000001a,
|
||||
0x00000003,0x5963646e,0x00000031,0x00060006,0x0000001a,0x00000004,0x54657375,0x6c417865,
|
||||
0x00616870,0x00030005,0x0000001c,0x00006370,0x00040047,0x0000000d,0x00000021,0x00000000,
|
||||
0x00040047,0x0000000d,0x00000022,0x00000000,0x00040047,0x00000011,0x0000001e,0x00000000,
|
||||
0x00040047,0x00000015,0x0000001e,0x00000000,0x00030047,0x0000001a,0x00000002,0x00050048,
|
||||
0x0000001a,0x00000000,0x00000023,0x00000000,0x00050048,0x0000001a,0x00000001,0x00000023,
|
||||
0x00000004,0x00050048,0x0000001a,0x00000002,0x00000023,0x00000008,0x00050048,0x0000001a,
|
||||
0x00000003,0x00000023,0x0000000c,0x00050048,0x0000001a,0x00000004,0x00000023,0x00000010,
|
||||
0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,
|
||||
0x00040017,0x00000007,0x00000006,0x00000004,0x00040020,0x00000008,0x00000007,0x00000007,
|
||||
0x00090019,0x0000000a,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,
|
||||
0x00000000,0x0003001b,0x0000000b,0x0000000a,0x00040020,0x0000000c,0x00000000,0x0000000b,
|
||||
0x0004003b,0x0000000c,0x0000000d,0x00000000,0x00040017,0x0000000f,0x00000006,0x00000002,
|
||||
0x00040020,0x00000010,0x00000001,0x0000000f,0x0004003b,0x00000010,0x00000011,0x00000001,
|
||||
0x00040020,0x00000014,0x00000003,0x00000007,0x0004003b,0x00000014,0x00000015,0x00000003,
|
||||
0x00040017,0x00000016,0x00000006,0x00000003,0x00040015,0x00000019,0x00000020,0x00000001,
|
||||
0x0007001e,0x0000001a,0x00000006,0x00000006,0x00000006,0x00000006,0x00000019,0x00040020,
|
||||
0x0000001b,0x00000009,0x0000001a,0x0004003b,0x0000001b,0x0000001c,0x00000009,0x0004002b,
|
||||
0x00000019,0x0000001d,0x00000004,0x00040020,0x0000001e,0x00000009,0x00000019,0x0004002b,
|
||||
0x00000019,0x00000021,0x00000000,0x00020014,0x00000022,0x00040020,0x00000024,0x00000007,
|
||||
0x00000006,0x00040015,0x00000028,0x00000020,0x00000000,0x0004002b,0x00000028,0x00000029,
|
||||
0x00000003,0x0004002b,0x00000006,0x0000002d,0x3f800000,0x00050036,0x00000002,0x00000004,
|
||||
0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008,0x00000009,0x00000007,
|
||||
0x0004003b,0x00000024,0x00000025,0x00000007,0x0004003d,0x0000000b,0x0000000e,0x0000000d,
|
||||
0x0004003d,0x0000000f,0x00000012,0x00000011,0x00050057,0x00000007,0x00000013,0x0000000e,
|
||||
0x00000012,0x0003003e,0x00000009,0x00000013,0x0004003d,0x00000007,0x00000017,0x00000009,
|
||||
0x0008004f,0x00000016,0x00000018,0x00000017,0x00000017,0x00000000,0x00000001,0x00000002,
|
||||
0x00050041,0x0000001e,0x0000001f,0x0000001c,0x0000001d,0x0004003d,0x00000019,0x00000020,
|
||||
0x0000001f,0x000500ab,0x00000022,0x00000023,0x00000020,0x00000021,0x000300f7,0x00000027,
|
||||
0x00000000,0x000400fa,0x00000023,0x00000026,0x0000002c,0x000200f8,0x00000026,0x00050041,
|
||||
0x00000024,0x0000002a,0x00000009,0x00000029,0x0004003d,0x00000006,0x0000002b,0x0000002a,
|
||||
0x0003003e,0x00000025,0x0000002b,0x000200f9,0x00000027,0x000200f8,0x0000002c,0x0003003e,
|
||||
0x00000025,0x0000002d,0x000200f9,0x00000027,0x000200f8,0x00000027,0x0004003d,0x00000006,
|
||||
0x0000002e,0x00000025,0x00050051,0x00000006,0x0000002f,0x00000018,0x00000000,0x00050051,
|
||||
0x00000006,0x00000030,0x00000018,0x00000001,0x00050051,0x00000006,0x00000031,0x00000018,
|
||||
0x00000002,0x00070050,0x00000007,0x00000032,0x0000002f,0x00000030,0x00000031,0x0000002e,
|
||||
0x0003003e,0x00000015,0x00000032,0x000100fd,0x00010038
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
// 1116.3.0
|
||||
#pragma once
|
||||
const uint32_t window_vert_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x0000004a,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0008000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000a,0x0000003a,0x00000044,
|
||||
0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,0x00000000,0x00030005,
|
||||
0x00000008,0x00006978,0x00060005,0x0000000a,0x565f6c67,0x65747265,0x646e4978,0x00007865,
|
||||
0x00030005,0x0000000f,0x00006979,0x00030005,0x00000014,0x00000078,0x00030005,0x0000001b,
|
||||
0x00004350,0x00050006,0x0000001b,0x00000000,0x5863646e,0x00000030,0x00050006,0x0000001b,
|
||||
0x00000001,0x5963646e,0x00000030,0x00050006,0x0000001b,0x00000002,0x5863646e,0x00000031,
|
||||
0x00050006,0x0000001b,0x00000003,0x5963646e,0x00000031,0x00030005,0x0000001d,0x00006370,
|
||||
0x00030005,0x00000027,0x00000079,0x00060005,0x00000038,0x505f6c67,0x65567265,0x78657472,
|
||||
0x00000000,0x00060006,0x00000038,0x00000000,0x505f6c67,0x7469736f,0x006e6f69,0x00070006,
|
||||
0x00000038,0x00000001,0x505f6c67,0x746e696f,0x657a6953,0x00000000,0x00070006,0x00000038,
|
||||
0x00000002,0x435f6c67,0x4470696c,0x61747369,0x0065636e,0x00070006,0x00000038,0x00000003,
|
||||
0x435f6c67,0x446c6c75,0x61747369,0x0065636e,0x00030005,0x0000003a,0x00000000,0x00060005,
|
||||
0x00000044,0x67617266,0x43786554,0x64726f6f,0x00000000,0x00040047,0x0000000a,0x0000000b,
|
||||
0x0000002a,0x00030047,0x0000001b,0x00000002,0x00050048,0x0000001b,0x00000000,0x00000023,
|
||||
0x00000000,0x00050048,0x0000001b,0x00000001,0x00000023,0x00000004,0x00050048,0x0000001b,
|
||||
0x00000002,0x00000023,0x00000008,0x00050048,0x0000001b,0x00000003,0x00000023,0x0000000c,
|
||||
0x00030047,0x00000038,0x00000002,0x00050048,0x00000038,0x00000000,0x0000000b,0x00000000,
|
||||
0x00050048,0x00000038,0x00000001,0x0000000b,0x00000001,0x00050048,0x00000038,0x00000002,
|
||||
0x0000000b,0x00000003,0x00050048,0x00000038,0x00000003,0x0000000b,0x00000004,0x00040047,
|
||||
0x00000044,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,
|
||||
0x00040015,0x00000006,0x00000020,0x00000001,0x00040020,0x00000007,0x00000007,0x00000006,
|
||||
0x00040020,0x00000009,0x00000001,0x00000006,0x0004003b,0x00000009,0x0000000a,0x00000001,
|
||||
0x0004002b,0x00000006,0x0000000c,0x00000001,0x00030016,0x00000012,0x00000020,0x00040020,
|
||||
0x00000013,0x00000007,0x00000012,0x00020014,0x00000016,0x0006001e,0x0000001b,0x00000012,
|
||||
0x00000012,0x00000012,0x00000012,0x00040020,0x0000001c,0x00000009,0x0000001b,0x0004003b,
|
||||
0x0000001c,0x0000001d,0x00000009,0x0004002b,0x00000006,0x0000001e,0x00000002,0x00040020,
|
||||
0x0000001f,0x00000009,0x00000012,0x0004002b,0x00000006,0x00000023,0x00000000,0x0004002b,
|
||||
0x00000006,0x0000002d,0x00000003,0x00040017,0x00000034,0x00000012,0x00000004,0x00040015,
|
||||
0x00000035,0x00000020,0x00000000,0x0004002b,0x00000035,0x00000036,0x00000001,0x0004001c,
|
||||
0x00000037,0x00000012,0x00000036,0x0006001e,0x00000038,0x00000034,0x00000012,0x00000037,
|
||||
0x00000037,0x00040020,0x00000039,0x00000003,0x00000038,0x0004003b,0x00000039,0x0000003a,
|
||||
0x00000003,0x0004002b,0x00000012,0x0000003d,0x00000000,0x0004002b,0x00000012,0x0000003e,
|
||||
0x3f800000,0x00040020,0x00000040,0x00000003,0x00000034,0x00040017,0x00000042,0x00000012,
|
||||
0x00000002,0x00040020,0x00000043,0x00000003,0x00000042,0x0004003b,0x00000043,0x00000044,
|
||||
0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,
|
||||
0x0004003b,0x00000007,0x00000008,0x00000007,0x0004003b,0x00000007,0x0000000f,0x00000007,
|
||||
0x0004003b,0x00000013,0x00000014,0x00000007,0x0004003b,0x00000013,0x00000018,0x00000007,
|
||||
0x0004003b,0x00000013,0x00000027,0x00000007,0x0004003b,0x00000013,0x0000002a,0x00000007,
|
||||
0x0004003d,0x00000006,0x0000000b,0x0000000a,0x000500c3,0x00000006,0x0000000d,0x0000000b,
|
||||
0x0000000c,0x000500c7,0x00000006,0x0000000e,0x0000000d,0x0000000c,0x0003003e,0x00000008,
|
||||
0x0000000e,0x0004003d,0x00000006,0x00000010,0x0000000a,0x000500c7,0x00000006,0x00000011,
|
||||
0x00000010,0x0000000c,0x0003003e,0x0000000f,0x00000011,0x0004003d,0x00000006,0x00000015,
|
||||
0x00000008,0x000500aa,0x00000016,0x00000017,0x00000015,0x0000000c,0x000300f7,0x0000001a,
|
||||
0x00000000,0x000400fa,0x00000017,0x00000019,0x00000022,0x000200f8,0x00000019,0x00050041,
|
||||
0x0000001f,0x00000020,0x0000001d,0x0000001e,0x0004003d,0x00000012,0x00000021,0x00000020,
|
||||
0x0003003e,0x00000018,0x00000021,0x000200f9,0x0000001a,0x000200f8,0x00000022,0x00050041,
|
||||
0x0000001f,0x00000024,0x0000001d,0x00000023,0x0004003d,0x00000012,0x00000025,0x00000024,
|
||||
0x0003003e,0x00000018,0x00000025,0x000200f9,0x0000001a,0x000200f8,0x0000001a,0x0004003d,
|
||||
0x00000012,0x00000026,0x00000018,0x0003003e,0x00000014,0x00000026,0x0004003d,0x00000006,
|
||||
0x00000028,0x0000000f,0x000500aa,0x00000016,0x00000029,0x00000028,0x0000000c,0x000300f7,
|
||||
0x0000002c,0x00000000,0x000400fa,0x00000029,0x0000002b,0x00000030,0x000200f8,0x0000002b,
|
||||
0x00050041,0x0000001f,0x0000002e,0x0000001d,0x0000002d,0x0004003d,0x00000012,0x0000002f,
|
||||
0x0000002e,0x0003003e,0x0000002a,0x0000002f,0x000200f9,0x0000002c,0x000200f8,0x00000030,
|
||||
0x00050041,0x0000001f,0x00000031,0x0000001d,0x0000000c,0x0004003d,0x00000012,0x00000032,
|
||||
0x00000031,0x0003003e,0x0000002a,0x00000032,0x000200f9,0x0000002c,0x000200f8,0x0000002c,
|
||||
0x0004003d,0x00000012,0x00000033,0x0000002a,0x0003003e,0x00000027,0x00000033,0x0004003d,
|
||||
0x00000012,0x0000003b,0x00000014,0x0004003d,0x00000012,0x0000003c,0x00000027,0x00070050,
|
||||
0x00000034,0x0000003f,0x0000003b,0x0000003c,0x0000003d,0x0000003e,0x00050041,0x00000040,
|
||||
0x00000041,0x0000003a,0x00000023,0x0003003e,0x00000041,0x0000003f,0x0004003d,0x00000006,
|
||||
0x00000045,0x00000008,0x0004006f,0x00000012,0x00000046,0x00000045,0x0004003d,0x00000006,
|
||||
0x00000047,0x0000000f,0x0004006f,0x00000012,0x00000048,0x00000047,0x00050050,0x00000042,
|
||||
0x00000049,0x00000046,0x00000048,0x0003003e,0x00000044,0x00000049,0x000100fd,0x00010038
|
||||
};
|
||||
@@ -0,0 +1,218 @@
|
||||
// Upstream: termux/termux-app v0.118.0, terminal-emulator/src/main/jni/termux.c.
|
||||
// Licensed under the repository's GPL-3.0 license (see LICENSE).
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <jni.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define TERMUX_UNUSED(x) x __attribute__((__unused__))
|
||||
#ifdef __APPLE__
|
||||
# define LACKS_PTSNAME_R
|
||||
#endif
|
||||
|
||||
static int throw_runtime_exception(JNIEnv* env, char const* message)
|
||||
{
|
||||
jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
|
||||
(*env)->ThrowNew(env, exClass, message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int create_subprocess(JNIEnv* env,
|
||||
char const* cmd,
|
||||
char const* cwd,
|
||||
char* const argv[],
|
||||
char** envp,
|
||||
int* pProcessId,
|
||||
jint rows,
|
||||
jint columns)
|
||||
{
|
||||
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
|
||||
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
|
||||
|
||||
#ifdef LACKS_PTSNAME_R
|
||||
char* devname;
|
||||
#else
|
||||
char devname[64];
|
||||
#endif
|
||||
if (grantpt(ptm) || unlockpt(ptm) ||
|
||||
#ifdef LACKS_PTSNAME_R
|
||||
(devname = ptsname(ptm)) == NULL
|
||||
#else
|
||||
ptsname_r(ptm, devname, sizeof(devname))
|
||||
#endif
|
||||
) {
|
||||
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
|
||||
}
|
||||
|
||||
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
|
||||
struct termios tios;
|
||||
tcgetattr(ptm, &tios);
|
||||
tios.c_iflag |= IUTF8;
|
||||
tios.c_iflag &= ~(IXON | IXOFF);
|
||||
tcsetattr(ptm, TCSANOW, &tios);
|
||||
|
||||
/** Set initial winsize. */
|
||||
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
|
||||
ioctl(ptm, TIOCSWINSZ, &sz);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return throw_runtime_exception(env, "Fork failed");
|
||||
} else if (pid > 0) {
|
||||
*pProcessId = (int) pid;
|
||||
return ptm;
|
||||
} else {
|
||||
// Clear signals which the Android java process may have blocked:
|
||||
sigset_t signals_to_unblock;
|
||||
sigfillset(&signals_to_unblock);
|
||||
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
|
||||
|
||||
close(ptm);
|
||||
setsid();
|
||||
|
||||
int pts = open(devname, O_RDWR);
|
||||
if (pts < 0) exit(-1);
|
||||
|
||||
dup2(pts, 0);
|
||||
dup2(pts, 1);
|
||||
dup2(pts, 2);
|
||||
|
||||
DIR* self_dir = opendir("/proc/self/fd");
|
||||
if (self_dir != NULL) {
|
||||
int self_dir_fd = dirfd(self_dir);
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(self_dir)) != NULL) {
|
||||
int fd = atoi(entry->d_name);
|
||||
if (fd > 2 && fd != self_dir_fd) close(fd);
|
||||
}
|
||||
closedir(self_dir);
|
||||
}
|
||||
|
||||
clearenv();
|
||||
if (envp) for (; *envp; ++envp) putenv(*envp);
|
||||
|
||||
if (chdir(cwd) != 0) {
|
||||
char* error_message;
|
||||
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
|
||||
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
|
||||
perror(error_message);
|
||||
fflush(stderr);
|
||||
}
|
||||
execvp(cmd, argv);
|
||||
// Show terminal output about failing exec() call:
|
||||
char* error_message;
|
||||
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
|
||||
perror(error_message);
|
||||
_exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
|
||||
JNIEnv* env,
|
||||
jclass TERMUX_UNUSED(clazz),
|
||||
jstring cmd,
|
||||
jstring cwd,
|
||||
jobjectArray args,
|
||||
jobjectArray envVars,
|
||||
jintArray processIdArray,
|
||||
jint rows,
|
||||
jint columns)
|
||||
{
|
||||
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
|
||||
char** argv = NULL;
|
||||
if (size > 0) {
|
||||
argv = (char**) malloc((size + 1) * sizeof(char*));
|
||||
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
|
||||
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
|
||||
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
|
||||
argv[i] = strdup(arg_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
|
||||
(*env)->DeleteLocalRef(env, arg_java_string);
|
||||
}
|
||||
argv[size] = NULL;
|
||||
}
|
||||
|
||||
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
|
||||
char** envp = NULL;
|
||||
if (size > 0) {
|
||||
envp = (char**) malloc((size + 1) * sizeof(char *));
|
||||
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
|
||||
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
|
||||
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
|
||||
envp[i] = strdup(env_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
|
||||
(*env)->DeleteLocalRef(env, env_java_string);
|
||||
}
|
||||
envp[size] = NULL;
|
||||
}
|
||||
|
||||
int procId = 0;
|
||||
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
|
||||
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
|
||||
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, cwd, cmd_cwd);
|
||||
|
||||
if (argv) {
|
||||
for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
|
||||
free(argv);
|
||||
}
|
||||
if (envp) {
|
||||
for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
|
||||
free(envp);
|
||||
}
|
||||
|
||||
int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL);
|
||||
if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
|
||||
|
||||
*pProcId = procId;
|
||||
(*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0);
|
||||
|
||||
return ptm;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
|
||||
{
|
||||
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
|
||||
ioctl(fd, TIOCSWINSZ, &sz);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd)
|
||||
{
|
||||
struct termios tios;
|
||||
tcgetattr(fd, &tios);
|
||||
if ((tios.c_iflag & IUTF8) == 0) {
|
||||
tios.c_iflag |= IUTF8;
|
||||
tcsetattr(fd, TCSANOW, &tios);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
|
||||
{
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
if (WIFEXITED(status)) {
|
||||
return WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
return -WTERMSIG(status);
|
||||
} else {
|
||||
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor)
|
||||
{
|
||||
close(fileDescriptor);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
# generated/ (no host wayland-scanner needed at app build time). Built by the app
|
||||
# (add_subdirectory from ../CMakeLists.txt) and on its own by standalone/ for
|
||||
# build-waylandcomp.yml.
|
||||
add_library(bannerwayland SHARED
|
||||
add_library(droiddeckwayland SHARED
|
||||
src/compositor.c
|
||||
src/vk_present.c
|
||||
src/effects_chain.c
|
||||
@@ -12,12 +12,15 @@ add_library(bannerwayland SHARED
|
||||
src/sc_layer.c
|
||||
src/ahb_swapchain.c
|
||||
src/waylandcomp_jni.c
|
||||
src/banner_ext.c
|
||||
src/droiddeck_ext.c
|
||||
src/wl_dmabuf.c
|
||||
src/wl_clipboard.c
|
||||
src/wl_text_input.c
|
||||
src/wl_toplevel_icon.c
|
||||
src/wl_single_pixel.c
|
||||
src/wl_color_mgmt.c
|
||||
src/hdr_compose.c
|
||||
src/blend_pass.c
|
||||
generated/color-management-v1-protocol.c
|
||||
generated/xdg-shell-protocol.c
|
||||
generated/linux-dmabuf-v1-protocol.c
|
||||
@@ -29,76 +32,80 @@ add_library(bannerwayland SHARED
|
||||
generated/wlr-data-control-unstable-v1-protocol.c
|
||||
generated/text-input-unstable-v3-protocol.c
|
||||
generated/xdg-toplevel-icon-v1-protocol.c
|
||||
generated/single-pixel-buffer-v1-protocol.c
|
||||
generated/banner-ahb-v1-protocol.c
|
||||
src/framegen_bridge.c
|
||||
src/framegen_jni.c
|
||||
)
|
||||
target_include_directories(bannerwayland PRIVATE
|
||||
# Warnings on for the compositor's own C. They do not fail the build; they are there to be read.
|
||||
target_compile_options(droiddeckwayland PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:C>:-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers>)
|
||||
target_include_directories(droiddeckwayland PRIVATE
|
||||
generated
|
||||
prebuilt/include
|
||||
src
|
||||
# The X11 Vulkan renderer's committed SPIR-V headers (*_frag.h / upscale_vert.h, made by
|
||||
# winlator/gen_shaders.sh): the screen-effect chain (src/effects_chain.c) runs the same bytes.
|
||||
# Bannerlator's gen_shaders.sh): the screen-effect chain (src/effects_chain.c) runs the same bytes.
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../shaders
|
||||
)
|
||||
|
||||
# ── Frame generation (feat/wayland-framegen) ─────────────────────────────────
|
||||
# The two native engines the X11 renderer runs (LSFG Native: winlator/lsfg, Win-FG Native:
|
||||
# winlator/winfg) are compiled into this library unmodified and driven by
|
||||
# The two native engines the X11 renderer runs (LSFG Native: framegen/lsfg, Win-FG Native:
|
||||
# framegen/winfg) are compiled into this library unmodified and driven by
|
||||
# src/framegen_engine.cpp through the compositor's own Turnip entry points. They need the
|
||||
# winlator tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format),
|
||||
# framegen tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format),
|
||||
# which only the app build has: the standalone build (standalone/) gets the stub, which reports
|
||||
# every engine unavailable and generates nothing.
|
||||
set(BANNER_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../winlator/lsfg)
|
||||
set(BANNER_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../winlator/winfg)
|
||||
if (TARGET dxbc AND EXISTS ${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
AND EXISTS ${BANNER_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
|
||||
message(STATUS "bannerwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
|
||||
target_sources(bannerwayland PRIVATE
|
||||
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
|
||||
set(DROIDDECK_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg)
|
||||
if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
AND EXISTS ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
|
||||
target_sources(droiddeckwayland PRIVATE
|
||||
src/framegen_engine.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_probe.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_dll.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_dxbc.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_vkd.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_common.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_shaders.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_mipmaps.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_alpha.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_beta.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_gamma.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_delta.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_generate.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_chain.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_pacer.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_probe.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_dll.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_dxbc.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_common.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_shaders.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_mipmaps.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_alpha.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_beta.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_gamma.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_delta.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_generate.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_chain.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_pacer.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp
|
||||
# The Lossless.dll import (validate / extract / cache) for the app's LsfgNative.
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp
|
||||
${BANNER_FRAMEGEN_WINFG_DIR}/framegen.cpp
|
||||
${BANNER_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp
|
||||
${BANNER_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/framegen.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
|
||||
)
|
||||
target_include_directories(bannerwayland PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/.. # <thirdparty/...> as the winlator target sees it
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../winlator # VulkanRendererContext.h (VkTable), lsfg/, winfg/
|
||||
target_include_directories(droiddeckwayland PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/.. # <thirdparty/...> as the frame-generation engines see it
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../framegen # VulkanRendererContext.h (VkTable), lsfg/, winfg/
|
||||
)
|
||||
target_compile_options(bannerwayland PRIVATE
|
||||
target_compile_options(droiddeckwayland PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17 -fvisibility=hidden>)
|
||||
target_compile_definitions(bannerwayland PRIVATE
|
||||
target_compile_definitions(droiddeckwayland PRIVATE
|
||||
WINFG_UPSTREAM="${WINFG_UPSTREAM}"
|
||||
WINFG_CHAIN_HASH="${WINFG_CHAIN_HASH}"
|
||||
)
|
||||
target_link_libraries(bannerwayland dxbc atomic)
|
||||
target_link_libraries(droiddeckwayland dxbc atomic)
|
||||
else()
|
||||
message(STATUS "bannerwayland: frame-generation engines NOT built in (standalone build): stub")
|
||||
target_sources(bannerwayland PRIVATE src/framegen_engine_stub.c)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines NOT built in (standalone build): stub")
|
||||
target_sources(droiddeckwayland PRIVATE src/framegen_engine_stub.c)
|
||||
endif()
|
||||
add_library(wayland-server SHARED IMPORTED)
|
||||
set_target_properties(wayland-server PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/lib/libwayland-server.so)
|
||||
# Vulkan is loaded at runtime via adrenotools (Turnip), not linked directly — so the
|
||||
# Vulkan is loaded at runtime via adrenotools (Turnip), not linked directly - so the
|
||||
# compositor uses Turnip's driver, which supports dmabuf import (drm_format_modifier).
|
||||
target_link_libraries(bannerwayland
|
||||
target_link_libraries(droiddeckwayland
|
||||
wayland-server
|
||||
adrenotools
|
||||
dl
|
||||
|
||||
@@ -1,910 +0,0 @@
|
||||
# HDR on the Wayland backend — reconnaissance
|
||||
|
||||
> **2026-09-14: round 1 of Phase A is built** (branch `feat/wayland-hdr`, opt-in, testing only) —
|
||||
> see **§10** at the end for what was built, how it is gated and how a tester's log proves it.
|
||||
> The recon below is kept as written.
|
||||
|
||||
Written 2026-09-13. **Recon only: no product code was changed, nothing was built, nothing was
|
||||
installed on the device.** Branch surveyed: app `feat/wayland-phase2`, proton-wine
|
||||
`feat/winewayland-desktop-11.0-2`, Banners-Turnip `wayland` (Mesa `7cda7850`, v26.3.0).
|
||||
Device reads are `dumpsys` / `getprop` / `strings` only.
|
||||
|
||||
Companion docs: `ZERO_COPY_SPIKE.md` (buffer path), `WAYLAND_RUNTIME.md`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Answer in one paragraph
|
||||
|
||||
**The game half of HDR is already shipping and nobody noticed.** The Wayland Turnip inside
|
||||
wcp v6 (`Proton-11.0-2.1-arm64ec-6/lib/libvulkan_freedreno_wayland.so`, verified on the device)
|
||||
already contains Mesa's full `wp_color_manager_v1` client implementation and advertises
|
||||
`VK_EXT_swapchain_colorspace` + `VK_EXT_hdr_metadata`; the shipping DXVK v3.1-gplasync already
|
||||
carries `dxgi.enableHDR` / `DXVK_HDR` and the `VK_COLOR_SPACE_HDR10_ST2084_EXT` path;
|
||||
`winewayland.drv` is a transparent 121-line shim that neither adds nor removes colour
|
||||
information. **Everything that is missing is in our compositor**, which advertises no colour
|
||||
protocol, offers only four 8-bit DRM formats over `zwp_linux_dmabuf_v1`, takes
|
||||
`surfaceFormats[0]` for its own swapchain, runs an 8-bit scene/effects chain, and never calls
|
||||
`ASurfaceTransaction_setBufferDataSpace`. The work is therefore **app-side only** — the memory
|
||||
note in `project_bannerlator_wayland_phase4` that HDR "does need a new wcp" is **wrong for the
|
||||
HDR10 (10-bit PQ) path**; a new wcp is only needed for scRGB/FP16 zero-copy. Estimated
|
||||
**4–6 engineer-days** to a plumbed, log-verifiable HDR10 path. But the Pocket FIT's panel
|
||||
reports `supportedHdrTypes=[]`, `supportedColorModes=[0]` (NATIVE only), `hdrSdrRatio
|
||||
not_available` and `displayBrightnessNits=-1`, so **nothing of it can be seen on this device**,
|
||||
and turning it on there would actively *cost* us the zero-copy DEVICE-composition win. Verdict
|
||||
in §8: **not now.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Game side — how far an HDR request gets today
|
||||
|
||||
### 1.1 What a Windows game does
|
||||
|
||||
| Call | Where it lands in DXVK | Notes |
|
||||
|---|---|---|
|
||||
| `IDXGIOutput6::GetDesc1` → `DXGI_OUTPUT_DESC1::ColorSpace` | `src/dxgi/dxgi_output.cpp:220-259` | **Never queries Vulkan.** Reports `RGB_FULL_G2084_NONE_P2020` purely because `dxgi.enableHDR` is on. |
|
||||
| `IDXGISwapChain3::CheckColorSpaceSupport` | `src/dxgi/dxgi_swapchain.cpp:655-667` | This *is* backed by Vulkan (below). Note it is a swapchain method, not a factory method. |
|
||||
| `IDXGISwapChain3::SetColorSpace1` | `dxgi_swapchain.cpp:672-688` → `d3d11_swapchain.cpp:306-331` → `Presenter::supportsColorSpace` (`dxvk_presenter.cpp:319-343`) | Returns `E_INVALIDARG` if the Vulkan surface does not list the colour space. |
|
||||
| `IDXGISwapChain4::SetHDRMetaData` | `dxgi_swapchain.cpp:690-717` → `vkSetHdrMetadataEXT` | Only `DXGI_HDR_METADATA_TYPE_HDR10`. Silently dropped if `VK_EXT_hdr_metadata` is absent (`dxvk_presenter.cpp:143-149`). |
|
||||
|
||||
DXGI→Vulkan mapping (`src/d3d11/d3d11_swapchain.cpp:21-28`):
|
||||
|
||||
```
|
||||
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 -> VK_COLOR_SPACE_HDR10_ST2084_EXT
|
||||
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 -> VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT (scRGB)
|
||||
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709 -> VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
|
||||
```
|
||||
|
||||
Swapchain formats DXVK will accept per colour space (`Presenter::pickFormat`,
|
||||
`dxvk_presenter.cpp:1043-1081`):
|
||||
|
||||
* `HDR10_ST2084` → `A2R10G10B10_UNORM_PACK32`, `A2B10G10R10_UNORM_PACK32`,
|
||||
`E5B9G9R9_UFLOAT_PACK32`, `R16G16B16A16_UNORM`, `R16G16B16A16_SFLOAT`
|
||||
* `EXTENDED_SRGB_LINEAR` (scRGB) → **`R16G16B16A16_SFLOAT` only**
|
||||
|
||||
`VK_EXT_hdr_metadata` is optional in both DXVK and vkd3d-proton. `VK_EXT_swapchain_colorspace`
|
||||
is *not* requested as an instance extension by DXVK's win32 WSI — it simply trusts the driver to
|
||||
report the extra `VkColorSpaceKHR` values from `vkGetPhysicalDeviceSurfaceFormatsKHR`. Mesa's
|
||||
Wayland WSI does exactly that (§2.3), so this is not a problem for us.
|
||||
|
||||
**vkd3d-proton (D3D12)** ships its own swapchain (`libs/vkd3d/swapchain.c`), same colour-space
|
||||
mapping (`convert_color_space`, :1717-1730), but **no fallback table**: if the requested
|
||||
HDR colour space is not in `vkGetPhysicalDeviceSurfaceFormatsKHR`, `dxgi_vk_swap_chain_select_format`
|
||||
(:1773-1791) returns `false` with the comment *"Refuse to present unsupported HDR since it will
|
||||
look completely bogus"*, and the caller skips swapchain recreation for that cycle. A D3D12 game
|
||||
that force-sets HDR without checking can therefore **stall presentation** rather than degrade.
|
||||
DXGI enumeration for D3D12 titles is still DXVK's `dxgi.dll`, so `DXVK_HDR` still drives
|
||||
`DXGI_OUTPUT_DESC1`.
|
||||
|
||||
**Verified in our shipped binaries** (device, `strings`):
|
||||
`contents/DXVK/v3.1-gplasync-0/system32/dxgi.dll` contains `dxgi.enableHDR`, `DXVK_HDR`,
|
||||
`VK_COLOR_SPACE_HDR10_ST2084_EXT`; `d3d11.dll`/`d3d9.dll`/`dxgi.dll` contain
|
||||
`VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT`; `contents/VKD3D/3.1.0-wave64-relax-1/.../d3d12core.dll`
|
||||
contains `VK_EXT_hdr_metadata`. Nothing needs rebuilding on the D3D side.
|
||||
|
||||
Proton-level gating: GE's `proton` script maps `PROTON_ENABLE_HDR` / `PROTON_USE_HDR` to
|
||||
`DXVK_HDR=1` and nothing else. For us that is one container env var — no launcher work.
|
||||
|
||||
### 1.2 The failure mode today, exactly
|
||||
|
||||
DXVK's `DXGI_OUTPUT_DESC1` will claim HDR10 as soon as `DXVK_HDR=1`, regardless of Vulkan.
|
||||
`SetColorSpace1` then fails with `E_INVALIDARG` because our compositor advertises no colour
|
||||
management, so Mesa lists only `SRGB_NONLINEAR` + `PASS_THROUGH` (§2.3). Well-behaved games fall
|
||||
back to SDR. Games that trust `GetDesc1` without checking `CheckColorSpaceSupport` can render an
|
||||
HDR-authored image into an SDR swapchain (washed out) — this is a pre-existing foot-gun, not
|
||||
something we introduce. **Recommendation regardless of whether we do HDR: do not expose a
|
||||
`DXVK_HDR` container toggle until the compositor answers truthfully**, or we invite exactly that
|
||||
bug class.
|
||||
|
||||
### 1.3 `winewayland.drv` — carries nothing for colour, and blocks nothing
|
||||
|
||||
`/home/claude-user/proton-wine-wayland2-wt/dlls/winewayland.drv/vulkan.c` is 121 lines:
|
||||
`wayland_vulkan_surface_create` wraps the `wl_surface` in a `VkSurfaceKHR`,
|
||||
`wayland_map_instance_extensions` aliases `VK_KHR_win32_surface` ⇄ `VK_KHR_wayland_surface`.
|
||||
There is **no surface-format or colour-space filtering**, so whatever Mesa advertises reaches
|
||||
DXVK unmodified. A grep for `color|colour|hdr|st2084|bt2020|gamut|10.?bit` across the whole
|
||||
driver returns only GDI colour-bitmap helpers — nothing colorimetric.
|
||||
|
||||
One real consequence: `wayland_output.c` publishes no EDID, and nothing in the tree writes the
|
||||
`EDID` registry value `win32u/sysparams.c:815-818` reads. DXVK's
|
||||
`src/wsi/win32/wsi_monitor_win32.cpp:288-361` therefore finds no EDID and falls back to its dummy
|
||||
HDR luminances (`wsi_edid.h:38-56`: min 0.01 / max 1499 / maxFullFrame 799 nits). A game will
|
||||
size its tonemapper for a 1499-nit display. Correctable later by synthesising an EDID in
|
||||
`winewayland.drv` (that *would* need a new wcp), but not a blocker.
|
||||
|
||||
### 1.4 Our bundled Turnip already speaks colour management — verified on the device
|
||||
|
||||
```
|
||||
$ strings .../Proton/11.0-2.1-arm64ec-6/lib/libvulkan_freedreno_wayland.so
|
||||
wp_color_manager_v1 PRESENT
|
||||
wp_color_management_surface_v1 PRESENT
|
||||
wp_image_description_creator_params_v1 PRESENT
|
||||
banner_ahb_v1 PRESENT (our zero-copy patch)
|
||||
VK_EXT_swapchain_colorspace PRESENT
|
||||
VK_EXT_hdr_metadata PRESENT
|
||||
```
|
||||
|
||||
The plain adrenotools driver the compositor itself loads
|
||||
(`contents/adrenotools/Mesa Turnip v26.3.0-7cda785 (Android + Wayland)/libvulkan_freedreno.so`)
|
||||
has the same colour-management symbols. Our own WSI patch is already aware of the protocol —
|
||||
`patches/wayland/banner_ahb_wsi.py` anchors one of its edits on Mesa's
|
||||
`if (display->color_manager) wp_color_manager_v1_destroy(...)` teardown line.
|
||||
|
||||
**And the zero-copy patch already maps 10-bit to gralloc** — `banner_ahb_wsi.py:99-100, 225-237`:
|
||||
|
||||
```c
|
||||
#define BANNER_AHB_FORMAT_R8G8B8A8_UNORM 1u
|
||||
#define BANNER_AHB_FORMAT_R10G10B10A2_UNORM 0x2bu /* AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM */
|
||||
...
|
||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return BANNER_AHB_FORMAT_R10G10B10A2_UNORM;
|
||||
```
|
||||
|
||||
So a 10-bit HDR10 swapchain can already be gralloc-backed and go onto the display layer with the
|
||||
**shipped** wcp. `VK_FORMAT_R16G16B16A16_SFLOAT` is *not* mapped, so `banner_ahb_skip_format`
|
||||
(:239-242, applied at `:606-617`) hides FP16 surface formats whenever zero-copy is on — scRGB
|
||||
zero-copy is the one thing that needs a driver change (one enum: `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT = 0x16`).
|
||||
|
||||
### 1.5 X11 has no path — confirmed
|
||||
|
||||
`app/src/main/cpp/winlator/VulkanRendererContext.cpp:439` hardcodes
|
||||
`swapchainFmt = VK_FORMAT_R8G8B8A8_UNORM` and `:492` hardcodes
|
||||
`imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR`. Upstream of that, the game presents through
|
||||
`winex11.drv` into our pure-Java X11 server, which has no colour extension of any kind. HDR on X11
|
||||
would mean inventing a private X extension *and* teaching DXVK to use it. Correctly out of scope.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wayland side
|
||||
|
||||
### 2.1 Protocol status
|
||||
|
||||
`wp_color_management_v1` is **merged and stable-ish**: wayland-protocols MR !14, merged
|
||||
2025-02-13, shipped in **wayland-protocols 1.41**, file
|
||||
`staging/color-management/color-management-v1.xml`. Interfaces are at **version 3** upstream today
|
||||
(v2 added `get_image_description`/`preferred_changed2`; v3 added `create_windows_bt2100` and the
|
||||
`absolute_no_adaptation` intent). Predecessors: `zwp_linux_color_management_v1` →
|
||||
`xx_color_management_v4` (the name most 2023–2025 shipping code targeted) → `wp_*` on landing.
|
||||
`frog_color_management_v1` (Valve) is still vendored by gamescope alongside the real one and is
|
||||
still functionally load-bearing on the Deck.
|
||||
|
||||
Implementers: KWin (reference impl, most mature), Mutter (merged 2025-02-25 but still does not
|
||||
advertise enough features for automatic HDR), wlroots 0.20 / Sway 1.12 (HDR10 on the Vulkan
|
||||
renderer), gamescope (v1 semantics + frog), Weston (experimental, no tone mapping).
|
||||
Proton-CachyOS's changelog notes that its `winewayland.drv` `windows_bt2100` path needs protocol
|
||||
**v3** and that *"no compositor supports it yet"* — everybody is converged on **v1**.
|
||||
|
||||
**We only need v1.** That is what Mesa binds.
|
||||
|
||||
### 2.2 Minimum viable compositor subset
|
||||
|
||||
At bind, on the `wp_color_manager_v1` global, send one event per supported item then `done`:
|
||||
|
||||
* `supported_intent`: `perceptual` (mandatory anyway — it is all Mesa ever asks for)
|
||||
* `supported_feature`: `parametric` (gates `create_parametric_creator`); optionally
|
||||
`set_mastering_display_primaries`, and `extended_target_volume` **only if** we want scRGB
|
||||
* `supported_primaries_named`: `bt2020` (HDR10), `srgb` (needed for scRGB and for Mesa's
|
||||
"use a colour surface for sRGB too" branch), `display_p3` (wide gamut)
|
||||
* `supported_tf_named`: `st2084_pq` (HDR10), `ext_linear` (scRGB), `srgb`
|
||||
|
||||
Then implement: `create_parametric_creator` → `wp_image_description_creator_params_v1`
|
||||
(`set_primaries_named`, `set_tf_named`, `set_mastering_display_primaries`,
|
||||
`set_mastering_luminance`, `set_max_cll`, `set_max_fall`) → `create` → emit
|
||||
`wp_image_description_v1.ready` (or `failed`) → `wp_color_manager_v1.get_surface` →
|
||||
`wp_color_management_surface_v1.set_image_description(desc, intent)`, double-buffered on
|
||||
`wl_surface.commit`.
|
||||
|
||||
**Not needed:** ICC creator, `wp_color_management_surface_feedback_v1`,
|
||||
`wp_image_description_info_v1`, output image descriptions, v2/v3 requests. Mesa uses none of them.
|
||||
|
||||
### 2.3 What Mesa's WSI does — read from the exact commit we build
|
||||
|
||||
Read from `src/vulkan/wsi/wsi_common_wayland.c` at Mesa `7cda7850` (4121 lines — the commit
|
||||
recorded in `banners-turnip-wayland/mesa_hash.txt`). Landed upstream as MR !32038 (Xaver Hugl,
|
||||
merged 2025-02-25, Mesa 25.1).
|
||||
|
||||
* **Bind:** `registry_handle_global` binds `wp_color_manager_v1` at **version 1** unconditionally
|
||||
when advertised (`:1536-1538`), and installs `color_manager_listener` (`:1217-1223`).
|
||||
* **Colour-space list:** `wsi_wl_display_determine_colorspaces` (`:1122-1160`) always adds
|
||||
`VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` and `VK_COLOR_SPACE_PASS_THROUGH_EXT`; everything else comes
|
||||
from the static `colorspace_mapping[]` table (`:1038-1118`) and is added **only if** the
|
||||
compositor advertised that entry's primaries *and* transfer function (and, for entries flagged
|
||||
`needs_extended_range`, `feature.extended_target_volume`). Table entries include
|
||||
`HDR10_ST2084` (bt2020 + st2084_pq), `EXTENDED_SRGB_LINEAR` (srgb + ext_linear + extended range),
|
||||
`DISPLAY_P3_NONLINEAR`, `DISPLAY_P3_LINEAR`, `BT709_LINEAR`, `BT709_NONLINEAR`, `BT2020_LINEAR`,
|
||||
`HDR10_HLG`, `ADOBERGB_LINEAR`.
|
||||
* **Surface formats:** `wsi_wl_surface_get_formats2` (`:2205-2246`) emits the **full cross product** of
|
||||
`display.colorspaces` × `display.formats` with **no bit-depth gating**:
|
||||
|
||||
```c
|
||||
u_vector_foreach(cs, &display.colorspaces) {
|
||||
u_vector_foreach(disp_fmt, &display.formats) {
|
||||
if (!(disp_fmt->flags & WSI_WL_FMT_ALPHA) || !(disp_fmt->flags & WSI_WL_FMT_OPAQUE))
|
||||
continue;
|
||||
out_fmt->surfaceFormat.format = disp_fmt->vk_format;
|
||||
out_fmt->surfaceFormat.colorSpace = *cs;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Consequence: if we advertise BT.2020 + PQ but keep only 8-bit DRM formats, Mesa will happily
|
||||
offer `R8G8B8A8_UNORM` + `HDR10_ST2084` and DXVK will **not** take it (`pickFormat`'s HDR10 list
|
||||
is 10-bit/16-bit only) — so there is no accidental 8-bit-PQ trap, but there is also no HDR
|
||||
unless we also advertise a 10-bit DRM format.
|
||||
* **Apply:** `wsi_wl_swapchain_update_colorspace` (`:1323-1455`) builds the parametric creator,
|
||||
blocks on `ready`/`failed` (it dispatches the queue in a loop), sets mastering primaries /
|
||||
luminance / max_cll / max_fall when `should_use_hdr_metadata` is set for that colour space, and
|
||||
calls `set_image_description(..., RENDER_INTENT_PERCEPTUAL)`. `is_hdr_metadata_legal`
|
||||
(`:1277-1299`) drops illegal combinations rather than risking a protocol error. On `failed`
|
||||
without `extended_target_volume` it retries once without metadata.
|
||||
* **`VK_EXT_hdr_metadata`:** `wsi_wl_swapchain_set_hdr_metadata` (`:1459-1463`) just stashes
|
||||
`VkHdrMetadataEXT`; the real work is in `update_colorspace` above.
|
||||
|
||||
**Nothing to build in the driver for HDR10.** Mesa is done.
|
||||
|
||||
### 2.4 Our compositor advertises nothing colorimetric
|
||||
|
||||
Globals (`compositor.c:2527-2538`, plus `ahb_swapchain.c:406`,
|
||||
`wl_clipboard.c:522-523`, `wl_text_input.c:275`, `wl_toplevel_icon.c:35`):
|
||||
`wl_compositor` v6, `wl_subcompositor`, `wp_viewporter`, `wl_output` v2, `xdg_wm_base`,
|
||||
`zwp_linux_dmabuf_v1` **v3**, `wl_seat` v5, `banner_desktop_v1`, `wp_presentation` v2,
|
||||
pointer constraints, relative pointer, `banner_ahb_v1` v2, data device/control, text input,
|
||||
toplevel icon. **No `wp_color_manager_v1`.** `protocols/` has no colour-management XML, and
|
||||
protocol C glue is pre-generated into `generated/` (no `wayland-scanner` at app build time —
|
||||
`CMakeLists.txt:1-4`), so adding a protocol means generating and committing the files offline.
|
||||
|
||||
The dmabuf format table is 8-bit only (`compositor.c:1323-1326`):
|
||||
|
||||
```c
|
||||
#define DMABUF_NFMT 4
|
||||
static struct { uint32_t fmt; uint64_t mods[DMABUF_NMOD]; int n; } g_dmabuf_fmts[DMABUF_NFMT] = {
|
||||
{DRM_ARGB8888}, {DRM_XRGB8888}, {DRM_ABGR8888}, {DRM_XBGR8888}};
|
||||
```
|
||||
|
||||
Good news: the machinery around it is already format-agnostic.
|
||||
`vkp_dmabuf_modifiers` (`vk_present.c`) takes a DRM fourcc and asks the compositor's own Turnip via
|
||||
`VkDrmFormatModifierPropertiesListEXT`, and `drm_to_vk` (`vk_present.c:105-123`) **already** maps
|
||||
`AB30`/`XB30` → `A2B10G10R10_UNORM_PACK32`, `AR30`/`XR30` → `A2R10G10B10_UNORM_PACK32`, and
|
||||
`AB4H`/`XB4H` → `R16G16B16A16_SFLOAT`. Adding 10-bit is a table edit plus the matching
|
||||
`FOURCC` defines next to `compositor.c:415-421`.
|
||||
|
||||
The compositor's own output swapchain is oblivious (`vk_present.c:301-311, 437-466`): two instance
|
||||
extensions (`VK_KHR_surface`, `VK_KHR_android_surface` — **no `VK_EXT_swapchain_colorspace`**),
|
||||
and `VkSurfaceFormatKHR chosen = fmts[0];` (`:441`) — the first format the Android WSI happens to
|
||||
return, with whatever colour space rides along. The scene image and the whole 13-pass effects
|
||||
chain are 8-bit (`vk_present.c:54` `SCENE_FMT = VK_FORMAT_R8G8B8A8_UNORM`; `effects_chain.c:29`
|
||||
`FX_FORMAT = VK_FORMAT_R8G8B8A8_UNORM`).
|
||||
|
||||
> **Naming collision, flag it in any UI work.** The drawer already has an "HDR" effect. It is
|
||||
> `effects_chain.c:197` `P_HDR` / `app/src/main/cpp/winlator/hdr.frag` — a dual-radius bloom +
|
||||
> contrast lift ported from `HDREffect.java`, `HDRPower = 1.30`, entirely SDR and entirely fake.
|
||||
> Real HDR must be named something else ("HDR output", "HDR10") or users will conflate them.
|
||||
|
||||
### 2.5 Is there a protocol-free shortcut?
|
||||
|
||||
No. A `DRM_FORMAT_ABGR2101010` buffer is equally valid as SDR BT.709 or as PQ BT.2020; the format
|
||||
and modifier describe byte layout, not colorimetry. That ambiguity is precisely why
|
||||
`wp_color_management_v1` exists. We *could* add a `set_dataspace` request to our own
|
||||
`banner_ahb_v1` and skip the standard protocol — but Mesa would never call it, so that would need
|
||||
a driver patch (new wcp) to buy something Mesa already gives us for free. **Implement
|
||||
`wp_color_manager_v1` v1 server-side; it is strictly cheaper.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Android side
|
||||
|
||||
### 3.1 The real symbols, their API levels, and what the device has
|
||||
|
||||
`frameworks/native/include/android/surface_control.h`:
|
||||
|
||||
| Symbol | API level | Present in `/system/lib64/libandroid.so` on the Pocket FIT (API 34)? |
|
||||
|---|---|---|
|
||||
| `ASurfaceTransaction_setBufferDataSpace(tx, sc, ADataSpace)` | **29** | **PRESENT** |
|
||||
| `ASurfaceTransaction_setHdrMetadata_smpte2086(tx, sc, AHdrMetadata_smpte2086*)` | **29** | **PRESENT** |
|
||||
| `ASurfaceTransaction_setHdrMetadata_cta861_3(tx, sc, AHdrMetadata_cta861_3*)` | **29** | **PRESENT** |
|
||||
| `ASurfaceTransaction_setExtendedRangeBrightness(tx, sc, float cur, float desired)` | **34** | **PRESENT** |
|
||||
| `ASurfaceTransaction_setDesiredHdrHeadroom(tx, sc, float)` | **35** | absent (expected — device is 34) |
|
||||
| `ASurfaceTransaction_setLuts` | 36 | n/a |
|
||||
| `ASurfaceTransaction_setColorSpace` | **does not exist** | — |
|
||||
| `ASurfaceTransaction_setLuma` | **does not exist** | — |
|
||||
|
||||
(Checked with `strings` on the device's own `libandroid.so`; `setEnableBackPressure` and
|
||||
`setFrameRateWithChangeStrategy` confirmed present alongside, matching what `sc_layer.c` already
|
||||
dlsyms.)
|
||||
|
||||
Our dlsym table is `sc_layer.c:82-97` — 15 entries, **none of them colorimetric**. The additions
|
||||
needed are `setBufferDataSpace` (the only mandatory one), plus optionally
|
||||
`setHdrMetadata_smpte2086` / `_cta861_3` and `setExtendedRangeBrightness`. All four are API ≤ 34,
|
||||
i.e. inside the range `sc_layer.c` already targets, so the existing "missing symbol → path
|
||||
unavailable" pattern covers older devices unchanged.
|
||||
|
||||
### 3.2 Dataspaces we would emit
|
||||
|
||||
`ADataSpace` = `STANDARD (63<<16) | TRANSFER (31<<22) | RANGE (7<<27)`, all in the NDK since 29:
|
||||
|
||||
| Constant | Value | Maps from |
|
||||
|---|---|---|
|
||||
| `ADATASPACE_BT2020_PQ` | 163971072 | `VK_COLOR_SPACE_HDR10_ST2084_EXT` |
|
||||
| `ADATASPACE_BT2020_HLG` | 168165376 | `VK_COLOR_SPACE_HDR10_HLG_EXT` |
|
||||
| `ADATASPACE_SCRGB_LINEAR` | 406913024 | `VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT` |
|
||||
| `ADATASPACE_DISPLAY_P3` | 143261696 | `VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT` |
|
||||
| `ADATASPACE_SRGB` | 142671872 | `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` (today's implicit default) |
|
||||
|
||||
Every layer on the device right now reports `dataspace=UNKNOWN (0)`, which SF treats as sRGB.
|
||||
|
||||
### 3.3 Buffer formats
|
||||
|
||||
`AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM = 0x2b` (API 26) ↔ `VK_FORMAT_A2B10G10R10_UNORM_PACK32`
|
||||
— the normal 10-bit scanout format on Adreno DPUs. QTI gralloc on this device knows it:
|
||||
`strings /vendor/lib64/libqdMetaData.so` lists `RGBA_1010102`, `RGBX_1010102`, `BGRA_1010102`,
|
||||
`BGRX_1010102`. `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT = 0x16` (API 26) exists but is a
|
||||
compositing intermediate, not normally a scanout format — it was **not** found in the vendor
|
||||
gralloc strings, so treat FP16 gralloc allocation on this SoC as unproven.
|
||||
`AHardwareBuffer_isSupported()` (API 29) is the correct runtime probe, and we should use it rather
|
||||
than assume.
|
||||
|
||||
The compositor's own AHB pool is hardcoded 8-bit (`sc_layer.c:302`
|
||||
`.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM`) and the imported view is built with
|
||||
`DRM_ABGR8888` (`sc_layer.c:326`). That is the *copy* path; the zero-copy path
|
||||
(`sc_layer_present_ahb`, `sc_layer.c:430`) takes the game's own buffer and so already carries
|
||||
whatever format the game allocated — it just never labels it.
|
||||
|
||||
### 3.4 What SurfaceFlinger does with an HDR layer on an SDR panel
|
||||
|
||||
From `services/surfaceflinger/CompositionEngine/src/Output.cpp`, `getBestDataspace()` /
|
||||
`pickColorProfile()`:
|
||||
|
||||
* A `BT2020_PQ` layer sets `outHdrDataSpace = BT2020_PQ` but the output only *adopts* it when
|
||||
`DisplayColorProfile::hasLegacyHdrSupport(BT2020_PQ)` is true — i.e. when the Composer HAL
|
||||
reported HDR types for that display. Otherwise the output dataspace stays `DISPLAY_P3` with
|
||||
`COLORIMETRIC` intent, and the layer is **tone-mapped, not rejected**.
|
||||
* Because the HWC for an SDR-only display cannot consume a PQ buffer, the standard
|
||||
`validateDisplay()` negotiation marks that layer **CLIENT** instead of **DEVICE**, and
|
||||
RenderEngine/Skia composites it with the `libtonemap` SkSL gain shader
|
||||
(`libtonemap_LookupTonemapGain`, uniforms `in_libtonemap_displayMaxLuminance` /
|
||||
`in_libtonemap_inputMaxLuminance`).
|
||||
* **This is the trap for us.** Our whole zero-copy win is that `banner_wayland_game` composites
|
||||
DEVICE (proven 2026-09-13: DEVICE with ROT_90 + 1.5× scale at 144 Hz, −4 pts GPU busy, −3 %
|
||||
power). Tagging that layer `BT2020_PQ` on a panel with no HDR types is very likely to drop it to
|
||||
CLIENT — i.e. HDR would **buy a washed-out tone-map and pay for it with the zero-copy saving**.
|
||||
Any implementation must gate the dataspace on the *display's* reported HDR types, not on the
|
||||
game's request.
|
||||
* `setExtendedRangeBrightness` is a different mechanism: it applies only to `RANGE_EXTENDED`
|
||||
(scRGB) layers and works by having SF dim the *other* layers by `whitePointNits / maxWhitePoint`
|
||||
(`renderengine/LayerSettings.h`) while leaving the extended layer undimmed, so the boosted layer
|
||||
reads brighter by simultaneous contrast without exceeding the panel ceiling.
|
||||
**It needs the display to expose an HDR/SDR ratio.** This panel does not (§4).
|
||||
* `setDesiredHdrHeadroom` (API 35) is the PQ/HLG equivalent and overrides/`is overridden by`
|
||||
`setExtendedRangeBrightness` — last call wins. Not available on the test device at all.
|
||||
|
||||
### 3.5 `has_HDR_display` vs `getHdrCapabilities()`
|
||||
|
||||
`ro.surface_flinger.has_HDR_display` is a **build-time** declaration (`TARGET_HAS_HDR_DISPLAY`)
|
||||
that the device *has* a wide-gamut, high-luminance panel; it gates SF-internal colour-management
|
||||
machinery. `Display.getHdrCapabilities().getSupportedHdrTypes()` is the **runtime, per-display**
|
||||
answer, built from the Composer HAL's `getHdrCapabilities()` for that connector, itself derived
|
||||
from the EDID CTA-861.3 Static Metadata Data Block. **They can disagree, and on this device they
|
||||
do.** An empty list does not mean rejection — it means "tone-map it."
|
||||
|
||||
### 3.6 External display
|
||||
|
||||
On hotplug, the framework queries `getDisplayConfigs`, `getColorModes`, `getHdrCapabilities`,
|
||||
`getDisplayCapabilities` for the new display handle exactly as for the internal panel, so
|
||||
`Display.getHdrCapabilities()` on an external `Display` *is* architecturally wired to reflect the
|
||||
monitor's EDID. Whether it actually does is a vendor-HWC question; empty lists for genuinely
|
||||
HDR10 USB-C monitors are a commonly reported Snapdragon gap. **Unproven for the Pocket FIT — the
|
||||
only way to know is to plug one in.**
|
||||
|
||||
---
|
||||
|
||||
## 4. What the test device can and cannot show
|
||||
|
||||
All read today from the Pocket FIT (Adreno 750, Android 14 / API 34), read-only:
|
||||
|
||||
```
|
||||
DisplayDeviceInfo{"Built-in Screen": 1080 x 1920, ...
|
||||
supportedModes [... supportedHdrTypes=[] (all four modes) ],
|
||||
colorMode 0, supportedColorModes [0],
|
||||
hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0,
|
||||
mMaxAverageLuminance=500.0, mMinLuminance=0.0},
|
||||
hdrSdrRatio NaN, brightnessMaximum 1.0 ...}
|
||||
mBaseDisplayInfo: ... hdrSdrRatio not_available, userDisabledHdrTypes []
|
||||
```
|
||||
|
||||
```
|
||||
dumpsys SurfaceFlinger:
|
||||
Wide-Color information:
|
||||
Device supports wide color: 1
|
||||
Device uses color management: 1
|
||||
DisplayColorSetting: Managed
|
||||
Display 4630946852602928515 color modes:
|
||||
ColorMode::NATIVE (0)
|
||||
Current color mode: ColorMode::NATIVE (0)
|
||||
colorMode=NATIVE (0) renderIntent=COLORIMETRIC (0) dataspace=UNKNOWN (0) targetDataspace=UNKNOWN (0)
|
||||
displayBrightnessNits=-1.000000 sdrWhitePointNits=-1.000000 displayBrightness=nullopt
|
||||
(every layer) dataspace=UNKNOWN (0) hdr metadata types=0
|
||||
```
|
||||
|
||||
```
|
||||
getprop:
|
||||
[ro.surface_flinger.has_HDR_display]: [true]
|
||||
[ro.surface_flinger.has_wide_color_display]: [true]
|
||||
[ro.surface_flinger.wcg_composition_dataspace]: [143261696] = ADATASPACE_DISPLAY_P3
|
||||
[persist.sys.sf.color_mode]: [9] = ColorMode::DISPLAY_P3
|
||||
[vendor.display.enable_hdr10_gpu_target]: [1]
|
||||
```
|
||||
|
||||
Reading:
|
||||
|
||||
* The **build** declares an HDR, wide-colour device and even asks for `DISPLAY_P3` as the system
|
||||
colour mode (`persist.sys.sf.color_mode=9`), but the **HWC reports `supportedColorModes=[0]`
|
||||
(NATIVE only) and no HDR types**, so SF runs the display in NATIVE with COLORIMETRIC intent and
|
||||
the P3 request is inert. The props are aspirational; the HAL is the truth.
|
||||
* `hdrSdrRatio not_available` / `NaN` and `displayBrightnessNits = -1` mean
|
||||
`Display.isHdrSdrRatioAvailable()` is false → **`setExtendedRangeBrightness` has nothing to act
|
||||
on here.** The "brighten highlights on an SDR panel" trick is unavailable on this device.
|
||||
* `supportedColorModes=[0]` also means **Display-P3 wide gamut is not available either** — not
|
||||
just HDR. A P3-tagged layer would be colour-converted into the panel's native gamut, i.e. into
|
||||
whatever the panel natively is, with no way for us to know what that is.
|
||||
* 500 nits max luminance is below the ~540-nit bar AOSP's own `has_HDR_display` doc cites, and
|
||||
there is no HDR type to signal anyway.
|
||||
|
||||
**Conclusion: visual HDR cannot be demonstrated on the Pocket FIT's internal panel, and neither
|
||||
can wide gamut, and neither can extended-range brightness.** The only things provable on this
|
||||
device are (a) 10-bit buffers end to end (banding reduction, measurable), and (b) plumbing
|
||||
correctness by logs and `dumpsys`.
|
||||
|
||||
Vulkan-side, for completeness (device `strings`):
|
||||
|
||||
| Library | `VK_EXT_swapchain_colorspace` | `VK_EXT_hdr_metadata` |
|
||||
|---|---|---|
|
||||
| `/system/lib64/libvulkan.so` (platform loader — implements the Android swapchain) | **PRESENT** | **PRESENT** |
|
||||
| `/vendor/lib64/hw/vulkan.adreno.so` (Qualcomm blob) | absent | absent |
|
||||
| `/vendor/lib64/hw/vulkan.turnip.so` (vendor-shipped Turnip) | **PRESENT** | **PRESENT** |
|
||||
| wcp v6 `libvulkan_freedreno_wayland.so` (game driver) | **PRESENT** | **PRESENT** |
|
||||
|
||||
The platform loader contains the string `native_window_set_buffers_data_space(%d) failed: %s (%d)`
|
||||
— i.e. Android's own swapchain implementation is what turns a `VkColorSpaceKHR` into a surface
|
||||
dataspace. So for the **copy path**, the compositor's output HDR is genuinely just "enable
|
||||
`VK_EXT_swapchain_colorspace` at instance creation and pick a better surface format than
|
||||
`fmts[0]`". No driver work.
|
||||
|
||||
---
|
||||
|
||||
## 5. The gap list, ordered, with sizes
|
||||
|
||||
Sizes are engineer-days for someone who already knows this code. "Prove" = device-visible in logs
|
||||
or `dumpsys`, not by eye. **App-only unless marked.**
|
||||
|
||||
### Phase A — make the truth reachable (no user-visible HDR yet)
|
||||
|
||||
| # | Repo | Work | Size |
|
||||
|---|---|---|---|
|
||||
| A1 | app | **Advertise 10-bit DRM formats over `zwp_linux_dmabuf_v1`.** Add `AB30`/`XB30` (and optionally `AR30`/`XR30`, `AB4H`/`XB4H`) to `compositor.c:1323-1326` + `FOURCC` defines near `:415-421`. `vkp_dmabuf_modifiers` and `drm_to_vk` already handle them, so this is a table edit + a log line. Risk: a client picking a 10-bit format while the scene/effects chain stays 8-bit — the import works (`drm_to_vk`), the blit downconverts. Gate behind a flag until A4. | **0.5 d** |
|
||||
| A2 | app | **Generate + commit `color-management-v1` protocol glue.** Fetch `staging/color-management/color-management-v1.xml` from wayland-protocols ≥ 1.41, run `wayland-scanner` offline, commit XML + `generated/color-management-v1-{protocol.c,server-protocol.h}`, add to `CMakeLists.txt`. Mechanical. | **0.5 d** |
|
||||
| A3 | app | **Implement `wp_color_manager_v1` v1 server-side** (new `wl_color_mgmt.c`, ~400–500 lines): the global + bind-time feature/intent/primaries/tf events; the parametric creator with `set_primaries_named`/`set_tf_named`/`set_max_cll`/`set_max_fall`/`set_mastering_*`; image-description objects with `ready`/`failed`; `get_surface` → `wp_color_management_surface_v1` and `set_image_description` stored double-buffered on our `struct surface`. **Watch the blocking loop:** Mesa dispatches its queue waiting for `ready`, so we must send `ready` from the request handler's own flush, not defer it to the next frame, or a game stalls at swapchain creation. Advertise only what the *current output* can actually honour (see A5). | **2 d** |
|
||||
| A4 | app | **Carry the image description onto the Android layer.** dlsym `ASurfaceTransaction_setBufferDataSpace` (+ `setHdrMetadata_smpte2086` / `_cta861_3`) in `sc_layer.c:82-97`; map surface image description → `ADataSpace` + `AHdrMetadata_*`; apply in `sc_layer_present_ahb` (`sc_layer.c:430-470`, next to the existing `setBuffer`). Plumb the per-surface description from `compositor.c` through `ahb_swapchain.c:219`. Also set an explicit `ADATASPACE_SRGB` on the existing 8-bit path so we stop relying on `UNKNOWN`. | **1 d** |
|
||||
| A5 | app | **Capability gate.** Read `Display.getHdrCapabilities()` / `getSupportedHdrTypes()` / `isHdrSdrRatioAvailable()` (and the external `Display` when `ExternalDisplayController` has one) in Java, pass down over JNI, and *only then* advertise `bt2020`/`st2084_pq` from A3. Without this we advertise HDR on a panel that will tone-map it and drop the layer to CLIENT (§3.4). Include a `banner_log("color", ...)` line naming exactly what was advertised and why. | **0.5 d** |
|
||||
|
||||
**Phase A total: ~4.5 days. App-only. No new wcp.** End state: a game with `DXVK_HDR=1` on a
|
||||
genuinely HDR display gets an HDR10 swapchain, its buffer reaches an `ASurfaceControl` layer
|
||||
tagged `BT2020_PQ` with real mastering metadata, and `dumpsys SurfaceFlinger` proves it.
|
||||
|
||||
### Phase B — the copy path and the effects chain (only if A is kept)
|
||||
|
||||
| # | Repo | Work | Size |
|
||||
|---|---|---|---|
|
||||
| B1 | app | **10-bit output swapchain.** Add `VK_EXT_swapchain_colorspace` to `vk_present.c:301-311` (`enabledExtensionCount` 2 → 3), replace `chosen = fmts[0]` (`:441`) with a real preference walk (HDR10 + `A2B10G10R10` when the display supports it, else `R8G8B8A8_UNORM` + sRGB). The Android platform loader does the rest. | **0.5 d** |
|
||||
| B2 | app | **Widen the scene + effects chain.** `SCENE_FMT` (`vk_present.c:54`) and `FX_FORMAT` (`effects_chain.c:29`) → `A2B10G10R10_UNORM_PACK32` (or FP16 for real HDR headroom). Every one of the 13 passes samples/writes it; the debanding pass in particular must be re-tuned because its strength is calibrated for 8-bit quantisation. Memory and bandwidth go up ~25 % (or 100 % for FP16). This is the expensive item and it regresses SDR perf if done unconditionally. | **2 d** |
|
||||
| B3 | app | **Layer pool at 10-bit.** `sc_layer.c:302` pool format → `R10G10B10A2_UNORM` when the chain is 10-bit, guarded by `AHardwareBuffer_isSupported()`, with the existing `sniff_modifier`/linear-fallback retry extended. | **0.5 d** |
|
||||
|
||||
**Phase B total: ~3 days. App-only. No new wcp.**
|
||||
|
||||
### Phase C — scRGB, and the one thing that needs a wcp
|
||||
|
||||
| # | Repo | Work | Size |
|
||||
|---|---|---|---|
|
||||
| C1 | **driver (new wcp)** | Map `VK_FORMAT_R16G16B16A16_SFLOAT` → `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT` (0x16) in `banner_ahb_format_for` (`patches/wayland/banner_ahb_wsi.py:225-237`) so FP16 swapchains are not hidden by `banner_ahb_skip_format` when zero-copy is on. One enum + a rebuild of eight drivers. **This is the only wcp-requiring item, and only for scRGB zero-copy.** Blocked on proving FP16 gralloc allocation works on Adreno at all (§3.3). | **0.5 d + a driver build cycle** |
|
||||
| C2 | app | Advertise `extended_target_volume` + `ext_linear` + `AB4H`/`XB4H`, emit `ADATASPACE_SCRGB_LINEAR`, and wire `ASurfaceTransaction_setExtendedRangeBrightness` from `Display.getHdrSdrRatio()`. | **1 d** |
|
||||
| C3 | **proton-wine (new wcp)** | *Optional, quality only.* Synthesise an EDID with a CTA-861.3 HDR static metadata block in `winewayland.drv` so DXVK reports the real panel luminance in `DXGI_OUTPUT_DESC1` instead of its 1499-nit dummy. | **1–2 d** |
|
||||
|
||||
---
|
||||
|
||||
## 6. Cheap partial wins — and which are visible on a 500-nit panel
|
||||
|
||||
| Idea | Verdict on the Pocket FIT | Notes |
|
||||
|---|---|---|
|
||||
| **10-bit output (banding)** | **Visible, and the best value here.** | This is the one real win. B1 alone (0.5 d) gets a 10-bit output swapchain; A1 (0.5 d) lets the *game* render 10-bit. Adreno 750 + the DPU handle `RGBA_1010102` natively, and `vendor.display.enable_hdr10_gpu_target=1` says the GPU target path expects it. Pairs with the existing debanding pass — arguably makes it unnecessary for 10-bit-capable games, which saves a full-screen pass. Visible on gradients (skyboxes, fog, dark interiors) where our deband pass currently earns its keep. |
|
||||
| **Display-P3 wide gamut on SDR** | **Not visible. Do not build.** | `supportedColorModes=[0]` / `ColorMode::NATIVE` — the HWC exposes no P3 mode. `persist.sys.sf.color_mode=9` asks for P3 and is ignored. Tagging a layer `ADATASPACE_DISPLAY_P3` would make SF convert P3 → the panel's native gamut, which on a NATIVE-mode display is an unknown-to-us transform. Net effect on this device: at best nothing, at worst a colour shift. |
|
||||
| **`setExtendedRangeBrightness` to lift highlights** | **Not available.** | `hdrSdrRatio not_available` / `NaN`, `displayBrightnessNits=-1`, `sdrWhitePointNits=-1`. `Display.isHdrSdrRatioAvailable()` is false, so there is no headroom to claim and SF has no nits data to dim the other layers against. |
|
||||
| **`setDesiredHdrHeadroom`** | **Not available.** | API 35; device is API 34. Absent from `libandroid.so`. |
|
||||
| **Set `ADATASPACE_SRGB` explicitly on our layers** | **Invisible but worth doing anyway (part of A4, ~0 extra cost).** | Every layer currently reports `dataspace=UNKNOWN (0)`. It works today because SF treats UNKNOWN as sRGB, but it is an accident we are relying on, and it will become wrong the moment any colour management is enabled. |
|
||||
|
||||
**So: of the three "cheap wins" in the brief, exactly one — 10-bit — is real on this hardware,
|
||||
and it is worth about 1 engineer-day (A1 + B1) on its own, independent of HDR.**
|
||||
|
||||
---
|
||||
|
||||
## 7. How we would ever prove it
|
||||
|
||||
### 7.1 Which games actually emit HDR under DXVK / VKD3D
|
||||
|
||||
Requirements: a flip-model swapchain at `R10G10B10A2_UNORM` or `R16G16B16A16_FLOAT`, an
|
||||
`IDXGISwapChain3::SetColorSpace1` call, and `DXVK_HDR=1` in the environment. Known-good
|
||||
candidates, easiest first:
|
||||
|
||||
* **D3D11 + scRGB:** *Shadow of the Tomb Raider*, *Resident Evil 2/3 Remake*, *Devil May Cry 5*,
|
||||
*Mass Effect Legendary Edition*. RE Engine titles are the usual smoke test on Linux because they
|
||||
call `SetColorSpace1` early and unconditionally when DXGI reports HDR.
|
||||
* **D3D12 + HDR10:** *Cyberpunk 2077*, *Forza Horizon 5*, *Horizon Zero Dawn*. These go through
|
||||
vkd3d-proton's stricter path (§1.1) — good for catching a half-done implementation, since a
|
||||
wrong answer **stalls presentation** instead of degrading.
|
||||
* **Sanity harness, no game needed:** anything that calls `vkGetPhysicalDeviceSurfaceFormatsKHR`
|
||||
under the layer. `vulkaninfo` is not available in the prefix, but a tiny Win32 Vulkan probe
|
||||
built against the layer's headers, or simply DXVK's own `VK_INSTANCE_LAYERS`-free log with
|
||||
`DXVK_LOG_LEVEL=info` and `DXVK_HDR=1`, will say whether the colour space was accepted. **Start
|
||||
here** — it is the cheapest signal and needs no HDR display at all.
|
||||
|
||||
### 7.2 Platform-side verification (not by eye)
|
||||
|
||||
Order of proof, each step gating the next:
|
||||
|
||||
1. **Compositor log.** New `banner_log("color", ...)` lines: which primaries/tf we advertised and
|
||||
why; every image description created (primaries, tf, max_cll, max_fall); every dataspace set on
|
||||
the layer. This is the only step that works on any device.
|
||||
2. **Guest log.** `DXVK_HDR=1` + `DXVK_LOG_LEVEL=info` → DXVK prints the chosen colour space and
|
||||
format at swapchain creation. If `SetColorSpace1` was refused, DXVK says so. Mesa's WSI prints
|
||||
`Not using HDR metadata to avoid protocol errors` when `is_hdr_metadata_legal` rejects metadata
|
||||
— catching that string is a direct test of A3's metadata plumbing.
|
||||
3. **`dumpsys SurfaceFlinger | grep banner_wayland_game`.** Today it prints
|
||||
`dataspace=UNKNOWN (0) hdr metadata types=0`. Success = `dataspace:0x9c60000` (`BT2020_PQ`, 163971072 =
|
||||
`STANDARD_BT2020|TRANSFER_ST2084|RANGE_FULL`) and `hdr metadata types=` non-zero. **Also record `composition:` on the same line**
|
||||
— if it flips DEVICE → CLIENT, HDR cost us the zero-copy win (§3.4) and that is a failure, not a
|
||||
success.
|
||||
4. **`dumpsys display`.** On whatever display is in use: `supportedHdrTypes=[...]` non-empty,
|
||||
`colorMode` ≠ 0, `hdrSdrRatio` ≠ `not_available`. This is the gate for steps 5–6; on the Pocket
|
||||
FIT's internal panel it will never pass.
|
||||
5. **`dumpsys SurfaceFlinger` Wide-Color section.** `Current color mode` should move off
|
||||
`ColorMode::NATIVE (0)`, and the output line's `dataspace=`/`targetDataspace=` should stop being
|
||||
`UNKNOWN (0)`.
|
||||
6. **Only then, by eye.**
|
||||
|
||||
### 7.3 Getting a display that can actually show it — three options, ranked
|
||||
|
||||
1. **External HDR display over USB-C (recommended, cheapest).** The app already reparents the game
|
||||
into a `Presentation` on a secondary display —
|
||||
`app/src/main/java/com/winlator/star/display/ExternalDisplayController.java`,
|
||||
`findPresentationDisplay()` picks a `DISPLAY_CATEGORY_PRESENTATION` display that is not the
|
||||
activity's own. In the Wayland session the compositor's `SurfaceView` is the reparented view, so
|
||||
`sc_layer.c`'s `ASurfaceControl_createFromWindow(vkp_window(), ...)` follows it automatically —
|
||||
**no new plumbing to test on an external panel.** Risk: `Display.getHdrCapabilities()` for a
|
||||
DP-alt-mode display depends on the vendor HWC reading the external EDID, and empty lists for
|
||||
genuinely HDR10 monitors are a common Snapdragon gap (§3.6). **Step zero is a five-minute,
|
||||
zero-code test: plug an HDR monitor in and run `dumpsys display | grep -A2 hdrCapabilities`.**
|
||||
If the external display reports HDR types, everything else follows. If it does not, options 2
|
||||
and 3 are all that remain.
|
||||
2. **The user's Galaxy Fold (Adreno 840).** Samsung foldables/flagships do report HDR10+ on the
|
||||
internal panel, so this is the most likely device to show real HDR with no extra hardware. But
|
||||
the Fold is *already* the outstanding unknown for basic Wayland bring-up (checkpoint item 1:
|
||||
"Other GPUs… the Galaxy Fold (A840) logs are still outstanding"). Chasing HDR there before
|
||||
plain Wayland is proven there inverts the priority order.
|
||||
3. **Plumbing-only verification on SDR.** Everything in §7.2 steps 1–3 works on the Pocket FIT.
|
||||
This proves the code is *correct* but proves nothing about whether it *looks right* — and
|
||||
tone-mapping quality is the entire user-visible payload of HDR. **Do not ship on this evidence
|
||||
alone.**
|
||||
|
||||
---
|
||||
|
||||
## 8. Verdict
|
||||
|
||||
**Not now. Do the 10-bit slice; park HDR behind the Fold.**
|
||||
|
||||
The reasoning, plainly:
|
||||
|
||||
* **The effort is genuinely small and smaller than we thought.** ~4.5 days to a plumbed HDR10
|
||||
path, **app-only, no new wcp** — because Mesa's colour-management client is already inside the
|
||||
shipped wcp v6 driver and our zero-copy WSI patch already maps 10-bit to gralloc. The phase-4
|
||||
memory note saying HDR "does need a new wcp" should be corrected: only scRGB/FP16 zero-copy (C1)
|
||||
does, and that is optional.
|
||||
* **But the payload is zero on the only device we can test on.** The Pocket FIT reports no HDR
|
||||
types, no colour modes but NATIVE, no HDR/SDR ratio and no brightness nits. We could not see the
|
||||
feature, could not tune the tone-mapping, and could not tell a correct implementation from a
|
||||
subtly wrong one. Shipping HDR proven only by log lines would be the exact opposite of this
|
||||
project's "device-proven, not CI-green" rule.
|
||||
* **On this device it would be a net negative if enabled.** §3.4: an HDR-dataspace layer on a
|
||||
display with no HDR types drops from DEVICE to CLIENT composition. That trades the measured
|
||||
zero-copy win (−4 pts GPU busy, −3 % power) for a GPU tone-map pass and a washed-out image. A
|
||||
capability gate (A5) prevents the harm, but a gate that is always closed on our test hardware is
|
||||
a feature nobody can exercise.
|
||||
* **It is also out of order.** The pre-4 checkpoint's ranked list puts "other GPUs" and "prove
|
||||
zero-copy where it can win, then make it the default" first, with multi-layer presentation in
|
||||
flight. HDR competes with those for the same `sc_layer.c` surface area — and multi-layer will
|
||||
rewrite exactly the code A4 touches. Landing HDR into `sc_layer.c` now means merging it twice.
|
||||
|
||||
**What the user would actually see for the effort, honestly:**
|
||||
|
||||
* **Today, Pocket FIT, full HDR (7.5 days):** nothing. Identical picture, plus a `dumpsys` line
|
||||
that says `BT2020_PQ`, plus a risk of losing zero-copy.
|
||||
* **Today, Pocket FIT, the 10-bit slice only (A1 + B1, ~1 day):** *slightly* smoother gradients in
|
||||
dark scenes and skyboxes for games that offer a 10-bit swapchain, and a plausible route to
|
||||
retiring the debanding pass (one fewer fullscreen pass) for those games. Small, real, and it is
|
||||
the foundation Phase A sits on anyway — so it is not wasted work if HDR later goes ahead.
|
||||
* **Later, on an HDR display (external monitor or the Fold), full Phase A:** genuinely brighter
|
||||
highlights and a wider gamut in the ~15 PC titles that implement HDR properly, on the one Android
|
||||
emulator that can do it. That is a real headline feature and a real differentiator — but only
|
||||
once there is a panel to show it on.
|
||||
|
||||
**Recommended sequence:**
|
||||
|
||||
1. **Five minutes, zero code:** plug an HDR monitor into the Pocket FIT's USB-C and run
|
||||
`dumpsys display | grep -A2 hdrCapabilities`. This single reading decides everything —
|
||||
it is the difference between "we can develop and prove HDR today on hardware we own" and
|
||||
"HDR is blocked on the Fold."
|
||||
2. **~1 day, independent of HDR:** the 10-bit slice (A1 + B1), gated and A/B'd for banding and
|
||||
perf. Ship it as a banding/quality improvement on its own merits.
|
||||
3. **Park Phase A (A2–A5)** until either step 1 comes back positive or the Fold is in the loop
|
||||
and plain Wayland is proven there. Revisit *after* multi-layer presentation lands, so the
|
||||
`sc_layer.c` dataspace work is written once against the final layer API.
|
||||
|
||||
---
|
||||
|
||||
## 9. What I could not establish, and why
|
||||
|
||||
* **Whether FP16 (`R16G16B16A16_FLOAT`) AHardwareBuffers are allocatable on this SoC.** Not in the
|
||||
vendor gralloc strings, and I did not install or run a probe binary (other agents are testing on
|
||||
the device). Decides whether scRGB zero-copy (C1) is possible at all. Cheap to settle later with
|
||||
a one-call `AHardwareBuffer_isSupported()` check inside the compositor's existing startup log.
|
||||
* **Whether an external HDR display over USB-C actually reports HDR types on the Pocket FIT.**
|
||||
No external display was connected and I did not connect one. This is the single highest-value
|
||||
unknown in the whole report (§8, step 1).
|
||||
* **Whether an HDR-tagged layer really drops DEVICE → CLIENT on this HWC.** The AOSP code path is
|
||||
clear (§3.4) but it is vendor-HWC behaviour and I did not test it — testing it would mean
|
||||
launching a game, which was out of scope.
|
||||
* **What the Galaxy Fold (Adreno 840, Android version unknown to me) reports.** Not connected.
|
||||
Its `getHdrCapabilities()` and `isHdrSdrRatioAvailable()` are the other decisive readings.
|
||||
* **Exact Mesa line numbers.** Cited against `wsi_common_wayland.c` fetched from
|
||||
`gitlab.freedesktop.org/mesa/mesa` at `7cda7850edd103ace21aac37d416d2fdf7a282e1`, the commit in
|
||||
`banners-turnip-wayland/mesa_hash.txt`. They are accurate for that file; they will drift if the
|
||||
Turnip base moves.
|
||||
* **DXVK/vkd3d-proton line numbers** are cited against upstream `doitsujin/dxvk@8759acd1` and
|
||||
`HansKristian-Work/vkd3d-proton@2a38e799` (master as of today), not against the exact
|
||||
v3.1-gplasync / 3.1.0-wave64-relax builds we ship. The feature presence in *our* builds was
|
||||
verified independently by `strings` on the device (§1.1); the line numbers are for orientation.
|
||||
* **Whether any of the games in §7.1 launch and run correctly under our Wayland backend at all.**
|
||||
Untested. Picking the HDR test title is its own small exercise and should start from games
|
||||
already known to work in a Wayland container.
|
||||
|
||||
---
|
||||
|
||||
## 10. Round 1 — as built (2026-09-14, `feat/wayland-hdr`)
|
||||
|
||||
Phase A (A1–A5) for HDR10, opt-in, proven by the app's own logs because the first HDR panel in the
|
||||
loop is a tester's non-rooted Galaxy Fold (Adreno 840, Android API 37: HDR10/HLG/HDR10+, 1351 nits,
|
||||
HDR/SDR ratio available). App-only; no layer change.
|
||||
|
||||
### 10.1 The gate (`wl_color_mgmt.c`, `banner_color_init`)
|
||||
|
||||
Open only when ALL hold when the compositor starts:
|
||||
|
||||
| Input | Where it comes from |
|
||||
|---|---|
|
||||
| `BANNER_WAYLAND_HDR=1` (or `true`/`on`) | container or shortcut env (`XServerDisplayActivity.resolvedWaylandHdrMode`) |
|
||||
| the game's display lists **HDR10** | `DisplayHdrInfo.supportsHdr10` for `hdrTargetDisplay()` (the TV when the game is on it) |
|
||||
| display layers with dataspace control | `sc_layer_can_tag_hdr()` = SurfaceControl API + `ASurfaceTransaction_setBufferDataSpace` |
|
||||
| the zero-copy global | `ahb_swapchain_advertised()` |
|
||||
|
||||
`BANNER_WAYLAND_HDR=force` skips the display row only (testing the negotiation on an SDR panel —
|
||||
SurfaceFlinger then tone-maps the layer). A **closed** gate advertises nothing (no
|
||||
`wp_color_manager_v1` global, no 10-bit dma-buf rows), writes `color … HDR gate CLOSED: <why>` and
|
||||
`HDR on screen: no, because <why>`, and the session is otherwise byte-for-byte the old one. With the
|
||||
switch unset the module is silent unless the display lists HDR10 or `DXVK_HDR=1` is set (one hint line).
|
||||
On an HDR10 display the app also turns **zero-copy presentation on** for the session (an HDR frame is
|
||||
only right on the game's own layer) and exports `BANNER_WSI_AHB=1` to match.
|
||||
|
||||
### 10.2 What an open gate offers (§2.2, exactly what Mesa binds)
|
||||
|
||||
`wp_color_manager_v1` **version 1**: intent `perceptual`; features `parametric` +
|
||||
`set_mastering_display_primaries` (Mesa only sends mastering luminance inside that feature's branch);
|
||||
primaries `bt2020`; transfer `st2084_pq`. Not advertised: `srgb` (so Mesa creates no colour surface for
|
||||
SDR swapchains at all), HLG (no DXGI swapchain uses it), `ext_linear`/`extended_target_volume` (scRGB =
|
||||
Phase C, needs FP16 gralloc + a layer change), ICC, `windows_scrgb`. `zwp_linux_dmabuf_v1` gains
|
||||
`AB30`/`XB30` (A2B10G10R10 — Mesa lists a VkFormat only when both the alpha and opaque fourcc are there),
|
||||
appended after the four 8-bit rows. Protocol glue generated from wayland-protocols **1.41** with
|
||||
wayland-scanner 1.24.0 (`protocols/color-management-v1.xml`).
|
||||
|
||||
Strictness: protocol errors only where a conforming client can never hit them (un-advertised
|
||||
feature/intent/primaries/tf, a property set twice, an incomplete set, a not-ready description).
|
||||
Everything a conforming client can produce on a bad day is logged and absorbed instead — out-of-range
|
||||
HDR metadata is dropped from what Android gets (and `max_cll`/`max_fall` of 0 = "unknown", which Mesa
|
||||
sends as-is), a second colour surface for the same `wl_surface` replaces the first, requests on inert
|
||||
objects are ignored. `ready` is sent from inside `create` (Mesa blocks its swapchain on it).
|
||||
Output and surface-feedback image descriptions answer `failed` (Mesa uses neither).
|
||||
|
||||
### 10.3 The layer path (`sc_layer.c`)
|
||||
|
||||
`setBufferDataSpace` (+ `setHdrMetadata_smpte2086` / `_cta861_3` when present) ride the same
|
||||
transaction as the buffer, per layer, only on change, re-sent on every new SurfaceControl (recovery
|
||||
swap, window change). A layer that was never tagged is never touched; once tagged, an untagged frame
|
||||
puts it back to `UNKNOWN` and clears the metadata. The zero-copy frame gets the surface's current
|
||||
description; the 8-bit pool copy of an HDR frame keeps the PQ tag (colours right, 8-bit precision,
|
||||
logged once); the effects pass and the overlay layer are always untagged.
|
||||
|
||||
### 10.4 When an HDR frame cannot be zero-copy — round 1's decision
|
||||
|
||||
No tone-mapping anywhere (the copy path is `vkCmdBlitImage` into an 8-bit sRGB swapchain; a PQ→SDR
|
||||
shader pass is its own project). Instead:
|
||||
|
||||
- **Effects on / frame generation on:** an HDR fullscreen game **keeps its display layer**; the
|
||||
effects chain and frame generation are skipped for it (both are 8-bit SDR and would wash it out),
|
||||
and the base surface's black frame skips them too (`vkp_render_plain`) so generated presents cannot
|
||||
pace the loop. Logged when it starts and when it ends.
|
||||
- **A window above the game on a display that cannot compose a second layer** (phase 5's
|
||||
rotated+scaled rule is untouched), **a windowed HDR game**, **zero-copy switched off**, **layer
|
||||
unavailable:** the scene goes through the copy path and the HDR frames are shown **untone-mapped
|
||||
(washed out)**; every such scene is counted and the reason logged.
|
||||
|
||||
### 10.5 Proof without root — what a tester's logs say
|
||||
|
||||
`Download/Wayland-logs/wayland-*.log`, tag `color` (plus the existing `display` capability line):
|
||||
|
||||
| Line | Means |
|
||||
|---|---|
|
||||
| `HDR gate OPEN: … "Built-in Screen" … reports HDR types HDR10, …` | the offer is on, with its inputs and which Android colour calls exist |
|
||||
| `<program> bound wp_color_manager_v1 version 1 …` | Mesa saw it: `VK_COLOR_SPACE_HDR10_ST2084_EXT` is now listed |
|
||||
| `<window>: colour-management surface created` | a swapchain asked for a non-sRGB colour space |
|
||||
| `image description #N from <program>: BT.2020, ST 2084 (PQ); mastering …; max CLL …, max FALL …` | the game's colour space + `vkSetHdrMetadataEXT` values |
|
||||
| `<window> presents WxH buffers in XB30 (10-bit A2B10G10R10) …, gralloc RGBA1010102 (10-bit)` | the 10-bit swapchain really arrived |
|
||||
| `banner_wayland_game: dataspace BT2020_PQ (0x9c60000) set on the display layer …` | the tag + metadata SurfaceFlinger received |
|
||||
| `HDR last 10 s: N frames on the display layer tagged BT2020_PQ …` | steady state |
|
||||
| `display HDR/SDR ratio X (was Y; HDR frames on screen: yes …)` | `Display.getHdrSdrRatio()` — rises above 1.00 only when Android grants HDR headroom |
|
||||
| `HDR on screen: yes / tagged but NOT confirmed / no, because …` | the verdict (written on changes, at game exit and at session end; the last one counts) |
|
||||
|
||||
DXVK's own `<exe>_dxgi.log` / `<exe>_d3d11.log` add `Color space: VK_COLOR_SPACE_HDR10_ST2084_EXT` and
|
||||
the 10-bit format at swapchain creation; Mesa's `Not using HDR metadata to avoid protocol errors`
|
||||
(stderr → `wine_debug.log`) means the game's metadata failed Mesa's own legality check.
|
||||
|
||||
## 11. Round 2 — as built (2026-09-14, `feat/wayland-hdr`; CI only, not device-proven yet)
|
||||
|
||||
Round 1 is proven on the tester's Fold (God of War, DXVK v3.1: 10-bit zero-copy, `BT2020_PQ` on the
|
||||
layer, HDR/SDR ratio 1.00 → 2.51, `HDR on screen: yes`). Round 2 makes it a setting and removes
|
||||
round 1's big limitation (§10.4: no tone-mapping, effects/frame generation skipped, washed-out copies).
|
||||
The gate itself is unchanged (§10.1) apart from its first input.
|
||||
|
||||
### 11.1 The setting, and what the launch does with it
|
||||
|
||||
- `Container.isWaylandHdr()` (extraData `waylandHdr`) and the shortcut extra `waylandHdr` (`1`/`0`/unset
|
||||
= the container's) — one resolver, `display/WaylandHdr.effective()`, used by the three editors
|
||||
(container, game shortcut, XMB game settings) and the launch path. `BANNER_WAYLAND_HDR` in the
|
||||
environment still overrides it (`1` / `0` / `force`). Editors grey the row with the reason when the
|
||||
built-in screen does not list HDR10 (`WaylandHdr.unavailableReason`), and say it applies from the
|
||||
next launch.
|
||||
- HDR on for the session (setting resolved on AND HDR10 display, or `force`): `DXVK_HDR=1` is exported
|
||||
after the user's env merge unless the user set `DXVK_HDR` themselves.
|
||||
- Every Wayland session whose display lists **HDR10** exports `BANNER_WAYLAND_HDR_MAX_NITS`,
|
||||
`…_MAX_AVG_NITS`, `…_MIN_NITS` (decimal nits from `Display.getHdrCapabilities()`; unknown values and
|
||||
a max of 0 are left out) — the contract the Wayland layer from versionCode 10 turns into EDID HDR
|
||||
metadata for DXGI (checked by the Wine side against DXVK's own EDID parser: the Fold's 1351/1351/0 →
|
||||
DXGI max 1345.43, max full-frame 1345.43, min 0.01). Never on an SDR display, whose EDID would then
|
||||
claim PQ support. One `session environment:` line logs them. Android has no live absolute nits; the
|
||||
live evidence stays the HDR/SDR ratio.
|
||||
- A "DXVK before 3.0 needs layer v10" warning was built and then **removed**: its premise was wrong
|
||||
(Wine already exposes `VK_EXT_swapchain_colorspace` at instance level; DXVK ≥ 2.1 looks for it in the
|
||||
device list, logs 0 on every Mesa system — the working v3.1 run logs 0 too — and never uses it for the
|
||||
HDR decision). Why God of War offered no HDR option on DXVK 2.4.1 is open (Fold A/B).
|
||||
|
||||
### 11.2 HDR-aware composition (`hdr_compose.c`, `hdr_encode.frag`, compositor.c `hdr_plan`)
|
||||
|
||||
Every scene with an HDR draw takes one of four routes (logged on each change, with the reason):
|
||||
|
||||
| Route | When | What reaches the display |
|
||||
|---|---|---|
|
||||
| 0 | the HDR game alone fullscreen, zero-copy on, no effects, nothing above it (or one window the overlay layer can carry) | the game's own 10-bit gralloc frames on its layer, `BT2020_PQ` (round 1) |
|
||||
| 1 | effects on, a window above on a display that cannot take a second layer, a windowed game, zero-copy off | **the HDR picture**: every draw blitted 1:1 into a 10-bit "mixed" image (each pixel still in its own encoding), ONE encode pass to PQ BT.2020 (SDR pixels sRGB → linear → BT.2020, placed at 203 nits, `BANNER_WAYLAND_HDR_SDR_NITS` overrides), the effects chain run on it in 10-bit (`vkp_effects_set_formats`), copied into a 10-bit (`RGBA1010102`) layer buffer, tagged with the game's description; 8-bit buffers if gralloc refuses 10-bit (logged) |
|
||||
| 2 | frame generation on | composed into PQ in the 8-bit scene image, through the frame-generation engine, presented through an **HDR10 swapchain** (`VK_EXT_swapchain_colorspace` enabled on the compositor instance in HDR sessions only; first `A2B10G10R10` else FP16 pair the surface lists with `HDR10_ST2084`); where the surface lists none, **tone-mapped** into the ordinary swapchain. 8 bits through the engine: gradients may band |
|
||||
| 3 | an HDR frame the compositor could not import | layer only; effects/frame generation skipped for it (round 1's behaviour, now the rare case) |
|
||||
|
||||
The tone map (mode 1 of `hdr_encode.frag`): PQ → nits → BT.2020 → BT.709, relative to SDR white, maxRGB
|
||||
curve linear to a 0.8 knee then an exponential roll-off that puts the content peak (the game's max CLL,
|
||||
else mastering max, else 1000 nits) at 98 %, sRGB-encoded. Which pixels are HDR comes from up to six
|
||||
top-first scene rects + a mask in the push constants (more windows over the game → the lowest are
|
||||
treated as SDR, logged once). A composed frame that cannot reach the layer falls back to route 2 for as
|
||||
long as that lasts (logged once each way).
|
||||
|
||||
### 11.3 The live switch (lead's addition): drawer → Graphics → "HDR output"
|
||||
|
||||
Shown only while the gate is open (the activity's 1 s HDR sampler flips `waylandHdrAvailable`). Per
|
||||
session, starts on, nothing saved. Off (`banner_color_set_output(0)`, posted through the host queue
|
||||
to the compositor thread, logged, redraw): route 0 becomes route 1 with the tone map, the picture goes
|
||||
on the layer **untagged** on 8-bit buffers (the layer's dataspace returns to `UNKNOWN`); route 2
|
||||
rebuilds the swapchain as SDR and tone-maps; route 3 cannot (nothing can read those frames) and stays
|
||||
HDR, said in the log. The game is told nothing (its DXVK_HDR and colour-manager offer were decided at
|
||||
launch). The HUD reads `Wayland · HDR` while HDR frames are on screen (tagged in the last 1.5 s and
|
||||
ratio > 1.01 where reported), `Wayland · HDR off` while the switch is off. Frames tone-mapped by the
|
||||
switch are counted separately in the verdict.
|
||||
|
||||
### 11.4 Live headroom (lead's addition after the second Fold run)
|
||||
|
||||
The Fold's second run (brightness slider at maximum) showed the ratio at 3.23 for a minute and then
|
||||
**1.00 for 3+ minutes with HDR frames on screen the whole time**, while the verdict kept saying "rose to
|
||||
3.23". Android simply gave the layer no headroom (SDR white already at the panel's limit). Now, from the
|
||||
ratio samples the app already sends (1 s + the display listener): the time with HDR frames on screen and
|
||||
the part of it with ratio > 1.01; a no-headroom streak (HDR frames on screen, ratio <= 1.01) that after
|
||||
5 s writes `no HDR headroom for 5 s … the screen brightness is probably at maximum …` (and `HDR headroom
|
||||
is back …` when it ends), tags the 10 s line and the steady ratio line, puts the HUD on `Wayland · HDR
|
||||
(no headroom)` and the drawer row on the same hint (`banner_color_hdr_state` = 2); and the verdict
|
||||
reports `headroom above 1.00 for P% of the HDR time (a of b s), now R` plus the hint while the streak
|
||||
lasts. Cheap: no new sampling, a few integers under the existing lock.
|
||||
|
||||
**Round-2 Fold run (20:23, run 34911468350):** the first no-headroom streak was a **screen recording**,
|
||||
not the brightness: the user's recording (210.8 s, BT.709 SDR H.264, finalised 20:27:30, so started
|
||||
~20:23:59) lines up with the ratio falling 2.98 → 1.00 at 20:24:00.9 and staying there to the end. So
|
||||
Android (at least Samsung's) turns HDR headroom off while the screen is recorded. Every hint now names
|
||||
both causes; whether maximum brightness does it too is still unmeasured (tester note step H).
|
||||
|
||||
### 11.5 The HUD's HDR line (round 2b, the user's layout request)
|
||||
|
||||
`40.3ms · Wayland · HDR (no headroom)` ran off the pill's right edge (the capsule's ends curve in, and the
|
||||
latency line is the bottom of the stack). The display-server label is plain `Wayland` again, and the HDR
|
||||
state is a line of its own directly under it, fed as a `FusionHdr` code (`FusionHudView.setHdrState`):
|
||||
`HDR` / `HDR (no headroom)` / `HDR off` / `HDR tone-mapped` (switch on, frame generation without an HDR10
|
||||
swapchain) / `HDR ready` (gate open, no HDR frames right now). `NONE` - every session whose gate is
|
||||
closed - draws nothing, so those HUDs are unchanged to the pixel. Pill: the extra line makes the capsule
|
||||
taller and its ends rounder, so in HDR sessions `fitCapsule()` measures every glyph's ink against the
|
||||
two rounded ends (plus the outline and 2 sp) and shifts/widens the pill until nothing crosses. Full: an
|
||||
`HDR` row under `DISP`; Tiles: the *Display* tile's sub-line; Minimal: a line under the footer; Mega: a
|
||||
fragment after `DISP` in the bottom band. Only the Fusion HUD shows the display server at all; the
|
||||
classic, GameHub and GameNative HUDs never received it and are untouched.
|
||||
|
||||
### 11.6 Round 2d (Fold evidence from 2c)
|
||||
|
||||
- **The format scan was a false negative.** FG with the AIO HDR card logged `lists no HDR10 swapchain
|
||||
format (37/0 37/1000104001 … 37/1000104012)`: `swap_init` read the surface's list into
|
||||
`VkSurfaceFormatKHR fmts[32]`, and with `VK_EXT_swapchain_colorspace` the Android WSI lists every
|
||||
format once per colour space (11 on the Fold) - the 10-bit/FP16 rows were past entry 32, and the log's
|
||||
160-char buffer cut the list too. Now: all pairs read (heap), one line with the total, colour spaces
|
||||
per format and every HDR-capable pair (10-bit/FP16 first); HDR10 pick A2B10G10R10 → A2R10G10B10 →
|
||||
FP16 → 8-bit; a driver that refuses the HDR10 swapchain gets the SDR one at once. The only other
|
||||
fixed array near it (queue families, 16) does not hold surface formats or present modes; FIFO is the
|
||||
only present mode used, so no list is read.
|
||||
- **VK_EXT_hdr_metadata** enabled in HDR sessions where the device lists it; the HDR10 swapchain gets the
|
||||
game's SMPTE 2086 / CTA-861.3 once per swapchain and image description (none if the game sent none).
|
||||
- **FP16 frame generation for HDR.** The bridge and both engines already take a format (the chain,
|
||||
the ring); lsfg-vk itself runs HDR in `R16G16B16A16_SFLOAT`. With HDR frames through an HDR10
|
||||
swapchain the scene (`g_fgscene`), the encode pass (new FP16 output), the effects and the engine run
|
||||
in FP16 where `fge_format_ok` (storage + linear sampling) passes; the PQ signal is fed as-is (the
|
||||
shaders' own HDR flags stay off - PQ is already perceptual). A chain that fails to build in FP16 is
|
||||
sticky inside both engines, so the bridge refuses the format for the session and restarts the engine
|
||||
in 8 bits instead of marking frame generation failed.
|
||||
- **Evidence beside the headroom** (app, non-root): PowerManager thermal status (+ listener) and
|
||||
`getThermalHeadroom(10)` every 10 s, `Settings.System` brightness + mode (+ observer), screenshots
|
||||
(`registerScreenCaptureCallback`, API 34) and screen recording (`addScreenRecordingCallback`, API 35,
|
||||
reflection) with the normal `DETECT_SCREEN_CAPTURE` / `DETECT_SCREEN_RECORDING` permissions. Changes
|
||||
are logged; the no-headroom lines, the 10 s line and the verdict carry the values and name the likely
|
||||
cause (recording / hot / brightness at maximum) or say none is visible. The verdict lists only the
|
||||
paths that carried frames, to leave room for it.
|
||||
|
||||
### 11.7 Round 2e (headroom request, display ceiling, coverage)
|
||||
|
||||
2d on the Fold: all pass (68 surface pairs, HDR10 swapchain A2B10G10R10, FP16 FG at 120 fps, 0 tone-mapped,
|
||||
VK_EXT_hdr_metadata, headroom 3.00 at brightness 255 **auto**). A tester's ROG Phone 9 Pro (Adreno 830,
|
||||
API 36): correct HDR frames, composed path, HDR layer ~80% of the 2400x1080 screen (windowed card), not
|
||||
hot, not recording, brightness 8-223 manual - and the HDR/SDR ratio at **1.00 the whole session**.
|
||||
- `ASurfaceTransaction_setDesiredHdrHeadroom` (API 35) on the game layer, per HDR frame, only on change:
|
||||
content peak (max CLL, else mastering max, else the display's peak) / SDR white, capped at the display's
|
||||
highest ratio; 0 (no preference) when the layer stops carrying HDR; re-sent on a fresh SurfaceControl.
|
||||
The screen surface (HDR10 swapchain for frame generation) gets the same through
|
||||
`SurfaceView.setDesiredHdrHeadroom` (API 35, reflection; `SurfaceControl.Transaction` fallback), applied
|
||||
by the app from `banner_color_screen_headroom()`.
|
||||
- `Display.getHighestHdrSdrRatio()` (Android 16+, reflection) in the display line and the verdict; 1.00
|
||||
means no boost at all, and the request is then not made.
|
||||
- HDR layer coverage in the dataspace line, the composed-picture line and on geometry changes.
|
||||
- "brightness at maximum" only for MANUAL >= 250/255; no visible cause -> "this phone may not boost HDR
|
||||
from apps (the requested headroom was …, the display's highest ratio is …)".
|
||||
|
||||
### 11.8 What is still NOT done / not proven
|
||||
|
||||
- Round 2's composition, switch and HUD line were device-tested in parts on the Fold (setting-driven
|
||||
gate, auto environment, composed picture, headroom lines, HUD); 2d is CI only.
|
||||
- The engines' own HDR flags (lsfg `hdr_support` / FSR3 HDR input) stay off: the PQ signal is treated as
|
||||
perceptual SDR-like data. If interpolation artefacts show only in HDR, that is the next thing to try.
|
||||
- Colour effects (brightness/contrast/saturation/gamma, the "HDR" bloom) operate on the PQ signal in
|
||||
route 1 and look stronger than in SDR; sharpening/AA/CRT/upscalers are perceptually fine.
|
||||
- scRGB (FP16) swapchains (Phase C) — unchanged: needs FP16 gralloc and a layer change.
|
||||
@@ -1,472 +0,0 @@
|
||||
# Bannerlator Wayland runtime (feat/wayland-runtime)
|
||||
|
||||
Experimental **parallel** display runtime: run/launch games through Wine's
|
||||
`winewayland.drv` talking to our own embedded Wayland compositor, instead of the
|
||||
X11 path (pure-Java X11 server + `libwinlator.so`). The X11 runtime stays the
|
||||
default and is untouched — this is a separate flavor/branch.
|
||||
|
||||
## What's already proven (spike repo `bannerlator-wayland`, device-tested on Adreno 750)
|
||||
- Minimal libwayland-server compositor: globals + xdg-shell handshake + buffer commit.
|
||||
- **Turnip's Vulkan WSI exports real zero-copy dmabufs to our external compositor**
|
||||
(same Mesa path winewayland.drv uses for DXVK/VKD3D) — risk #1 retired.
|
||||
- Compositor imports that dmabuf into its own Turnip VkImage (`vk_import.c`).
|
||||
The staged `src/` here is that proven code, to grow into the app-embedded compositor.
|
||||
|
||||
## Dependencies
|
||||
1. **A Proton 11 arm64ec wcp that ships `winewayland.drv`** — built on branch
|
||||
`The412Banner/proton-wine:feat/winewayland` (task #1). Nothing runs end-to-end
|
||||
without it.
|
||||
2. **Wayland runtime libs in the imagefs** so `winewayland.so` (unixlib) loads:
|
||||
`libwayland-client.so`, `libwayland-egl.so`, `libxkbcommon.so`, `libxkbregistry.so`
|
||||
(bionic aarch64). The wcp bundles them in its `lib/` as a fallback; the clean home
|
||||
is the imagefs — add via `ImageFsInstaller` (new `installWaylandLibs()`), same
|
||||
pattern as `installFFmpeg8()`.
|
||||
|
||||
## Integration plan (M4)
|
||||
- **CMake**: add `waylandcomp` as a native lib (`libbannerwayland.so`) built with the
|
||||
NDK, linking the bionic `libwayland-server`/`libvulkan`. Generate protocol glue from
|
||||
`protocols/*.xml` at build time (host `wayland-scanner`).
|
||||
- **Surface**: a `WaylandDisplayActivity` (parallel to `XServerDisplayActivity`) hosts a
|
||||
`SurfaceView`; JNI hands the `ANativeWindow` to the compositor, which creates a Vulkan
|
||||
swapchain on it and blits the imported game VkImage each frame (the last un-proven
|
||||
render step; standard Vulkan once the window exists).
|
||||
- **Input**: Android `MotionEvent`/`KeyEvent` → `wl_seat`/`wl_pointer`/`wl_keyboard`.
|
||||
- **Launch wiring**: start the compositor, export `WAYLAND_DISPLAY`, select the wayland
|
||||
driver per-prefix (registry `Drivers\Graphics = winewayland`) instead of `winex11`,
|
||||
and point the container at the winewayland wcp.
|
||||
|
||||
## Status
|
||||
- ✅ **Native lib foundation done + compile-verified.** Compositor + vk_import + pre-generated
|
||||
protocol glue build as `libbannerwayland.so` (CMake target added), linking the vendored
|
||||
bionic `libwayland-server`. Verified: compiles/links clean as an aarch64 bionic `.so`,
|
||||
exports `banner_wayland_run`, NEEDED = libwayland-server + libvulkan. JNI entry
|
||||
(`waylandcomp_jni.c`) + `WaylandCompositor.java` bring it up on a thread. Compositor-process
|
||||
runtime deps (libwayland-server/libffi/libandroid-support) staged in `jniLibs/arm64-v8a`.
|
||||
- ⏭️ **Next phase (gated on the winewayland wcp landing green):** `WaylandDisplayActivity`
|
||||
(SurfaceView) + JNI `ANativeWindow`→Vulkan swapchain + blit the imported game VkImage to the
|
||||
window (last un-proven render step) + input + launch wiring (start compositor, `WAYLAND_DISPLAY`,
|
||||
per-prefix `Drivers\Graphics=winewayland`) + `ImageFsInstaller.installWaylandLibs()` (client/egl/xkb
|
||||
into the imagefs for winewayland.so).
|
||||
|
||||
## Pointer lock / relative mouse (zwp_pointer_constraints_v1 + zwp_relative_pointer_manager_v1)
|
||||
- Both globals are advertised (version 1); glue is pre-generated in `generated/` with
|
||||
`wayland-scanner server-header` / `private-code` from `protocols/*-unstable-v1.xml` (same
|
||||
scanner version as the rest, 1.24.0), no build-time scanner needed.
|
||||
- winewayland's use: `ClipCursor`/fullscreen + hidden cursor → `lock_pointer` (persistent);
|
||||
`SetCursorPos` → lock + `set_cursor_position_hint` + commit + unlock; visible cursor +
|
||||
`ClipCursor(rect)` → `confine_pointer` with a one-rectangle region. It only turns relative
|
||||
motion on for a window its `wl_pointer` has entered, so **pointer focus follows an active
|
||||
constraint** (the desktop surface gets `leave`, the constrained surface `enter`) and goes
|
||||
back to the desktop on the next motion after the constraint ends.
|
||||
- Lock: the pointer is frozen; every input delta becomes `relative_motion` (no `motion`).
|
||||
Confine: absolute motion is clamped to surface ∩ region. The position hint is applied on
|
||||
the surface's commit (pointer moves there) and is where the pointer stays when the lock ends.
|
||||
One constraint holds at a time (a newer request ends the older one); oneshot constraints
|
||||
are defunct after ending, persistent ones re-take hold on focus re-entry.
|
||||
- App side: input type 6 = relative delta (1/256 px); `banner_on_pointer_lock(locked, x, y)`
|
||||
tells Java to switch its touch/mouse path to deltas (`XServer.setExternalRelativeMode`) and,
|
||||
on unlock, to re-sync the X pointer (the absolute input's source) to x,y. With Wayland mode
|
||||
`WinHandler.mouseEvent` routes relative-mode input to the compositor instead of the guest.
|
||||
- Session log tag `pointer`: `lock requested by …`, `locked: … frozen at x,y`, `unlocked: … (why)`,
|
||||
`confined: …`, `unconfined: …`, `relative pointer created for …`, `position hint: …`.
|
||||
Test FIFO gained `rel DX DY`.
|
||||
|
||||
## Screen surface, swapchain recovery (vk_present.c)
|
||||
- The app's UI thread never waits on the renderer: `vk_present_set_window()` only leaves the new
|
||||
`ANativeWindow` (or NULL) in a request slot; the compositor thread applies it before its next
|
||||
frame and on every vsync tick (`vkp_apply_window_request`), tearing the old swapchain down and
|
||||
releasing the old window's reference. Acquire waits at most 1 s, never forever.
|
||||
- OUT_OF_DATE / SURFACE_LOST on acquire or present rebuild the swapchain (once inline on acquire, so
|
||||
the frame isn't lost). SUBOPTIMAL is presented as is: the swapchain uses IDENTITY preTransform on
|
||||
purpose, so a rotated panel reports it on every frame. DEVICE_LOST logs
|
||||
`GPU device lost … the compositor has stopped presenting` once and stops touching the device;
|
||||
clients keep being paced (`pace_without_output`) so they don't wedge.
|
||||
- A swapchain that can't be created is retried every 0.5 s and logged once per streak.
|
||||
|
||||
## Buffer lifetime, FPS limiter (compositor.c)
|
||||
- `struct dmabuf_buffer` is reference counted: one ref for the wl_buffer resource, one per surface
|
||||
showing it. Mesa destroys a swapchain's wl_buffers when the game rebuilds its swapchain, while the
|
||||
last committed one is still on screen; the surface keeps the import (`s->dmabuf_buf`) until its
|
||||
next commit, so the window neither blinks to black nor is unmapped/remapped ("closed"/"opened").
|
||||
- Surface destroy releases its buffer immediately (`drop_dmabuf(s, 0)`), replaced buffers go back on
|
||||
the limiter's cadence. The per-surface release schedule is bounded: a slot in the past is brought
|
||||
to now, and the schedule never runs further ahead than `(releases still pending + 1)` intervals.
|
||||
|
||||
## Fullscreen mode + screen alignment
|
||||
- `WaylandCompositor.nativeSetScaleMode(fullscreenMode, screenAlignment)` takes the app's
|
||||
`Container.FULLSCREEN_OFF/FIT/STRETCH/FILL/INTEGER` (0..4) and `ALIGN_CENTER/TOP/BOTTOM` (0..2),
|
||||
any thread, any time (the drawer changes it live). `update_map()` in vk_present.c is a line-for-line
|
||||
mirror of `ViewTransformation.update()`: OFF and FIT both letterbox (OFF only differs in the app's
|
||||
fullscreen gates), TOP/BOTTOM confine the picture to the top/bottom half of the output (the
|
||||
handheld split), FILL overflow is clipped to the region. The app maps touch through the same class
|
||||
(TouchpadView → X server → input sink → scene input), the overlay arrow through
|
||||
`waylandSceneToView`, and the SurfaceView's own INPUT_SPACE touches through `vkp_output_to_scene`.
|
||||
- Log tag `screen`: `<mode>, <alignment>: WxH scene shown WxH at x,y on the WxH output` whenever the
|
||||
mapping changes (mode, alignment, scene or output size).
|
||||
|
||||
## Clipboard, text input, window icons (feat/wayland-clipboard-ime)
|
||||
Three globals winewayland used to complain about at startup, each in its own file behind the
|
||||
small interface in `src/banner_ext.h` (compositor.c only gained hook calls + accessors):
|
||||
- **Clipboard** (`src/wl_clipboard.c`): `wl_data_device_manager` **v3** and
|
||||
`zwlr_data_control_manager_v1` **v1**, selection only (start_drag is refused with `cancelled`).
|
||||
Wine prefers data-control: its desktop process then owns the clipboard without needing keyboard
|
||||
focus and the "clipboard functionality will be limited" ERR goes away too. One shared selection:
|
||||
a program's source, or text from Android (`nativeSetClipboardText`), or nothing. Data-control
|
||||
devices hear every change; `wl_data_device`s only while their client holds keyboard focus (hook in
|
||||
`keyboard_focus()`), and on focus arrival. Guest → Android: a selection with a text mime is read
|
||||
once over a pipe on the event loop (1 MiB cap, 3 s timeout) → `banner_on_clipboard_text` →
|
||||
`WaylandClipboardSync` → `ClipboardManager`. Android → guest: `OnPrimaryClipChangedListener` plus a
|
||||
re-read on resume / window focus (Android hides clipboard changes from background apps); `receive`
|
||||
is served by a non-blocking writer. Echo guard on both sides. Log tag `clipboard`.
|
||||
- **Text input** (`src/wl_text_input.c`): `zwp_text_input_manager_v3` **v1**, double-buffered state,
|
||||
`done(serial)` only when we sent events. winewayland enables text input for whichever surface gets
|
||||
`enter` and posts IME updates inside *that window's process*, so text input can't follow keyboard
|
||||
focus (the desktop surface): it follows the last clicked program window (`g_ime_click`, set in
|
||||
`pointer_input`), else the topmost non-shell window. `set_cursor_rectangle` arrives when an edit
|
||||
control calls `ImmSetCompositionWindow`; the app (`WaylandTextInput`) shows the soft keyboard only
|
||||
then (and with no hardware keyboard), hides it on disable unless the user's own toggle opened it.
|
||||
Typed text: `SurfaceInputView`'s `InputConnection` → `commit_string` / `preedit_string` + `done`.
|
||||
Deletions become Backspace/Delete key presses — winewayland's `delete_surrounding_text` handler is
|
||||
empty. Log tag `text-input`.
|
||||
- **Window icons** (`src/wl_toplevel_icon.c`): `xdg_toplevel_icon_manager_v1` **v1**, accepts and
|
||||
drops icons (`done` at bind, no sizes).
|
||||
- Host → compositor text crosses as UTF-8 `byte[]` through `banner_ext.c`'s queue (mutex + wake
|
||||
pipe), drained on the compositor thread. Glue for the three protocols is pre-generated with
|
||||
wayland-scanner 1.24.0 from `protocols/`.
|
||||
|
||||
## Compressed (UBWC) game buffers (feat/wayland-ubwc)
|
||||
- `zwp_linux_dmabuf_v1` used to advertise `LINEAR` (+`INVALID`) only, so Turnip's Wayland WSI in the
|
||||
game allocated linear swapchain images and DXVK/VKD3D resolved every frame from their tiled/UBWC
|
||||
render targets into that linear copy, before the compositor's own blit. Now `bind_dmabuf` builds
|
||||
its table at the first bind from the renderer's driver (`vkp_dmabuf_modifiers`):
|
||||
`vkGetPhysicalDeviceFormatProperties2` + `VkDrmFormatModifierPropertiesListEXT` per fourcc
|
||||
(AR24/XR24/AB24/XB24), each modifier confirmed with `vkGetPhysicalDeviceImageFormatProperties2`
|
||||
for a dma-buf-backed `TRANSFER_SRC` image. Only `DRM_FORMAT_MOD_LINEAR` (0) and
|
||||
`DRM_FORMAT_MOD_QCOM_COMPRESSED` (`0x0500000000000001`, UBWC, single memory plane on Adreno) are
|
||||
ever advertised; anything else the driver reports goes to logcat as "not advertised".
|
||||
- Mesa's WSI hands every advertised+supported modifier to `vkCreateImage` as a modifier *list* and
|
||||
Turnip picks `QCOM_COMPRESSED` whenever it is in the list (`tu_image.cc`), so with the
|
||||
advertisement the game's swapchain is UBWC. The `wl_buffer` then arrives with that modifier and
|
||||
the pixel-plane pitch/offset from the game's `vkGetImageSubresourceLayout(MEMORY_PLANE_0)`;
|
||||
`vkp_image_import_dmabuf` creates the compositor's image with the explicit modifier and that one
|
||||
plane layout (`VkImageDrmFormatModifierExplicitCreateInfoEXT`), which Turnip validates
|
||||
(`INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT` if the pitch does not fit its `fdl6` alignment). The
|
||||
swapchain blit (`vkp_render`) and the layer-mode blit (`vkp_blit_image`) read it like any other
|
||||
source; nothing CPU-side ever touches a dma-buf. Layer mode still blits into its own gralloc pool.
|
||||
- **A/B switch:** `BANNER_WAYLAND_UBWC=0` (or `false`/`off`) in the container's/shortcut's
|
||||
environment variables → `nativeSetUbwc(false)` → linear-only advertisement, exactly the old
|
||||
behaviour. Default on. The driver is still queried, so the log shows what it could have done.
|
||||
- Log tag `dmabuf`: `formats: AR24 linear+qcom_compressed, XR24 …, AB24 …, XB24 …` at the first
|
||||
bind (with ` (BANNER_WAYLAND_UBWC=0: qcom_compressed not advertised)` when off, or a second line
|
||||
`the compositor's driver (…) reports no importable qcom_compressed layout: game swapchains stay
|
||||
linear` when the adrenotools Turnip lacks it). Per window, the existing `vulkan` line now names
|
||||
the modifier: `… is presenting GPU frames through Wayland: 1920x1080, format XB24, qcom_compressed
|
||||
(zero-copy)` vs `… linear (zero-copy)`. Import failures are `error` lines naming the stage,
|
||||
modifier, size, pitch and VkResult (`dmabuf: vkCreateImage(qcom_compressed, …) -> …`).
|
||||
- Caveat: both sides must agree on the UBWC encoding for the GPU. The game runs the wcp's Wayland
|
||||
Turnip, the compositor the user's adrenotools Turnip; the layout code (`fdl6`) is the same, but a
|
||||
very different Mesa version on one side is the first suspect if a compressed frame imports fine
|
||||
yet looks scrambled — `=0` is the workaround, and the `gpu` line names both the GPU and the
|
||||
compositor's driver.
|
||||
|
||||
## Phones with no DRM node (OpenGL) — `BANNER_WAYLAND_NO_RENDER_NODE=1`
|
||||
- The feedback's `main_device` is the first of `/dev/dri/renderD128`, `renderD129`, `card0` the app
|
||||
can `stat`; retail phones (Adreno 830/840 reports, 2026-09-14) expose none, so it is `0:0`.
|
||||
Vulkan games do not care. Mesa's EGL did: its Wayland DRM initialiser failed the display in
|
||||
`dri2_setup_device()` without a render node and fell back to a software path that has no
|
||||
rasteriser in these layers — native OpenGL black, sound playing. Wayland layer versionCode 9
|
||||
(Banners-Turnip `patches/wayland/egl_wayland_no_drm_node.py`) runs zink + kopper on the Vulkan
|
||||
device without a node instead; nothing app-side is needed for that.
|
||||
- `BANNER_WAYLAND_NO_RENDER_NODE=1` (container or shortcut env) makes the compositor advertise
|
||||
`main device 0:0` on a device that does have a node, to reproduce those phones here.
|
||||
- Log lines: `dmabuf` `… no display (DRM) device …: OpenGL games need Wayland layer versionCode 9
|
||||
or newer …` when the main device is 0:0; `opengl` `<program> asked for GPU buffers but has drawn
|
||||
only software (shared-memory) frames …` once per program that asked for dma-buf feedback (EGL's
|
||||
GPU path always does) and then committed 150 wl_shm frames without making a dma-buf buffer.
|
||||
|
||||
## Zero-copy window layers (spike, `BANNER_WAYLAND_ZERO_COPY=1`)
|
||||
Research + host-side prototype in `ZERO_COPY_SPIKE.md`: why a dma-buf can't become an
|
||||
`AHardwareBuffer` (the game's buffers are DMA-heap allocations, not gralloc's), why the interop
|
||||
must start from a gralloc buffer whose native-handle fd the drivers import (Turnip's own AHB path),
|
||||
and the WSI patch this needs in our Wayland Turnip. `src/sc_layer.c` implements the receive side:
|
||||
with the variable in the container's environment, one fullscreen game window is shown on its own
|
||||
`ASurfaceControl` child of the SurfaceView (pool of three compositor-allocated AHBs, one blit,
|
||||
release fences from `setOnComplete`, geometry from the fullscreen mode); any other scene falls back
|
||||
to the swapchain blit. Log tag `layer`. Off by default.
|
||||
|
||||
## Zero-copy game frames (`BANNER_WAYLAND_ZERO_COPY=1`, drawer switch live)
|
||||
Option (a') of `ZERO_COPY_SPIKE.md`, both halves:
|
||||
- **Guest (our Wayland Turnip, banners-turnip-wayland `patches/wayland/banner_ahb_wsi.py`, all
|
||||
variants):** while the compositor's mode is on (see the live switch below), the Wayland
|
||||
WSI allocates every swapchain image as a gralloc `AHardwareBuffer` (usage GPU_SAMPLED_IMAGE |
|
||||
GPU_FRAMEBUFFER | COMPOSER_OVERLAY; UBWC when gralloc picks it and a test `vkCreateImage` with the
|
||||
explicit `QCOM_COMPRESSED` layout succeeds, else linear via a CPU usage bit; `BANNER_WSI_AHB_LINEAR=1`
|
||||
forces linear), imports the handle's dma-buf with an explicit modifier + pitch (the compositor's own
|
||||
pool-import path; `vk_android.c` is not in a platforms=wayland build), shares it through
|
||||
`zwp_linux_dmabuf_v1` as before (blit fallback), and sends the `AHardwareBuffer` once per image:
|
||||
`socketpair` + `AHardwareBuffer_sendHandleToUnixSocket` + `banner_ahb_v1.attach(wl_buffer, fd, w, h,
|
||||
stride, modifier hi/lo, image_count)`. Surface formats are limited to `R8G8B8A8_UNORM/SRGB` (and
|
||||
`A2B10G10R10`) while the mode is on: gralloc has no BGRA (DXVK/Zink blit into the swapchain image
|
||||
anyway). Marker: `strings libvulkan_freedreno_wayland*.so | grep banner_ahb_v1`.
|
||||
- **Compositor (`src/ahb_swapchain.c`, protocol `protocols/banner-ahb-v1.xml`):** the global is
|
||||
advertised on every session where a display layer is possible at all (`sc_layer_available()`);
|
||||
whether gralloc buffers are WANTED right now is the `mode` event, not the global's presence.
|
||||
`attach` → `AHardwareBuffer_recvHandleFromUnixSocket`, record on the `dmabuf_buffer`.
|
||||
When that buffer is the one fullscreen frame (`layer_candidate`, or the layer-only candidate when
|
||||
this renderer could not import it), `sc_layer_present_ahb` sets it on the `banner_wayland_game`
|
||||
SurfaceControl as is. **Acquire fence** = `DMA_BUF_IOCTL_EXPORT_SYNC_FILE(READ)` of the dma-buf: Mesa
|
||||
imports the render fence into the dma-buf (`wsi_signal_dma_buf_from_semaphore`) *before* the
|
||||
`wl_surface.commit`, so the export always carries the frame; if the ioctl fails the compositor
|
||||
polls the dma-buf instead (logged once). **Release**: `setOnComplete` →
|
||||
`getPreviousReleaseFenceFd` → `DMA_BUF_IOCTL_IMPORT_SYNC_FILE(READ)` into the dma-buf → then
|
||||
`wl_buffer.release` (still on the FPS limiter's cadence); Mesa's acquire
|
||||
(`wsi_create_sync_for_dma_buf_wait`) exports every fence of the dma-buf, so the game waits for the
|
||||
display. If the import fails the release waits on the fence fd in the event loop instead. A hidden
|
||||
or retired layer gets a 16x16 blank buffer so SurfaceFlinger actually lets go of the game's buffer.
|
||||
- **App:** `BANNER_WAYLAND_ZERO_COPY=1` (container/shortcut env) is the session's STARTING state and
|
||||
also exports `BANNER_WSI_AHB=1` into the guest (`XServerDisplayActivity.isWaylandZeroCopyRequested`),
|
||||
which is what an older driver (bound at `banner_ahb_v1` version 1) needs.
|
||||
|
||||
### The live switch ("Zero-copy presentation", Graphics tab)
|
||||
`banner_ahb_v1` version 2 adds one event, `mode(enabled)`, sent to every bound client on bind and
|
||||
again on every flip. The compositor owns the state:
|
||||
- `WaylandCompositor.nativeSetZeroCopy(on)` is live and callable from any thread. Before the
|
||||
compositor starts it just seeds `g_zero_copy`; afterwards it posts through the `banner_ext.c` host
|
||||
queue (like the clipboard/IME setters) so `ahb_swapchain_set_mode()` runs on the compositor thread,
|
||||
which flips `g_zero_copy`, broadcasts `mode`, flushes the clients and redraws.
|
||||
- **Guest:** the WSI follows `display->banner_ahb_mode`, decided per swapchain at creation. When a
|
||||
live swapchain was built for the other mode, the next `vkAcquireNextImageKHR` /
|
||||
`vkQueuePresentKHR` returns `VK_ERROR_OUT_OF_DATE_KHR` (it reuses Mesa's own `chain->retired`
|
||||
early-outs), which DXVK, vkd3d-proton and Zink all answer by rebuilding the swapchain. The mode
|
||||
event is read with a non-blocking dispatch of the display queue on each acquire/present, because
|
||||
in MAILBOX that queue is otherwise only dispatched when the acquire loop runs out of images.
|
||||
- **Compatibility, both directions.** The registry bind clamps to `MIN(advertised, 2)` — binding
|
||||
above the advertised version is a fatal `wl_display` error, and a Bannerlator from before the live
|
||||
switch advertises version 1. On version 1, and on version 2 before the first `mode` event arrives,
|
||||
the driver keeps the original contract exactly: gralloc images iff `BANNER_WSI_AHB=1`, decided per
|
||||
swapchain, and no swapchain is ever retired. Once the mode is in play `BANNER_WSI_AHB=1` no longer
|
||||
forces it on (the app exports that on every zero-copy launch, which would freeze the switch);
|
||||
`BANNER_WSI_AHB=0` still forces the whole feature off.
|
||||
- **No black frame, either way.** Switching OFF does not tear down anything: the old gralloc chain's
|
||||
frames keep being shown — on the layer when this renderer could not import them, through the copy
|
||||
path when it could — until the game has rebuilt, and only then does `sc_layer_hide()` run. Buffers
|
||||
already on the layer keep their deferred release (`ahb_swapchain_defer_release` is no longer gated
|
||||
on `g_zero_copy`, or the display would be handed a buffer it is still scanning out). Switching ON,
|
||||
the layer path resumes as soon as the first `attach` of the new chain lands.
|
||||
- **Drawer:** the toggle applies live *and* writes the env as the next launch's default. Its status
|
||||
line follows the compositor, not the switch: `nativeZeroCopyLastFrameAgeMs()` (updated on every
|
||||
zero-copy present, unlike the 10 s counter) is what makes it say "Switching on…" / "Switching off…"
|
||||
only for as long as it really is.
|
||||
- **Log lines (tag `layer`):** `zero-copy: banner_ahb_v1 version 2 advertised …`, `zero-copy: <exe>
|
||||
bound banner_ahb_v1 version N …`, `zero-copy: on|off at launch …`, `zero-copy switched on|off from
|
||||
the drawer: N bound programs told to rebuild their swapchains …`, `zero-copy: AHB swapchain from <exe> (N images, WxH, UBWC (QCOM_COMPRESSED)|linear,
|
||||
stride S px)`, `zero-copy: presenting "<title>" (<exe>) without a copy`; the 10 s `stats` line ends
|
||||
with `| N zero-copy frames`. Guest side (Mesa log, stderr of the game): `banner-ahb: WxH swapchain
|
||||
(N images) on gralloc buffers: UBWC|linear, stride S px` or the reason it stayed on standard
|
||||
buffers, plus `banner-ahb: compositor wants gralloc|standard swapchain images` and `banner-ahb:
|
||||
zero-copy switched on|off: retiring the WxH swapchain …` on a live flip.
|
||||
|
||||
## Multi-layer presentation (feat/wayland-multilayer, `src/sc_layer.c`)
|
||||
Layer mode hands the scene to SurfaceFlinger as a small, deliberately ordered **set** of Android
|
||||
display layers instead of one. The whole model, top to bottom:
|
||||
|
||||
```
|
||||
app window ....... Compose UI, the in-game drawer, the perf HUD, the on-screen controls, the
|
||||
pointer arrow - ordinary Android views in the activity's window, ALWAYS on top
|
||||
+-- SurfaceView .. the compositor's Vulkan swapchain (black while layer mode is up)
|
||||
+-- z=1 "banner_wayland_game" ...... the one fullscreen game window
|
||||
+-- z=2 "banner_wayland_overlay" ... at most ONE window drawn above the game
|
||||
```
|
||||
|
||||
- **Two layers is the hard cap** (`SC_LAYER_COUNT`). HWC composes only a few layers before
|
||||
SurfaceFlinger falls back to GPU client composition, which would throw the whole benefit away, so
|
||||
the compositor never asks for a third: a scene with two or more windows above the game goes back
|
||||
to the copy path. The count is said out loud the first time the overlay goes up (`layer`: `2
|
||||
display layers in use: "banner_wayland_game" (z=1) and "banner_wayland_overlay" (z=2) above it …`).
|
||||
- **Who owns what.** Each layer has its own `ASurfaceControl`, its own gralloc buffer pool (3
|
||||
buffers each), its own geometry and its own lifetime; `struct layer`
|
||||
in `sc_layer.c` holds all of it and the z-order is fixed by the id (nothing is re-ordered at
|
||||
runtime). The SurfaceControls are children of the SurfaceView's surface, created on first use and
|
||||
retired together when the output window changes or goes away.
|
||||
- **What can be on the game layer**, cheapest first: the game's own gralloc buffer (zero-copy, no
|
||||
copy anywhere — `ahb_swapchain.c`); one blit of the game's frame into a compositor buffer
|
||||
(`sc_layer_present`); or, with screen effects on, the compositor pass's **result** blitted into
|
||||
such a buffer (`sc_layer_present_pass` → `vkp_pass_begin` / `vkp_pass_copy_to`). The last one is
|
||||
what keeps a Look from dropping the session back to the app's swapchain: the scene → output
|
||||
mapping (Fullscreen Mode / Alignment, and the scaling a mode like FSR asks for) is then done by
|
||||
the display through `setGeometry` instead of by a second full-screen GPU blit.
|
||||
- **Present order matters, and it was measured.** The layers go up first; the base surface's black
|
||||
frame is presented *after* the layer transaction, and the effects chain runs in its own submit
|
||||
with **no swapchain image acquired**. A present holds an acquired image and the acquire semaphore
|
||||
is a vblank gate: folding the chain into that submit (one command buffer, one present — the
|
||||
shape that looks cheaper) put the whole 13-pass chain behind a vblank and measured **72 fps**
|
||||
against **111 fps** for the split shape, on HL2 + Retro CRT at 1920x1080 on the Pocket FIT (the
|
||||
copy path is 129 fps on the same scene). Anyone tempted to "optimise" this into one submit should
|
||||
read this paragraph first.
|
||||
- **What goes on the overlay layer**: exactly one draw above the game — a second Wayland toplevel
|
||||
(a launcher or settings window, a message box), copied 1:1 into the overlay pool and then
|
||||
*cropped and placed* by the display through the same `vkp_map_draw` mapping the copy path uses.
|
||||
The point is what does **not** happen: the game's frames are no longer copied through the
|
||||
compositor's swapchain just because something small sits on top of them.
|
||||
- **Transparency.** The compositor composes with blits, which overwrite — it never alpha-blends,
|
||||
on either path — so every layer is marked `ASC_TRANSPARENCY_OPAQUE` and the overlay layer is
|
||||
cropped to the window it carries. There is no blending to get wrong: no black box over the game,
|
||||
no double-darkening, and the picture is the copy path's, pixel for pixel.
|
||||
- **Input is untouched.** An `ASurfaceControl` has no input channel, so neither layer can take a
|
||||
touch: everything still reaches the app's `SurfaceView` (and the views above it) exactly as
|
||||
before, and the scene mapping touch goes through (`vkp_output_to_scene` / `ViewTransformation`)
|
||||
is the same one the layers are placed with, so the cursor still lands where it is pointing. The
|
||||
SurfaceView is not Z-on-top, so its whole subtree — both layers included — stays under the
|
||||
activity's own window: the drawer, the HUD and every Compose dialog draw above them.
|
||||
- **What cannot be layered, and why.**
|
||||
- *Frame generation*: each generated frame needs a present of its own on consecutive vblanks, and
|
||||
a display layer latches one buffer per refresh — pacing that is the swapchain's job. While an
|
||||
engine is armed the whole scene takes the copy path (unchanged).
|
||||
- *Effects with a window above the game*: the chain has to see the whole scene to look the way it
|
||||
does on the copy path (and on X11), and the game layer only carries the game. Copy path.
|
||||
- *Two or more draws above the game* (several windows, or a window with subsurfaces): a third
|
||||
layer is not worth the client-composition risk. Copy path.
|
||||
- **Lifecycle.** Hiding is top-down so nothing is uncovered for a frame; a hidden or retired layer
|
||||
gets the shared 16x16 blank buffer so SurfaceFlinger really lets go of the game's buffer (and
|
||||
reports a release fence for it) instead of holding it while invisible. `sc_layer_hide()` takes
|
||||
every layer down (scene no longer a fullscreen game, zero-copy switched off live, session end),
|
||||
`sc_layer_hide_overlay()` is used when only the window above the game closed, and
|
||||
`sc_layer_window_gone()` retires both SurfaceControls and drains both pools on a surface loss /
|
||||
HOME / resume. **The overlay layer is always RETIRED, never merely hidden** (both paths) — see
|
||||
the HWC note below: a live second SurfaceControl keeps SurfaceFlinger composing on the GPU even
|
||||
when it is invisible. The game layer keeps its SurfaceControl through a hide, because it goes up
|
||||
and down with every effects / frame-generation toggle.
|
||||
- **The display frame-rate vote belongs to one layer.** The refresh-rate stream's
|
||||
`sc_layer_set_frame_rate()` (the game's cadence, the same value the app votes on its own surface)
|
||||
is carried **only** by the layer the game presents on — the game layer. The overlay layer is
|
||||
explicitly voted `0`, so a window that redraws once a second can never hold the panel at the
|
||||
game's cadence, nor drag the game's cadence down to its own. Each layer remembers what its live
|
||||
SurfaceControl carries (`fps_applied`) and re-applies on the next transaction, and a retired
|
||||
SurfaceControl resets it so a re-created layer is re-voted from scratch. Log: `display frame-rate
|
||||
vote on banner_wayland_game: 60.00 Hz`.
|
||||
- **Stats.** The 10 s `stats` line now ends with `| N zero-copy frames` (the game's own buffers) and
|
||||
`| N layer frames` (frames the compositor put on a layer through one of its own buffers — the
|
||||
plain layer blit or the effects result). Both are hardware-composed; only the first is copy-free.
|
||||
- **Measured on the Pocket FIT (Adreno 750, portrait panel, landscape session → every layer is
|
||||
ROT_90 + scaled), `dumpsys android.hardware.graphics.composer3.IComposer/default`:** one layer is
|
||||
`composition: DEVICE/DEVICE` (game's own gralloc buffer scanned out by the DPU), with effects on
|
||||
the layer it stays `DEVICE/DEVICE`, but **two** layers flip the whole frame to `DEVICE/CLIENT` —
|
||||
SurfaceFlinger composes it on the GPU — **and it does not come back when the overlay goes away**:
|
||||
retiring the overlay's SurfaceControl (which the compositor now does rather than merely hiding
|
||||
it) leaves the frame in client composition; only re-creating the *game* layer's SurfaceControl
|
||||
clears it (HOME + resume does, reproduced twice). Retiring is still right — one fewer live layer,
|
||||
and it leaves the HWC list — it is just not the whole cure; the open follow-up is to retire and
|
||||
immediately re-create the game layer when the overlay goes, which would cost one black frame
|
||||
unless the new SurfaceControl is shown before the old one is dropped. The likely mechanism is the
|
||||
DPU's rotator budget (one rotated+scaled layer), not the layer count as such, so a device or
|
||||
orientation that needs no rotation may well take both on the DPU. Even in client composition the
|
||||
overlay layer is not a loss (SurfaceFlinger does the one blit the compositor would have done), but
|
||||
the hardware-composition win is only real for the single-layer cases. Note `VRI[ScreenDecorHwcOverlay]`
|
||||
is always `DISPLAY_DECORATION/CLIENT` (the system's rounded corners) — that is why SurfaceFlinger's
|
||||
`clientCompositionFrames` counter reads 100 % on this device in every state and is useless here.
|
||||
- **Not done yet:** a second toplevel that is itself rendering into gralloc buffers could go on the
|
||||
overlay layer zero-copy too (the token machinery in `ahb_swapchain.c` is already per-buffer, not
|
||||
per-layer); today the overlay always costs one small blit.
|
||||
|
||||
## Screen effects (feat/wayland-effects, `src/effects_chain.c`)
|
||||
The X11 renderer's screen-effect chain, run by the compositor between the composited scene and the
|
||||
output. Same SPIR-V as the X11 Vulkan renderer (`winlator/*_frag.h`, made by `winlator/gen_shaders.sh`
|
||||
and committed — the NDK build compiles no GLSL, so there is no build-time shader tooling), same
|
||||
push-constant layouts, uniform ranges and pass order, so a saved preset looks the same on both backends.
|
||||
|
||||
**Present-pass hook order (`vk_present.c`, `vkp_render`):**
|
||||
```
|
||||
scene (every draw blitted 1:1 into a scene-sized R8G8B8A8 image; compositor pass only)
|
||||
-> vkp_effects_run() effects_chain.c: scaling -> effects (below)
|
||||
-> vkp_framegen_run() framegen_bridge.c: frame generation on the chain's result (next section)
|
||||
-> update_map / blit Fullscreen Mode + Screen Alignment map the result (and each generated
|
||||
frame) onto its own swapchain image
|
||||
-> present generated frames first, the real frame last, FIFO
|
||||
```
|
||||
The compositor pass runs when either stage needs it (`vkp_effects_active() || vkp_framegen_active()`).
|
||||
Effects operate on the scene; the mapping operates on the output — the chain's result stands for
|
||||
the whole scene and goes through the same `draw_to_blit` as a plain frame. With everything off the
|
||||
frame takes the old path unchanged (draws blitted straight through the mapping); only the blit filter
|
||||
follows the scaling mode (Nearest = `VK_FILTER_NEAREST`).
|
||||
|
||||
**Pass order (locked to the X11 Vulkan renderer, `VulkanRendererContext::recordUpscalePasses`):**
|
||||
scaling → FXAA → Toon → Colour (brightness/contrast/gamma/saturation) → CAS → HDR → NTSC → CRT →
|
||||
Debanding (terminal dither). Scaling modes: 0 None / 1 Linear / 2 Nearest = a filtered resize;
|
||||
3 SGSR, 8 SGSR HQ, 7 NIS = one pass; 4 FSR / 5 FSR Fit = EASU → RCAS (fill vs fit is the Fullscreen
|
||||
Mode's business here, both run the same pair); 6 Sharpen = RCAS 1:1 (nearest scale + sharpen, as X11).
|
||||
Render scale is "Not used on Wayland": the scaling modes resize the scene to its **mapped output size**
|
||||
(3/4/5/7/8 only when that is larger than the scene, the X11 "render below display" gate; 6 always).
|
||||
Every later pass runs at that resolution, ping-ponging between two targets (+ one EASU mid target).
|
||||
|
||||
**Settings** (`WaylandCompositor.nativeSetUpscaler / nativeSetUpscaleSharpness / nativeSetCas /
|
||||
nativeSetHdr / nativeSetDeband / nativeSetScreenEffects / nativeSetLookName`, 1:1 with
|
||||
`VulkanRenderer`'s): any thread; the compositor thread snapshots them per frame, redraws even a
|
||||
static scene, and writes one `effects` line per change:
|
||||
`effects scaling=SGSR HQ 75%, CAS on 55%, Look="Game Clarity", colour b=+2 c=+12 g=1.00 s=108%`
|
||||
(`effects all off: scaling=Linear (plain blit)` when nothing is on). The chain's Vulkan objects are
|
||||
built on the first active frame (`effects chain ready: 13 passes … on <GPU>`; a driver that refuses
|
||||
them logs an `error` and effects stay off for the session). The app wires the drawer's Vulkan
|
||||
post-chain block (`XServerDialogState.on*Apply`) to these in `XServerDisplayActivity.initWaylandEffects`
|
||||
and remembers the same per-game keys as the Vulkan path (#382).
|
||||
|
||||
**Zero-copy interplay:** the chain's result no longer has to land in the app's swapchain — with a
|
||||
fullscreen game alone on screen it is copied into the game layer's own gralloc buffer and the game
|
||||
**stays on its display layer** while a Look is applied (see "Multi-layer presentation" below). It
|
||||
still takes the copy path when a window is drawn above the game (the chain has to see the whole
|
||||
scene to look as it does on X11) — `effects zero-copy paused: a window above the game needs the
|
||||
compositor pass for the whole scene` — and always while frame generation is armed
|
||||
(`framegen zero-copy paused: frame generation needs the compositor pass`); the layer comes back with
|
||||
`effects zero-copy resumed: the game is back on its own display layer`. A frame this renderer could
|
||||
not import (layer-only candidate) still goes on the layer, effects/frame generation skipped, said once.
|
||||
|
||||
## Frame generation (feat/wayland-framegen, `src/framegen_bridge.c`)
|
||||
The two **native** engines the X11 renderer hosts inside `libwinlator`'s compositor run inside the
|
||||
Wayland compositor's own Turnip device: **LSFG Native** (the user's Lossless.dll chain,
|
||||
`winlator/lsfg`) and **Win-FG Native** (our FSR3-derived optical-flow chain, `winlator/winfg`).
|
||||
The engine sources are compiled into `libbannerwayland.so` unmodified (CMake, app build only; the
|
||||
standalone build gets `framegen_engine_stub.c`) and driven by `src/framegen_engine.cpp`, which fills
|
||||
their `VkTable` by name from the compositor's `vkGetInstanceProcAddr`/`vkGetDeviceProcAddr`.
|
||||
**bionic-fg** (the guest-side win-fg Vulkan layer) is X11-only: its frames are born inside the guest
|
||||
and have nothing to attach to here, so on Wayland the "bionic" engine is always Win-FG Native.
|
||||
- **Where it runs** (the hook order above): frame generation takes the effects chain's RESULT
|
||||
(`vkp_effects_run`'s output, at its size - the scene, or the mapped output size under a scaling
|
||||
mode - moved to GENERAL), so generated frames carry the same look as real ones. The ONE hook
|
||||
`vkp_framegen_run(cmd, image, view, w, h, fmt, gens[])` records the engine's chain into the same
|
||||
command buffer and returns 0..3 generated images. Each image is then blitted through the scene ->
|
||||
output mapping into its own acquired swapchain image and presented: **generated frames first, the
|
||||
real frame last** (interpolation produces frames between N-1 and N; that one output interval of
|
||||
latency is inherent). One acquire/render-done semaphore pair and command buffer per present
|
||||
(`MAX_PRESENTS` = 4), the frame fence on the last submit. With the FIFO swapchain the presentation
|
||||
engine shows them on consecutive vblanks: that is the pacing, there are no sleeps and no host pacer.
|
||||
The swapchain is rebuilt with `multiplier - 1` extra images while armed so every present of a frame
|
||||
queues without a vblank wait. Frame generation off and effects off = the direct path, byte-for-byte
|
||||
as before.
|
||||
- **Device**: `dev_init` asks the bridge for the LSFG feature chain (`VkPhysicalDeviceVulkan12Features`
|
||||
vulkanMemoryModel + `shaderStorageImageWriteWithoutFormat/ExtendedFormats`, the same chain
|
||||
`VulkanRendererContext::createLogicalDevice` uses; instance apiVersion is 1.3 for the probe) and
|
||||
retries device creation without it if the driver refuses. Win-FG needs only a storage-capable ring
|
||||
format (RGBA8). The generation ring (`STORAGE|SAMPLED|TRANSFER_SRC|DST`, GENERAL) and the engines'
|
||||
own history live for the session; arming/disarming is a flag, a multiplier change resizes the ring.
|
||||
- **FPS limiter / vsync**: untouched. The limiter still paces the game's buffer returns (real frames);
|
||||
`applyNativeFgLocks` forces it on while multiplying, exactly as on X11 (`pacedLimitWithSlack` still
|
||||
applies through `nativeSetFpsLimit`). Generated frames never touch the limiter, the frame callbacks
|
||||
or the `wp_presentation` feedback - the guest sees one present per real frame.
|
||||
- **Zero-copy**: the layer bypasses the compositor pass, so while frame generation is armed the
|
||||
fullscreen window takes the copy path (`render_scene`: `framegen zero-copy paused: frame generation
|
||||
needs the compositor pass`) and the layer resumes when both it and the effects are off (see the
|
||||
effects section's zero-copy interplay).
|
||||
- **App side**: `WaylandCompositor.nativeSetFrameGenEngine/Armed/Tuning`, `nativeSetLsfgCachePath`,
|
||||
`nativeSetWinFgTuning`, `nativeFrameGenProblem/CapsReason/Stats` (same codes and six-float shape
|
||||
as `VulkanRenderer`). `XServerDisplayActivity.applyWaylandFrameGen` is the single apply:
|
||||
`applyLsfgNative`/`applyWinFgNative` route to it in Wayland mode (launch and drawer), the drawer's
|
||||
`onBionicFgConfigChange` has a Wayland branch, `nativeFgProblemReason`/`startLsfgStatsReadout` read
|
||||
the bridge. Every native setter is a value store, so the launch code may arm before the compositor
|
||||
thread exists.
|
||||
- **Log tag `framegen`**: `engines ready on the compositor's device: LSFG Native available (supported),
|
||||
Win-FG Native available`, `<engine> engine ready (...)`, `<engine> x2 armed (flow scale 0.80, panel
|
||||
120 Hz): ...`, `generating: <engine> x2 at 1920x1080 (1 interpolated frame per game frame)`, the
|
||||
fallbacks (`... can't start: no shader cache`, `... failed to start on the compositor's driver`,
|
||||
`... could not build its chain at WxH`, `the driver refused the LSFG feature set`), `frame generation
|
||||
off (N frames generated this session)`, and the zero-copy pause/resume above. The 10 s `stats` line
|
||||
counts generated frames in `frames on screen` and ends with `| N generated frames`; `GPU frames from
|
||||
games` never includes them.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Zero-copy window layers on Wayland — research spike + host-side prototype
|
||||
# Zero-copy window layers on Wayland - research spike + host-side prototype
|
||||
|
||||
Branch `spike/wayland-zero-copy-layers` (on `feat/wayland-phase2`). Written 2026-09-13.
|
||||
|
||||
@@ -14,22 +14,22 @@ adrenotools Turnip.
|
||||
**Answer in one paragraph.** A raw dma-buf can never become an `AHardwareBuffer` (option b is
|
||||
closed: no public API, and the game's buffers are `/dev/dma_heap/system` allocations, not
|
||||
gralloc buffers). The interop must go the other way: the buffer is a **gralloc `AHardwareBuffer`
|
||||
first**, and its dma-buf fd (`native_handle->data[0]`) is what the Vulkan drivers import — which is
|
||||
first**, and its dma-buf fd (`native_handle->data[0]`) is what the Vulkan drivers import - which is
|
||||
exactly how Turnip already imports every gralloc buffer on Android (`vk_android.c`). Both Turnips
|
||||
in play already have every extension needed; nothing is missing in the drivers. What is missing is
|
||||
a **WSI hook**: Mesa's Wayland WSI always allocates its own dma-bufs and has no way to receive or
|
||||
allocate compositor-visible buffers, so **option (a) or (c) each need a ~300-line patch to
|
||||
`wsi_common_wayland.c` in our own Wayland Turnip build** (banners-turnip-wayland
|
||||
`build_turnip_wayland_wsi.sh`, Mesa `7cda7850`) plus a small private Wayland protocol. Nothing in
|
||||
the app can remove the copy on its own. Recommended: **option (a′) — the game's WSI allocates its
|
||||
the app can remove the copy on its own. Recommended: **option (a′) - the game's WSI allocates its
|
||||
swapchain images as `AHardwareBuffer`s (gralloc-native, UBWC), imports them through the AHB path
|
||||
Turnip already has, and hands the compositor the AHB over a Unix socket (`AHardwareBuffer_send/
|
||||
recvHandleFromUnixSocket`, the same mechanism the X11 renderer already uses for `GPUImage`) plus
|
||||
the dma-buf fd through `zwp_linux_dmabuf_v1` as today (so the blit path stays as the fallback).**
|
||||
The host half — one `ASurfaceControl` layer per fullscreen window, geometry from the fullscreen
|
||||
The host half - one `ASurfaceControl` layer per fullscreen window, geometry from the fullscreen
|
||||
mode, release fences from `ASurfaceTransaction_setOnComplete`, black base surface, HUD/pointer as
|
||||
Android views above — is implemented in this branch as a gated prototype (`sc_layer.c`,
|
||||
`BANNER_WAYLAND_ZERO_COPY=1`) with a compositor-owned AHB pool and one blit, so the
|
||||
Android views above - is implemented in this branch as a gated prototype (`sc_layer.c`,
|
||||
`DROIDDECK_WAYLAND_ZERO_COPY=1`) with a compositor-owned AHB pool and one blit, so the
|
||||
SurfaceFlinger side can be measured on the device before the driver work starts.
|
||||
|
||||
---
|
||||
@@ -47,7 +47,7 @@ Facts the options rest on (Mesa `7cda7850`, the commit both Turnips are built fr
|
||||
- **Dma-buf import works on both Turnips**: `VK_KHR_external_memory_fd`,
|
||||
`VK_EXT_external_memory_dma_buf`, `VK_EXT_image_drm_format_modifier` are unconditional
|
||||
(`tu_device.cc:253, 354, 369`); the compositor proves it every frame. Only
|
||||
`VK_EXT_physical_device_drm` is off on KGSL (`:385`) — relevant to dmabuf-v4 feedback (§2).
|
||||
`VK_EXT_physical_device_drm` is off on KGSL (`:385`) - relevant to dmabuf-v4 feedback (§2).
|
||||
- **AHB import in Turnip is dma-buf import.** `VK_ANDROID_external_memory_android_hardware_buffer`
|
||||
is exposed whenever a u_gralloc backend exists (`tu_device.cc:219, 428`), and the fallback backend
|
||||
always exists. `vkGetAndroidHardwareBufferPropertiesANDROID` does
|
||||
@@ -59,7 +59,7 @@ Facts the options rest on (Mesa `7cda7850`, the commit both Turnips are built fr
|
||||
int) the UBWC flag `0x08000000` in the next int selects `DRM_FORMAT_MOD_QCOM_COMPRESSED`, else
|
||||
`DRM_FORMAT_MOD_LINEAR`; pitch = `pixel_stride × bpp`, offset 0. The qcom backend
|
||||
(`u_gralloc_qcom.c`, `hw_get_module`) cannot load the vendor gralloc from an app process, so the
|
||||
fallback is the one in use — the local KGSL build imports exactly `AHardwareBuffer_allocate /
|
||||
fallback is the one in use - the local KGSL build imports exactly `AHardwareBuffer_allocate /
|
||||
describe / getNativeHandle / acquire / release / isSupported` and `hw_get_module`, NEEDED
|
||||
`libnativewindow.so` + `libhardware.so` (readelf on `turnip608/stg/libvulkan_freedreno.so`).
|
||||
So **the Wine process already loads `libnativewindow.so`** (as a dependency of the ICD), and
|
||||
@@ -69,7 +69,7 @@ Facts the options rest on (Mesa `7cda7850`, the commit both Turnips are built fr
|
||||
|
||||
### (a) Game side: `AHardwareBuffer_allocate` in the guest, exported as a dma-buf
|
||||
|
||||
Feasible with the current drivers, **but only through a WSI patch** — the app cannot do it.
|
||||
Feasible with the current drivers, **but only through a WSI patch** - the app cannot do it.
|
||||
Mesa's Wayland WSI creates every native swapchain image with `wsi_create_native_image_mem`
|
||||
(`wsi_common_drm.c:760-850`): `vkAllocateMemory` with `VkExportMemoryAllocateInfo{DMA_BUF}` →
|
||||
`vkGetMemoryFdKHR` → `zwp_linux_buffer_params_v1.add(fd, …)` + `create_immed`
|
||||
@@ -80,7 +80,7 @@ The patch (in our Wayland Turnip build): a new `create_mem` for the Wayland chai
|
||||
compositor advertises a private global `banner_layer_buffer_v1`:
|
||||
1. `AHardwareBuffer_allocate(w, h, R8G8B8A8, GPU_COLOR_OUTPUT | GPU_SAMPLED_IMAGE)`;
|
||||
`vkGetAndroidHardwareBufferPropertiesANDROID` → `vkAllocateMemory` with
|
||||
`VkImportAndroidHardwareBufferInfoANDROID` (Turnip: `tu_device.cc:3828`), bind — the image was
|
||||
`VkImportAndroidHardwareBufferInfoANDROID` (Turnip: `tu_device.cc:3828`), bind - the image was
|
||||
created with the modifier/pitch gralloc reports, i.e. UBWC on Adreno gralloc (§2).
|
||||
2. `wsi_image.dma_buf_fd = dup(native_handle->data[0])`, `drm_modifier` / `row_pitches[0]` from
|
||||
the same layout → the existing `zwp_linux_dmabuf_v1` path stays untouched: the compositor keeps
|
||||
@@ -88,17 +88,17 @@ compositor advertises a private global `banner_layer_buffer_v1`:
|
||||
3. `banner_layer_buffer_v1.attach(wl_buffer, socket_fd)`: the WSI creates a `socketpair`, sends one
|
||||
end in the request, and calls `AHardwareBuffer_sendHandleToUnixSocket(ahb, other_end)`; the
|
||||
compositor's request handler does `AHardwareBuffer_recvHandleFromUnixSocket` and stores the AHB
|
||||
on `struct dmabuf_buffer`. Per swapchain image, once — not per frame.
|
||||
on `struct dmabuf_buffer`. Per swapchain image, once - not per frame.
|
||||
Extensions used by the game: `VK_ANDROID_external_memory_android_hardware_buffer` only (the
|
||||
dma-buf/modifier extensions are used internally by the same driver). Note winevulkan does not
|
||||
expose the AHB extension to PE code — irrelevant here, the WSI is unix-side inside the ICD.
|
||||
expose the AHB extension to PE code - irrelevant here, the WSI is unix-side inside the ICD.
|
||||
Effort: 2–3 days WSI + protocol + wcp rebuild (wine-compat-engineer for the packaging).
|
||||
|
||||
### (b) Import the game's existing dma-buf as an `AHardwareBuffer`
|
||||
|
||||
**No.** There is no public API: `AHardwareBuffer_createFromHandle` exists only in
|
||||
`vndk/hardware_buffer.h` (exported by `libnativewindow.so`, so `dlsym` finds it), but it takes a
|
||||
gralloc `native_handle_t` that the vendor mapper (`IMapper::importBuffer`) validates — on QTI a
|
||||
gralloc `native_handle_t` that the vendor mapper (`IMapper::importBuffer`) validates - on QTI a
|
||||
`private_handle_t` with the `'gmsm'` magic, a metadata dma-buf, and vendor-specific ints. The game's
|
||||
buffers are bare DMA-heap fds. Fabricating a vendor handle around a foreign fd is unversioned
|
||||
private ABI (differs across gralloc4 releases/OEMs) and is exactly the kind of thing that
|
||||
@@ -115,8 +115,8 @@ before the swapchain image is created, plus reallocation on `vkCreateSwapchainKH
|
||||
plus fence plumbing in both directions. `zwp_linux_dmabuf_v1` v4 feedback cannot express this (it
|
||||
only steers format/modifier/device; the client still allocates), and on KGSL v4 is a trap anyway:
|
||||
the driver has no DRM `dev_t`, so `same_gpu` stays false (`wsi_common_wayland.c:1660-1686`) and
|
||||
Mesa takes the prime-blit path (one extra copy per frame). The one advantage over (a) — the
|
||||
compositor decides linear vs UBWC — is moot because gralloc's decision is the right one for the DPU.
|
||||
Mesa takes the prime-blit path (one extra copy per frame). The one advantage over (a) - the
|
||||
compositor decides linear vs UBWC - is moot because gralloc's decision is the right one for the DPU.
|
||||
Effort: 3–4 days; strictly more moving parts than (a). Not recommended.
|
||||
|
||||
**Recommendation: (a).** It reuses two proven mechanisms (Turnip's AHB import, the X11 path's
|
||||
@@ -135,7 +135,7 @@ to our own Mesa build plus one small protocol.
|
||||
- The compositor advertises `LINEAR` and `INVALID` only (`compositor.c: bind_dmabuf`,
|
||||
`uint64_t mods[] = {MOD_LINEAR, MOD_INVALID}`); `INVALID` is dropped by the WSI
|
||||
(`wsi_common_wayland.c:449-451`), so every game swapchain is linear today. DXVK/VKD3D render to
|
||||
their own tiled/UBWC targets and copy into the linear swapchain image at present — a full-frame
|
||||
their own tiled/UBWC targets and copy into the linear swapchain image at present - a full-frame
|
||||
linear write per frame on the game side, on top of the compositor's blit. This is a real part of
|
||||
the 20 %.
|
||||
- Advertising `QCOM_COMPRESSED` as well: one line in `bind_dmabuf`. The WSI intersects the
|
||||
@@ -149,23 +149,23 @@ to our own Mesa build plus one small protocol.
|
||||
`GPU_COLOR_OUTPUT` RGBA8 unless a CPU usage bit is set (the prototype exploits exactly that:
|
||||
`sc_layer.c: alloc_slot` retries with `CPU_READ_RARELY` when a UBWC import fails). The DPU
|
||||
scans out UBWC RGBA natively; a prior device experiment (GL native rendering, 2026-06-29)
|
||||
found the SDE pipes accepted both UBWC and linear layers — rotation+scale was the blocker, not
|
||||
found the SDE pipes accepted both UBWC and linear layers - rotation+scale was the blocker, not
|
||||
UBWC.
|
||||
|
||||
## 3. Per-layer composition (design; the prototype implements the single-layer subset)
|
||||
|
||||
- **wl_surface → SurfaceControl.** One `ASurfaceControl_createFromWindow(surfaceView_window,
|
||||
name)` child per mapped toplevel (and per subsurface tree flattened per toplevel); the desktop
|
||||
surface is the base layer at z 0 (or, with `g_hide_shell`, a black base — the SurfaceView's own
|
||||
surface is the base layer at z 0 (or, with `g_hide_shell`, a black base - the SurfaceView's own
|
||||
buffer); toplevels get `setZOrder` from the `g_toplevels` order (`apply_zorder`), with
|
||||
`INT_MAX` reserved for the app's cursor/HUD if they ever move native. Position/scale:
|
||||
`ASurfaceTransaction_setGeometry(src = buffer rect, dst = output rect, transform 0)`, with the
|
||||
output rect produced by the same `update_map()` / `draw_to_blit()` arithmetic the blit path uses
|
||||
(`vk_present.c: vkp_map_draw`) — letterbox/FILL crop/TOP-BOTTOM half all come out identical, so
|
||||
(`vk_present.c: vkp_map_draw`) - letterbox/FILL crop/TOP-BOTTOM half all come out identical, so
|
||||
**input stays consistent by construction**: the app maps touch through `ViewTransformation`
|
||||
with the same inputs (see `WAYLAND_RUNTIME.md` "Fullscreen mode"), and the compositor's own
|
||||
`vkp_output_to_scene` keeps working because the map is still updated every frame.
|
||||
`setBufferTransparency(OPAQUE)` for XR24/XB24; `setBufferTransform` stays 0 — the DPU refuses
|
||||
`setBufferTransparency(OPAQUE)` for XR24/XB24; `setBufferTransform` stays 0 - the DPU refuses
|
||||
rotation+scale on one pipe (prior experiment), so a rotated panel must be handled by the
|
||||
activity's orientation, not the layer.
|
||||
- **HUD.** Already a separate layer: `PerfHudView`/`FrameRating`/cursor are Android views in
|
||||
@@ -177,7 +177,7 @@ to our own Mesa build plus one small protocol.
|
||||
fires `wl_callback.done` for those surfaces (as today, `fire_all_frames`). `wp_presentation`
|
||||
feedback moves from "after our present" to the transaction's `ASurfaceTransaction_setOnComplete`
|
||||
→ `ASurfaceTransactionStats_getLatchTime` / `getPresentFenceFd` (API 29) give the real on-glass
|
||||
time — better than today's `now_ns()`; `setFrameTimeline` (API 33) can be added later for
|
||||
time - better than today's `now_ns()`; `setFrameTimeline` (API 33) can be added later for
|
||||
`VK_GOOGLE_display_timing` work. Without a window, `pace_without_output` stays as is.
|
||||
- **`wl_buffer.release` timing.** Today a replaced buffer is released on the limiter's cadence,
|
||||
which is safe because the blit was waited for. With layers the buffer is on the display until
|
||||
@@ -185,14 +185,14 @@ to our own Mesa build plus one small protocol.
|
||||
transaction that **replaced** it delivers `ASurfaceTransactionStats_getPreviousReleaseFenceFd`
|
||||
(this is what `ASurfaceRendererContext::transactionCompleteCallback` does), wait that fence on
|
||||
the compositor thread (or, better, hand it to the game: with option (a) the WSI can import it as
|
||||
the `WSI_ES_RELEASE` point — Turnip/KGSL implements `vk_sync.import_sync_file`,
|
||||
the `WSI_ES_RELEASE` point - Turnip/KGSL implements `vk_sync.import_sync_file`,
|
||||
`tu_knl_kgsl.cc:1261-1303`), then `wl_buffer_send_release`. The FPS limiter keeps working: it only
|
||||
delays the release further. Android 16's `setBufferWithRelease` is not available on 14.
|
||||
- **Acquire fence.** Two sources: (1) if the kernel supports `DMA_BUF_IOCTL_EXPORT_SYNC_FILE`
|
||||
(6.x GKI), the WSI already attaches its present semaphore to the dma-buf as a sync_file
|
||||
(`wsi_drm_init_swapchain_implicit_sync`, `wsi_common_drm.c:368-395`, gated on the driver
|
||||
exporting `SYNC_FD` semaphores — KGSL does, `:1278-1303`), and the compositor can
|
||||
`EXPORT_SYNC_FILE(READ)` it and pass the fd to `setBuffer` — the prototype probes this on the
|
||||
exporting `SYNC_FD` semaphores - KGSL does, `:1278-1303`), and the compositor can
|
||||
`EXPORT_SYNC_FILE(READ)` it and pass the fd to `setBuffer` - the prototype probes this on the
|
||||
first frame and logs the answer; (2) otherwise the option-(a) protocol carries a sync_fd per
|
||||
present (`vkGetSemaphoreFdKHR`), which is what the X11 wrapper does. Never `-1` for a real
|
||||
zero-copy frame.
|
||||
@@ -210,11 +210,11 @@ to our own Mesa build plus one small protocol.
|
||||
src == dst was DEVICE on every layer, GPU_TARGET empty. So: no SurfaceControl rotation ever; on
|
||||
a landscape-native panel (handheld) the identity-transform game layer with a DPU scale should
|
||||
promote; on a portrait phone in landscape it will not, and the base swapchain path is the better
|
||||
one there — decide per device by measurement, not by assumption. Measure with
|
||||
one there - decide per device by measurement, not by assumption. Measure with
|
||||
`dumpsys SurfaceFlinger` (layer `composition: DEVICE/DEVICE` vs `DEVICE/CLIENT`, and the SDE pipe
|
||||
table `GPU_TARGET` line) with the app foreground and the drawer closed.
|
||||
- **Layer count.** The DPU has a handful of pipes (4–8 usable RGB pipes on SDM/SM8xxx); 2–3 game
|
||||
layers + base + HUD + cursor is fine, a desktop with 10 windows is not — SurfaceFlinger then
|
||||
layers + base + HUD + cursor is fine, a desktop with 10 windows is not - SurfaceFlinger then
|
||||
composites the overflow on the GPU (still correct, just no gain). Policy: layers only for
|
||||
fullscreen/topmost windows, everything else stays in the base swapchain (the prototype's rule).
|
||||
- **Buffer ownership across processes.** The game's `AHardwareBuffer` is alive in the guest; the
|
||||
@@ -226,7 +226,7 @@ to our own Mesa build plus one small protocol.
|
||||
real thing (the prototype waits on the CPU instead, which is safe).
|
||||
- **Background / rotation / device lost.** Covered by the window-request serialisation; a device
|
||||
loss in the compositor's Turnip does not affect layers already on screen (SurfaceFlinger owns
|
||||
them) — the old path's `device_lost` message stays.
|
||||
them) - the old path's `device_lost` message stays.
|
||||
- **Non-Adreno / old SoCs.** Everything here is NDK API 29 + gralloc-allocated buffers; the only
|
||||
vendor-specific piece is the `'gmsm'` handle sniff, which degrades to "linear with a CPU bit".
|
||||
Same risk class as the X11 ASR renderer; keep it opt-in until proven on a second SoC.
|
||||
@@ -240,21 +240,21 @@ Prerequisite: a build of this branch, container on Wayland, the AIO Graphics Tes
|
||||
D3D12 tabs) as the workload, session log in `Download/Wayland-logs/`.
|
||||
|
||||
0. **UBWC modifier A/B (no layers).** Done on `feat/wayland-ubwc` (advertised when the compositor's
|
||||
driver imports it, `BANNER_WAYLAND_UBWC=0` = old linear-only list; see `WAYLAND_RUNTIME.md`
|
||||
driver imports it, `DROIDDECK_WAYLAND_UBWC=0` = old linear-only list; see `WAYLAND_RUNTIME.md`
|
||||
"Compressed (UBWC) game buffers"). Run the AIO Vulkan/D3D12 with and without. Expect
|
||||
`vulkan: … is presenting GPU frames through Wayland: WxH, format XB24, tiled (zero-copy)` and
|
||||
no `dmabuf import failed`. Records how much of the 20 % is the linear swapchain alone.
|
||||
1. **Layer mode smoke.** Put `BANNER_WAYLAND_ZERO_COPY=1` in the container's environment
|
||||
1. **Layer mode smoke.** Put `DROIDDECK_WAYLAND_ZERO_COPY=1` in the container's environment
|
||||
variables, launch the AIO fullscreen. Expected log lines (tag `layer`):
|
||||
`SurfaceControl "banner_wayland_game" created as a child of the screen surface`,
|
||||
`SurfaceControl "droiddeck_wayland_game" created as a child of the screen surface`,
|
||||
`pool buffer 1920x1080 UBWC (QCOM_COMPRESSED), stride 1920 px (gralloc handle 2 fds / N ints)`
|
||||
(or `linear` after `import of a UBWC … failed` — also a result: it tells whether the compositor
|
||||
(or `linear` after `import of a UBWC … failed` - also a result: it tells whether the compositor
|
||||
Turnip accepts gralloc's UBWC pitch), `geometry: buffer 0,0-1920,1080 -> screen …`,
|
||||
`presenting 1920x1080 game frames on their own SurfaceControl layer (UBWC pool, 3 buffers)`,
|
||||
and the probe line `kernel exports sync_file fences from the game's dma-buf …` or
|
||||
`DMA_BUF_IOCTL_EXPORT_SYNC_FILE … failed (…)`. Picture correct, HUD and pointer on top, fullscreen
|
||||
mode changes move the layer (`geometry:` lines).
|
||||
2. **Composition type.** With the AIO running: `dumpsys SurfaceFlinger | grep -A3 banner_wayland_game`
|
||||
2. **Composition type.** With the AIO running: `dumpsys SurfaceFlinger | grep -A3 droiddeck_wayland_game`
|
||||
and the `GPU_TARGET` line of the pipe table. DEVICE/DEVICE on the game layer with an empty GPU
|
||||
target = the DPU does the scaling; CLIENT = no gain on this panel orientation → try the
|
||||
handheld's native orientation / a resolution equal to the panel's.
|
||||
@@ -271,7 +271,7 @@ harden: async fence instead of CPU wait, per-window layers, presentation feedbac
|
||||
`getLatchTime`); option (a) WSI + protocol + wcp = 2–3 days + 1 day device proving; option (c) =
|
||||
3–4 days + the same proving; (b) = not buildable.
|
||||
|
||||
## 6. What the prototype in this branch does (`BANNER_WAYLAND_ZERO_COPY=1`)
|
||||
## 6. What the prototype in this branch does (`DROIDDECK_WAYLAND_ZERO_COPY=1`)
|
||||
|
||||
Files: `src/sc_layer.{c,h}` (new), `src/vk_present.{c,h}` (`vkp_image_import_dmabuf` with a
|
||||
blit-destination role, `vkp_blit_image`, `vkp_update_map`, `vkp_map_draw`, `vkp_window`,
|
||||
@@ -293,7 +293,7 @@ hides the layer and draws the old way; the layer is retired when the SurfaceView
|
||||
Everything else (frame callbacks, presentation feedback, FPS limiter, HUD counting, input) is
|
||||
unchanged. Off by default: with the variable unset no new code runs.
|
||||
|
||||
Not zero-copy yet — it is the receive side. The copy disappears when step 5 lands and
|
||||
Not zero-copy yet - it is the receive side. The copy disappears when step 5 lands and
|
||||
`sc_layer_present` is handed the game's own `AHardwareBuffer` instead of a pool slot.
|
||||
|
||||
## 7. Step 5 implemented (feat/wayland-zero-copy-wsi + banners-turnip-wayland `banner_ahb_wsi.py`)
|
||||
@@ -302,8 +302,8 @@ Option (a′) as recommended, with one deviation forced by the build: the Waylan
|
||||
`platforms=wayland` build with Android detection off, so `VK_ANDROID_external_memory_android_hardware_buffer`
|
||||
/ `vk_android.c` do not exist in it. The WSI therefore allocates the `AHardwareBuffer` itself
|
||||
(`libnativewindow.so` dlopen'd), sniffs the gralloc handle for UBWC exactly like `u_gralloc_fallback.c`,
|
||||
and imports `native_handle->data[0]` as a dma-buf with an explicit modifier + pitch — the same path
|
||||
this compositor's pool uses (`vkp_image_import_dmabuf`) — after a test `vkCreateImage` proves the
|
||||
and imports `native_handle->data[0]` as a dma-buf with an explicit modifier + pitch - the same path
|
||||
this compositor's pool uses (`vkp_image_import_dmabuf`) - after a test `vkCreateImage` proves the
|
||||
driver accepts that layout (linear retry otherwise). Fences: implicit via the dma-buf in both
|
||||
directions (§3's option 1, proven by the probe; the compositor imports SurfaceFlinger's release
|
||||
fence back into the dma-buf before `wl_buffer.release`). Details in `WAYLAND_RUNTIME.md`.
|
||||
@@ -0,0 +1,61 @@
|
||||
/* Generated by wayland-scanner 1.26.0 */
|
||||
|
||||
/*
|
||||
* Copyright © 2022 Simon Ser
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice (including the next
|
||||
* paragraph) shall be included in all copies or substantial portions of the
|
||||
* Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include "wayland-util.h"
|
||||
|
||||
#ifndef __has_attribute
|
||||
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
|
||||
#endif
|
||||
|
||||
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
|
||||
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
|
||||
#else
|
||||
#define WL_PRIVATE
|
||||
#endif
|
||||
|
||||
extern const struct wl_interface wl_buffer_interface;
|
||||
|
||||
static const struct wl_interface *single_pixel_buffer_v1_types[] = {
|
||||
&wl_buffer_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message wp_single_pixel_buffer_manager_v1_requests[] = {
|
||||
{ "destroy", "", single_pixel_buffer_v1_types + 0 },
|
||||
{ "create_u32_rgba_buffer", "nuuuu", single_pixel_buffer_v1_types + 0 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface wp_single_pixel_buffer_manager_v1_interface = {
|
||||
"wp_single_pixel_buffer_manager_v1", 1,
|
||||
2, wp_single_pixel_buffer_manager_v1_requests,
|
||||
0, NULL,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/* Generated by wayland-scanner 1.26.0 */
|
||||
|
||||
#ifndef SINGLE_PIXEL_BUFFER_V1_SERVER_PROTOCOL_H
|
||||
#define SINGLE_PIXEL_BUFFER_V1_SERVER_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "wayland-server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct wl_client;
|
||||
struct wl_resource;
|
||||
|
||||
/**
|
||||
* @page page_single_pixel_buffer_v1 The single_pixel_buffer_v1 protocol
|
||||
* single pixel buffer factory
|
||||
*
|
||||
* @section page_desc_single_pixel_buffer_v1 Description
|
||||
*
|
||||
* This protocol extension allows clients to create single-pixel buffers.
|
||||
*
|
||||
* Compositors supporting this protocol extension should also support the
|
||||
* viewporter protocol extension. Clients may use viewporter to scale a
|
||||
* single-pixel buffer to a desired size.
|
||||
*
|
||||
* Warning! The protocol described in this file is currently in the testing
|
||||
* phase. Backward compatible changes may be added together with the
|
||||
* corresponding interface version bump. Backward incompatible changes can
|
||||
* only be done by creating a new major version of the extension.
|
||||
*
|
||||
* @section page_ifaces_single_pixel_buffer_v1 Interfaces
|
||||
* - @subpage page_iface_wp_single_pixel_buffer_manager_v1 - global factory for single-pixel buffers
|
||||
* @section page_copyright_single_pixel_buffer_v1 Copyright
|
||||
* <pre>
|
||||
*
|
||||
* Copyright © 2022 Simon Ser
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice (including the next
|
||||
* paragraph) shall be included in all copies or substantial portions of the
|
||||
* Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
* </pre>
|
||||
*/
|
||||
struct wl_buffer;
|
||||
struct wp_single_pixel_buffer_manager_v1;
|
||||
|
||||
#ifndef WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_INTERFACE
|
||||
#define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_INTERFACE
|
||||
/**
|
||||
* @page page_iface_wp_single_pixel_buffer_manager_v1 wp_single_pixel_buffer_manager_v1
|
||||
* @section page_iface_wp_single_pixel_buffer_manager_v1_desc Description
|
||||
*
|
||||
* The wp_single_pixel_buffer_manager_v1 interface is a factory for
|
||||
* single-pixel buffers.
|
||||
* @section page_iface_wp_single_pixel_buffer_manager_v1_api API
|
||||
* See @ref iface_wp_single_pixel_buffer_manager_v1.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_wp_single_pixel_buffer_manager_v1 The wp_single_pixel_buffer_manager_v1 interface
|
||||
*
|
||||
* The wp_single_pixel_buffer_manager_v1 interface is a factory for
|
||||
* single-pixel buffers.
|
||||
*/
|
||||
extern const struct wl_interface wp_single_pixel_buffer_manager_v1_interface;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @ingroup iface_wp_single_pixel_buffer_manager_v1
|
||||
* @struct wp_single_pixel_buffer_manager_v1_interface
|
||||
*/
|
||||
struct wp_single_pixel_buffer_manager_v1_interface {
|
||||
/**
|
||||
* destroy the manager
|
||||
*
|
||||
* Destroy the wp_single_pixel_buffer_manager_v1 object.
|
||||
*
|
||||
* The child objects created via this interface are unaffected.
|
||||
*/
|
||||
void (*destroy)(struct wl_client *client,
|
||||
struct wl_resource *resource);
|
||||
/**
|
||||
* create a 1×1 buffer from 32-bit RGBA values
|
||||
*
|
||||
* Create a single-pixel buffer from four 32-bit RGBA values.
|
||||
*
|
||||
* Unless specified in another protocol extension, the RGBA values
|
||||
* use pre-multiplied alpha.
|
||||
*
|
||||
* The width and height of the buffer are 1.
|
||||
*
|
||||
* The r, g, b and a arguments valid range is from UINT32_MIN (0)
|
||||
* to UINT32_MAX (0xffffffff).
|
||||
*
|
||||
* These arguments should be interpreted as a percentage, i.e. -
|
||||
* UINT32_MIN = 0% of the given color component - UINT32_MAX = 100%
|
||||
* of the given color component
|
||||
* @param r value of the buffer's red channel
|
||||
* @param g value of the buffer's green channel
|
||||
* @param b value of the buffer's blue channel
|
||||
* @param a value of the buffer's alpha channel
|
||||
*/
|
||||
void (*create_u32_rgba_buffer)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
uint32_t r,
|
||||
uint32_t g,
|
||||
uint32_t b,
|
||||
uint32_t a);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_wp_single_pixel_buffer_manager_v1
|
||||
*/
|
||||
#define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_DESTROY_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_wp_single_pixel_buffer_manager_v1
|
||||
*/
|
||||
#define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_CREATE_U32_RGBA_BUFFER_SINCE_VERSION 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="single_pixel_buffer_v1">
|
||||
<copyright>
|
||||
Copyright © 2022 Simon Ser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="single pixel buffer factory">
|
||||
This protocol extension allows clients to create single-pixel buffers.
|
||||
|
||||
Compositors supporting this protocol extension should also support the
|
||||
viewporter protocol extension. Clients may use viewporter to scale a
|
||||
single-pixel buffer to a desired size.
|
||||
|
||||
Warning! The protocol described in this file is currently in the testing
|
||||
phase. Backward compatible changes may be added together with the
|
||||
corresponding interface version bump. Backward incompatible changes can
|
||||
only be done by creating a new major version of the extension.
|
||||
</description>
|
||||
|
||||
<interface name="wp_single_pixel_buffer_manager_v1" version="1">
|
||||
<description summary="global factory for single-pixel buffers">
|
||||
The wp_single_pixel_buffer_manager_v1 interface is a factory for
|
||||
single-pixel buffers.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the manager">
|
||||
Destroy the wp_single_pixel_buffer_manager_v1 object.
|
||||
|
||||
The child objects created via this interface are unaffected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="create_u32_rgba_buffer">
|
||||
<description summary="create a 1×1 buffer from 32-bit RGBA values">
|
||||
Create a single-pixel buffer from four 32-bit RGBA values.
|
||||
|
||||
Unless specified in another protocol extension, the RGBA values use
|
||||
pre-multiplied alpha.
|
||||
|
||||
The width and height of the buffer are 1.
|
||||
|
||||
The r, g, b and a arguments valid range is from UINT32_MIN (0)
|
||||
to UINT32_MAX (0xffffffff).
|
||||
|
||||
These arguments should be interpreted as a percentage, i.e.
|
||||
- UINT32_MIN = 0% of the given color component
|
||||
- UINT32_MAX = 100% of the given color component
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="wl_buffer"/>
|
||||
<arg name="r" type="uint" summary="value of the buffer's red channel"/>
|
||||
<arg name="g" type="uint" summary="value of the buffer's green channel"/>
|
||||
<arg name="b" type="uint" summary="value of the buffer's blue channel"/>
|
||||
<arg name="a" type="uint" summary="value of the buffer's alpha channel"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -2,8 +2,8 @@
|
||||
* See ahb_swapchain.h and ZERO_COPY_SPIKE.md. */
|
||||
#define _GNU_SOURCE
|
||||
#include "ahb_swapchain.h"
|
||||
#include "banner_color.h"
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "sc_layer.h"
|
||||
#include "vk_present.h"
|
||||
#include "banner-ahb-v1-server-protocol.h"
|
||||
@@ -22,10 +22,10 @@
|
||||
#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL /* DRM_FORMAT_MOD_QCOM_COMPRESSED (UBWC) */
|
||||
|
||||
/* linux/dma-buf.h (not in the NDK sysroot's older headers): sync_file export / import. */
|
||||
struct banner_dma_buf_sync_file { uint32_t flags; int32_t fd; };
|
||||
#define BANNER_DMA_BUF_SYNC_READ 1u
|
||||
#define BANNER_DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR('b', 2, struct banner_dma_buf_sync_file)
|
||||
#define BANNER_DMA_BUF_IOCTL_IMPORT_SYNC_FILE _IOW('b', 3, struct banner_dma_buf_sync_file)
|
||||
struct droiddeck_dma_buf_sync_file { uint32_t flags; int32_t fd; };
|
||||
#define DROIDDECK_DMA_BUF_SYNC_READ 1u
|
||||
#define DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR('b', 2, struct droiddeck_dma_buf_sync_file)
|
||||
#define DROIDDECK_DMA_BUF_IOCTL_IMPORT_SYNC_FILE _IOW('b', 3, struct droiddeck_dma_buf_sync_file)
|
||||
|
||||
extern volatile int g_zero_copy;
|
||||
|
||||
@@ -90,9 +90,9 @@ static void buf_free(struct ahb_buf *ab) {
|
||||
if (ab->fence_src) { wl_event_source_remove(ab->fence_src); ab->fence_src = NULL; }
|
||||
if (ab->fence_fd >= 0) { close(ab->fence_fd); ab->fence_fd = -1; }
|
||||
wl_list_remove(&ab->link);
|
||||
*banner_dmabuf_ahb_slot(ab->b) = NULL;
|
||||
*droiddeck_dmabuf_ahb_slot(ab->b) = NULL;
|
||||
AHardwareBuffer_release(ab->ahb);
|
||||
banner_dmabuf_unref(ab->b);
|
||||
droiddeck_dmabuf_unref(ab->b);
|
||||
free(ab);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) {
|
||||
|
||||
static void send_deferred_release(struct ahb_buf *ab) {
|
||||
if (ab->release_pending && ab->resource)
|
||||
banner_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
|
||||
droiddeck_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
|
||||
ab->release_pending = 0;
|
||||
ab->surface = NULL;
|
||||
}
|
||||
@@ -138,14 +138,14 @@ static void handle_released(uint64_t id, int fd) {
|
||||
if (!ab) { if (fd >= 0) close(fd); return; }
|
||||
ab->on_layer = 0;
|
||||
if (fd >= 0) {
|
||||
struct banner_dma_buf_sync_file imp = {.flags = BANNER_DMA_BUF_SYNC_READ, .fd = fd};
|
||||
int dmabuf_fd = banner_dmabuf_fd(ab->b);
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, BANNER_DMA_BUF_IOCTL_IMPORT_SYNC_FILE, &imp) == 0) {
|
||||
struct droiddeck_dma_buf_sync_file imp = {.flags = DROIDDECK_DMA_BUF_SYNC_READ, .fd = fd};
|
||||
int dmabuf_fd = droiddeck_dmabuf_fd(ab->b);
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, DROIDDECK_DMA_BUF_IOCTL_IMPORT_SYNC_FILE, &imp) == 0) {
|
||||
close(fd);
|
||||
} else {
|
||||
if (!g_import_failed_logged) {
|
||||
g_import_failed_logged = 1;
|
||||
banner_log("layer", "zero-copy: DMA_BUF_IOCTL_IMPORT_SYNC_FILE failed (%s): releases wait for the display's fence here instead",
|
||||
droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_IMPORT_SYNC_FILE failed (%s): releases wait for the display's fence here instead",
|
||||
strerror(errno));
|
||||
}
|
||||
/* Wait it out in the event loop (a sync_file is readable once signalled). */
|
||||
@@ -196,23 +196,23 @@ void ahb_swapchain_layer_released(void *token, int release_fd) {
|
||||
/* ---- present */
|
||||
|
||||
int ahb_swapchain_has_ahb(const struct dmabuf_buffer *b) {
|
||||
return b && *banner_dmabuf_ahb_slot((struct dmabuf_buffer *)b) != NULL;
|
||||
return b && *droiddeck_dmabuf_ahb_slot((struct dmabuf_buffer *)b) != NULL;
|
||||
}
|
||||
|
||||
int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_w, int scene_h) {
|
||||
struct ahb_buf *ab = b ? *banner_dmabuf_ahb_slot(b) : NULL;
|
||||
struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot(b) : NULL;
|
||||
if (!ab) return -1;
|
||||
int dmabuf_fd = banner_dmabuf_fd(b);
|
||||
int dmabuf_fd = droiddeck_dmabuf_fd(b);
|
||||
/* Acquire fence: the game's render fence, which its driver put into the dma-buf before the
|
||||
* commit (Mesa's implicit sync). The display waits on it, not the CPU. */
|
||||
int acquire = -1;
|
||||
struct banner_dma_buf_sync_file exp = {.flags = BANNER_DMA_BUF_SYNC_READ, .fd = -1};
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, BANNER_DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &exp) == 0 && exp.fd >= 0) {
|
||||
struct droiddeck_dma_buf_sync_file exp = {.flags = DROIDDECK_DMA_BUF_SYNC_READ, .fd = -1};
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &exp) == 0 && exp.fd >= 0) {
|
||||
acquire = exp.fd;
|
||||
} else if (dmabuf_fd >= 0) {
|
||||
if (!g_export_failed_logged) {
|
||||
g_export_failed_logged = 1;
|
||||
banner_log("layer", "zero-copy: DMA_BUF_IOCTL_EXPORT_SYNC_FILE failed (%s): waiting for each frame on the CPU instead",
|
||||
droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_EXPORT_SYNC_FILE failed (%s): waiting for each frame on the CPU instead",
|
||||
strerror(errno));
|
||||
}
|
||||
struct pollfd p = {.fd = dmabuf_fd, .events = POLLIN}; /* readable = the writers are done */
|
||||
@@ -220,7 +220,7 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_
|
||||
do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR);
|
||||
}
|
||||
int r = sc_layer_present_ahb(ab->ahb, ab->w, ab->h, acquire, (void *)(uintptr_t)ab->id, scene_w, scene_h,
|
||||
s ? banner_surface_color(s) : NULL, ab->format);
|
||||
s ? droiddeck_surface_color(s) : NULL, ab->format);
|
||||
if (r < 0) return -1;
|
||||
if (r == 1) return 0; /* nothing of it on screen: not on the layer, nothing to release later */
|
||||
if (!ab->on_layer) {
|
||||
@@ -231,8 +231,8 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_
|
||||
if (s && g_announced != s) {
|
||||
char name[160];
|
||||
g_announced = s;
|
||||
banner_surface_describe(s, name, sizeof(name));
|
||||
banner_log("layer", "zero-copy: presenting %s without a copy", name);
|
||||
droiddeck_surface_describe(s, name, sizeof(name));
|
||||
droiddeck_log("layer", "zero-copy: presenting %s without a copy", name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ int ahb_swapchain_last_frame_age_ms(void) {
|
||||
}
|
||||
|
||||
int ahb_swapchain_defer_release(struct dmabuf_buffer *b, struct wl_resource *buffer, struct surface *s, int paced) {
|
||||
struct ahb_buf *ab = b ? *banner_dmabuf_ahb_slot(b) : NULL;
|
||||
struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot(b) : NULL;
|
||||
if (!ab || !ab->on_layer || !buffer || ab->resource != buffer) return 0;
|
||||
ab->release_pending = 1;
|
||||
ab->deferred_ns = now_ns();
|
||||
@@ -268,7 +268,7 @@ unsigned ahb_swapchain_stats_take(void) {
|
||||
int ahb_swapchain_advertised(void) { return g_advertised; }
|
||||
|
||||
uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b) {
|
||||
struct ahb_buf *ab = b ? *banner_dmabuf_ahb_slot((struct dmabuf_buffer *)b) : NULL;
|
||||
struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot((struct dmabuf_buffer *)b) : NULL;
|
||||
return ab ? ab->format : 0;
|
||||
}
|
||||
|
||||
@@ -279,14 +279,14 @@ static void ahb_destroy(struct wl_client *c, struct wl_resource *r) { wl_resourc
|
||||
static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_resource *buffer, int32_t sock,
|
||||
uint32_t width, uint32_t height, uint32_t stride, uint32_t mod_hi, uint32_t mod_lo,
|
||||
uint32_t image_count) {
|
||||
struct dmabuf_buffer *b = banner_dmabuf_from_resource(buffer);
|
||||
struct dmabuf_buffer *b = droiddeck_dmabuf_from_resource(buffer);
|
||||
uint64_t modifier = ((uint64_t)mod_hi << 32) | mod_lo;
|
||||
if (!b) {
|
||||
banner_log("layer", "zero-copy: %s attached an AHardwareBuffer to a non-dma-buf wl_buffer, ignored", banner_client_name(c));
|
||||
droiddeck_log("layer", "zero-copy: %s attached an AHardwareBuffer to a non-dma-buf wl_buffer, ignored", droiddeck_client_name(c));
|
||||
close(sock);
|
||||
return;
|
||||
}
|
||||
void **slot = banner_dmabuf_ahb_slot(b);
|
||||
void **slot = droiddeck_dmabuf_ahb_slot(b);
|
||||
if (*slot) { close(sock); return; } /* once per buffer */
|
||||
|
||||
/* The handle was written into the socket before the request was sent; a short wait is just
|
||||
@@ -298,17 +298,17 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res
|
||||
int rc = pr > 0 ? AHardwareBuffer_recvHandleFromUnixSocket(sock, &ahb) : -1;
|
||||
close(sock);
|
||||
if (rc != 0 || !ahb) {
|
||||
banner_log("layer", "zero-copy: receiving %s's AHardwareBuffer failed (%s)", banner_client_name(c),
|
||||
droiddeck_log("layer", "zero-copy: receiving %s's AHardwareBuffer failed (%s)", droiddeck_client_name(c),
|
||||
pr > 0 ? "recvHandleFromUnixSocket" : "nothing on the socket");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_Desc d;
|
||||
AHardwareBuffer_describe(ahb, &d);
|
||||
int bw, bh;
|
||||
banner_dmabuf_size(b, &bw, &bh);
|
||||
droiddeck_dmabuf_size(b, &bw, &bh);
|
||||
if ((int)d.width != bw || (int)d.height != bh || (int)width != bw || (int)height != bh) {
|
||||
banner_log("layer", "zero-copy: %s's AHardwareBuffer is %ux%u but its wl_buffer %dx%d, ignored",
|
||||
banner_client_name(c), d.width, d.height, bw, bh);
|
||||
droiddeck_log("layer", "zero-copy: %s's AHardwareBuffer is %ux%u but its wl_buffer %dx%d, ignored",
|
||||
droiddeck_client_name(c), d.width, d.height, bw, bh);
|
||||
AHardwareBuffer_release(ahb);
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +317,7 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res
|
||||
if (!ab) { AHardwareBuffer_release(ahb); wl_client_post_no_memory(c); return; }
|
||||
ab->id = g_next_id++;
|
||||
ab->b = b;
|
||||
banner_dmabuf_ref(b);
|
||||
droiddeck_dmabuf_ref(b);
|
||||
ab->ahb = ahb;
|
||||
ab->w = bw; ab->h = bh;
|
||||
ab->stride = d.stride ? d.stride : stride;
|
||||
@@ -335,8 +335,8 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res
|
||||
if (g_last_chain.client != c || g_last_chain.w != bw || g_last_chain.h != bh ||
|
||||
g_last_chain.image_count != image_count || g_last_chain.modifier != modifier || g_last_chain.format != d.format) {
|
||||
g_last_chain = (struct chain_seen){c, bw, bh, image_count, modifier, d.format};
|
||||
banner_log("layer", "zero-copy: AHB swapchain from %s (%u images, %dx%d, %s, %s, stride %u px)", banner_client_name(c),
|
||||
image_count, bw, bh, banner_ahb_format_name(d.format), modifier == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)"
|
||||
droiddeck_log("layer", "zero-copy: AHB swapchain from %s (%u images, %dx%d, %s, %s, stride %u px)", droiddeck_client_name(c),
|
||||
image_count, bw, bh, droiddeck_ahb_format_name(d.format), modifier == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)"
|
||||
: modifier == 0 ? "linear" : "unknown modifier", ab->stride);
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,7 @@ static void bind_ahb(struct wl_client *c, void *data, uint32_t ver, uint32_t id)
|
||||
/* One line per program, not per surface-format query (each binds its own). */
|
||||
if (last_named != c) {
|
||||
last_named = c;
|
||||
banner_log("layer", "zero-copy: %s bound banner_ahb_v1 version %u (%s)", banner_client_name(c), ver,
|
||||
droiddeck_log("layer", "zero-copy: %s bound banner_ahb_v1 version %u (%s)", droiddeck_client_name(c), ver,
|
||||
ver >= 2 ? "follows the live switch" : "version 1: decides from its launch environment only");
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ void ahb_swapchain_set_mode(int on, int live) {
|
||||
on = on ? 1 : 0;
|
||||
g_zero_copy = on;
|
||||
if (!g_advertised) {
|
||||
if (live) banner_log("layer", "zero-copy switched %s from the drawer, but no display layer is available on this device: no change",
|
||||
if (live) droiddeck_log("layer", "zero-copy switched %s from the drawer, but no display layer is available on this device: no change",
|
||||
on ? "on" : "off");
|
||||
return;
|
||||
}
|
||||
@@ -390,37 +390,37 @@ void ahb_swapchain_set_mode(int on, int live) {
|
||||
memset(&g_last_chain, 0, sizeof(g_last_chain));
|
||||
g_announced = NULL;
|
||||
if (live)
|
||||
banner_log("layer", "zero-copy switched %s from the drawer: %d bound program%s told to rebuild their swapchains%s",
|
||||
droiddeck_log("layer", "zero-copy switched %s from the drawer: %d bound program%s told to rebuild their swapchains%s",
|
||||
on ? "on" : "off", told, told == 1 ? "" : "s",
|
||||
on ? "; frames go on the display layer once the new gralloc swapchain is up"
|
||||
: "; gralloc frames still in flight stay on the layer (or take the copy path) until then");
|
||||
else
|
||||
banner_log("layer", "zero-copy: %s at launch (%s)", on ? "on" : "off",
|
||||
on ? "BANNER_WAYLAND_ZERO_COPY=1: games present their own gralloc buffers on the display layer"
|
||||
droiddeck_log("layer", "zero-copy: %s at launch (%s)", on ? "on" : "off",
|
||||
on ? "DROIDDECK_WAYLAND_ZERO_COPY=1: games present their own gralloc buffers on the display layer"
|
||||
: "the drawer's Zero-copy presentation switch turns it on live");
|
||||
if (live) wl_display_flush_clients(banner_get_display());
|
||||
banner_request_redraw();
|
||||
if (live) wl_display_flush_clients(droiddeck_get_display());
|
||||
droiddeck_request_redraw();
|
||||
}
|
||||
|
||||
void ahb_swapchain_init(struct wl_display *display) {
|
||||
wl_list_init(&g_bufs);
|
||||
wl_list_init(&g_clients);
|
||||
if (!sc_layer_available()) {
|
||||
banner_log("layer", "zero-copy: no display layer on this device (see the line above): banner_ahb_v1 not advertised");
|
||||
droiddeck_log("layer", "zero-copy: no display layer on this device (see the line above): banner_ahb_v1 not advertised");
|
||||
return;
|
||||
}
|
||||
g_loop = wl_display_get_event_loop(display);
|
||||
if (pipe2(g_rel_pipe, O_CLOEXEC | O_NONBLOCK) != 0) {
|
||||
banner_log("error", "zero-copy: release pipe creation failed (%s); zero-copy presents disabled", strerror(errno));
|
||||
droiddeck_log("error", "zero-copy: release pipe creation failed (%s); zero-copy presents disabled", strerror(errno));
|
||||
g_rel_pipe[0] = g_rel_pipe[1] = -1;
|
||||
return;
|
||||
}
|
||||
wl_event_loop_add_fd(g_loop, g_rel_pipe[0], WL_EVENT_READABLE, on_release_pipe, NULL);
|
||||
if (!wl_global_create(display, &banner_ahb_v1_interface, 2, NULL, bind_ahb)) {
|
||||
banner_log("error", "zero-copy: banner_ahb_v1 global creation failed");
|
||||
droiddeck_log("error", "zero-copy: banner_ahb_v1 global creation failed");
|
||||
return;
|
||||
}
|
||||
g_advertised = 1;
|
||||
banner_log("layer", "zero-copy: banner_ahb_v1 version 2 advertised (games built for it present their own gralloc buffers while the switch is on)");
|
||||
droiddeck_log("layer", "zero-copy: banner_ahb_v1 version 2 advertised (games built for it present their own gralloc buffers while the switch is on)");
|
||||
ahb_swapchain_set_mode(g_zero_copy, 0);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef AHB_SWAPCHAIN_H
|
||||
#define AHB_SWAPCHAIN_H
|
||||
/*
|
||||
* Zero-copy game frames (layer mode, BANNER_WAYLAND_ZERO_COPY=1 at launch, the drawer's "Zero-copy
|
||||
* Zero-copy game frames (layer mode, DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's "Zero-copy
|
||||
* presentation" switch live): the guest half of the design in ZERO_COPY_SPIKE.md. Our Wayland Turnip
|
||||
* (banners-turnip-wayland, patches/wayland/banner_ahb_wsi.py) binds the private global banner_ahb_v1
|
||||
* (version 2), which this compositor advertises on every session where a display layer is possible,
|
||||
@@ -63,21 +63,21 @@ uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b);
|
||||
void ahb_swapchain_layer_released(void *token, int release_fd);
|
||||
|
||||
/* ---- ahb_swapchain.c -> compositor.c (hooks) */
|
||||
struct dmabuf_buffer *banner_dmabuf_from_resource(struct wl_resource *buffer);
|
||||
int banner_dmabuf_fd(const struct dmabuf_buffer *b); /* plane 0's dma-buf */
|
||||
void banner_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h);
|
||||
void **banner_dmabuf_ahb_slot(struct dmabuf_buffer *b); /* this module's per-buffer state */
|
||||
void banner_dmabuf_ref(struct dmabuf_buffer *b);
|
||||
void banner_dmabuf_unref(struct dmabuf_buffer *b);
|
||||
struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer);
|
||||
int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b); /* plane 0's dma-buf */
|
||||
void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h);
|
||||
void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b); /* this module's per-buffer state */
|
||||
void droiddeck_dmabuf_ref(struct dmabuf_buffer *b);
|
||||
void droiddeck_dmabuf_unref(struct dmabuf_buffer *b);
|
||||
/* Give a wl_buffer back to its client now (paced = 0) or on the FPS limiter's cadence (s != NULL).
|
||||
* since_ns = when the compositor let go of it (CLOCK_MONOTONIC; the perf line's release latency). */
|
||||
void banner_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns);
|
||||
void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns);
|
||||
/* Redraw the scene on the next tick (the present path may have changed). */
|
||||
void banner_request_redraw(void);
|
||||
void droiddeck_request_redraw(void);
|
||||
/* "<title>" (program) of the window a surface belongs to, for the log. */
|
||||
void banner_surface_describe(const struct surface *s, char *out, size_t size);
|
||||
/* The surface's current image description (banner_color.h), NULL = none / HDR gate closed. */
|
||||
struct banner_color;
|
||||
const struct banner_color *banner_surface_color(const struct surface *s);
|
||||
void droiddeck_surface_describe(const struct surface *s, char *out, size_t size);
|
||||
/* The surface's current image description (droiddeck_color.h), NULL = none / HDR gate closed. */
|
||||
struct droiddeck_color;
|
||||
const struct droiddeck_color *droiddeck_surface_color(const struct surface *s);
|
||||
|
||||
#endif
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Interface between compositor.c and the extension modules that live in their own files:
|
||||
* wl_clipboard.c wl_data_device_manager + zwlr_data_control_manager_v1 (selection only)
|
||||
* wl_text_input.c zwp_text_input_manager_v3 (host IME / soft keyboard)
|
||||
* wl_toplevel_icon.c xdg_toplevel_icon_manager_v1 (accepted, not rendered)
|
||||
* banner_ext.c the host→compositor message queue shared by the modules
|
||||
* Everything here runs on the compositor (dispatch) thread unless noted.
|
||||
*/
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <wayland-server.h>
|
||||
|
||||
void banner_log(const char *tag, const char *fmt, ...);
|
||||
|
||||
/* ---- compositor.c → modules */
|
||||
void banner_ext_init(struct wl_display *display); /* creates the globals + host queue */
|
||||
void banner_clipboard_keyboard_focus(struct wl_client *client); /* keyboard focus moved; NULL = none */
|
||||
void banner_text_input_refocus(void); /* the text-input target may have changed */
|
||||
void banner_text_input_surface_gone(struct wl_resource *surface);
|
||||
|
||||
/* ---- modules → compositor.c */
|
||||
const char *banner_client_name(struct wl_client *client);
|
||||
struct wl_display *banner_get_display(void);
|
||||
/* The surface text input follows (the last clicked window, else the topmost program window). */
|
||||
struct wl_resource *banner_ime_target(void);
|
||||
/* Scene (virtual desktop) origin of a toplevel surface; 0,0 when unknown. */
|
||||
void banner_surface_scene_origin(struct wl_resource *surface, int *x, int *y);
|
||||
/* Deliver a key press/release through the wl_keyboard path as if typed. */
|
||||
void banner_inject_key(uint32_t evdev, int pressed);
|
||||
|
||||
/* ---- modules → app (JNI upcalls; waylandcomp_jni.c) */
|
||||
void banner_on_clipboard_text(const char *utf8, int len);
|
||||
void banner_on_text_input(int enabled, const char *program, int x, int y, int w, int h);
|
||||
|
||||
/* ---- app → modules (callable from any thread; queued to the compositor thread) */
|
||||
void banner_host_clipboard_text(const char *utf8, int len); /* len 0 = clear */
|
||||
void banner_host_text_commit(const char *utf8, int len);
|
||||
void banner_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end);
|
||||
void banner_host_text_delete(int before, int after);
|
||||
void banner_host_zero_copy(int on, int live); /* the drawer's zero-copy switch (ahb_swapchain_set_mode) */
|
||||
void banner_host_hdr_output(int on); /* the drawer's HDR output switch (banner_color_set_output) */
|
||||
|
||||
/* ---- module internals reached through the queue (banner_ext.c → modules) */
|
||||
void clipboard_init(struct wl_display *display);
|
||||
void clipboard_host_text(char *utf8, size_t len); /* takes ownership of utf8 */
|
||||
void text_input_init(struct wl_display *display);
|
||||
void text_input_host_commit(const char *utf8, size_t len);
|
||||
void text_input_host_preedit(const char *utf8, size_t len, int cursor_begin, int cursor_end);
|
||||
void text_input_host_delete(int before, int after);
|
||||
void toplevel_icon_init(struct wl_display *display);
|
||||
@@ -0,0 +1,9 @@
|
||||
#version 450
|
||||
// Wayland buffers carry premultiplied alpha; the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
|
||||
layout(binding = 0) uniform sampler2D Surface;
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(Surface, uv);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 450
|
||||
// One surface as a quad: dst = the target rectangle in NDC, src = the buffer rectangle in UV.
|
||||
layout(push_constant) uniform PC { vec4 dst; vec4 src; } pc;
|
||||
layout(location = 0) out vec2 uv;
|
||||
|
||||
void main() {
|
||||
vec2 t = vec2(gl_VertexIndex & 1, gl_VertexIndex >> 1);
|
||||
gl_Position = vec4(mix(pc.dst.xy, pc.dst.zw, t), 0.0, 1.0);
|
||||
uv = mix(pc.src.xy, pc.src.zw, t);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 1115.1.0
|
||||
#pragma once
|
||||
const uint32_t blend_frag_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x00000014,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000009,0x00000011,0x00030010,
|
||||
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
|
||||
0x00000000,0x00050005,0x00000009,0x4374756f,0x726f6c6f,0x00000000,0x00040005,0x0000000d,
|
||||
0x66727553,0x00656361,0x00030005,0x00000011,0x00007675,0x00040047,0x00000009,0x0000001e,
|
||||
0x00000000,0x00040047,0x0000000d,0x00000021,0x00000000,0x00040047,0x0000000d,0x00000022,
|
||||
0x00000000,0x00040047,0x00000011,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,
|
||||
0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,
|
||||
0x00000004,0x00040020,0x00000008,0x00000003,0x00000007,0x0004003b,0x00000008,0x00000009,
|
||||
0x00000003,0x00090019,0x0000000a,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,
|
||||
0x00000001,0x00000000,0x0003001b,0x0000000b,0x0000000a,0x00040020,0x0000000c,0x00000000,
|
||||
0x0000000b,0x0004003b,0x0000000c,0x0000000d,0x00000000,0x00040017,0x0000000f,0x00000006,
|
||||
0x00000002,0x00040020,0x00000010,0x00000001,0x0000000f,0x0004003b,0x00000010,0x00000011,
|
||||
0x00000001,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,
|
||||
0x0004003d,0x0000000b,0x0000000e,0x0000000d,0x0004003d,0x0000000f,0x00000012,0x00000011,
|
||||
0x00050057,0x00000007,0x00000013,0x0000000e,0x00000012,0x0003003e,0x00000009,0x00000013,
|
||||
0x000100fd,0x00010038
|
||||
};
|
||||
@@ -0,0 +1,315 @@
|
||||
/* Alpha composition: one surface drawn source-over onto the scene - see blend_pass.h. Compositor thread. */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "blend_pass.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */
|
||||
#include <string.h>
|
||||
|
||||
/* SPIR-V, pre-compiled (glslangValidator -V blend.vert --vn blend_vert_code, the same for blend.frag)
|
||||
* and committed next to the GLSL: the NDK build compiles no shaders. */
|
||||
#include "blend_vert.h"
|
||||
#include "blend_frag.h"
|
||||
|
||||
struct pc_blend { float dst[4]; float src[4]; };
|
||||
|
||||
static VkDevice g_dev;
|
||||
static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */
|
||||
static VkSampler g_sampler;
|
||||
static VkDescriptorSetLayout g_dsl;
|
||||
static VkPipelineLayout g_pl;
|
||||
static VkDescriptorPool g_dpool;
|
||||
static VkShaderModule g_vert, g_frag;
|
||||
static unsigned g_frame = 1;
|
||||
|
||||
/* One render pass + pipeline per target format: the screen swapchain's, the scene image's, the HDR
|
||||
* composition's. */
|
||||
#define NFMT 4
|
||||
static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT];
|
||||
|
||||
#define NSRC 32
|
||||
static struct { VkImage img; VkImageView view; VkDescriptorSet ds; unsigned frame; } g_src[NSRC];
|
||||
#define NDST 12
|
||||
static struct { VkImage img; VkFormat fmt; int w, h; VkImageView view; VkFramebuffer fb; unsigned frame; } g_dst[NDST];
|
||||
|
||||
void blendp_bind_device(VkDevice dev) { g_dev = dev; }
|
||||
|
||||
void blendp_begin_frame(void) { g_frame++; }
|
||||
|
||||
static void src_release(int i) {
|
||||
if (g_src[i].ds) g_vk.FreeDescriptorSets(g_dev, g_dpool, 1, &g_src[i].ds);
|
||||
if (g_src[i].view) g_vk.DestroyImageView(g_dev, g_src[i].view, NULL);
|
||||
memset(&g_src[i], 0, sizeof(g_src[i]));
|
||||
}
|
||||
|
||||
static void dst_release(int i) {
|
||||
if (g_dst[i].fb) g_vk.DestroyFramebuffer(g_dev, g_dst[i].fb, NULL);
|
||||
if (g_dst[i].view) g_vk.DestroyImageView(g_dev, g_dst[i].view, NULL);
|
||||
memset(&g_dst[i], 0, sizeof(g_dst[i]));
|
||||
}
|
||||
|
||||
void blendp_forget_image(VkImage img) {
|
||||
if (!g_dev || !img) return;
|
||||
for (int i = 0; i < NSRC; i++) if (g_src[i].img == img) src_release(i);
|
||||
for (int i = 0; i < NDST; i++) if (g_dst[i].img == img) dst_release(i);
|
||||
}
|
||||
|
||||
static VkShaderModule shader(const uint32_t *code, size_t size) {
|
||||
VkShaderModule m = VK_NULL_HANDLE;
|
||||
VkShaderModuleCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = size, .pCode = code};
|
||||
if (g_vk.CreateShaderModule(g_dev, &ci, NULL, &m) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return m;
|
||||
}
|
||||
|
||||
/* The target keeps what the blits put there (LOAD) and stays in TRANSFER_DST_OPTIMAL for the blits
|
||||
* that follow. The dependencies order the pass between them. */
|
||||
static VkRenderPass make_rp(VkFormat fmt) {
|
||||
VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL};
|
||||
VkAttachmentReference ref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription sub = {.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, .colorAttachmentCount = 1,
|
||||
.pColorAttachments = &ref};
|
||||
VkSubpassDependency deps[2] = {
|
||||
{.srcSubpass = VK_SUBPASS_EXTERNAL, .dstSubpass = 0,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT},
|
||||
{.srcSubpass = 0, .dstSubpass = VK_SUBPASS_EXTERNAL,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT |
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT}};
|
||||
VkRenderPassCreateInfo rci = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, .attachmentCount = 1,
|
||||
.pAttachments = &att, .subpassCount = 1, .pSubpasses = &sub,
|
||||
.dependencyCount = 2, .pDependencies = deps};
|
||||
VkRenderPass rp = VK_NULL_HANDLE;
|
||||
if (g_vk.CreateRenderPass(g_dev, &rci, NULL, &rp) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return rp;
|
||||
}
|
||||
|
||||
static VkPipeline make_pipe(VkRenderPass rp) {
|
||||
VkPipelineShaderStageCreateInfo st[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_VERTEX_BIT,
|
||||
.module = g_vert, .pName = "main"},
|
||||
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
.module = g_frag, .pName = "main"}};
|
||||
VkPipelineVertexInputStateCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo ia = {.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
|
||||
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP};
|
||||
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo ds = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
|
||||
.dynamicStateCount = 2, .pDynamicStates = dyn};
|
||||
VkPipelineViewportStateCreateInfo vp = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
|
||||
.viewportCount = 1, .scissorCount = 1};
|
||||
VkPipelineRasterizationStateCreateInfo rs = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
|
||||
.polygonMode = VK_POLYGON_MODE_FILL, .cullMode = VK_CULL_MODE_NONE,
|
||||
.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, .lineWidth = 1.0f};
|
||||
VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
|
||||
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT};
|
||||
/* The target's alpha is left alone: the screen is opaque whatever is drawn onto it. */
|
||||
VkPipelineColorBlendAttachmentState ba = {
|
||||
.blendEnable = VK_TRUE, .srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
|
||||
.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, .colorBlendOp = VK_BLEND_OP_ADD,
|
||||
.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
|
||||
.alphaBlendOp = VK_BLEND_OP_ADD,
|
||||
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT};
|
||||
VkPipelineColorBlendStateCreateInfo cb = {.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
|
||||
.attachmentCount = 1, .pAttachments = &ba};
|
||||
VkGraphicsPipelineCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .stageCount = 2,
|
||||
.pStages = st, .pVertexInputState = &vi, .pInputAssemblyState = &ia,
|
||||
.pViewportState = &vp, .pRasterizationState = &rs, .pMultisampleState = &ms,
|
||||
.pColorBlendState = &cb, .pDynamicState = &ds, .layout = g_pl,
|
||||
.renderPass = rp, .subpass = 0};
|
||||
VkPipeline p = VK_NULL_HANDLE;
|
||||
if (g_vk.CreateGraphicsPipelines(g_dev, VK_NULL_HANDLE, 1, &pi, NULL, &p) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return p;
|
||||
}
|
||||
|
||||
static void objects_destroy(void) {
|
||||
if (g_vert) g_vk.DestroyShaderModule(g_dev, g_vert, NULL);
|
||||
if (g_frag) g_vk.DestroyShaderModule(g_dev, g_frag, NULL);
|
||||
if (g_dpool) g_vk.DestroyDescriptorPool(g_dev, g_dpool, NULL);
|
||||
if (g_pl) g_vk.DestroyPipelineLayout(g_dev, g_pl, NULL);
|
||||
if (g_dsl) g_vk.DestroyDescriptorSetLayout(g_dev, g_dsl, NULL);
|
||||
if (g_sampler) g_vk.DestroySampler(g_dev, g_sampler, NULL);
|
||||
g_vert = g_frag = VK_NULL_HANDLE; g_dpool = VK_NULL_HANDLE; g_pl = VK_NULL_HANDLE;
|
||||
g_dsl = VK_NULL_HANDLE; g_sampler = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
static int objects_ensure(void) {
|
||||
if (g_ready) return g_ready == 1 ? 0 : -1;
|
||||
g_ready = -1;
|
||||
if (!g_dev || !g_vk.CreateGraphicsPipelines) goto fail;
|
||||
VkSamplerCreateInfo sci = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_LINEAR,
|
||||
.minFilter = VK_FILTER_LINEAR, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
|
||||
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE};
|
||||
if (g_vk.CreateSampler(g_dev, &sci, NULL, &g_sampler) != VK_SUCCESS) goto fail;
|
||||
VkDescriptorSetLayoutBinding b = {.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT};
|
||||
VkDescriptorSetLayoutCreateInfo dli = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.bindingCount = 1, .pBindings = &b};
|
||||
if (g_vk.CreateDescriptorSetLayout(g_dev, &dli, NULL, &g_dsl) != VK_SUCCESS) goto fail;
|
||||
VkPushConstantRange pcr = {.stageFlags = VK_SHADER_STAGE_VERTEX_BIT, .offset = 0, .size = sizeof(struct pc_blend)};
|
||||
VkPipelineLayoutCreateInfo pli = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
||||
.pSetLayouts = &g_dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pcr};
|
||||
if (g_vk.CreatePipelineLayout(g_dev, &pli, NULL, &g_pl) != VK_SUCCESS) goto fail;
|
||||
VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NSRC};
|
||||
VkDescriptorPoolCreateInfo dpi = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, .maxSets = NSRC,
|
||||
.poolSizeCount = 1, .pPoolSizes = &ps};
|
||||
if (g_vk.CreateDescriptorPool(g_dev, &dpi, NULL, &g_dpool) != VK_SUCCESS) goto fail;
|
||||
if (!(g_vert = shader(blend_vert_code, sizeof(blend_vert_code)))) goto fail;
|
||||
if (!(g_frag = shader(blend_frag_code, sizeof(blend_frag_code)))) goto fail;
|
||||
g_ready = 1;
|
||||
droiddeck_log("gpu", "alpha composition ready on %s: translucent surfaces are blended over what is under them",
|
||||
vkp_gpu_name());
|
||||
return 0;
|
||||
fail:
|
||||
objects_destroy();
|
||||
droiddeck_log("error", "gpu: the alpha composition pass could not be built on this driver; translucent surfaces "
|
||||
"are drawn opaque this session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int out_slot(VkFormat fmt) {
|
||||
int i;
|
||||
for (i = 0; i < NFMT && g_out[i].pipe; i++) if (g_out[i].fmt == fmt) return i;
|
||||
if (i == NFMT) return -1;
|
||||
VkRenderPass rp = make_rp(fmt);
|
||||
VkPipeline pipe = rp ? make_pipe(rp) : VK_NULL_HANDLE;
|
||||
if (!pipe) {
|
||||
if (rp) g_vk.DestroyRenderPass(g_dev, rp, NULL);
|
||||
return -1;
|
||||
}
|
||||
g_out[i].fmt = fmt; g_out[i].rp = rp; g_out[i].pipe = pipe;
|
||||
return i;
|
||||
}
|
||||
|
||||
static VkImageView make_view(VkImage img, VkFormat fmt) {
|
||||
VkImageView v = VK_NULL_HANDLE;
|
||||
VkImageViewCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = img,
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = fmt,
|
||||
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
||||
if (g_vk.CreateImageView(g_dev, &ci, NULL, &v) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* The slot to recycle: a free one, else the one unused for longest - never one this command buffer
|
||||
* already refers to. -1 = every slot is in this frame. */
|
||||
#define OLDEST(arr, n, out) do { \
|
||||
unsigned age_ = 0; \
|
||||
(out) = -1; \
|
||||
for (int k_ = 0; k_ < (n); k_++) { \
|
||||
if (!(arr)[k_].img) { (out) = k_; break; } \
|
||||
if ((arr)[k_].frame != g_frame && g_frame - (arr)[k_].frame > age_) { age_ = g_frame - (arr)[k_].frame; (out) = k_; } \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static VkDescriptorSet src_ensure(VkImage img, VkFormat fmt, VkImageLayout layout) {
|
||||
int i;
|
||||
for (i = 0; i < NSRC; i++)
|
||||
if (g_src[i].img == img) { g_src[i].frame = g_frame; return g_src[i].ds; }
|
||||
OLDEST(g_src, NSRC, i);
|
||||
if (i < 0) return VK_NULL_HANDLE;
|
||||
src_release(i);
|
||||
if (!(g_src[i].view = make_view(img, fmt))) return VK_NULL_HANDLE;
|
||||
VkDescriptorSetAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
|
||||
.descriptorPool = g_dpool, .descriptorSetCount = 1, .pSetLayouts = &g_dsl};
|
||||
if (g_vk.AllocateDescriptorSets(g_dev, &ai, &g_src[i].ds) != VK_SUCCESS) {
|
||||
g_src[i].ds = VK_NULL_HANDLE;
|
||||
src_release(i);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
VkDescriptorImageInfo ii = {.sampler = g_sampler, .imageView = g_src[i].view, .imageLayout = layout};
|
||||
VkWriteDescriptorSet wr = {.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = g_src[i].ds, .dstBinding = 0,
|
||||
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
.pImageInfo = &ii};
|
||||
g_vk.UpdateDescriptorSets(g_dev, 1, &wr, 0, NULL);
|
||||
g_src[i].img = img; g_src[i].frame = g_frame;
|
||||
return g_src[i].ds;
|
||||
}
|
||||
|
||||
static VkFramebuffer dst_ensure(VkImage img, VkFormat fmt, int w, int h, VkRenderPass rp) {
|
||||
int i;
|
||||
for (i = 0; i < NDST; i++) {
|
||||
if (g_dst[i].img != img) continue;
|
||||
if (g_dst[i].fmt == fmt && g_dst[i].w == w && g_dst[i].h == h) { g_dst[i].frame = g_frame; return g_dst[i].fb; }
|
||||
if (g_dst[i].frame == g_frame) return VK_NULL_HANDLE;
|
||||
break;
|
||||
}
|
||||
if (i == NDST) OLDEST(g_dst, NDST, i);
|
||||
if (i < 0) return VK_NULL_HANDLE;
|
||||
dst_release(i);
|
||||
if (!(g_dst[i].view = make_view(img, fmt))) return VK_NULL_HANDLE;
|
||||
VkFramebufferCreateInfo fi = {.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .renderPass = rp,
|
||||
.attachmentCount = 1, .pAttachments = &g_dst[i].view, .width = (uint32_t)w,
|
||||
.height = (uint32_t)h, .layers = 1};
|
||||
if (g_vk.CreateFramebuffer(g_dev, &fi, NULL, &g_dst[i].fb) != VK_SUCCESS) {
|
||||
g_dst[i].fb = VK_NULL_HANDLE;
|
||||
dst_release(i);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
g_dst[i].img = img; g_dst[i].fmt = fmt; g_dst[i].w = w; g_dst[i].h = h; g_dst[i].frame = g_frame;
|
||||
return g_dst[i].fb;
|
||||
}
|
||||
|
||||
static void src_layout(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImageLayout to) {
|
||||
int sampling = to == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkImageMemoryBarrier b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = from, .newLayout = to,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1},
|
||||
.srcAccessMask = sampling ? VK_ACCESS_TRANSFER_READ_BIT : VK_ACCESS_SHADER_READ_BIT,
|
||||
.dstAccessMask = sampling ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT};
|
||||
g_vk.CmdPipelineBarrier(cmd, sampling ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
sampling ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, NULL, 0, NULL, 1, &b);
|
||||
}
|
||||
|
||||
int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general,
|
||||
VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit) {
|
||||
if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0 || objects_ensure() != 0) return -1;
|
||||
int slot = out_slot(dst_fmt);
|
||||
if (slot < 0) return -1;
|
||||
VkImageLayout sampled = src_general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkDescriptorSet ds = src_ensure(src, src_fmt, sampled);
|
||||
if (!ds) return -1;
|
||||
VkFramebuffer fb = dst_ensure(dst, dst_fmt, dst_w, dst_h, g_out[slot].rp);
|
||||
if (!fb) return -1;
|
||||
|
||||
const VkOffset3D *d = blit->dstOffsets, *s = blit->srcOffsets;
|
||||
int x0 = d[0].x < d[1].x ? d[0].x : d[1].x, x1 = d[0].x < d[1].x ? d[1].x : d[0].x;
|
||||
int y0 = d[0].y < d[1].y ? d[0].y : d[1].y, y1 = d[0].y < d[1].y ? d[1].y : d[0].y;
|
||||
if (x0 < 0) x0 = 0;
|
||||
if (y0 < 0) y0 = 0;
|
||||
if (x1 > dst_w) x1 = dst_w;
|
||||
if (y1 > dst_h) y1 = dst_h;
|
||||
if (x1 <= x0 || y1 <= y0) return 0;
|
||||
|
||||
struct pc_blend pc = {
|
||||
.dst = {2.0f * d[0].x / dst_w - 1.0f, 2.0f * d[0].y / dst_h - 1.0f,
|
||||
2.0f * d[1].x / dst_w - 1.0f, 2.0f * d[1].y / dst_h - 1.0f},
|
||||
.src = {(float)s[0].x / src_w, (float)s[0].y / src_h, (float)s[1].x / src_w, (float)s[1].y / src_h}};
|
||||
|
||||
if (!src_general) src_layout(cmd, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sampled);
|
||||
VkRenderPassBeginInfo rpi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = g_out[slot].rp,
|
||||
.framebuffer = fb,
|
||||
.renderArea = {{x0, y0}, {(uint32_t)(x1 - x0), (uint32_t)(y1 - y0)}}};
|
||||
VkViewport vp = {0, 0, (float)dst_w, (float)dst_h, 0, 1};
|
||||
g_vk.CmdBeginRenderPass(cmd, &rpi, VK_SUBPASS_CONTENTS_INLINE);
|
||||
g_vk.CmdSetViewport(cmd, 0, 1, &vp);
|
||||
g_vk.CmdSetScissor(cmd, 0, 1, &rpi.renderArea);
|
||||
g_vk.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_out[slot].pipe);
|
||||
g_vk.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_pl, 0, 1, &ds, 0, NULL);
|
||||
g_vk.CmdPushConstants(cmd, g_pl, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc);
|
||||
g_vk.CmdDraw(cmd, 4, 1, 0, 0);
|
||||
g_vk.CmdEndRenderPass(cmd);
|
||||
if (!src_general) src_layout(cmd, src, sampled, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
return 0;
|
||||
}
|
||||
Loaded 100 of 962 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user