Compare commits
607 Commits
feat/web-r
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
f40b6930ff | |
|
|
00f4b3351b | |
|
|
a31043146e | |
|
|
a1ca1e7bad | |
|
|
21def5c6f8 | |
|
|
9828710a38 | |
|
|
a14ae8e232 | |
|
|
a5f493f90d | |
|
|
f5cda839ec | |
|
|
e1544df5f2 | |
|
|
f265cd5593 | |
|
|
970316ae04 | |
|
|
486420f91a | |
|
|
60ad9ad47c | |
|
|
fcb30fd977 | |
|
|
a8cb091eba | |
|
|
39bf2b9e9d | |
|
|
0fb2f6c89a | |
|
|
3e4fe4ce31 | |
|
|
0a56aaaf47 | |
|
|
d1b3aa1372 | |
|
|
a7836b1495 | |
|
|
aa1927872f | |
|
|
094a34c1ab | |
|
|
94f96c8e2c | |
|
|
a21d73733c | |
|
|
a1e1b4d161 | |
|
|
0defec4386 | |
|
|
c693bd7566 | |
|
|
75ceccd5db | |
|
|
622fb542d2 | |
|
|
9b6555bd47 | |
|
|
cdea950b7e | |
|
|
1e277eef6e | |
|
|
91e5f60880 | |
|
|
6d032559c3 | |
|
|
2ba3147ac8 | |
|
|
803cbf2f81 | |
|
|
c5f839f651 | |
|
|
0df6e295c5 | |
|
|
71c71f50b7 | |
|
|
fbf619ef0d | |
|
|
016db923af | |
|
|
c29e06c693 | |
|
|
c871b5893d | |
|
|
e9249a0a47 | |
|
|
a98d42d08c | |
|
|
52d134c028 | |
|
|
eacadec269 | |
|
|
959d39fb2b | |
|
|
4586fdbc3b | |
|
|
dad29cac43 | |
|
|
dff25dd604 | |
|
|
5a81f6085b | |
|
|
1633843382 | |
|
|
e5a272d7a6 | |
|
|
3881dac903 | |
|
|
54c2e14dc0 | |
|
|
e41cb23415 | |
|
|
52686e7959 | |
|
|
4ddce7993d | |
|
|
3e485ed53c | |
|
|
b9e0e4ed30 | |
|
|
97fce094c2 | |
|
|
a87019d4ce | |
|
|
11b24ee816 | |
|
|
8a437941bd | |
|
|
095a653c85 | |
|
|
c5038c719f | |
|
|
34355eeeac | |
|
|
dd7174423a | |
|
|
0af3f150fa | |
|
|
1535e2c688 | |
|
|
1ebf8410a9 | |
|
|
2c444bb9fb | |
|
|
488ccece53 | |
|
|
2e7ab67e8d | |
|
|
7937c97237 | |
|
|
8a51486bd2 | |
|
|
d1c4618729 | |
|
|
c101c91bbb | |
|
|
979a4b9bb7 | |
|
|
c6926c9c00 | |
|
|
fe54938490 | |
|
|
fc1e388ed2 | |
|
|
8e0d6324af | |
|
|
9849a1db64 | |
|
|
acbc573212 | |
|
|
8c554cc42a | |
|
|
394c5a231e | |
|
|
ee9037d851 | |
|
|
cd90ca50be | |
|
|
b16a1a72f3 | |
|
|
19785b50e9 | |
|
|
d7bb2fee3d | |
|
|
604ce71864 | |
|
|
1f1115dc90 | |
|
|
0dea07ab27 | |
|
|
b7d7e71bf6 | |
|
|
f32a9407e4 | |
|
|
71da166a18 | |
|
|
d9852df2a2 | |
|
|
4307e3cb54 | |
|
|
d275a58814 | |
|
|
ca95f74097 | |
|
|
ce5c839533 | |
|
|
29d5f067b9 | |
|
|
af991ac404 | |
|
|
cb320fd884 | |
|
|
520ee1a2e8 | |
|
|
afc8541d18 | |
|
|
7cc6cd8633 | |
|
|
d1527840ea | |
|
|
8d8fe39eed | |
|
|
955b56a2b5 | |
|
|
56d1eda2f0 | |
|
|
e94f12bd17 | |
|
|
feddf7f4c6 | |
|
|
014f3623a7 | |
|
|
3ebad9045e | |
|
|
75c25e16f0 | |
|
|
d1670009fb | |
|
|
99eaf73102 | |
|
|
12851ef037 | |
|
|
72211b70fe | |
|
|
7a0c838ca4 | |
|
|
6ffc99bedb | |
|
|
773e7f218c | |
|
|
b962be8536 | |
|
|
32fdaa65ae | |
|
|
30f961266a | |
|
|
83924acb18 | |
|
|
296ff64ae9 | |
|
|
0b46b3080a | |
|
|
3cab3c05ec | |
|
|
3915f6dc09 | |
|
|
8aa0d42824 | |
|
|
bf8dc610e6 | |
|
|
703aa8f63d | |
|
|
bfc10637d4 | |
|
|
65beff9b19 | |
|
|
c7601b7971 | |
|
|
696d1660c1 | |
|
|
665ad7da74 | |
|
|
091438f99b | |
|
|
1ba0a81820 | |
|
|
8d98e8851f | |
|
|
cf5f5b53b5 | |
|
|
d638e02860 | |
|
|
7d757bbf0a | |
|
|
ea2b2d364f | |
|
|
7b2104ee0d | |
|
|
b6a10e91aa | |
|
|
0aa4cb1c2f | |
|
|
eb78c6a45f | |
|
|
a0f5916a29 | |
|
|
852c808373 | |
|
|
3960519b1e | |
|
|
22f6b1735c | |
|
|
40ebfae70b | |
|
|
ef52b16920 | |
|
|
855823e85e | |
|
|
d45fee9b69 | |
|
|
48b69fad4d | |
|
|
299a736b54 | |
|
|
df32a44f79 | |
|
|
2ad825f659 | |
|
|
0e087e974e | |
|
|
dffdb5002e | |
|
|
89a8ec58b1 | |
|
|
dacd78525e | |
|
|
dfc9223248 | |
|
|
8b5cfb9850 | |
|
|
4dd97f7dbe | |
|
|
50b06110bb | |
|
|
15955c089e | |
|
|
286cf28ec7 | |
|
|
18c438b109 | |
|
|
2096410b29 | |
|
|
5d3ea5f511 | |
|
|
8cf6a0bc3a | |
|
|
bdded28693 | |
|
|
cb8b0caf7d | |
|
|
61b8742b6b | |
|
|
9baec8793f | |
|
|
40047f46bd | |
|
|
04421e7d4d | |
|
|
74c3fb6576 | |
|
|
664ad77037 | |
|
|
ce8944f853 | |
|
|
70663fec42 | |
|
|
a69f5e0371 | |
|
|
ed3f2776ed | |
|
|
1ed6c21067 | |
|
|
0c144502e5 | |
|
|
37ea3b651a | |
|
|
5c802bbf10 | |
|
|
ac82f3c6a9 | |
|
|
8652dbe35b | |
|
|
0214069e33 | |
|
|
32accfa938 | |
|
|
6f856a7cda | |
|
|
9d0a41d5a1 | |
|
|
34cee1ef15 | |
|
|
3e8d0793b7 | |
|
|
85c090d49d | |
|
|
13b4de565b | |
|
|
27477e5f47 | |
|
|
8b20708599 | |
|
|
9dd3c2d1a6 | |
|
|
67e86d93d5 | |
|
|
c7beafd21a | |
|
|
0265f33636 | |
|
|
091c136ba6 | |
|
|
6679168360 | |
|
|
5030fbb5cb | |
|
|
0f7cd5b7ac | |
|
|
fdefd76609 | |
|
|
e6fae88a42 | |
|
|
068ab2aba6 | |
|
|
6629c09392 | |
|
|
3f2782cc7c | |
|
|
765661581b | |
|
|
c35bc4b92c | |
|
|
2f6ae9fc88 | |
|
|
842766d39c | |
|
|
9c4686321b | |
|
|
284cb63dc9 | |
|
|
39890031e3 | |
|
|
a56c905df6 | |
|
|
9c05bf25f5 | |
|
|
5a76fd716f | |
|
|
382c451acf | |
|
|
c422b57fb3 | |
|
|
7e4edf509a | |
|
|
d7818c9e55 | |
|
|
cba01c87d1 | |
|
|
00f0589101 | |
|
|
242bae7445 | |
|
|
9510e71d6a | |
|
|
eb5bee4bfe | |
|
|
8bf10e51f4 | |
|
|
71f98dd746 | |
|
|
d67a9e5a45 | |
|
|
1a217cd4a3 | |
|
|
a74f2efe84 | |
|
|
1a2a4265c1 | |
|
|
7370aaf6d0 | |
|
|
bc568a1f6f | |
|
|
38bc0fd348 | |
|
|
3b94f923a5 | |
|
|
fffe7351ad | |
|
|
72a8e67122 | |
|
|
430e5c833e | |
|
|
0adbdcf45c | |
|
|
60fb618708 | |
|
|
c496fd530f | |
|
|
a3a3f3a9ce | |
|
|
b8683834f3 | |
|
|
f2c314e902 | |
|
|
8ea8ea386a | |
|
|
2f5c3a4bf5 | |
|
|
b4d76ba609 | |
|
|
204355cc64 | |
|
|
ffa2a6ecd3 | |
|
|
78f620cf9e | |
|
|
69b4ca5d9b | |
|
|
bfffd3359c | |
|
|
417ea99df5 | |
|
|
b6c224af45 | |
|
|
36c11dc926 | |
|
|
f7fd7569fa | |
|
|
066a8fbe30 | |
|
|
e0d388fa28 | |
|
|
c6b1f84a7e | |
|
|
fdfe2204aa | |
|
|
b478fbbb1c | |
|
|
9dac817b19 | |
|
|
c02df171c8 | |
|
|
2672a73178 | |
|
|
0f73f5f057 | |
|
|
80e70b4292 | |
|
|
b0d41be1ff | |
|
|
a8a78ded96 | |
|
|
f37eecfdac | |
|
|
ef42c5f2af | |
|
|
13818efc3b | |
|
|
f8a269e80a | |
|
|
7e5bec09f8 | |
|
|
de5279bc0c | |
|
|
146f171549 | |
|
|
b3fa9d8318 | |
|
|
a910dee09f | |
|
|
759c66564f | |
|
|
fc8165e8b9 | |
|
|
7015c0443e | |
|
|
5d7f1ffc1c | |
|
|
ac6d1b2013 | |
|
|
603d94a53f | |
|
|
e3c1d01815 | |
|
|
3953a8d49e | |
|
|
8c772ad342 | |
|
|
ab1755c258 | |
|
|
41b0ca597b | |
|
|
dc0d316fda | |
|
|
6a46563a93 | |
|
|
91ed4ce625 | |
|
|
94087d08b0 | |
|
|
f0da9edbfe | |
|
|
d0711248b1 | |
|
|
745857448e | |
|
|
9b6e7f66c4 | |
|
|
7637cac13e | |
|
|
e0263e2b87 | |
|
|
0d30059bf5 | |
|
|
ae9978a98e | |
|
|
72a685b906 | |
|
|
326e0bc29f | |
|
|
7a8e1b2559 | |
|
|
57b2dc8584 | |
|
|
e152ba8f69 | |
|
|
6cd052848a | |
|
|
0998eb4f23 | |
|
|
2cf23fcc56 | |
|
|
6c0f83c7cf | |
|
|
36eb627051 | |
|
|
81364388a0 | |
|
|
794714983c | |
|
|
58f7242ea2 | |
|
|
3a60a0d054 | |
|
|
1ab578a3f7 | |
|
|
9c383e3295 | |
|
|
90c64af925 | |
|
|
0c05d3cfc7 | |
|
|
b4e2ee0a13 | |
|
|
5b08344585 | |
|
|
3973e5ad77 | |
|
|
73eaa1f18b | |
|
|
e22a77eba2 | |
|
|
3faa9cb400 | |
|
|
a252fcd75f | |
|
|
b81ababfc6 | |
|
|
320bd37c92 | |
|
|
cdf4b174f4 | |
|
|
77016caa24 | |
|
|
4f1ae1cc3a | |
|
|
79e162110a | |
|
|
6ac63bf0cb | |
|
|
5aaa394cff | |
|
|
1e7f4967ed | |
|
|
21bdab007a | |
|
|
1a0de9e91f | |
|
|
2ec20dbd37 | |
|
|
5eb99f8b65 | |
|
|
352bf304fe | |
|
|
2f453805aa | |
|
|
c98fda869a | |
|
|
4008e68e39 | |
|
|
d5835a613b | |
|
|
76075edd38 | |
|
|
50cd318bb8 | |
|
|
6ba5d68e07 | |
|
|
352d04e8dc | |
|
|
e5f0938409 | |
|
|
dd1deb6965 | |
|
|
0afa191660 | |
|
|
c6a4223420 | |
|
|
6f0f7c7461 | |
|
|
8e3b2a06f2 | |
|
|
49d8cf3e61 | |
|
|
d991cdccf9 | |
|
|
a37ee5ff2c | |
|
|
b1cf01464a | |
|
|
dd49e1df93 | |
|
|
89414d3039 | |
|
|
5942b8c7f1 | |
|
|
1c32fd93c8 | |
|
|
b12d6f5394 | |
|
|
8b9529b99a | |
|
|
15bbc19501 | |
|
|
48ea54ed3e | |
|
|
1796482721 | |
|
|
f23c5c2fd2 | |
|
|
82f24e44a2 | |
|
|
44c4b3f8bf | |
|
|
0f2683cb7d | |
|
|
c827e11872 | |
|
|
afbc22fb1a | |
|
|
94d9957725 | |
|
|
a164b1d711 | |
|
|
bc014a887c | |
|
|
d34b78890d | |
|
|
398ccaaf96 | |
|
|
bc0375c72a | |
|
|
a60f8d940b | |
|
|
5181b30d79 | |
|
|
8485298e46 | |
|
|
a2a3a3a94d | |
|
|
96f3e9fe5d | |
|
|
14f2092c9e | |
|
|
14036cb0e8 | |
|
|
aef5972ff5 | |
|
|
247162dc4f | |
|
|
25a83722c7 | |
|
|
ff8fe4dbc1 | |
|
|
c63dd4442c | |
|
|
f20a32bec3 | |
|
|
ce8f7eeb3f | |
|
|
84e2a8c0b3 | |
|
|
230b5c2683 | |
|
|
1137c57572 | |
|
|
99e4dfefe7 | |
|
|
44767a7b97 | |
|
|
2f969f147b | |
|
|
e57a793958 | |
|
|
3c273e7081 | |
|
|
9fa50827dd | |
|
|
e3cef332e6 | |
|
|
cd0e9dccd7 | |
|
|
18414447b9 | |
|
|
e09ee165d5 | |
|
|
bc1ded3a18 | |
|
|
95f34a0504 | |
|
|
4b7a357bd2 | |
|
|
37d9ef133f | |
|
|
e99a1b0aa9 | |
|
|
f56d638a5a | |
|
|
aebc3f1480 | |
|
|
5d50886ee4 | |
|
|
3ee394ea9a | |
|
|
6f7cda184b | |
|
|
50578eb936 | |
|
|
0745fdd72f | |
|
|
c8eb1f52d3 | |
|
|
d4b59ce9fa | |
|
|
319fa5779a | |
|
|
b1328fc0f9 | |
|
|
5c6a9d22c4 | |
|
|
e34281b558 | |
|
|
5d00bd7bbf | |
|
|
4f87ac0b1c | |
|
|
075169f7b3 | |
|
|
768cd93977 | |
|
|
1d7b075610 | |
|
|
3949fb9d44 | |
|
|
29b227bf35 | |
|
|
53aea1ff34 | |
|
|
387b8a0faa | |
|
|
99ebc650c3 | |
|
|
9efabff1cb | |
|
|
501453573c | |
|
|
c7632f036f | |
|
|
a1c2f9fa2c | |
|
|
cc3b47dda0 | |
|
|
e4a7cbfc83 | |
|
|
e4f212b2e2 | |
|
|
a96f8bd640 | |
|
|
d0df62bd1f | |
|
|
df3d027ebe | |
|
|
ff6c1ea5f3 | |
|
|
f6d5404f45 | |
|
|
9f9c351f54 | |
|
|
0817454371 | |
|
|
81f727c051 | |
|
|
7313cf5589 | |
|
|
653c5449f9 | |
|
|
988d304c5c | |
|
|
64e244eb70 | |
|
|
d15cc41299 | |
|
|
02dad4429f | |
|
|
572d330a13 | |
|
|
51c7206fd0 | |
|
|
290675799e | |
|
|
bde680c855 | |
|
|
5db1d8d5e9 | |
|
|
0612c181ff | |
|
|
dedb869fa2 | |
|
|
2ae227ef85 | |
|
|
78ad5d9dfb | |
|
|
13c4a2a6c6 | |
|
|
a103e48c8b | |
|
|
93a090ed8d | |
|
|
60a1649e76 | |
|
|
0d61dad293 | |
|
|
425a4daf16 | |
|
|
dd70286889 | |
|
|
97d192eeae | |
|
|
1055617751 | |
|
|
3f314030b4 | |
|
|
a296b9cacf | |
|
|
65eb466ddd | |
|
|
e2ee9430bf | |
|
|
23342340cc | |
|
|
e5449936f4 | |
|
|
44623222c9 | |
|
|
13444c82e9 | |
|
|
837c862e22 | |
|
|
7bb59f73b4 | |
|
|
2fe382c658 | |
|
|
396b5189a9 | |
|
|
68c2b0f746 | |
|
|
a7ea2bdcaa | |
|
|
ebd71eb2e7 | |
|
|
4029b5d2c9 | |
|
|
a4752266b8 | |
|
|
eefe4db018 | |
|
|
acd8c8c527 | |
|
|
45a157d931 | |
|
|
a88a4c8e15 | |
|
|
ce98f83033 | |
|
|
3e238ff11d | |
|
|
8d455b8018 | |
|
|
e8fd542f03 | |
|
|
6fb0727c48 | |
|
|
60fb528572 | |
|
|
74e239408b | |
|
|
7ebad36341 | |
|
|
0daf1d5ba3 | |
|
|
6706e44d95 | |
|
|
9cdcd58171 | |
|
|
a141b8eb67 | |
|
|
007721d43e | |
|
|
12d07b8edd | |
|
|
6bf9ab86ff | |
|
|
c7aca86c61 | |
|
|
66edc7463f | |
|
|
2a91815210 | |
|
|
2c23ced70e | |
|
|
f745a00a5d | |
|
|
fa889c063b | |
|
|
abc997b0da | |
|
|
d39cb2c62f | |
|
|
c1bcd0b697 | |
|
|
097bc538ee | |
|
|
2f1eb100a0 | |
|
|
7cba4ce60a | |
|
|
d6698c9045 | |
|
|
69b9d186db | |
|
|
4ccf888291 | |
|
|
665e6f1782 | |
|
|
126a1e4137 | |
|
|
c15e9f2a37 | |
|
|
baaad78e81 | |
|
|
51c043ba51 | |
|
|
584c59e5ac | |
|
|
0900586191 | |
|
|
819c475266 | |
|
|
92e0970d6c | |
|
|
a7e320d1f7 | |
|
|
7f39ac991b | |
|
|
d31e283088 | |
|
|
5031432db0 | |
|
|
c35572ec18 | |
|
|
85cf9c3ac7 | |
|
|
1dc99ba0a2 | |
|
|
d0cef9afdb | |
|
|
78d594d9f6 | |
|
|
ac8106bda2 | |
|
|
acd821f7ce | |
|
|
6054923cd5 | |
|
|
2c0d8d48bc | |
|
|
61fbec8ef1 | |
|
|
0a1dff2fcc | |
|
|
c55f377fb0 | |
|
|
63022e23cc | |
|
|
a7799c4315 | |
|
|
79d88af192 | |
|
|
22d03d497f | |
|
|
0950b19854 | |
|
|
ad43a486f0 | |
|
|
3cb434715a | |
|
|
1c500fb298 | |
|
|
4df7877334 | |
|
|
89d89ac048 | |
|
|
f25eba673d | |
|
|
39bed4b9d7 | |
|
|
617e7acfa8 | |
|
|
abe8fb8193 | |
|
|
bb443667ec | |
|
|
6e0f942f0f | |
|
|
0c52b2a0b0 | |
|
|
db577b4a34 | |
|
|
17a3d8a1a0 | |
|
|
521cc95b18 | |
|
|
53d10eac9a | |
|
|
a6f42f5119 | |
|
|
7dfd790d08 | |
|
|
aa4a9b4faf | |
|
|
4f78bd534f | |
|
|
feffc67acb | |
|
|
53cbdebe82 | |
|
|
b19a74f5bf | |
|
|
febc36223b | |
|
|
4d37c93364 | |
|
|
bf6a054c52 | |
|
|
aa88949e2e | |
|
|
ca67860168 | |
|
|
4feeef6981 | |
|
|
69d0dc2288 | |
|
|
ff3eae6c9a | |
|
|
3bc3e35a02 | |
|
|
b390831a58 | |
|
|
ae96779914 | |
|
|
6effc5dcec | |
|
|
ab3e926058 | |
|
|
d759a43ae5 | |
|
|
67cdc9edcc |
|
|
@ -14,4 +14,5 @@
|
|||
- [ ] Generic behaviour, not specific to one title
|
||||
- [ ] Depends on: <!-- #PR, or none -->
|
||||
- [ ] Unblocks: <!-- title ID and Game bring-up discussion, e.g. PPSA01325 #3232, or none -->
|
||||
- [ ] GPUs tested: <!-- for AGC, shader recompiler or VideoOut changes: vendor, model, driver and OS of each GPU the GPU tests ran on; otherwise n/a -->
|
||||
- [ ] AI-assisted: <!-- yes / no -->
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ on:
|
|||
description: Pull request number to build
|
||||
required: true
|
||||
type: string
|
||||
linux:
|
||||
description: Also build and test on Linux
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -103,7 +108,7 @@ jobs:
|
|||
}
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
|
||||
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
|
||||
const touches = (...prefixes) => manual || files.length >= 3000 ||
|
||||
const touches = (...prefixes) => files.length >= 3000 ||
|
||||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
|
||||
const workflow = '.github/workflows/build.yml';
|
||||
const libc = 'core/libs/prx/libc/';
|
||||
|
|
@ -214,7 +219,7 @@ jobs:
|
|||
|
||||
linux:
|
||||
needs: eligibility
|
||||
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
|
||||
if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
|
|
@ -241,6 +246,15 @@ jobs:
|
|||
evict-old-files: job
|
||||
- name: Install lavapipe and the SDL audio and XRandR development files
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev
|
||||
- id: mesa
|
||||
run: |
|
||||
echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT"
|
||||
echo "cpu=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .mesa-shader-cache
|
||||
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
|
||||
restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-
|
||||
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
|
||||
- name: Check that every test executable is registered with ctest
|
||||
run: python3 tools/check_test_registration.py build
|
||||
|
|
@ -250,6 +264,12 @@ jobs:
|
|||
env:
|
||||
ANYPS5_REQUIRE_VULKAN: 1
|
||||
GALLIUM_OVERRIDE_CPU_CAPS: avx
|
||||
MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache
|
||||
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: .mesa-shader-cache
|
||||
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
|
||||
- run: python3 tools/check_skips.py build/ctest.xml
|
||||
- name: Report tests slower than 30 s
|
||||
if: ${{ !cancelled() }}
|
||||
|
|
@ -304,7 +324,7 @@ jobs:
|
|||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
evict-old-files: job
|
||||
- shell: cmd
|
||||
run: cmake -S . -B build %CONFIGURE%
|
||||
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
|
||||
- name: Check that every test executable is registered with ctest
|
||||
shell: cmd
|
||||
run: python tools/check_test_registration.py build
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler)
|
||||
add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests)
|
||||
set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20)
|
||||
add_test_executable(request_serializer_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp)
|
||||
target_include_directories(request_serializer_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include)
|
||||
add_test(NAME request_serializer COMMAND request_serializer_tests)
|
||||
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
|
||||
|
|
@ -84,6 +92,10 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py)
|
||||
add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py)
|
||||
add_test(NAME progress_wrappers COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_wrappers.py)
|
||||
add_test(NAME progress_read_encoding COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py)
|
||||
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
|
||||
add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE}
|
||||
|
|
@ -100,12 +112,16 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
|
||||
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py)
|
||||
add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py)
|
||||
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
|
||||
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
|
||||
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
|
||||
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
|
||||
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
|
||||
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
|
||||
|
|
@ -115,6 +131,10 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
|
||||
add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py)
|
||||
add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ The [pull request template](.github/pull_request_template.md) is the checklist f
|
|||
- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`.
|
||||
- Run the tests before opening the pull request and describe what was tested (OS, title or homebrew).
|
||||
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form.
|
||||
- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
|
||||
- Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments.
|
||||
- Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
Tool for automatic executables porting to Linux and Windows.
|
||||
|
||||
[**Online converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
|
||||
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
|
||||
|
||||

|
||||
|
||||
[Discord](https://discord.gg/BHFztBPUe)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i
|
|||
target_compile_features(nid_patcher PRIVATE cxx_std_20)
|
||||
|
||||
if(MINGW)
|
||||
target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||
include(CheckCXXSourceCompiles)
|
||||
set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables)
|
||||
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
|
||||
|
|
@ -179,23 +179,49 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
|
||||
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
|
||||
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
|
||||
if(MINGW)
|
||||
target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
|
||||
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
|
||||
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $<TARGET_FILE:nid_patcher>)
|
||||
add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $<TARGET_FILE:nid_patcher>)
|
||||
add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $<TARGET_FILE:nid_patcher>)
|
||||
set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
|
||||
add_dependencies(patched_libraries_tests libs)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(MINGW)
|
||||
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp)
|
||||
target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_tests)
|
||||
add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
|
|
@ -219,6 +245,10 @@ if(BUILD_TESTING)
|
|||
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
|
||||
configure_windows_unwind(guest_offline_imports_tests)
|
||||
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
|
||||
|
|
@ -411,6 +441,16 @@ if(BUILD_TESTING)
|
|||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePosixForWebKit>")
|
||||
endif()
|
||||
|
||||
add_test_executable(MemoryPoolTest
|
||||
tests/MemoryPoolTest.cpp
|
||||
prx/libkernel/DirectMemory/MemoryPool.cpp
|
||||
)
|
||||
target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(MINGW)
|
||||
target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest)
|
||||
|
||||
add_test_executable(guest_acm_tests tests/GuestAcm.cpp)
|
||||
target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
#ifndef NID_NATIVEPATH_HPP
|
||||
#define NID_NATIVEPATH_HPP
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace Nid::Internal {
|
||||
|
||||
inline std::filesystem::path NativePath(const std::string& path) {
|
||||
return std::filesystem::path(std::u8string(path.begin(), path.end()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) {
|
|||
|
||||
template<typename T>
|
||||
T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
|
||||
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("read out of bounds");
|
||||
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds");
|
||||
T v;
|
||||
std::memcpy(&v, buf.data() + offset, sizeof(T));
|
||||
return v;
|
||||
|
|
@ -69,7 +69,7 @@ T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
|
|||
|
||||
template<typename T>
|
||||
void Write(std::vector<std::uint8_t>& buf, std::size_t offset, const T& v) {
|
||||
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds");
|
||||
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds");
|
||||
std::memcpy(buf.data() + offset, &v, sizeof(T));
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +78,7 @@ inline std::string ReadCStr(const std::vector<std::uint8_t>& buf, std::size_t of
|
|||
std::string s;
|
||||
while (offset < buf.size() && buf[offset] != 0u)
|
||||
s.push_back(static_cast<char>(buf[offset++]));
|
||||
if (offset == buf.size()) throw std::runtime_error("unterminated cstr");
|
||||
return s;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ namespace Nid {
|
|||
namespace {
|
||||
|
||||
|
||||
void RequireElfRange(const std::vector<std::uint8_t>& elf, std::uint64_t offset, std::uint64_t size, const char* table) {
|
||||
if (offset > elf.size() || size > elf.size() - offset)
|
||||
throw std::runtime_error(std::string("invalid ELF ") + table + " range");
|
||||
}
|
||||
|
||||
std::uint32_t GnuHash(const std::string& name) {
|
||||
std::uint32_t h = 5381u;
|
||||
for (const unsigned char c : name)
|
||||
|
|
@ -36,6 +41,18 @@ GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_
|
|||
return layout;
|
||||
}
|
||||
|
||||
void ValidateGnuHash(const std::vector<std::uint8_t>& elf, std::size_t offset, std::size_t size, std::size_t symCount) {
|
||||
if (offset > elf.size() || size > elf.size() - offset || size < 16u)
|
||||
throw std::runtime_error("invalid gnu.hash section range");
|
||||
const auto layout = ReadGnuHashLayout(elf, offset);
|
||||
if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) ||
|
||||
layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount))
|
||||
throw std::runtime_error("invalid gnu.hash geometry");
|
||||
const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u +
|
||||
std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u;
|
||||
if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place");
|
||||
}
|
||||
|
||||
void ReorderDynSymForGnuHash(
|
||||
std::vector<std::uint8_t>& elf,
|
||||
std::size_t dynSymOffset,
|
||||
|
|
@ -204,49 +221,65 @@ void RemapDynamicNeededOffsets(
|
|||
}
|
||||
}
|
||||
|
||||
struct VersionNameField {
|
||||
std::size_t Offset;
|
||||
std::uint32_t Name;
|
||||
};
|
||||
|
||||
std::vector<VersionNameField> ReadVersionNameFields(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
|
||||
using namespace Internal;
|
||||
std::vector<VersionNameField> fields;
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const auto section = Read<Elf64_Shdr>(elf, static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr));
|
||||
if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue;
|
||||
if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset)
|
||||
throw std::runtime_error("invalid .gnu.version_r section range");
|
||||
const auto location = [&](std::uint64_t relative, std::size_t size) {
|
||||
if (relative > section.sh_size || size > section.sh_size - relative)
|
||||
throw std::runtime_error(".gnu.version_r record outside section");
|
||||
return static_cast<std::size_t>(section.sh_offset + relative);
|
||||
};
|
||||
const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) {
|
||||
if (step < size || step > section.sh_size - current)
|
||||
throw std::runtime_error("invalid .gnu.version_r record link");
|
||||
return current + step;
|
||||
};
|
||||
std::uint64_t current = 0u;
|
||||
while (true) {
|
||||
const auto offset = location(current, sizeof(Elf64_Verneed));
|
||||
const auto need = Read<Elf64_Verneed>(elf, offset);
|
||||
if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version");
|
||||
fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file});
|
||||
if (need.vn_cnt != 0u) {
|
||||
auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed));
|
||||
for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) {
|
||||
const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux));
|
||||
const auto aux = Read<Elf64_Vernaux>(elf, auxOffset);
|
||||
fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name});
|
||||
if (index + 1u == need.vn_cnt) {
|
||||
if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch");
|
||||
} else {
|
||||
auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (need.vn_next == 0u) break;
|
||||
current = advance(current, need.vn_next, sizeof(Elf64_Verneed));
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
void RemapVerneedOffsets(
|
||||
std::vector<std::uint8_t>& elf,
|
||||
const Elf64_Ehdr& ehdr,
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>>& oldToNewOffset
|
||||
) {
|
||||
using namespace Internal;
|
||||
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != kShtGnuVerneed) continue;
|
||||
|
||||
const std::size_t verneedOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
std::size_t entryOffset = verneedOffset;
|
||||
|
||||
while (true) {
|
||||
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
|
||||
|
||||
const auto mappedFile = std::find_if(
|
||||
oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == verneed.vn_file; }
|
||||
);
|
||||
if (mappedFile != oldToNewOffset.end())
|
||||
Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second);
|
||||
|
||||
std::size_t auxOffset = entryOffset + verneed.vn_aux;
|
||||
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
|
||||
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
|
||||
|
||||
const auto mappedName = std::find_if(
|
||||
oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == vernaux.vna_name; }
|
||||
);
|
||||
if (mappedName != oldToNewOffset.end())
|
||||
Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second);
|
||||
|
||||
if (vernaux.vna_next == 0u) break;
|
||||
auxOffset += vernaux.vna_next;
|
||||
}
|
||||
|
||||
if (verneed.vn_next == 0u) break;
|
||||
entryOffset += verneed.vn_next;
|
||||
}
|
||||
for (const auto& field : ReadVersionNameFields(elf, ehdr)) {
|
||||
const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == field.Name; });
|
||||
if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,35 +308,8 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
|
|||
}
|
||||
|
||||
std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
|
||||
using namespace Internal;
|
||||
|
||||
std::vector<std::uint32_t> offsets;
|
||||
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != kShtGnuVerneed) continue;
|
||||
|
||||
std::size_t entryOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
|
||||
while (true) {
|
||||
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
|
||||
offsets.push_back(verneed.vn_file);
|
||||
|
||||
std::size_t auxOffset = entryOffset + verneed.vn_aux;
|
||||
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
|
||||
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
|
||||
offsets.push_back(vernaux.vna_name);
|
||||
|
||||
if (vernaux.vna_next == 0u) break;
|
||||
auxOffset += vernaux.vna_next;
|
||||
}
|
||||
|
||||
if (verneed.vn_next == 0u) break;
|
||||
entryOffset += verneed.vn_next;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name);
|
||||
return offsets;
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +322,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
if (elf[4] != 2) throw std::runtime_error("not ELF64");
|
||||
|
||||
const auto ehdr = Read<Elf64_Ehdr>(elf, 0u);
|
||||
if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr))
|
||||
throw std::runtime_error("unsupported ELF byte order or section header size");
|
||||
RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table");
|
||||
|
||||
std::size_t dynSymOffset = 0u, dynSymSize = 0u;
|
||||
std::size_t gnuHashOffset = 0u, gnuHashSize = 0u;
|
||||
|
|
@ -326,6 +335,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type == kShtDynsym) {
|
||||
if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum)
|
||||
throw std::runtime_error("invalid ELF dynsym layout");
|
||||
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym");
|
||||
dynSymOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
dynSymSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
dynSymSectionIndex = i;
|
||||
|
|
@ -333,7 +345,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
if (shdr.sh_type == kShtGnuHash) {
|
||||
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
if (shdr.sh_type == kShtGnuVersym)
|
||||
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
|
|
@ -345,6 +356,10 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
|
||||
const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym);
|
||||
if (symCount == 0u) throw std::runtime_error(".dynsym is empty");
|
||||
if (gnuHashOffset != 0u) {
|
||||
ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
|
||||
const std::size_t dynStrSectionLink = [&]() -> std::size_t {
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
|
|
@ -359,6 +374,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
{
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type");
|
||||
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr");
|
||||
dynStrOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
dynStrSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
}
|
||||
|
|
@ -427,7 +444,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
const auto addName = [&](std::uint32_t oldOffset, std::string value) {
|
||||
const auto existing = std::find_if(
|
||||
names.begin(), names.end(),
|
||||
[&](const auto& entry) { return entry.first == oldOffset; }
|
||||
[&](const auto& entry) { return entry.first == oldOffset && entry.second == value; }
|
||||
);
|
||||
if (existing == names.end()) names.emplace_back(oldOffset, std::move(value));
|
||||
};
|
||||
|
|
@ -463,8 +480,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
newDynStr.push_back(0u);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> oldToNewOffset;
|
||||
for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second));
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> preservedToNewOffset;
|
||||
for (const auto offset : preservedOffsets)
|
||||
preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset)));
|
||||
|
||||
if (newDynStr.size() > dynStrSize) {
|
||||
throw std::runtime_error(
|
||||
|
|
@ -475,25 +493,15 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
newDynStr.resize(dynStrSize, 0u);
|
||||
|
||||
std::vector<std::string> newSymbolNames(symCount);
|
||||
for (std::size_t i = 1u; i < symCount; ++i) {
|
||||
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
|
||||
const auto sym = Read<Elf64_Sym>(elf, symOffset);
|
||||
if (sym.st_name == 0u) continue;
|
||||
|
||||
const auto mapped = std::find_if(
|
||||
oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == sym.st_name; }
|
||||
);
|
||||
if (mapped == oldToNewOffset.end()) continue;
|
||||
|
||||
newSymbolNames[i] = ReadCStr(newDynStr, mapped->second);
|
||||
Write(elf, symOffset + offsetof(Elf64_Sym, st_name), mapped->second);
|
||||
for (const auto& use : uses) {
|
||||
newSymbolNames[use.symIndex] = use.newValue;
|
||||
Write(elf, dynSymOffset + use.symIndex * sizeof(Elf64_Sym) + offsetof(Elf64_Sym, st_name), valueOffsets.at(use.newValue));
|
||||
}
|
||||
|
||||
std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size());
|
||||
|
||||
RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset);
|
||||
RemapVerneedOffsets(elf, ehdr, oldToNewOffset);
|
||||
RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset);
|
||||
RemapVerneedOffsets(elf, ehdr, preservedToNewOffset);
|
||||
|
||||
if (gnuHashOffset != 0u) {
|
||||
const auto bucketCount = Read<std::uint32_t>(elf, gnuHashOffset);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <nid/PeNidPatcher.hpp>
|
||||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <nid/NativePath.hpp>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
|
@ -49,7 +50,12 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
|||
if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports");
|
||||
const auto sections = optional + optionalSize;
|
||||
RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader));
|
||||
const auto headersSize = ReadValue<std::uint32_t>(binary, optional + 60);
|
||||
const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t {
|
||||
if (rva < headersSize && size <= headersSize - rva) {
|
||||
RequireRange(binary, rva, size);
|
||||
return rva;
|
||||
}
|
||||
for (std::uint16_t index = 0; index < sectionCount; ++index) {
|
||||
const auto section = ReadValue<PeSectionHeader>(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader));
|
||||
if (rva < section.VirtualAddress) continue;
|
||||
|
|
@ -64,14 +70,20 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
|||
const auto table = ReadValue<PeExportDirectory>(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory)));
|
||||
if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports");
|
||||
const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4);
|
||||
const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2);
|
||||
const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4);
|
||||
std::unordered_set<std::string> result;
|
||||
for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) {
|
||||
const auto ordinal = ReadValue<std::uint16_t>(binary, ordinals + std::uint64_t{index} * 2);
|
||||
if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal");
|
||||
if (!ReadValue<std::uint32_t>(binary, functions + std::uint64_t{ordinal} * 4)) continue;
|
||||
const auto rva = ReadValue<std::uint32_t>(binary, names + std::uint64_t{index} * 4);
|
||||
const auto offset = mapRva(rva, 1);
|
||||
auto name = ReadName(binary, offset, binary.size() - offset);
|
||||
mapRva(rva, name.size() + 1);
|
||||
if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name);
|
||||
}
|
||||
if (result.empty()) throw std::runtime_error("reference PE has no named exports");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +122,7 @@ std::string NormalizeExportName(const std::string& name) {
|
|||
|
||||
std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
|
||||
if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host");
|
||||
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
||||
std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate);
|
||||
if (!input) throw std::runtime_error("cannot open export reference: " + path);
|
||||
const auto size = input.tellg();
|
||||
if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <nid/BinaryPatcherFactory.hpp>
|
||||
#include <nid/ExportExclusions.hpp>
|
||||
#include <nid/NativePath.hpp>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
|
@ -9,20 +10,20 @@
|
|||
namespace {
|
||||
|
||||
std::vector<std::uint8_t> _readFile(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary);
|
||||
if (!f) throw std::runtime_error("cannot open: " + path);
|
||||
return {std::istreambuf_iterator<char>(f), {}};
|
||||
}
|
||||
|
||||
void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) {
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary);
|
||||
if (!f) throw std::runtime_error("cannot write: " + path);
|
||||
f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int Run(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n";
|
||||
return 1;
|
||||
|
|
@ -65,3 +66,27 @@ int main(int argc, char* argv[]) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t* argv[]) {
|
||||
try {
|
||||
std::vector<std::string> values;
|
||||
values.reserve(static_cast<std::size_t>(argc));
|
||||
for (int index = 0; index < argc; ++index) {
|
||||
const auto utf8 = std::filesystem::path(argv[index]).u8string();
|
||||
values.emplace_back(utf8.begin(), utf8.end());
|
||||
}
|
||||
std::vector<char*> arguments;
|
||||
arguments.reserve(values.size());
|
||||
for (auto& value : values) arguments.push_back(value.data());
|
||||
return Run(argc, arguments.data());
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "FAIL: " << error.what() << '\n';
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
#else
|
||||
int main(int argc, char* argv[]) {
|
||||
return Run(argc, argv);
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition) {
|
||||
if (!condition) throw std::runtime_error("NID buffer validation failed");
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
void RequireRejected(TAction action) {
|
||||
try { action(); }
|
||||
catch (const std::runtime_error&) { return; }
|
||||
throw std::runtime_error("Malformed NID buffer was accepted");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
using namespace Nid::Internal;
|
||||
const std::vector<std::uint8_t> unterminated{'n', 'a', 'm', 'e'};
|
||||
RequireRejected([&] { ReadCStr(unterminated, 0); });
|
||||
RequireRejected([&] { ReadCStr(unterminated, 3); });
|
||||
const std::vector<std::uint8_t> strings{'a', 0, 'b', 'c', 0};
|
||||
Require(ReadCStr(strings, 0) == "a");
|
||||
Require(ReadCStr(strings, 1).empty());
|
||||
Require(ReadCStr(strings, 2) == "bc");
|
||||
Require(ReadCStr(strings, 4).empty());
|
||||
RequireRejected([&] { ReadCStr(strings, strings.size()); });
|
||||
RequireRejected([&] { ReadCStr({}, 0); });
|
||||
|
||||
std::vector<std::uint8_t> bytes(8, 0xa5);
|
||||
const std::uint64_t value = 0x1020304050607080;
|
||||
Write(bytes, 0, value);
|
||||
Require(Read<std::uint64_t>(bytes, 0) == value);
|
||||
Write<std::uint32_t>(bytes, 4, 0x12345678);
|
||||
Require(Read<std::uint32_t>(bytes, 4) == 0x12345678);
|
||||
const auto before = bytes;
|
||||
for (const auto offset : {std::size_t{5}, std::size_t{8}, std::size_t{9},
|
||||
std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max() - 1,
|
||||
std::numeric_limits<std::size_t>::max() - 3}) {
|
||||
RequireRejected([&] { Read<std::uint32_t>(bytes, offset); });
|
||||
RequireRejected([&] { Write<std::uint32_t>(bytes, offset, 0); });
|
||||
Require(bytes == before);
|
||||
}
|
||||
RequireRejected([&] { Read<std::uint64_t>(bytes, 1); });
|
||||
std::vector<std::uint8_t> empty;
|
||||
RequireRejected([&] { Read<std::uint8_t>(empty, 0); });
|
||||
RequireRejected([&] { Write<std::uint8_t>(empty, 0, 0); });
|
||||
std::cout << "NID buffer validation passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF
|
||||
|
||||
|
||||
class ElfLayoutTests(unittest.TestCase):
|
||||
def test_declared_layout_must_match_supported_tables(self):
|
||||
changes = ((5, "B", 2), (0x3a, "H", 32),
|
||||
(SHOFF + 64 + 56, "Q", 8),
|
||||
(SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1),
|
||||
(SHOFF + 128 + 4, "I", 1))
|
||||
for offset, kind, value in changes:
|
||||
with self.subTest(offset=offset, value=value):
|
||||
image = build_image(symbols(False), False)
|
||||
struct.pack_into("<" + kind, image, offset, value)
|
||||
self.reject(image)
|
||||
|
||||
def test_table_ranges_and_links_are_checked(self):
|
||||
changes = ((0x28, "Q", 0x10000),
|
||||
(SHOFF + 64 + 24, "Q", 0xff0),
|
||||
(SHOFF + 64 + 40, "I", 100),
|
||||
(SHOFF + 128 + 24, "Q", 0xff0),
|
||||
(SHOFF + 128 + 32, "Q", 0xffffffffffffffff))
|
||||
for offset, kind, value in changes:
|
||||
with self.subTest(offset=offset, value=value):
|
||||
image = build_image(symbols(False), False)
|
||||
struct.pack_into("<" + kind, image, offset, value)
|
||||
self.reject(image)
|
||||
|
||||
def reject(self, image):
|
||||
result, after = patch(PATCHER, bytes(image), "layout.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr")
|
||||
self.assertEqual(after, image)
|
||||
|
||||
def test_valid_supported_layout_still_patches(self):
|
||||
image = build_image(symbols(False), False)
|
||||
result, after = patch(PATCHER, bytes(image), "valid-layout.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertNotEqual(after, image)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash
|
||||
|
||||
|
||||
class GnuHashTests(unittest.TestCase):
|
||||
def image(self, buckets=4, bloom=1, shift=6, symoffset=3):
|
||||
image = build_image(symbols(False), False)
|
||||
count = len(symbols(False))
|
||||
struct.pack_into("<IIII", image, HASH_OFFSET, buckets, symoffset, bloom, shift)
|
||||
size = 16 + 8 * bloom + 4 * buckets + 4 * max(0, count - symoffset)
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
return image
|
||||
|
||||
def test_non_power_of_two_bloom_is_rejected(self):
|
||||
self.reject(self.image(bloom=3))
|
||||
|
||||
def test_hash_shift_outside_word_is_rejected(self):
|
||||
self.reject(self.image(shift=32))
|
||||
|
||||
def test_unsafe_geometry_is_rejected(self):
|
||||
for fields in ({"bloom": 0}, {"buckets": 0}, {"symoffset": 100}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(self.image(**fields))
|
||||
|
||||
def test_declared_section_extent_is_checked(self):
|
||||
for size in (12, 16, 0x10000):
|
||||
with self.subTest(size=size):
|
||||
image = self.image()
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
self.reject(image)
|
||||
|
||||
def reject(self, image):
|
||||
result, after = patch(PATCHER, bytes(image), "geometry.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"gnu.hash", result.stderr)
|
||||
self.assertEqual(after, image)
|
||||
|
||||
def test_rebuilt_hash_finds_every_export(self):
|
||||
image = self.image(bloom=2)
|
||||
result, after = patch(PATCHER, image, "valid.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
entries = read_dynsym(after)
|
||||
buckets, first, bloom, shift = struct.unpack_from("<4I", after, HASH_OFFSET)
|
||||
bucket_offset = HASH_OFFSET + 16 + bloom * 8
|
||||
chain_offset = bucket_offset + buckets * 4
|
||||
for index in range(first, len(entries)):
|
||||
name = entries[index][0]
|
||||
hashed = gnu_hash(name)
|
||||
word = struct.unpack_from("<Q", after, HASH_OFFSET + 16 + ((hashed // 64) % bloom) * 8)[0]
|
||||
self.assertTrue(word & (1 << (hashed % 64)))
|
||||
self.assertTrue(word & (1 << ((hashed >> shift) % 64)))
|
||||
start = struct.unpack_from("<I", after, bucket_offset + (hashed % buckets) * 4)[0]
|
||||
found = False
|
||||
for candidate in range(start, len(entries)):
|
||||
value = struct.unpack_from("<I", after, chain_offset + (candidate - first) * 4)[0]
|
||||
if (value | 1) == (hashed | 1) and entries[candidate][0] == name:
|
||||
found = True
|
||||
if value & 1:
|
||||
break
|
||||
self.assertTrue(found, name)
|
||||
|
||||
def test_empty_hash_with_no_buckets_is_supported(self):
|
||||
image = self.image(buckets=0, symoffset=len(symbols(False)))
|
||||
result, after = patch(PATCHER, image, "empty.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid
|
||||
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
|
||||
|
||||
class NativePathTests(unittest.TestCase):
|
||||
def invoke(self, *arguments):
|
||||
return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)],
|
||||
capture_output=True, encoding="utf-8", errors="replace", timeout=10)
|
||||
|
||||
def test_non_ascii_directory_and_filename_patch_the_intended_file(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary) / "\u6e2c\u8a66"
|
||||
root.mkdir()
|
||||
file = root / "\u043c\u043e\u0434\u0443\u043b\u044c.prx"
|
||||
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
|
||||
result = self.invoke(file)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(file.read_bytes()), {"_" + nid("puts"): (0, 0x100)})
|
||||
self.assertIn(str(file), result.stdout)
|
||||
self.assertEqual(list(root.iterdir()), [file])
|
||||
|
||||
def test_non_ascii_export_reference_preserves_the_named_export(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
reference = root / "\u6e2c\u8a66-reference.prx"
|
||||
original = build_dylib([("_puts", 0, 0x100)], [])
|
||||
reference.write_bytes(original)
|
||||
file = root / "target.prx"
|
||||
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
|
||||
result = self.invoke("--preserve-exports", reference, file)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(file.read_bytes()), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
|
||||
self.assertEqual(reference.read_bytes(), original)
|
||||
|
||||
def test_missing_unicode_input_does_not_modify_another_file(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
existing = root / "existing.prx"
|
||||
existing.write_bytes(b"unchanged")
|
||||
missing = root / "\u6e2c\u8a66-missing.prx"
|
||||
result = self.invoke(missing)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn(str(missing), result.stderr)
|
||||
self.assertFalse(missing.exists())
|
||||
self.assertEqual(existing.read_bytes(), b"unchanged")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid
|
||||
|
||||
|
||||
def reference(*, headers=False, address=0x2000, ordinal=0):
|
||||
image = bytearray(1280)
|
||||
image[:2] = b"MZ"
|
||||
struct.pack_into("<I", image, 0x3c, 0x40)
|
||||
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664, 2, 0, 0, 0, 240, 0x2022)
|
||||
optional = 0x58
|
||||
struct.pack_into("<H", image, optional, 0x20b)
|
||||
struct.pack_into("<I", image, optional + 60, 512)
|
||||
struct.pack_into("<I", image, optional + 108, 16)
|
||||
start, rva = (416, 416) if headers else (512, 0x1000)
|
||||
struct.pack_into("<II", image, optional + 112, rva, 80)
|
||||
for index, (name, va, offset, size) in enumerate(((b".edata", 0x1000, 512, 256), (b".text", 0x2000, 768, 512))):
|
||||
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + index * 40,
|
||||
name, size, va, size, offset, 0, 0, 0, 0, 0x40000040)
|
||||
struct.pack_into("<IIHHIIIIIII", image, start, 0, 0, 0, 0, rva + 50, 1, 1, 1, rva + 40, rva + 44, rva + 48)
|
||||
struct.pack_into("<IIH", image, start + 40, address, rva + 50, ordinal)
|
||||
name = b"puts_nid_postfix\0"
|
||||
image[start + 50:start + 50 + len(name)] = name
|
||||
return image
|
||||
|
||||
|
||||
class ReferenceTests(unittest.TestCase):
|
||||
def patch_with_reference(self, data):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
source, ref = root / "target.dylib", root / "reference.bin"
|
||||
original = build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])
|
||||
source.write_bytes(original)
|
||||
ref.write_bytes(data)
|
||||
result = subprocess.run([str(PATCHER), "libc", str(source), "--preserve-exports", str(ref)], capture_output=True, text=True)
|
||||
self.assertEqual(ref.read_bytes(), data)
|
||||
return result, source.read_bytes(), original
|
||||
|
||||
def test_header_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference(headers=True))
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(image), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
|
||||
|
||||
def test_section_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference())
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("_puts_nid_postfix", exports_of(image))
|
||||
|
||||
def test_null_function_slot_does_not_preserve_a_name(self):
|
||||
result, image, original = self.patch_with_reference(reference(address=0))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("no named exports", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
def test_invalid_name_ordinal_is_rejected_without_changing_target(self):
|
||||
result, image, original = self.patch_with_reference(reference(ordinal=1))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("ordinal", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS
|
||||
|
||||
|
||||
class SharedStringTests(unittest.TestCase):
|
||||
def test_dynamic_names_keep_original_string_when_export_shares_offset(self):
|
||||
for tag in (1, 14, 15, 29):
|
||||
with self.subTest(tag=tag):
|
||||
data = build_image(symbols(False), False)
|
||||
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 6, 2, 0, 0x580, 32, 2, 0, 8, 16)
|
||||
struct.pack_into("<qQqQ", data, 0x580, tag, name_offset, 0, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "shared-dynamic.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
new_offset = struct.unpack_from("<Q", after, 0x588)[0]
|
||||
start = strings[4] + new_offset
|
||||
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
|
||||
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
|
||||
self.assertNotEqual(renamed, DEFINITIONS[0][0])
|
||||
|
||||
def test_version_requirement_strings_keep_original_names(self):
|
||||
data = build_image(symbols(False), False)
|
||||
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, 0x580, 32, 2, 1, 8, 0)
|
||||
struct.pack_into("<HHIII", data, 0x580, 1, 1, name_offset, 16, 0)
|
||||
struct.pack_into("<IHHII", data, 0x590, 0, 0, 2, name_offset, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "shared-version.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for field in (0x584, 0x598):
|
||||
offset = struct.unpack_from("<I", after, field)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
|
||||
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
|
||||
self.assertNotEqual(renamed, DEFINITIONS[0][0])
|
||||
|
||||
def test_local_symbol_keeps_name_shared_with_renamed_global(self):
|
||||
entries = symbols(False)
|
||||
entries[1] = (DEFINITIONS[0][0], 1, 9, 2, 0x1500)
|
||||
data = build_image(entries, False)
|
||||
data[DYNSYM_OFFSET + 24 + 4] = 2
|
||||
result, after = patch(PATCHER, bytes(data), "shared-local.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
names = {value: name for name, _, value in read_dynsym(after)}
|
||||
self.assertEqual(names[0x1500], DEFINITIONS[0][0])
|
||||
self.assertNotEqual(names[DEFINITIONS[0][2]], DEFINITIONS[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections
|
||||
|
||||
VERNEED = 0x580
|
||||
|
||||
|
||||
def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0):
|
||||
data = build_image(symbols(False), False)
|
||||
strings = sections(data)[2]
|
||||
filename = strings[5]
|
||||
names = b"libfixture.so\0FIXTURE_1\0"
|
||||
data[DYNSTR_OFFSET + filename:DYNSTR_OFFSET + filename + len(names)] = names
|
||||
struct.pack_into("<Q", data, SHOFF + 2 * 64 + 32, filename + len(names))
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, VERNEED, size, 2, 1, 8, 0)
|
||||
struct.pack_into("<HHIII", data, VERNEED, version, count, filename, auxiliary, nextrecord)
|
||||
struct.pack_into("<IHHII", data, VERNEED + auxiliary, 0, 0, 2, filename + len(b"libfixture.so\0"), auxnext)
|
||||
return data
|
||||
|
||||
|
||||
class VersionRequirementsTests(unittest.TestCase):
|
||||
def test_auxiliary_record_cannot_leave_declared_section(self):
|
||||
self.reject(image(auxiliary=32))
|
||||
|
||||
def test_auxiliary_count_must_match_chain(self):
|
||||
self.reject(image(count=2))
|
||||
|
||||
def test_unsupported_structure_version_is_rejected(self):
|
||||
self.reject(image(version=2))
|
||||
|
||||
def test_invalid_forward_links_and_section_ranges_are_rejected(self):
|
||||
for fields in ({"auxnext": 16}, {"nextrecord": 64}, {"size": 15},
|
||||
{"size": 0x10000}, {"auxiliary": 8}, {"count": 2, "auxnext": 8}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(image(**fields))
|
||||
|
||||
def test_valid_multiple_auxiliaries_and_requirements(self):
|
||||
data = image(count=2, size=80, auxnext=16, nextrecord=48)
|
||||
original_name = struct.unpack_from("<I", data, VERNEED + 24)[0]
|
||||
original_file = struct.unpack_from("<I", data, VERNEED + 4)[0]
|
||||
struct.pack_into("<IHHII", data, VERNEED + 32, 0, 0, 3, original_name, 0)
|
||||
struct.pack_into("<HHIII", data, VERNEED + 48, 1, 1, original_file, 16, 0)
|
||||
struct.pack_into("<IHHII", data, VERNEED + 64, 0, 0, 4, original_name, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "multiple-versions.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for position, expected in ((4, b"libfixture.so"), (24, b"FIXTURE_1"),
|
||||
(40, b"FIXTURE_1"), (52, b"libfixture.so"), (72, b"FIXTURE_1")):
|
||||
offset = struct.unpack_from("<I", after, VERNEED + position)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)], expected)
|
||||
|
||||
def reject(self, data):
|
||||
result, after = patch(PATCHER, bytes(data), "versions.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"version_r", result.stderr)
|
||||
self.assertEqual(after, data)
|
||||
|
||||
def test_valid_names_are_preserved_when_strings_are_rebuilt(self):
|
||||
data = image()
|
||||
result, after = patch(PATCHER, bytes(data), "valid-versions.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for position, expected in ((VERNEED + 4, b"libfixture.so"), (VERNEED + 16 + 8, b"FIXTURE_1")):
|
||||
offset = struct.unpack_from("<I", after, position)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)], expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -33,6 +33,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
|
||||
|
|
@ -95,6 +96,19 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp)
|
||||
target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_semaphore_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp)
|
||||
target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev
|
|||
Agc::Command::CheckAddress(guestAddress, 8, __func__);
|
||||
return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast<std::uint32_t>(guestAddress), static_cast<std::uint32_t>(guestAddress >> 32u)}, __func__);
|
||||
}
|
||||
Agc::Command::Require(address == nullptr, __func__, "this event does not use an address");
|
||||
const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u;
|
||||
return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#include "prx/libSceAgc/Misc/include/Semaphore.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
|
||||
constexpr std::uint64_t semaphoreLabelBytes = 32u;
|
||||
constexpr std::uint64_t semaphoreCounterBytes = 8u;
|
||||
|
||||
std::mutex semaphoreMutex;
|
||||
std::uint8_t* semaphoreMemory = nullptr;
|
||||
std::uint64_t semaphoreBytes = 0;
|
||||
|
||||
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
|
||||
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
|
||||
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
|
||||
if (memory[label + byte] != 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(memory);
|
||||
std::lock_guard lock(semaphoreMutex);
|
||||
Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown");
|
||||
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
|
||||
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
|
||||
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
|
||||
auto* labels = static_cast<std::uint8_t*>(memory);
|
||||
Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown");
|
||||
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
|
||||
std::memset(labels + label, 0, semaphoreCounterBytes);
|
||||
}
|
||||
semaphoreMemory = labels;
|
||||
semaphoreBytes = sizeInBytes;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
|
||||
std::lock_guard lock(semaphoreMutex);
|
||||
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
|
||||
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
|
||||
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
|
||||
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -35,16 +35,6 @@ int APS5_VABI sceAgcGetDefaultCxStateFlat() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcGetSemaphoreLabel() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcSetAmmSemaphoreMemory() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcSetSemaphoreMemory() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address);
|
||||
extern "C" std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
|
||||
|
|
@ -641,6 +643,47 @@ void testDefaults() {
|
|||
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
|
||||
}
|
||||
|
||||
void testEventWrite() {
|
||||
Storage storage;
|
||||
const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1000));
|
||||
check(samplePacket == storage.words.data(), "incorrect event 0x38 return pointer");
|
||||
check(storage.words[0] == 0xc0024600u, "incorrect event 0x38 packet header");
|
||||
check(storage.words[1] == (0x100u | 0x38u), "incorrect event 0x38 first payload word");
|
||||
check(storage.words[2] == 0x1000u, "incorrect event 0x38 low address word");
|
||||
check(storage.words[3] == 0u, "incorrect event 0x38 high address word");
|
||||
check(sceAgcDcbEventWriteGetSize(0x38) == 16, "incorrect event 0x38 size");
|
||||
check(sceAgcDcbEventWriteGetSize(0x39) == 16, "incorrect event 0x39 size");
|
||||
|
||||
Storage sample39;
|
||||
sceAgcDcbEventWrite(&sample39.buffer, 0x39, reinterpret_cast<const void*>(0x2008));
|
||||
check(sample39.words[0] == 0xc0024600u && sample39.words[1] == (0x100u | 0x39u) && sample39.words[2] == 0x2008u && sample39.words[3] == 0u, "incorrect event 0x39 packet");
|
||||
|
||||
expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1001)); });
|
||||
expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x40, nullptr); });
|
||||
|
||||
Storage partialFlush;
|
||||
sceAgcDcbEventWrite(&partialFlush.buffer, 7, nullptr);
|
||||
check(partialFlush.words[0] == 0xc0004600u && partialFlush.words[1] == 0x407u, "incorrect event 7 partial flush packet");
|
||||
check(sceAgcDcbEventWriteGetSize(7) == 8, "incorrect event 7 size");
|
||||
|
||||
Storage partialFlush15;
|
||||
sceAgcDcbEventWrite(&partialFlush15.buffer, 15, nullptr);
|
||||
check(partialFlush15.words[0] == 0xc0004600u && partialFlush15.words[1] == 0x40fu, "incorrect event 15 partial flush packet");
|
||||
|
||||
Storage partialFlush16;
|
||||
sceAgcDcbEventWrite(&partialFlush16.buffer, 16, nullptr);
|
||||
check(partialFlush16.words[0] == 0xc0004600u && partialFlush16.words[1] == 0x410u, "incorrect event 16 partial flush packet");
|
||||
|
||||
Storage cacheFlushNull;
|
||||
sceAgcDcbEventWrite(&cacheFlushNull.buffer, 0x16, nullptr);
|
||||
check(cacheFlushNull.words[0] == 0xc0004600u && cacheFlushNull.words[1] == 0x16u, "incorrect event 0x16 null address packet");
|
||||
check(sceAgcDcbEventWriteGetSize(0x16) == 8, "incorrect event 0x16 size");
|
||||
|
||||
Storage cacheFlushWithAddress;
|
||||
sceAgcDcbEventWrite(&cacheFlushWithAddress.buffer, 0x16, reinterpret_cast<const void*>(0x2d2a10000ull));
|
||||
check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
|
@ -652,6 +695,7 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
testPackets();
|
||||
testEventWrite();
|
||||
testNop();
|
||||
testClearState();
|
||||
testIndexedIndirectDraws();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
|
||||
extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t blockBytes = 0x4000u;
|
||||
constexpr std::uint64_t labelBytes = 32u;
|
||||
constexpr std::uint64_t counterBytes = 8u;
|
||||
constexpr std::uint64_t memoryBytes = 2u * blockBytes;
|
||||
constexpr std::uint64_t storageBytes = 4u * blockBytes;
|
||||
constexpr std::uint32_t labelCount = static_cast<std::uint32_t>(memoryBytes / labelBytes);
|
||||
constexpr std::uint8_t guardByte = 0xa5u;
|
||||
constexpr std::uint8_t counterByte = 0x5au;
|
||||
|
||||
struct AlignedRelease {
|
||||
void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); }
|
||||
};
|
||||
|
||||
std::unique_ptr<std::uint8_t, AlignedRelease> storage;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template <typename TAction>
|
||||
void expectFailure(TAction action) {
|
||||
try {
|
||||
action();
|
||||
} catch (const std::runtime_error& error) {
|
||||
check(error.what()[0] != '\0', "empty exception message");
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("expected an exception");
|
||||
}
|
||||
|
||||
std::uint8_t* memory() {
|
||||
return storage.get() + blockBytes;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> snapshot() {
|
||||
return {storage.get(), storage.get() + storageBytes};
|
||||
}
|
||||
|
||||
void checkUnchanged(const std::vector<std::uint8_t>& before, const char* message) {
|
||||
check(std::equal(before.begin(), before.end(), storage.get()), message);
|
||||
}
|
||||
|
||||
void fillStorage() {
|
||||
std::fill(storage.get(), storage.get() + storageBytes, guardByte);
|
||||
for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) {
|
||||
std::fill(memory() + label, memory() + label + counterBytes, counterByte);
|
||||
std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0});
|
||||
}
|
||||
}
|
||||
|
||||
void testUnregistered() {
|
||||
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
|
||||
expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); });
|
||||
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
|
||||
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
|
||||
}
|
||||
|
||||
void testRegistrationRejections() {
|
||||
const auto before = snapshot();
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); });
|
||||
expectFailure([] {
|
||||
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
|
||||
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
|
||||
});
|
||||
checkUnchanged(before, "a rejected registration changed memory");
|
||||
testUnregistered();
|
||||
}
|
||||
|
||||
void testUnsettledClearing() {
|
||||
for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) {
|
||||
memory()[offset] = 0x77u;
|
||||
const auto before = snapshot();
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
|
||||
checkUnchanged(before, "a registration with unsettled clearing changed memory");
|
||||
testUnregistered();
|
||||
memory()[offset] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void testRegistration() {
|
||||
const auto before = snapshot();
|
||||
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
|
||||
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
|
||||
check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory");
|
||||
check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory");
|
||||
}
|
||||
|
||||
void testLabels() {
|
||||
for (const std::uint32_t index : {0u, 1u, 7u, labelCount / 2u, labelCount - 1u}) {
|
||||
std::array<void*, 3> labels{storage.get(), nullptr, storage.get()};
|
||||
check(sceAgcGetSemaphoreLabel(index, &labels[1]) == 0, "a semaphore label lookup failed");
|
||||
check(labels[1] == memory() + index * labelBytes, "incorrect semaphore label address");
|
||||
check(labels[0] == storage.get() && labels[2] == storage.get(), "a semaphore label lookup wrote past its output");
|
||||
}
|
||||
}
|
||||
|
||||
void testLabelRejections() {
|
||||
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
|
||||
void* label = storage.get();
|
||||
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
|
||||
check(label == storage.get(), "a rejected semaphore label changed the output");
|
||||
}
|
||||
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
|
||||
}
|
||||
|
||||
void testRepeatedRegistration() {
|
||||
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
|
||||
const auto before = snapshot();
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
|
||||
checkUnchanged(before, "a repeated registration changed memory");
|
||||
void* label = nullptr;
|
||||
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
storage.reset(static_cast<std::uint8_t*>(::operator new(storageBytes, std::align_val_t{blockBytes})));
|
||||
fillStorage();
|
||||
testUnregistered();
|
||||
testRegistrationRejections();
|
||||
testUnsettledClearing();
|
||||
testRegistration();
|
||||
testLabels();
|
||||
testLabelRejections();
|
||||
testRepeatedRegistration();
|
||||
storage.reset();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC semaphore tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -325,6 +325,29 @@ foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
|
|||
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
endforeach()
|
||||
|
||||
add_test_executable(agc_driver_register_key_tests EXCLUDE_FROM_ALL tests/RegisterKey.cpp)
|
||||
target_include_directories(agc_driver_register_key_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_driver_register_key_tests PRIVATE
|
||||
libSceAgcDriver
|
||||
libc
|
||||
libkernel
|
||||
)
|
||||
set_target_properties(agc_driver_register_key_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_register_key_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_driver_register_key_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_driver_register_key COMMAND agc_driver_register_key_tests)
|
||||
set_tests_properties(agc_driver_register_key PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_VERIFY_REGISTER_KEYS=1")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_register_key PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL
|
||||
tests/Submit.cpp
|
||||
)
|
||||
|
|
@ -543,6 +566,11 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv
|
|||
set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_masked_select_tests)
|
||||
|
||||
add_test_executable(agc_ir_reclaim_tests EXCLUDE_FROM_ALL tests/IrReclaim.cpp)
|
||||
target_link_libraries(agc_ir_reclaim_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_ir_reclaim_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_ir_reclaim_tests)
|
||||
|
||||
add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp)
|
||||
target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
|
@ -705,6 +733,11 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_formatted_buffer_byte_stores_tests EXCLUDE_FROM_ALL tests/FormattedBufferByteStores.cpp)
|
||||
target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_formatted_buffer_byte_stores_tests)
|
||||
|
||||
add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp)
|
||||
target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
|
|
@ -834,6 +867,19 @@ target_link_libraries(agc_driver_cmask_fast_clear_tests PRIVATE
|
|||
set_target_properties(agc_driver_cmask_fast_clear_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_cmask_fast_clear_tests)
|
||||
|
||||
set(agcBufferUpdateSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/BufferUpdate.comp)
|
||||
set(agcBufferUpdateSpv ${agcTestShaderGeneratedRoot}/BufferUpdate.spv)
|
||||
set(agcBufferUpdateHeader ${agcTestShaderGeneratedRoot}/BufferUpdate_spv.h)
|
||||
add_custom_command(
|
||||
OUTPUT "${agcBufferUpdateHeader}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
|
||||
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcBufferUpdateSpv}" "${agcBufferUpdateSource}"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcBufferUpdateSpv} -DOUTPUT=${agcBufferUpdateHeader}
|
||||
-DSYMBOL=BUFFER_UPDATE_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${agcBufferUpdateSource}" glslang-standalone
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
|
||||
tests/Recorder.cpp
|
||||
tests/ResidentPresent.cpp
|
||||
|
|
@ -841,6 +887,7 @@ add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
|
|||
${agcSampleLodShaderHeader}
|
||||
${agcSampleArrayShaderHeader}
|
||||
${agcCoverShaderHeaders}
|
||||
${agcBufferUpdateHeader}
|
||||
)
|
||||
|
||||
target_include_directories(agc_driver_recorder_tests PRIVATE
|
||||
|
|
@ -908,6 +955,12 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(agc_storage_written_buffer_order PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_copied_direct_ordering COMMAND agc_driver_recorder_tests --copied-direct-only)
|
||||
set_tests_properties(agc_copied_direct_ordering PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only)
|
||||
set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only)
|
||||
|
|
@ -916,6 +969,11 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
|
||||
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
|
||||
if(WIN32)
|
||||
add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only)
|
||||
set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only)
|
||||
set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only)
|
||||
|
|
@ -927,6 +985,9 @@ if(BUILD_TESTING)
|
|||
if(WIN32)
|
||||
set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
|
||||
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
@ -987,47 +1048,11 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
|
|||
tests/GuestSamplerResource.cpp
|
||||
tests/TextureDetiler.cpp
|
||||
tests/DepthSurfaceReuse.cpp
|
||||
Graphics/src/Draw.cpp
|
||||
Graphics/src/DepthSurface.cpp
|
||||
Graphics/src/MultisampleTarget.cpp
|
||||
Graphics/src/Pipeline.cpp
|
||||
Graphics/src/PipelineLibrary.cpp
|
||||
Graphics/src/State.cpp
|
||||
Graphics/src/ColorTargetLayout.cpp
|
||||
Graphics/src/ColorTargetTransfer.cpp
|
||||
Graphics/src/Resources.cpp
|
||||
Graphics/src/PlaceholderImages.cpp
|
||||
Graphics/src/ShaderResources.cpp
|
||||
Graphics/src/ShaderAddressResources.cpp
|
||||
Graphics/src/GuestBufferMemory.cpp
|
||||
Graphics/src/BdaResources.cpp
|
||||
Graphics/src/BufferAddress.cpp
|
||||
Graphics/src/ShaderValidation.cpp
|
||||
Graphics/src/GuestTextureResource.cpp
|
||||
Graphics/src/GuestSamplerResource.cpp
|
||||
Graphics/src/TextureFormat.cpp
|
||||
Graphics/src/DccMetadata.cpp
|
||||
Graphics/src/TextureTiling.cpp
|
||||
Graphics/src/TextureDetiler.cpp
|
||||
Graphics/src/Texture.cpp
|
||||
Graphics/src/PassHazards.cpp
|
||||
Graphics/src/UnitShadow.cpp
|
||||
Graphics/src/Sampler.cpp
|
||||
Graphics/src/Recorder.cpp
|
||||
Graphics/src/ParallelCompare.cpp
|
||||
Graphics/src/ShaderInputState.cpp
|
||||
${agcTextureDetileShaderHeader}
|
||||
${agcCmaskClearShaderHeader}
|
||||
${agcMeshArgumentsShaderHeader}
|
||||
${agcTextureDetileImageShaderHeader}
|
||||
${agcSampleCounterShaderHeader}
|
||||
${agcSampleDumpsShaderHeader}
|
||||
Execution/src/GuestMemory.cpp
|
||||
Execution/src/Pm4.cpp
|
||||
)
|
||||
|
||||
target_include_directories(agc_driver_graphics_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
${agcTextureDetileGeneratedRoot}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler/IntermediateRepresentation/include
|
||||
|
|
@ -1038,7 +1063,7 @@ target_include_directories(agc_driver_graphics_tests PRIVATE
|
|||
)
|
||||
|
||||
target_link_libraries(agc_driver_graphics_tests PRIVATE
|
||||
shader_recompiler_agc_driver
|
||||
libSceAgcDriver
|
||||
libc
|
||||
libkernel
|
||||
)
|
||||
|
|
@ -1046,45 +1071,71 @@ target_link_libraries(agc_driver_graphics_tests PRIVATE
|
|||
set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_graphics_tests)
|
||||
|
||||
foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag)
|
||||
string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}")
|
||||
string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol)
|
||||
set(agcVertexRangeSpv ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}.spv)
|
||||
set(agcVertexRangeHeader ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}_spv.h)
|
||||
add_custom_command(
|
||||
OUTPUT "${agcVertexRangeHeader}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
|
||||
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcVertexRangeSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcVertexRangeSpv} -DOUTPUT=${agcVertexRangeHeader}
|
||||
-DSYMBOL=${agcVertexRangeSymbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" glslang-standalone
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND agcVertexRangeHeaders ${agcVertexRangeHeader})
|
||||
endforeach()
|
||||
|
||||
target_sources(agc_driver_recorder_tests PRIVATE ${agcVertexRangeHeaders})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_vertex_target_range_copied COMMAND agc_driver_recorder_tests --vertex-target-range-copied)
|
||||
add_test(NAME agc_driver_vertex_target_range_imported COMMAND agc_driver_recorder_tests --vertex-target-range-imported)
|
||||
set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL
|
||||
tests/BdaDevice.cpp
|
||||
tests/BdaShader.cpp
|
||||
tests/BdaExecution.cpp
|
||||
tests/BdaContracts.cpp
|
||||
Execution/src/BdaFeatures.cpp
|
||||
Graphics/src/Resources.cpp
|
||||
Graphics/src/PlaceholderImages.cpp
|
||||
Graphics/src/BufferAddress.cpp
|
||||
)
|
||||
|
||||
target_include_directories(agc_driver_bda_device_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shader_recompiler_agc_driver)
|
||||
target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc)
|
||||
set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_bda_device_tests)
|
||||
|
||||
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL
|
||||
tests/Mesh.cpp
|
||||
${agcDriverSources}
|
||||
)
|
||||
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp)
|
||||
target_include_directories(agc_driver_mesh_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
${agcTextureDetileGeneratedRoot}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_driver_mesh_tests PRIVATE shader_recompiler_agc_driver SDL2-static libc libkernel jpeg_turbo ${CMAKE_DL_LIBS})
|
||||
add_dependencies(agc_driver_mesh_tests agc_trace_revision)
|
||||
target_link_libraries(agc_driver_mesh_tests PRIVATE libSceAgcDriver SDL2-static libc)
|
||||
set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_mesh_tests)
|
||||
|
||||
set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all")
|
||||
file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp)
|
||||
set(agcExecutionTests)
|
||||
foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
|
||||
get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE)
|
||||
if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS)
|
||||
set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp)
|
||||
endif()
|
||||
string(REGEX REPLACE "([a-z0-9])([A-Z])" "\\1_\\2" agcExecutionTestName "${agcExecutionTestName}")
|
||||
string(TOLOWER "agc_driver_${agcExecutionTestName}_tests" agcExecutionTest)
|
||||
add_test_executable(${agcExecutionTest} EXCLUDE_FROM_ALL ${agcExecutionTestSource})
|
||||
|
|
@ -1099,6 +1150,11 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
|
|||
configure_windows_unwind(${agcExecutionTest})
|
||||
list(APPEND agcExecutionTests ${agcExecutionTest})
|
||||
endforeach()
|
||||
foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
|
||||
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp)
|
||||
message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
|
||||
|
||||
|
|
@ -1169,7 +1225,7 @@ if(BUILD_TESTING)
|
|||
if(WIN32)
|
||||
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests})
|
||||
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests})
|
||||
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
|
||||
continue()
|
||||
endif()
|
||||
|
|
@ -1182,6 +1238,11 @@ if(BUILD_TESTING)
|
|||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
|
||||
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes)
|
||||
set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30)
|
||||
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
|
||||
|
|
@ -1227,11 +1288,25 @@ if(BUILD_TESTING)
|
|||
if(WIN32)
|
||||
set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_layered_volume_vertex COMMAND agc_driver_layered_volume_draw_tests --vertex-only)
|
||||
add_test(NAME agc_driver_layered_volume_vertex_no_gpl COMMAND agc_driver_layered_volume_draw_tests --vertex-only)
|
||||
set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_property(TEST agc_driver_layered_volume_vertex_no_gpl APPEND PROPERTY ENVIRONMENT "APS5_NO_GPL=1")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode)
|
||||
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv)
|
||||
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests)
|
||||
set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests)
|
||||
|
|
@ -1288,8 +1363,10 @@ if(BUILD_TESTING)
|
|||
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
|
||||
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
|
||||
add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown)
|
||||
set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
|
|
@ -1503,8 +1580,11 @@ if(BUILD_TESTING)
|
|||
configure_windows_unwind(agc_compute_pipeline_capture_tests)
|
||||
add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags)
|
||||
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30)
|
||||
add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state)
|
||||
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
|
||||
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
|
||||
|
|
@ -1809,6 +1889,21 @@ add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/Positio
|
|||
target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_position_invariance_tests)
|
||||
|
||||
add_test_executable(agc_vertex_layer_export_tests EXCLUDE_FROM_ALL tests/VertexLayerExport.cpp)
|
||||
target_link_libraries(agc_vertex_layer_export_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_vertex_layer_export_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_vertex_layer_export_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_vertex_layer_export_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_vertex_layer_export COMMAND agc_vertex_layer_export_tests)
|
||||
set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_position_invariance_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_position_invariance COMMAND agc_position_invariance_tests)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ namespace AgcDriver::DriverDetail {
|
|||
|
||||
double TraceMs();
|
||||
|
||||
std::uint64_t SkippedShader();
|
||||
|
||||
void require(bool condition, const char* reason);
|
||||
|
||||
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);
|
||||
|
|
|
|||
|
|
@ -75,6 +75,19 @@ struct DrawEntryCounters {
|
|||
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
|
||||
};
|
||||
|
||||
class DrawKeySightings {
|
||||
public:
|
||||
static constexpr std::size_t Ways = 4;
|
||||
explicit DrawKeySightings(std::size_t keys);
|
||||
bool SeenBefore(std::uint64_t key);
|
||||
|
||||
private:
|
||||
std::vector<std::uint64_t> keys;
|
||||
std::vector<std::uint8_t> filled;
|
||||
std::vector<std::uint8_t> next;
|
||||
std::size_t bucketMask = 0;
|
||||
};
|
||||
|
||||
enum class DrawVerdict { Drawn, Nothing, Rejected };
|
||||
|
||||
struct StageCapture {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -115,6 +115,7 @@ private:
|
|||
static std::size_t drawCacheEntries();
|
||||
void accountDrawVariant(const DispatchVariant& variant, bool added);
|
||||
void insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode);
|
||||
bool admitDrawKey(std::uint64_t key);
|
||||
void eraseDrawEntry(std::uint64_t key, const std::shared_ptr<DrawEntry>& expected);
|
||||
std::shared_ptr<const DrawRecipe> findDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages);
|
||||
void attachDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages, std::shared_ptr<const DrawRecipe> recipe);
|
||||
|
|
@ -310,6 +311,7 @@ private:
|
|||
std::list<std::uint64_t> drawOrder;
|
||||
std::mutex drawCacheMutex;
|
||||
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
|
||||
std::optional<DrawKeySightings> drawKeySightings;
|
||||
|
||||
DrawEntryCounters drawEntryCounters;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP
|
||||
|
||||
#include "Recompiler.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
|
@ -18,6 +20,10 @@ struct DwordPatternFill {
|
|||
};
|
||||
|
||||
std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern);
|
||||
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ struct PreparedShaders : PreparedShaderState {
|
|||
std::mutex mutex;
|
||||
std::condition_variable settled;
|
||||
std::exception_ptr failure;
|
||||
std::atomic<bool> demanded{false};
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
|
|
@ -107,7 +108,8 @@ std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const Shade
|
|||
std::shared_ptr<const ShaderSnapshot> RegisteredHeader(const ShaderRegistry& registry, const Shader* shader);
|
||||
std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list<std::uint8_t> types);
|
||||
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
|
||||
void SubmitRegistrationPreparation(std::function<void()> task);
|
||||
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared = nullptr);
|
||||
void PrioritizeRegistrationPreparation(PreparedShaders& prepared);
|
||||
void SubmitLibraryPrebuild(std::function<void()> task);
|
||||
void CloseRegistrationPreparation();
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ struct UnchangedQuery {
|
|||
};
|
||||
bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
|
||||
bool UnchangedSinceAll(std::span<const UnchangedQuery> queries);
|
||||
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges);
|
||||
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation);
|
||||
// Returns the generation the blocks were stamped with (0 when the arena is not write-watched):
|
||||
// UnchangedSince(range, it) holds until the next store over the range.
|
||||
std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
|
@ -23,6 +24,29 @@ public:
|
|||
using mapped_type = std::uint32_t;
|
||||
using value_type = std::pair<const std::uint32_t, std::uint32_t>;
|
||||
|
||||
class Mask {
|
||||
public:
|
||||
explicit Mask(std::vector<std::uint64_t> bits) : bits(std::move(bits)) {}
|
||||
bool Contains(std::uint32_t offset) const { return offset / 64 < bits.size() && ((bits[offset / 64] >> (offset % 64)) & 1u) != 0; }
|
||||
private:
|
||||
std::vector<std::uint64_t> bits;
|
||||
};
|
||||
|
||||
class Reference {
|
||||
public:
|
||||
Reference(Registers& owner, std::uint32_t offset) : owner(owner), offset(offset) {}
|
||||
operator std::uint32_t() const { return owner.values[offset]; }
|
||||
Reference& operator=(std::uint32_t value) { owner.insert_or_assign(offset, value); return *this; }
|
||||
Reference& operator=(const Reference& other) { return *this = static_cast<std::uint32_t>(other); }
|
||||
Reference& operator|=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) | value; }
|
||||
Reference& operator&=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) & value; }
|
||||
Reference& operator^=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) ^ value; }
|
||||
Reference& operator+=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) + value; }
|
||||
private:
|
||||
Registers& owner;
|
||||
std::uint32_t offset;
|
||||
};
|
||||
|
||||
class const_iterator {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
|
@ -57,6 +81,18 @@ public:
|
|||
using iterator = const_iterator;
|
||||
|
||||
Registers() = default;
|
||||
Registers(const Registers&) = default;
|
||||
Registers& operator=(const Registers&) = default;
|
||||
Registers(Registers&& other) noexcept : values(std::move(other.values)), present(std::move(other.present)), entries(std::exchange(other.entries, 0)), mask(std::move(other.mask)), fingerprint(std::exchange(other.fingerprint, 0)) {}
|
||||
Registers& operator=(Registers&& other) noexcept {
|
||||
if (this == &other) return *this;
|
||||
values = std::move(other.values);
|
||||
present = std::move(other.present);
|
||||
entries = std::exchange(other.entries, 0);
|
||||
mask = std::move(other.mask);
|
||||
fingerprint = std::exchange(other.fingerprint, 0);
|
||||
return *this;
|
||||
}
|
||||
Registers(std::initializer_list<std::pair<std::uint32_t, std::uint32_t>> entries) {
|
||||
for (const auto& [offset, value] : entries) emplace(offset, value);
|
||||
}
|
||||
|
|
@ -83,27 +119,33 @@ public:
|
|||
values.clear();
|
||||
present.clear();
|
||||
entries = 0;
|
||||
fingerprint = 0;
|
||||
}
|
||||
std::pair<const_iterator, bool> emplace(std::uint32_t offset, std::uint32_t value) {
|
||||
if (contains(offset)) return {const_iterator{this, offset}, false};
|
||||
mark(offset) = value;
|
||||
changeFingerprint(offset, value);
|
||||
return {const_iterator{this, offset}, true};
|
||||
}
|
||||
std::pair<const_iterator, bool> insert_or_assign(std::uint32_t offset, std::uint32_t value) {
|
||||
if (contains(offset)) {
|
||||
if (values[offset] == value) return {const_iterator{this, offset}, false};
|
||||
changeFingerprint(offset, values[offset]);
|
||||
values[offset] = value;
|
||||
changeFingerprint(offset, value);
|
||||
return {const_iterator{this, offset}, false};
|
||||
}
|
||||
mark(offset) = value;
|
||||
changeFingerprint(offset, value);
|
||||
return {const_iterator{this, offset}, true};
|
||||
}
|
||||
std::uint32_t& operator[](std::uint32_t offset) {
|
||||
if (contains(offset)) return values[offset];
|
||||
return mark(offset) = 0;
|
||||
Reference operator[](std::uint32_t offset) {
|
||||
if (!contains(offset)) emplace(offset, 0);
|
||||
return {*this, offset};
|
||||
}
|
||||
std::uint32_t& at(std::uint32_t offset) {
|
||||
Reference at(std::uint32_t offset) {
|
||||
if (!contains(offset)) throw std::out_of_range("register is not set");
|
||||
return values[offset];
|
||||
return {*this, offset};
|
||||
}
|
||||
const std::uint32_t& at(std::uint32_t offset) const {
|
||||
if (!contains(offset)) throw std::out_of_range("register is not set");
|
||||
|
|
@ -111,6 +153,7 @@ public:
|
|||
}
|
||||
std::size_t erase(std::uint32_t offset) {
|
||||
if (!contains(offset)) return 0;
|
||||
changeFingerprint(offset, values[offset]);
|
||||
present[offset / 64] &= ~(std::uint64_t{1} << (offset % 64));
|
||||
--entries;
|
||||
return 1;
|
||||
|
|
@ -123,8 +166,31 @@ public:
|
|||
}
|
||||
return a == end() && b == other.end();
|
||||
}
|
||||
std::uint64_t Fingerprint(const std::shared_ptr<const Mask>& selected) const {
|
||||
if (mask != selected) {
|
||||
fingerprint = RecomputeFingerprint(*selected);
|
||||
mask = selected;
|
||||
}
|
||||
return fingerprint;
|
||||
}
|
||||
std::uint64_t RecomputeFingerprint(const Mask& selected) const {
|
||||
std::uint64_t result = 0;
|
||||
for (const auto [offset, value] : *this) {
|
||||
if (selected.Contains(offset)) result ^= contribution(offset, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::uint64_t contribution(std::uint32_t offset, std::uint32_t value) {
|
||||
auto hash = ((static_cast<std::uint64_t>(offset) << 32u) | value) + 0x9e3779b97f4a7c15ull;
|
||||
hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull;
|
||||
hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull;
|
||||
return hash ^ (hash >> 31u);
|
||||
}
|
||||
void changeFingerprint(std::uint32_t offset, std::uint32_t value) {
|
||||
if (mask != nullptr && mask->Contains(offset)) fingerprint ^= contribution(offset, value);
|
||||
}
|
||||
static constexpr std::size_t End = ~std::size_t{0};
|
||||
std::uint32_t& mark(std::uint32_t offset) {
|
||||
if (offset >= values.size()) {
|
||||
|
|
@ -147,6 +213,8 @@ private:
|
|||
std::vector<std::uint32_t> values;
|
||||
std::vector<std::uint64_t> present;
|
||||
std::size_t entries = 0;
|
||||
mutable std::shared_ptr<const Mask> mask;
|
||||
mutable std::uint64_t fingerprint = 0;
|
||||
};
|
||||
|
||||
inline Registers BuildInitialContextRegisters() {
|
||||
|
|
@ -172,6 +240,7 @@ inline Registers BuildInitialContextRegisters() {
|
|||
result.emplace(0x31c + 0xfu * i, 0);
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 16; ++i) {
|
||||
result.emplace(0x2fe + i, 0);
|
||||
result.emplace(0x94 + 2 * i, 0x80000000);
|
||||
result.emplace(0x95 + 2 * i, 0x40004000);
|
||||
result.emplace(0xb4 + 2 * i, 0);
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ struct DrawRecipe {
|
|||
std::optional<Graphics::State> masked;
|
||||
std::set<std::uint32_t> fragmentOutputs;
|
||||
VkPipelineStageFlags shaderStages = 0;
|
||||
bool hasDepth = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ public:
|
|||
// blit-submit times after the flip packet; noted in-place reads checked
|
||||
// (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence.
|
||||
struct PresentStatistics {
|
||||
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten;
|
||||
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops;
|
||||
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs;
|
||||
};
|
||||
static PresentStatistics PresentCounts();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
|
@ -9,6 +10,11 @@ double TraceMs() {
|
|||
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - origin).count();
|
||||
}
|
||||
|
||||
std::uint64_t SkippedShader() {
|
||||
static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }();
|
||||
return skipped;
|
||||
}
|
||||
|
||||
void require(bool condition, const char* reason) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(std::string("AGC driver: ") + reason);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace AgcDriver::DriverDetail {
|
|||
void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::uint64_t indirectArguments) {
|
||||
PerformanceTimer timing("Driver.Dispatch");
|
||||
const auto address = (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20d) & 0xffu) << 40u);
|
||||
if (address == SkippedShader() && address != 0) return;
|
||||
auto registeredShader = RegisteredProgram(*submission.shaders, address, {0});
|
||||
if (!registeredShader) registeredShader = ReadRawShader(address);
|
||||
const auto& snapshot = *registeredShader;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
|
@ -94,6 +95,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
|||
const auto& graphics = decode->state;
|
||||
auto pixel = decode->pixel;
|
||||
std::vector<DrawProgram> programs = decode->programs;
|
||||
if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn();
|
||||
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
|
||||
if (!graphics.stages.mesh) return;
|
||||
auto& words = programs.front().userData;
|
||||
|
|
@ -145,7 +147,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
|||
if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return;
|
||||
decodeReads[i].clear();
|
||||
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
|
||||
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207));
|
||||
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)) | graphics.layerExports;
|
||||
};
|
||||
if (!registerKey) {
|
||||
for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp"
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
|
@ -56,6 +57,36 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) {
|
|||
}
|
||||
}
|
||||
|
||||
DrawKeySightings::DrawKeySightings(std::size_t keyCount) {
|
||||
Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket");
|
||||
const auto buckets = std::bit_ceil(keyCount / Ways);
|
||||
keys.assign(buckets * Ways, 0);
|
||||
filled.assign(buckets, 0);
|
||||
next.assign(buckets, 0);
|
||||
bucketMask = buckets - 1;
|
||||
}
|
||||
|
||||
bool DrawKeySightings::SeenBefore(std::uint64_t key) {
|
||||
auto mixed = key;
|
||||
mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull;
|
||||
mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull;
|
||||
mixed ^= mixed >> 31u;
|
||||
const auto bucket = static_cast<std::size_t>(mixed) & bucketMask;
|
||||
auto* ways = keys.data() + bucket * Ways;
|
||||
if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true;
|
||||
ways[next[bucket]] = key;
|
||||
next[bucket] = static_cast<std::uint8_t>((next[bucket] + 1u) % Ways);
|
||||
if (filled[bucket] < Ways) ++filled[bucket];
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Driver::admitDrawKey(std::uint64_t key) {
|
||||
std::lock_guard cacheLock(drawCacheMutex);
|
||||
if (drawCache.contains(key)) return true;
|
||||
if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways);
|
||||
return drawKeySightings->SeenBefore(key);
|
||||
}
|
||||
|
||||
void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode) {
|
||||
std::lock_guard cacheLock(drawCacheMutex);
|
||||
auto& counters = drawEntryCounters;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
|
|||
program.binary,
|
||||
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
|
||||
localDevice->Target(),
|
||||
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
|
||||
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
|
||||
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
|
||||
};
|
||||
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
|
||||
|
|
@ -83,7 +83,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
|
|||
}
|
||||
|
||||
void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming) {
|
||||
if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu)) {
|
||||
if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) {
|
||||
phaseTiming.Phase(DrawRowVectors);
|
||||
std::uint64_t unstable = 0, mismatches = 0;
|
||||
for (std::size_t i = 0; i < programs.size(); ++i) {
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
|
|||
product->pixel.dualSourceBlend = product->state.dualSourceBlend;
|
||||
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
|
||||
if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state);
|
||||
if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0;
|
||||
return product;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
|
|||
mix(graphics.stages.tessellation.has_value());
|
||||
if (graphics.stages.tessellation) {
|
||||
const auto& tess = *graphics.stages.tessellation;
|
||||
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology}) mix(value);
|
||||
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology, tess.patchesPerGroup}) mix(value);
|
||||
}
|
||||
mix(graphics.rectList);
|
||||
mix(programs.size());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
|
|
@ -6,36 +7,69 @@
|
|||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
namespace {
|
||||
|
||||
bool runtimeRegister(std::uint32_t offset) {
|
||||
return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u;
|
||||
}
|
||||
|
||||
const auto& registerMasks() {
|
||||
static const auto masks = [] {
|
||||
std::array<std::vector<std::uint64_t>, 3> bits;
|
||||
for (const auto& range : Graphics::DrawKeyRegisters) {
|
||||
auto& bank = bits[static_cast<std::size_t>(range.bank)];
|
||||
bank.resize(std::max(bank.size(), (static_cast<std::size_t>(range.first) + range.count + 63) / 64));
|
||||
for (auto offset = range.first; offset < range.first + range.count; ++offset) {
|
||||
if (range.bank == Graphics::RegisterBank::Shader && runtimeRegister(offset)) continue;
|
||||
bank[offset / 64] |= std::uint64_t{1} << (offset % 64);
|
||||
}
|
||||
}
|
||||
std::array<std::shared_ptr<const Registers::Mask>, 3> result;
|
||||
for (std::size_t i = 0; i < bits.size(); ++i) result[i] = std::make_shared<const Registers::Mask>(std::move(bits[i]));
|
||||
return result;
|
||||
}();
|
||||
return masks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) {
|
||||
const auto& masks = registerMasks();
|
||||
const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) {
|
||||
const auto& mask = masks[static_cast<std::size_t>(bank)];
|
||||
return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask);
|
||||
};
|
||||
auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull;
|
||||
shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull;
|
||||
shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull;
|
||||
auto exact = shape;
|
||||
const auto words = [&](std::uint32_t base, std::uint32_t count) {
|
||||
for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) {
|
||||
exact = (exact ^ it->first) * 0x100000001b3ull;
|
||||
exact = (exact ^ it->second) * 0x100000001b3ull;
|
||||
}
|
||||
};
|
||||
for (const auto base : {0x00cu, 0x08cu, 0x10cu}) {
|
||||
const auto resources = queue.shader.find(base - 1);
|
||||
const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
|
||||
words(base, allUserWords ? 32u : std::min(count, 32u));
|
||||
}
|
||||
words(0x082u, 2);
|
||||
words(0x102u, 2);
|
||||
return exact;
|
||||
}
|
||||
|
||||
|
||||
std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) {
|
||||
static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr;
|
||||
std::uint64_t key = 0xcbf29ce484222325ull;
|
||||
static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr;
|
||||
auto key = RegisterStateKey(queue, allUserWords);
|
||||
if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state");
|
||||
const auto mix = [&](std::uint64_t value) {
|
||||
key ^= value;
|
||||
key *= 0x100000001b3ull;
|
||||
};
|
||||
const auto userEnd = [&](std::uint32_t base) {
|
||||
if (allUserWords) return base + 32u;
|
||||
const auto resources = queue.shader.find(base - 1);
|
||||
if (resources == queue.shader.end()) return base;
|
||||
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
|
||||
return base + std::min(count, 32u);
|
||||
};
|
||||
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
|
||||
const auto unread = [&](std::uint32_t offset) {
|
||||
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
|
||||
};
|
||||
mix(deviceSerial);
|
||||
for (const auto& range : Graphics::DrawKeyRegisters) {
|
||||
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
|
||||
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
|
||||
const auto end = range.first + range.count;
|
||||
const bool shader = range.bank == Graphics::RegisterBank::Shader;
|
||||
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
|
||||
if (shader && unread(it->first)) continue;
|
||||
mix(it->first);
|
||||
mix(it->second);
|
||||
}
|
||||
}
|
||||
for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
|
||||
const auto low = queue.shader.find(base);
|
||||
const auto high = queue.shader.find(base + 1);
|
||||
|
|
@ -84,7 +118,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
|
|||
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
|
||||
if (x.has_value() != y.has_value()) return false;
|
||||
if (!x) return true;
|
||||
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology;
|
||||
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup;
|
||||
};
|
||||
if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false;
|
||||
if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false;
|
||||
|
|
|
|||
|
|
@ -14,13 +14,22 @@
|
|||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
namespace {
|
||||
|
||||
bool matchesBufferPatternFillKernel(std::span<const std::uint32_t> code) {
|
||||
static constexpr std::array<std::uint32_t, 12> kernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u};
|
||||
return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Driver::matchesFillKernel(std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
|
||||
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
|
||||
static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr;
|
||||
if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false;
|
||||
if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true;
|
||||
static constexpr std::array<std::uint32_t, 9> fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u};
|
||||
if (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin())) return false;
|
||||
if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false;
|
||||
|
||||
const auto stride = (userData[1] >> 16u) & 0x3fffu;
|
||||
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
|
||||
|
|
@ -60,6 +69,50 @@ std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint3
|
|||
return fill;
|
||||
}
|
||||
|
||||
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
|
||||
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
|
||||
if (!enabled || userData.size() != 4 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt;
|
||||
static constexpr std::array<std::uint32_t, 6> kernel{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u};
|
||||
if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
|
||||
const auto stride = (userData[1] >> 16u) & 0x3fffu;
|
||||
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
|
||||
const bool addTid = ((userData[3] >> 23u) & 1u) != 0;
|
||||
const auto outOfBounds = (userData[3] >> 28u) & 3u;
|
||||
const auto format = (userData[3] >> 12u) & 0x7fu;
|
||||
const auto type = userData[3] >> 30u;
|
||||
if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds != 0 || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
|
||||
const auto dwords = std::min<std::uint64_t>(static_cast<std::uint64_t>(packet[1]) * 64u, userData[2]);
|
||||
const auto base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
|
||||
if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt;
|
||||
return DwordPatternFill{base, static_cast<std::size_t>(dwords * 4u), {}};
|
||||
}
|
||||
|
||||
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
|
||||
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
|
||||
if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt;
|
||||
const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]});
|
||||
if (!fill) return std::nullopt;
|
||||
if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt;
|
||||
return fill;
|
||||
}
|
||||
|
||||
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern) {
|
||||
return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
|
||||
}
|
||||
|
||||
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill) {
|
||||
if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt;
|
||||
switch (pattern[0] & 0xffu) {
|
||||
case 0x00: return Graphics::DccKeys::Clear0000;
|
||||
case 0x40: return Graphics::DccKeys::Clear0001;
|
||||
case 0x80: return Graphics::DccKeys::Clear1110;
|
||||
case 0xc0: return Graphics::DccKeys::Clear1111;
|
||||
case 0x20: return Graphics::DccKeys::ClearRegister;
|
||||
case 0xff: return Graphics::DccKeys::Uncompressed;
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool Driver::fillClearEnabled() {
|
||||
static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr;
|
||||
return enabled;
|
||||
|
|
@ -125,8 +178,11 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera
|
|||
}
|
||||
|
||||
bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr<VulkanDevice>& localDevice) {
|
||||
const auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
|
||||
if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false;
|
||||
auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
|
||||
if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute);
|
||||
std::optional<Graphics::MemoryValueFill> memoryFill;
|
||||
if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute);
|
||||
if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false;
|
||||
std::uint64_t base;
|
||||
std::size_t bytes;
|
||||
std::array<std::uint32_t, 4> pattern;
|
||||
|
|
@ -135,6 +191,15 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
|
|||
base = dwordFill->base;
|
||||
bytes = dwordFill->bytes;
|
||||
pattern = dwordFill->pattern;
|
||||
} else if (memoryFill) {
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
|
||||
inputLock.lock();
|
||||
recordLabelsForPacket(localDevice.get(), queueId);
|
||||
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(memoryFill->valueAddress), 4)) return false;
|
||||
GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4);
|
||||
std::fill(pattern.begin() + 1, pattern.end(), pattern[0]);
|
||||
base = memoryFill->address;
|
||||
bytes = memoryFill->bytes;
|
||||
} else if (MatchesPatternFillKernel(code, userData, compute)) {
|
||||
const auto range = DecodePatternFillRange(userData, packet);
|
||||
if (!range) return false;
|
||||
|
|
@ -166,7 +231,16 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
|
|||
const auto records = std::min<std::uint64_t>(static_cast<std::uint64_t>(groups[0]) * 64u, numRecords);
|
||||
base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
|
||||
bytes = static_cast<std::size_t>(records * 16u);
|
||||
pattern = {userData[4], userData[5], userData[6], userData[7]};
|
||||
if (matchesBufferPatternFillKernel(code)) {
|
||||
const auto source = userData[4] | (static_cast<std::uint64_t>(userData[5] & 0xffffu) << 32u);
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
|
||||
inputLock.lock();
|
||||
recordLabelsForPacket(localDevice.get(), queueId);
|
||||
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(source), sizeof(pattern))) return false;
|
||||
GuestMemory::Read(source, std::as_writable_bytes(std::span(pattern)), 4);
|
||||
} else {
|
||||
pattern = {userData[4], userData[5], userData[6], userData[7]};
|
||||
}
|
||||
}
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
static std::atomic<std::uint64_t> fills{0}, filledBytes{0}, cpuFills{0};
|
||||
|
|
@ -203,7 +277,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
|
|||
|
||||
static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
|
||||
if (traceKeys && coverage.cover == Graphics::StorageTexture::FillCover::Keys) std::fprintf(stderr, "[dcc-keys] title fills keys 0x%llx+0x%zx with %08x %08x %08x %08x (queue 0x%x)\n", static_cast<unsigned long long>(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId);
|
||||
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
|
||||
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern);
|
||||
if (uniformKeysFill) {
|
||||
Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
|
||||
Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
|
||||
|
|
@ -236,18 +310,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
|
|||
phase(FillFlush);
|
||||
const bool stored = cleared || localDevice->FillBuffer(base, bytes, pattern);
|
||||
phase(FillDevice);
|
||||
if (uniformKeysFill && stored && !cleared) {
|
||||
Graphics::DccKeys filled = Graphics::DccKeys::Mixed;
|
||||
switch (pattern[0] & 0xffu) {
|
||||
case 0x00: filled = Graphics::DccKeys::Clear0000; break;
|
||||
case 0x40: filled = Graphics::DccKeys::Clear0001; break;
|
||||
case 0x80: filled = Graphics::DccKeys::Clear1110; break;
|
||||
case 0xc0: filled = Graphics::DccKeys::Clear1111; break;
|
||||
case 0x20: filled = Graphics::DccKeys::ClearRegister; break;
|
||||
case 0xff: filled = Graphics::DccKeys::Uncompressed; break;
|
||||
default: break;
|
||||
}
|
||||
if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled);
|
||||
if (stored && !cleared) {
|
||||
if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled);
|
||||
}
|
||||
if (profile && stored && !cleared) {
|
||||
const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3];
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ void Driver::execute(const Submission& submission) {
|
|||
} else if (header == FlipPacketHeader) {
|
||||
CheckFailure();
|
||||
std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0;
|
||||
bool flipInterruptDeferred = false;
|
||||
if (!drains) {
|
||||
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
|
|
@ -212,8 +213,13 @@ void Driver::execute(const Submission& submission) {
|
|||
const auto localDevice = device.Load();
|
||||
|
||||
if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
|
||||
if (localDevice != nullptr) localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
|
||||
if (localDevice != nullptr) {
|
||||
localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
|
||||
const auto queueId = submission.queue;
|
||||
flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false);
|
||||
}
|
||||
}
|
||||
if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
|
||||
++flipsCounted;
|
||||
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
|
||||
flipBatchesUnsignaled += unsignaledAtFlip;
|
||||
|
|
|
|||
|
|
@ -154,10 +154,12 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) {
|
|||
const auto counts = VulkanDevice::PresentCounts();
|
||||
const auto presents = counts.presents - previous.presents;
|
||||
const double perPresent = presents != 0 ? 1.0 / static_cast<double>(presents) : 0.0;
|
||||
const auto dropped = counts.acquireDrops - previous.acquireDrops;
|
||||
const double perFrame = presents + dropped != 0 ? 1.0 / static_cast<double>(presents + dropped) : 0.0;
|
||||
const auto flips = flipsCounted.load() - previousFlips;
|
||||
const double perFlip = flips != 0 ? 1.0 / static_cast<double>(flips) : 0.0;
|
||||
const auto serial = flipSerial.load();
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per present: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perPresent, (counts.gpuGapMs - previous.gpuGapMs) * perPresent, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), static_cast<unsigned long long>(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
|
||||
previous = counts;
|
||||
previousFlips = flipsCounted.load();
|
||||
previousUnsignaled = flipBatchesUnsignaled.load();
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ bool RawGraphicsDrawn() {
|
|||
namespace {
|
||||
|
||||
void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock<std::mutex>& lock) {
|
||||
if (prepared.pending) PrioritizeRegistrationPreparation(prepared);
|
||||
prepared.settled.wait(lock, [&] { return !prepared.pending; });
|
||||
if (prepared.failure != nullptr) std::rethrow_exception(prepared.failure);
|
||||
}
|
||||
|
|
@ -398,6 +399,10 @@ void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler:
|
|||
APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize);
|
||||
return;
|
||||
}
|
||||
if (request.context.vertex && (request.context.vertex->paClVsOutCntl & (1u << 18u)) != 0) {
|
||||
APS5_LOG_OUT_DEBUG("Shader 0x%llx is drawn into a layered target; preparing its layered variant at draw", static_cast<unsigned long long>(snapshot.codeAddress));
|
||||
return;
|
||||
}
|
||||
if (!snapshot.header.empty()) {
|
||||
if (!snapshot.prepared->deferred) APS5_LOG_ERR("Shader 0x%llx stage %u has no artifact prepared at registration for the state of this draw or dispatch; preparing it at use", static_cast<unsigned long long>(request.shader.codeAddress), static_cast<std::uint32_t>(request.shader.stage));
|
||||
return;
|
||||
|
|
@ -709,6 +714,7 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
|
|||
plan->memory = {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
|
||||
auto& request = plan->request.emplace(ShaderRecompiler::RecompileRequest{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, plan->userData, compute, pixel, vertex, plan->memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics});
|
||||
if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes;
|
||||
if (graphics && graphics->tessellation) request.layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes;
|
||||
if (pixel && device.GraphicsPipelineLibraries()) request.layout.pushConstantOffsetBytes = Graphics::FixedPushOffset(Stage::Fragment);
|
||||
return plan;
|
||||
}
|
||||
|
|
@ -759,10 +765,18 @@ std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snap
|
|||
class RegistrationPreparePool {
|
||||
public:
|
||||
static RegistrationPreparePool& Instance() {
|
||||
static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
|
||||
static auto* pool = [] {
|
||||
auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
|
||||
PreparationSlot().store(created);
|
||||
return created;
|
||||
}();
|
||||
return *pool;
|
||||
}
|
||||
|
||||
static RegistrationPreparePool* Existing() {
|
||||
return PreparationSlot().load();
|
||||
}
|
||||
|
||||
static RegistrationPreparePool& Prebuild() {
|
||||
static auto* pool = [] {
|
||||
const char* text = std::getenv("APS5_PREBUILD_THREADS");
|
||||
|
|
@ -778,6 +792,11 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static std::atomic<RegistrationPreparePool*>& PreparationSlot() {
|
||||
static std::atomic<RegistrationPreparePool*> slot{nullptr};
|
||||
return slot;
|
||||
}
|
||||
|
||||
static std::atomic<RegistrationPreparePool*>& PrebuildSlot() {
|
||||
static std::atomic<RegistrationPreparePool*> slot{nullptr};
|
||||
return slot;
|
||||
|
|
@ -785,11 +804,12 @@ private:
|
|||
|
||||
public:
|
||||
|
||||
void Submit(std::function<void()> task) {
|
||||
void Submit(std::function<void()> task, const PreparedShaders* prepared = nullptr) {
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
if (threads != 0 && !closed) {
|
||||
tasks.push_back(std::move(task));
|
||||
if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared});
|
||||
else tasks.push_back({std::move(task), prepared});
|
||||
lock.unlock();
|
||||
ready.notify_one();
|
||||
return;
|
||||
|
|
@ -798,6 +818,14 @@ public:
|
|||
task();
|
||||
}
|
||||
|
||||
void Prioritize(const PreparedShaders& prepared) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; });
|
||||
if (found == tasks.end()) return;
|
||||
if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast<std::size_t>(std::distance(tasks.begin(), found)), tasks.size());
|
||||
std::rotate(tasks.begin(), found, std::next(found));
|
||||
}
|
||||
|
||||
void Close() {
|
||||
std::unique_lock lock(mutex);
|
||||
closed = true;
|
||||
|
|
@ -823,7 +851,7 @@ private:
|
|||
auto& pool = *static_cast<RegistrationPreparePool*>(argument);
|
||||
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
|
||||
for (;;) {
|
||||
std::function<void()> task;
|
||||
Task task;
|
||||
{
|
||||
std::unique_lock lock(pool.mutex);
|
||||
pool.ready.wait(lock, [&] { return !pool.tasks.empty(); });
|
||||
|
|
@ -831,8 +859,8 @@ private:
|
|||
pool.tasks.pop_front();
|
||||
++pool.running;
|
||||
}
|
||||
task();
|
||||
task = nullptr;
|
||||
task.function();
|
||||
task = {};
|
||||
{
|
||||
std::lock_guard lock(pool.mutex);
|
||||
--pool.running;
|
||||
|
|
@ -842,10 +870,15 @@ private:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
struct Task {
|
||||
std::function<void()> function;
|
||||
const PreparedShaders* prepared = nullptr;
|
||||
};
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable ready;
|
||||
std::condition_variable idle;
|
||||
std::deque<std::function<void()>> tasks;
|
||||
std::deque<Task> tasks;
|
||||
unsigned threads = 0;
|
||||
unsigned running = 0;
|
||||
bool closed = false;
|
||||
|
|
@ -857,6 +890,7 @@ bool AsyncRegistrationPrepare() {
|
|||
}
|
||||
|
||||
void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapshot, std::shared_ptr<RegisteredPreparation> plan) {
|
||||
const auto* preparedKey = snapshot->prepared.get();
|
||||
SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] {
|
||||
PerformanceContext timingContext(FrameTiming::Preparation());
|
||||
std::vector<PreparedShaders::Entry> entries;
|
||||
|
|
@ -878,13 +912,18 @@ void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapsho
|
|||
prepared.pending = false;
|
||||
}
|
||||
prepared.settled.notify_all();
|
||||
});
|
||||
}, preparedKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SubmitRegistrationPreparation(std::function<void()> task) {
|
||||
RegistrationPreparePool::Instance().Submit(std::move(task));
|
||||
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared) {
|
||||
RegistrationPreparePool::Instance().Submit(std::move(task), prepared);
|
||||
}
|
||||
|
||||
void PrioritizeRegistrationPreparation(PreparedShaders& prepared) {
|
||||
prepared.demanded.store(true);
|
||||
if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared);
|
||||
}
|
||||
|
||||
void SubmitLibraryPrebuild(std::function<void()> task) {
|
||||
|
|
@ -923,7 +962,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
|
|||
std::vector<PreparedGraphicsStage> prepared;
|
||||
std::uint32_t pushOffset = 0;
|
||||
const bool fixedSlots = target.fixedPushSlots;
|
||||
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes;
|
||||
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes;
|
||||
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
|
||||
if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue;
|
||||
const auto& program = decoded.programs[index];
|
||||
|
|
@ -931,7 +970,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
|
|||
const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize;
|
||||
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
|
||||
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr);
|
||||
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl);
|
||||
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl) | decoded.state.layerExports;
|
||||
pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots);
|
||||
const auto offset = pushOffset;
|
||||
const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes;
|
||||
|
|
|
|||
|
|
@ -1212,7 +1212,14 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
|
|||
saved.assign(source, source + (stop - first));
|
||||
}
|
||||
const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true);
|
||||
if (!import()) return false;
|
||||
bool imported = false;
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
const GuestArena::HostWrite open(reinterpret_cast<void*>(first), static_cast<std::size_t>(stop - first));
|
||||
#endif
|
||||
imported = import();
|
||||
}
|
||||
if (!imported) return false;
|
||||
if (!staysWatched) {
|
||||
if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast<std::size_t>(stop - first));
|
||||
unwatchLocked(tracker, address, bytes);
|
||||
|
|
@ -1263,6 +1270,30 @@ bool UnchangedSinceAll(std::span<const UnchangedQuery> queries) {
|
|||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges) {
|
||||
ranges.clear();
|
||||
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address || !WriteWatched()) return 0;
|
||||
auto committed = CommittedRanges(address, bytes);
|
||||
auto generation = TrackerGeneration();
|
||||
for (const auto& [begin, end] : committed) {
|
||||
generation = CollectWrites(begin, static_cast<std::size_t>(end - begin));
|
||||
if (generation == 0) return 0;
|
||||
}
|
||||
ranges = std::move(committed);
|
||||
return generation;
|
||||
}
|
||||
|
||||
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation) {
|
||||
auto& tracker = Tracker();
|
||||
const auto lock = lockTracker(tracker);
|
||||
tracker.initialize();
|
||||
if (!tracker.watched || generation == 0) return false;
|
||||
for (const auto& [begin, end] : ranges) {
|
||||
if (end <= begin || !tracker.covers(begin, static_cast<std::size_t>(end - begin)) || tracker.newestStamp(begin, end) > generation) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function<std::pair<std::uint64_t, std::uint64_t>()>& store) {
|
||||
if (bytes == 0) return 0;
|
||||
const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ bool CoreVulkanCapability(std::uint32_t capability) {
|
|||
}
|
||||
}
|
||||
|
||||
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) {
|
||||
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) {
|
||||
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
info.flags = flags;
|
||||
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
|
|
@ -107,6 +107,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create,
|
|||
subgroup.requiredSubgroupSize = subgroupSize;
|
||||
if (subgroupSize != 0) info.stage.pNext = &subgroup;
|
||||
info.layout = layout;
|
||||
auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{};
|
||||
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +274,7 @@ struct VulkanDevice::State {
|
|||
bool shaderClock = false;
|
||||
bool narrowSubgroupClock = false;
|
||||
bool float64DenormPreserve = false;
|
||||
bool float16DenormPreserve = false;
|
||||
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
|
||||
// graphics stages, and compute workgroups wider than a wave).
|
||||
bool descriptorIndexing = false;
|
||||
|
|
@ -283,6 +285,7 @@ struct VulkanDevice::State {
|
|||
VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT};
|
||||
bool imageInt64Atomics = false;
|
||||
bool bufferInt64Atomics = false;
|
||||
bool viewportIndexLayer = false;
|
||||
bool primitiveListRestart = false;
|
||||
bool depthClipControl = false;
|
||||
bool graphicsPipelineLibrary = false;
|
||||
|
|
@ -1149,6 +1152,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
|||
state->imageInt64Atomics = true;
|
||||
state->spirvExtensions.push_back("SPV_EXT_shader_image_int64");
|
||||
}
|
||||
if (hasExtension(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME)) {
|
||||
deviceExtensions.push_back(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilityShaderViewportIndexLayerEXT);
|
||||
state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer");
|
||||
state->viewportIndexLayer = true;
|
||||
}
|
||||
deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve);
|
||||
state->spirvExtensions.push_back("SPV_KHR_float_controls");
|
||||
|
|
@ -1167,6 +1176,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
|||
state->capabilities.push_back(spv::CapabilityRoundingModeRTE);
|
||||
state->capabilities.push_back(spv::CapabilityDenormPreserve);
|
||||
}
|
||||
state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE &&
|
||||
floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL;
|
||||
if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve);
|
||||
deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
|
||||
deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME);
|
||||
state->capabilities.push_back(4448);
|
||||
|
|
@ -2165,7 +2177,7 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st
|
|||
// a completion-deferred label): it lands only when that batch is reaped, whatever its fence.
|
||||
const auto completionStoreOverlaps = [&](std::uint64_t address) {
|
||||
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
|
||||
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.PendingLabelIn(address, bytes);
|
||||
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes);
|
||||
};
|
||||
// The CPU decision (see the header): pure queries, in the order that makes the first failing
|
||||
// test the reason; a signaled writer of the source is fine (its in-place stores are final in
|
||||
|
|
@ -2558,9 +2570,23 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
|
|||
static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP"));
|
||||
if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip");
|
||||
else Graphics::StorageTexture::FlushDumpSelected("flip");
|
||||
auto& dumps = Dumps();
|
||||
if (buffer.tilingMode == 1) {
|
||||
const auto pixels = ReadDisplayBuffer(buffer);
|
||||
return present(buffer.width, buffer.height, true, pixels);
|
||||
bool dumpFrame = false;
|
||||
if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
|
||||
if (dumps.cpu) {
|
||||
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale());
|
||||
} else {
|
||||
state->nextDumpIndex = dumps.dumped++;
|
||||
dumpFrame = true;
|
||||
}
|
||||
}
|
||||
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame)) {
|
||||
if (dumpFrame) --dumps.dumped;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
// The display buffer is usually a resident render target whose results are still on the GPU: it
|
||||
|
|
@ -2599,7 +2625,6 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
|
|||
}
|
||||
}
|
||||
const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt;
|
||||
auto& dumps = Dumps();
|
||||
bool dumpFrame = false;
|
||||
if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
|
||||
if (dumps.cpu) {
|
||||
|
|
@ -2642,11 +2667,15 @@ bool VulkanDevice::AcquireImage() {
|
|||
std::uint32_t index = 0;
|
||||
timing.Mark("fence_reset");
|
||||
// An out-of-date swapchain (the window changed) drops this frame; the next Resize recreates it.
|
||||
const auto acquired = state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index);
|
||||
const auto acquired = state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 0, VK_NULL_HANDLE, state->acquireFence, &index);
|
||||
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
state->extent = {0, 0};
|
||||
return false;
|
||||
}
|
||||
if (acquired == VK_NOT_READY || acquired == VK_TIMEOUT) {
|
||||
++presentCounters.acquireDrops;
|
||||
return false;
|
||||
}
|
||||
if (acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR");
|
||||
timing.Mark("acquire_image");
|
||||
APS5_LOG_OUT_DEBUG("vkAcquireNextImageKHR index=%u imageCount=%zu", index, state->images.size());
|
||||
|
|
@ -3023,6 +3052,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
|
|||
target.nonConstantImageOffsets = state->maintenance8;
|
||||
target.narrowSubgroupClock = state->narrowSubgroupClock;
|
||||
target.float64DenormPreserve = state->float64DenormPreserve;
|
||||
target.float16DenormPreserve = state->float16DenormPreserve;
|
||||
target.fixedPushSlots = state->graphicsPipelineLibrary;
|
||||
target.registrationModules = state->graphicsPipelineLibrary;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
|
|
@ -3104,6 +3134,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
|||
state->depthClamp,
|
||||
state->hostImportAlignment
|
||||
};
|
||||
context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr;
|
||||
context.dmaBufImport = state->dmaBufImport;
|
||||
context.recorder = state->recorder.get();
|
||||
context.descriptorCache = state->descriptorCache.get();
|
||||
|
|
@ -3136,6 +3167,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
|||
context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors();
|
||||
context.bufferInt64Atomics = state->bufferInt64Atomics;
|
||||
context.placeholderImages = state->placeholderImages.get();
|
||||
context.viewportIndexLayer = state->viewportIndexLayer;
|
||||
context.primitiveListRestart = state->primitiveListRestart;
|
||||
context.imageViewMinLod = state->imageViewMinLod;
|
||||
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
|
||||
|
|
@ -4258,7 +4290,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
|||
std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str());
|
||||
}
|
||||
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
|
||||
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline);
|
||||
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization);
|
||||
objects->destroyModule(objects->device, objects->module, nullptr);
|
||||
objects->module = VK_NULL_HANDLE;
|
||||
Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -28,6 +30,19 @@ class SamplerCache;
|
|||
class ShaderResources;
|
||||
class PlaceholderImages;
|
||||
|
||||
class PipelineCacheSynchronization {
|
||||
public:
|
||||
using PipelineUseLock = std::shared_lock<std::shared_mutex>;
|
||||
using SnapshotLock = std::unique_lock<std::shared_mutex>;
|
||||
|
||||
PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); }
|
||||
SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); }
|
||||
SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); }
|
||||
|
||||
private:
|
||||
mutable std::shared_mutex mutex;
|
||||
};
|
||||
|
||||
inline void Require(bool condition, const std::string& reason) {
|
||||
if (!condition) throw std::runtime_error("AGC graphics: " + reason);
|
||||
}
|
||||
|
|
@ -165,6 +180,7 @@ struct Context {
|
|||
bool nullDescriptors = false;
|
||||
bool bufferInt64Atomics = false;
|
||||
const PlaceholderImages* placeholderImages = nullptr;
|
||||
bool viewportIndexLayer = false;
|
||||
bool primitiveListRestart = false;
|
||||
bool imageViewMinLod = false;
|
||||
bool pipelineExecutableInfo = false;
|
||||
|
|
@ -175,6 +191,11 @@ struct Context {
|
|||
bool provokingVertexModePerPipeline = false;
|
||||
bool singlePassStorage = false;
|
||||
VkSampleCountFlags sampleLocationSampleCounts = 0;
|
||||
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
|
||||
|
||||
PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const {
|
||||
return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
|
||||
}
|
||||
|
||||
template<typename TFunction>
|
||||
TFunction Function(const char* name) const {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ struct HostImport {
|
|||
std::uint64_t serial = 0;
|
||||
bool unwatched = false;
|
||||
bool dmaBuf = false;
|
||||
bool joined = false;
|
||||
std::shared_ptr<void> chunk;
|
||||
mutable std::uint64_t lastUse = 0;
|
||||
};
|
||||
|
||||
enum class ImportWatch : std::uint8_t { Watch, Unwatch };
|
||||
|
|
@ -58,6 +60,10 @@ void SetImportWatch(const Context& context, ImportWatch watch);
|
|||
// the import's buffer.
|
||||
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
|
||||
void SetHostImportWindow(std::uint64_t bytes);
|
||||
// Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one).
|
||||
void SetHostImportRangeWindow(std::uint64_t bytes);
|
||||
void SetHostImportBudget(std::uint64_t bytes);
|
||||
std::uint64_t HostImportLiveBytes();
|
||||
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice);
|
||||
// Whether an existing import covers [address, address + bytes), without reconciling the imports
|
||||
// with the registry or making one (HostImportFor may take a registry lease): a hint for choices
|
||||
|
|
@ -378,6 +384,7 @@ private:
|
|||
// Import registry epoch when `direct` pointers were taken at acquire time; they are reused while
|
||||
// no import was destroyed since.
|
||||
std::uint64_t importsEpoch = 0;
|
||||
std::uint64_t importFloor = 0;
|
||||
std::vector<Region> regions;
|
||||
// Whether `regions` is in ascending address order (true right after AcquireRegistered, whose
|
||||
// regions follow the registry's order), so AddSnapshot can search instead of scanning.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel
|
|||
// the batch completes, so a recorded draw keeps the object (Recorder::Keep) like its pipeline.
|
||||
class Framebuffer {
|
||||
public:
|
||||
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent);
|
||||
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent, std::uint32_t layers = 1);
|
||||
~Framebuffer();
|
||||
Framebuffer(const Framebuffer&) = delete;
|
||||
Framebuffer& operator=(const Framebuffer&) = delete;
|
||||
|
|
@ -47,7 +47,7 @@ public:
|
|||
Pipeline(const Pipeline&) = delete;
|
||||
Pipeline& operator=(const Pipeline&) = delete;
|
||||
VkPipelineLayout Layout() const;
|
||||
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent = {});
|
||||
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers = 1, std::span<const std::uint8_t> persistent = {});
|
||||
// Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state.
|
||||
void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const;
|
||||
// The same inside a render pass another pipeline of the same attachments began (compatible by
|
||||
|
|
@ -68,6 +68,7 @@ private:
|
|||
std::vector<std::weak_ptr<StorageTexture>> owners;
|
||||
std::vector<bool> owned;
|
||||
VkExtent2D extent;
|
||||
std::uint32_t layers = 1;
|
||||
std::shared_ptr<Framebuffer> framebuffer;
|
||||
};
|
||||
void release() noexcept;
|
||||
|
|
@ -123,7 +124,7 @@ bool ValidateSampleLocations(const Context& context, const State& state);
|
|||
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
|
||||
bool ShadersWriteMemory(std::span<const CompiledShader> shaders);
|
||||
// Returns the color attachment locations the pixel shader writes.
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false);
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public:
|
|||
PipelineCache(const PipelineCache&) = delete;
|
||||
PipelineCache& operator=(const PipelineCache&) = delete;
|
||||
VkPipelineCache Handle() const { return cache; }
|
||||
PipelineCacheSynchronization& Synchronization() { return synchronization; }
|
||||
|
||||
private:
|
||||
void load(std::vector<std::byte>& initialData);
|
||||
|
|
@ -30,6 +31,7 @@ private:
|
|||
Context context;
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
VkPipelineCache cache = VK_NULL_HANDLE;
|
||||
PipelineCacheSynchronization synchronization;
|
||||
std::filesystem::path path;
|
||||
std::size_t savedBytes = 0;
|
||||
std::mutex mutex;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ private:
|
|||
};
|
||||
|
||||
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ TextureCacheUse TextureCacheUsage();
|
|||
std::uint64_t SampledTextureCacheBudget(const Context& context);
|
||||
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words);
|
||||
bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second);
|
||||
void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes);
|
||||
|
||||
// The cached storage image of a surface (render targets use it as their resident image); brought up
|
||||
// to date with guest memory before it is returned.
|
||||
|
|
@ -432,6 +433,7 @@ private:
|
|||
Context context;
|
||||
std::vector<std::uint32_t> layoutKey;
|
||||
GuestBufferMemory guestMemory;
|
||||
std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes();
|
||||
std::unique_ptr<BdaResources> bda;
|
||||
bool usesBda = false;
|
||||
bool usesFaultBuffer = false;
|
||||
|
|
@ -465,6 +467,7 @@ private:
|
|||
std::vector<std::shared_ptr<Texture>> textures;
|
||||
std::vector<bool> textureFirstLayer;
|
||||
std::vector<bool> textureBaseLevel;
|
||||
std::vector<bool> textureSrgb;
|
||||
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
|
||||
std::vector<std::uint32_t> storageMips;
|
||||
std::vector<std::uint64_t> storageKeys;
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span<const CompiledShader> sha
|
|||
VkShaderStageFlags result = 0;
|
||||
for (const auto& shader : shaders) {
|
||||
Require(shader.program != nullptr, "missing compiled shader");
|
||||
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh) result |= VulkanStage(shader.stage);
|
||||
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
|
@ -28,6 +29,8 @@ struct ShaderStages {
|
|||
std::uint32_t fragmentWaveSize;
|
||||
std::optional<ShaderRecompiler::MeshConfiguration> mesh;
|
||||
std::optional<ShaderRecompiler::TessellationConfiguration> tessellation;
|
||||
float minTessellationLevel = 1.0f;
|
||||
float maxTessellationLevel = 64.0f;
|
||||
};
|
||||
|
||||
struct ColorTarget {
|
||||
|
|
@ -58,6 +61,7 @@ struct ColorTarget {
|
|||
std::uint64_t arrayAddress = 0;
|
||||
std::uint32_t arrayLastSlice = 0;
|
||||
std::uint32_t arraySlice = 0;
|
||||
std::uint32_t layers = 1;
|
||||
std::uint32_t exportIndex = 0;
|
||||
bool uintExport = false;
|
||||
std::uint32_t pipeBankXor = 0;
|
||||
|
|
@ -102,6 +106,8 @@ struct State {
|
|||
bool rectList = false;
|
||||
bool dualSourceBlend = false;
|
||||
VkExtent2D renderExtent;
|
||||
std::uint32_t renderLayers = 1;
|
||||
std::uint32_t layerExports = 0;
|
||||
VkPrimitiveTopology topology;
|
||||
bool primitiveRestart = false;
|
||||
VkViewport viewport;
|
||||
|
|
@ -134,6 +140,7 @@ ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
|
|||
ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot);
|
||||
std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height);
|
||||
std::uint32_t ColorWriteMask(const Registers& context);
|
||||
GuestTextureResource SurfaceForTarget(const ColorTarget& color);
|
||||
|
||||
struct ColorMetadataPass {
|
||||
enum class Mode { EliminateFastClear, DccDecompress, Resolve };
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ public:
|
|||
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
|
||||
static VkFormat DepthCopyFormat(VkFormat colorFormat);
|
||||
static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
|
||||
struct BlockCopyTag {};
|
||||
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag);
|
||||
static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
|
||||
bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; }
|
||||
bool RefreshDepthCopy();
|
||||
struct DepthCopyCounts {
|
||||
|
|
@ -81,6 +84,8 @@ public:
|
|||
VkImageView View() const;
|
||||
VkImageView FirstLayerView() const { return firstLayerView; }
|
||||
VkImageView BaseLevelView() const;
|
||||
bool HasSrgbView() const { return srgbInfo.has_value(); }
|
||||
VkImageView SrgbView(bool firstLayer, bool baseLevel) const;
|
||||
struct ViewRange {
|
||||
VkImageViewType type;
|
||||
std::uint32_t levels;
|
||||
|
|
@ -106,8 +111,10 @@ public:
|
|||
|
||||
private:
|
||||
void release() noexcept;
|
||||
bool refreshBlockCopy();
|
||||
void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
|
||||
void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
|
||||
void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
|
||||
|
||||
// Held by value: cached textures outlive the Context of the draw that created them.
|
||||
Context context;
|
||||
|
|
@ -122,6 +129,10 @@ private:
|
|||
std::optional<VkImageViewCreateInfo> baseLevelInfo;
|
||||
mutable std::mutex baseLevelMutex;
|
||||
mutable VkImageView baseLevelView = VK_NULL_HANDLE;
|
||||
std::optional<VkImageViewCreateInfo> srgbInfo;
|
||||
float srgbMinLod = 0.0f;
|
||||
mutable std::mutex srgbMutex;
|
||||
mutable std::array<VkImageView, 3> srgbViews{};
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkDeviceSize allocationBytes = 0;
|
||||
VkDeviceSize countedBytes = 0;
|
||||
|
|
@ -133,10 +144,12 @@ private:
|
|||
bool copiedOnce = false;
|
||||
std::uint64_t copiedVersion = 0;
|
||||
VkFormat depthFormat = VK_FORMAT_UNDEFINED;
|
||||
VkFormat blockFormat = VK_FORMAT_UNDEFINED;
|
||||
std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0;
|
||||
};
|
||||
|
||||
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format);
|
||||
VkFormat SrgbTwinFormat(const Context& context, VkFormat format);
|
||||
|
||||
// A guest texture a shader writes through a storage image. It is uploaded like a sampled texture;
|
||||
// after the GPU work completes its results are stored to guest memory (retiled, changed bytes only),
|
||||
|
|
@ -162,7 +175,7 @@ public:
|
|||
// Render targets live in the same images: draws attach mip 0 through a view of the color
|
||||
// buffer's format and mark the image dirty like a storage write.
|
||||
bool Attachable() const { return attachable; }
|
||||
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0);
|
||||
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layers = 1, std::uint32_t layer = 0);
|
||||
VkImageView AttachmentProxyView();
|
||||
void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
|
||||
void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
|
||||
|
|
@ -316,7 +329,7 @@ public:
|
|||
// GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A.
|
||||
DccKeys UploadedKeys() const { return uploadedKeys; }
|
||||
DccKeys FilledKeys() const { return filledKeys; }
|
||||
bool GuestSnapshotValid() const { return originalValid; }
|
||||
bool GuestSnapshotValid() const { return originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)); }
|
||||
DccKeyProof& KeyProof() const { return keyProof; }
|
||||
DccRangeProof& TargetKeyProof() const { return targetKeyProof; }
|
||||
DccKeys ProvedKeys() const;
|
||||
|
|
@ -328,6 +341,7 @@ public:
|
|||
static std::uint64_t SinglePassMoves();
|
||||
static std::uint64_t RefreshesProved();
|
||||
std::uint64_t GuestBytes() const;
|
||||
std::size_t OriginalSnapshotBytes() const { return original.capacity(); }
|
||||
VkDeviceSize AllocationBytes() const { return memoryBytes; }
|
||||
|
||||
private:
|
||||
|
|
@ -338,6 +352,7 @@ private:
|
|||
// upload everything).
|
||||
void upload(const std::vector<bool>* layers = nullptr);
|
||||
void captureGuestBytes(const std::vector<bool>* layers);
|
||||
bool originalHolds(std::uint64_t address, std::size_t bytes) const;
|
||||
bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize = false) const;
|
||||
// Stores the pending tracked layers overlapping [address, address + bytes) to guest memory; in
|
||||
// each, 64 KiB blocks the CPU wrote since the layer's generation keep the CPU's bytes. Block
|
||||
|
|
@ -535,7 +550,7 @@ private:
|
|||
std::map<std::uint32_t, VkImageView> elementLayerViews;
|
||||
std::int8_t singlePassState = -1;
|
||||
bool attachable = false;
|
||||
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
|
||||
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
|
||||
VkImage proxyImage = VK_NULL_HANDLE;
|
||||
VkDeviceMemory proxyMemory = VK_NULL_HANDLE;
|
||||
VkImageView proxyView = VK_NULL_HANDLE;
|
||||
|
|
@ -544,7 +559,7 @@ private:
|
|||
bool dirty = false;
|
||||
std::uint64_t version = 0;
|
||||
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
|
||||
bool originalValid = true;
|
||||
std::vector<bool> layerOriginal;
|
||||
// The storage cache let the image go (Flush): a lookup makes a new image of the surface, so
|
||||
// no fill may put results into this one (guarded by the live-image registry lock).
|
||||
bool released = false;
|
||||
|
|
|
|||
|
|
@ -180,6 +180,13 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute&
|
|||
return static_cast<std::size_t>(required);
|
||||
}
|
||||
|
||||
inline std::size_t VertexBufferReadOffset(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t firstVertex, std::uint32_t firstInstance) {
|
||||
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
|
||||
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
|
||||
const auto index = attribute.fetchIndex == 0 ? firstVertex : firstInstance;
|
||||
return static_cast<std::size_t>(static_cast<std::uint64_t>(stride) * index);
|
||||
}
|
||||
|
||||
struct VertexFetch {
|
||||
std::uint64_t begin;
|
||||
std::uint64_t end;
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ void main() {
|
|||
uint nibble = ((y / 512u) * push.blocksPerRow + x / 1024u) * 8192u + offset;
|
||||
uint byteAddress = push.base + nibble / 2u;
|
||||
uint code = (cmask.words[byteAddress / 4u] >> ((byteAddress % 4u) * 8u + (nibble % 2u) * 4u)) & 0xfu;
|
||||
if (code == 0xfu) return;
|
||||
if (code == 0u && push.write != 0u) {
|
||||
if (code != 0u) return;
|
||||
if (push.write != 0u) {
|
||||
imageStore(target, ivec2(pixel), uvec4(push.clear0, push.clear1, 0u, 0u));
|
||||
return;
|
||||
}
|
||||
if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u << code);
|
||||
if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -356,6 +356,7 @@ bool LayoutFor(VkFormat format, Layout& layout) {
|
|||
case VK_FORMAT_R16G16_UINT: layout = {2, {16, 16}, Kind::Uint}; return true;
|
||||
case VK_FORMAT_R16G16_SINT: layout = {2, {16, 16}, Kind::Sint}; return true;
|
||||
case VK_FORMAT_R16G16_SFLOAT: layout = {2, {16, 16}, Kind::Float}; return true;
|
||||
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: layout = {3, {11, 11, 10}, Kind::Float}; return true;
|
||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Unorm}; return true;
|
||||
case VK_FORMAT_A2B10G10R10_UINT_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Uint}; return true;
|
||||
case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: layout = {4, {8, 8, 8, 8}, Kind::Unorm}; return true;
|
||||
|
|
@ -382,7 +383,14 @@ std::uint64_t One(Kind kind, std::uint32_t bits) {
|
|||
switch (kind) {
|
||||
case Kind::Unorm: case Kind::Uint: return bits >= 64 ? ~0ull : (1ull << bits) - 1u;
|
||||
case Kind::Snorm: case Kind::Sint: return (1ull << (bits - 1u)) - 1u;
|
||||
case Kind::Float: return bits == 16 ? 0x3c00u : 0x3f800000u;
|
||||
case Kind::Float:
|
||||
switch (bits) {
|
||||
case 10: return 0x1e0u;
|
||||
case 11: return 0x3c0u;
|
||||
case 16: return 0x3c00u;
|
||||
case 32: return 0x3f800000u;
|
||||
default: throw std::runtime_error("AGC graphics: DCC clear of a " + std::to_string(bits) + "-bit float channel is not implemented");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -460,7 +468,9 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized
|
|||
DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) {
|
||||
memoized = false;
|
||||
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
|
||||
const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
|
||||
DccKeys keys;
|
||||
if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known;
|
||||
else keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
|
||||
if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys;
|
||||
if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented");
|
||||
std::byte probe[16]{};
|
||||
|
|
|
|||
|
|
@ -153,18 +153,17 @@ public:
|
|||
float clearDepth = 0.0f;
|
||||
std::uint8_t clearStencil = 0;
|
||||
|
||||
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
|
||||
if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr;
|
||||
bool StorageViewMatches(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
|
||||
if (resource.width != target.extent.width || resource.height != target.extent.height) return false;
|
||||
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
|
||||
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
|
||||
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
|
||||
const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u);
|
||||
if (ResolveTextureFormat(resource.format) != expected && !depthBits) return nullptr;
|
||||
if (resource.dimension != TextureDimension::k2D) {
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
return ResolveTextureFormat(resource.format) == expected || depthBits;
|
||||
}
|
||||
|
||||
void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const {
|
||||
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
|
||||
Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented");
|
||||
if (!stencil && hostFormat != target.format) {
|
||||
char text[224];
|
||||
|
|
@ -176,9 +175,21 @@ public:
|
|||
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
|
||||
if (!StorageViewMatches(words, resource)) return nullptr;
|
||||
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
|
||||
const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0;
|
||||
if (resource.dimension != TextureDimension::k2D && !oneLayer) {
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
RequireStorable(resource, mip);
|
||||
ApplyFastClear();
|
||||
auto& plane = stencil ? stencilPlane : depthPlane;
|
||||
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress) {
|
||||
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) {
|
||||
std::erase_if(plane.replaced, [](const auto& old) { return old.expired(); });
|
||||
if (plane.storage != nullptr) plane.replaced.push_back(plane.storage);
|
||||
plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
|
||||
|
|
@ -438,6 +449,82 @@ std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFor
|
|||
return *copies;
|
||||
}
|
||||
|
||||
struct LayeredSlice {
|
||||
DepthSurface* surface;
|
||||
std::uint32_t layer;
|
||||
};
|
||||
|
||||
class LayeredDepthStorage {
|
||||
public:
|
||||
LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), layers(layers) {
|
||||
const auto bytes = DescribeSurface(resource).guestBytes;
|
||||
StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(bytes), nullptr, "storage image creation", PublishScope::None);
|
||||
storage = StorageTexture::Create(context, *context.detiler, resource, 0);
|
||||
Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers");
|
||||
staging = std::make_unique<DeviceBuffer>(context, static_cast<std::size_t>(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
}
|
||||
|
||||
void Copy(std::span<const LayeredSlice> slices, bool toStorage) {
|
||||
if (slices.empty()) return;
|
||||
auto* recorder = Recorder::Active();
|
||||
Require(recorder != nullptr, "depth surface storage copies need an active recorder");
|
||||
const auto commands = recorder->Commands();
|
||||
recorder->Keep(storage);
|
||||
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy);
|
||||
std::vector<VkBufferImageCopy> storageRegions;
|
||||
const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) {
|
||||
VkBufferImageCopy copy{};
|
||||
copy.bufferOffset = sliceBytes() * layer;
|
||||
copy.imageSubresource = {aspect, 0, imageLayer, 1};
|
||||
copy.imageExtent = {extent.width, extent.height, 1};
|
||||
return copy;
|
||||
};
|
||||
for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer));
|
||||
const auto copyToBuffer = context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer");
|
||||
const auto copyToImage = context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage");
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
if (toStorage) {
|
||||
for (const auto& slice : slices) {
|
||||
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
||||
copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, ©);
|
||||
}
|
||||
} else {
|
||||
copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
|
||||
}
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
if (toStorage) {
|
||||
copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
|
||||
} else {
|
||||
for (const auto& slice : slices) {
|
||||
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
||||
copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, ©);
|
||||
}
|
||||
}
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
|
||||
recorder->EndGpuTiming(timing, sliceBytes() * slices.size());
|
||||
}
|
||||
|
||||
const Context context;
|
||||
const DepthTarget base;
|
||||
const VkExtent2D extent;
|
||||
const std::uint32_t bytesPerTexel;
|
||||
const std::uint32_t layers;
|
||||
std::shared_ptr<StorageTexture> storage;
|
||||
|
||||
private:
|
||||
std::unique_ptr<DeviceBuffer> staging;
|
||||
|
||||
VkDeviceSize sliceBytes() const {
|
||||
return static_cast<VkDeviceSize>(extent.width) * extent.height * bytesPerTexel;
|
||||
}
|
||||
};
|
||||
|
||||
std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>& layeredStorages() {
|
||||
static auto* storages = new std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>();
|
||||
return *storages;
|
||||
}
|
||||
|
||||
bool sameSurface(const DepthTarget& a, const DepthTarget& b) {
|
||||
return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples;
|
||||
}
|
||||
|
|
@ -574,6 +661,7 @@ void ClearDepthSurfaces(VkDevice device) {
|
|||
drawnDepth = nullptr;
|
||||
std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; });
|
||||
std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
|
||||
std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
|
||||
|
|
@ -681,17 +769,71 @@ bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words
|
|||
return ResolveTextureFormat(resource.format) == expected || depthBits;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<LayeredSlice> layeredSlices(const Context& context, const LayeredDepthStorage& storage) {
|
||||
const auto& base = storage.base;
|
||||
std::vector<LayeredSlice> slices;
|
||||
const auto& list = surfaces();
|
||||
for (std::uint32_t layer = 0; layer < storage.layers; ++layer) {
|
||||
const auto address = base.address + static_cast<std::uint64_t>(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel);
|
||||
const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; });
|
||||
if (slice != list.rend()) slices.push_back({slice->get(), layer});
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) {
|
||||
const auto& target = surface.target;
|
||||
const auto layers = resource.depthOrLastArray + 1u;
|
||||
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
|
||||
const auto bytesPerTexel = d16 ? 2u : 4u;
|
||||
if (resource.baseArray != 0) {
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast<unsigned long long>(target.address), resource.baseArray, resource.depthOrLastArray);
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
surface.RequireStorable(resource, mip);
|
||||
const auto geometry = DescribeSurface(resource);
|
||||
if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) {
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast<unsigned long long>(target.address), static_cast<unsigned long long>(geometry.GuestLayerOffset(1)), static_cast<unsigned long long>(DepthSliceBytes(target.extent, bytesPerTexel)));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast<int>(resource.tileMode), static_cast<int>(resource.dimension)}];
|
||||
if (entry == nullptr) entry = std::make_unique<LayeredDepthStorage>(context, resource, target, bytesPerTexel, layers);
|
||||
else static_cast<void>(entry->storage->Refresh());
|
||||
const auto slices = layeredSlices(context, *entry);
|
||||
for (const auto& slice : slices) slice.surface->ApplyFastClear();
|
||||
entry->Copy(slices, true);
|
||||
return entry->storage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
const auto& list = surfaces();
|
||||
const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) {
|
||||
return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress));
|
||||
});
|
||||
return found == list.rend() ? nullptr : (*found)->Storage(words, resource, mip);
|
||||
if (found == list.rend()) return nullptr;
|
||||
auto& surface = **found;
|
||||
const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress;
|
||||
if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip);
|
||||
if (!surface.StorageViewMatches(words, resource)) return nullptr;
|
||||
return layeredDepthStorage(context, surface, resource, mip);
|
||||
}
|
||||
|
||||
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) {
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
for (auto& [key, entry] : layeredStorages()) {
|
||||
if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue;
|
||||
const auto slices = layeredSlices(context, *entry);
|
||||
entry->Copy(slices, false);
|
||||
for (const auto& slice : slices) slice.surface->written = ++depthWrites;
|
||||
return true;
|
||||
}
|
||||
for (const auto& surface : surfaces()) {
|
||||
if (surface->context.device == context.device && surface->StoreBack(storage)) return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,38 +61,6 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std:
|
|||
|
||||
namespace {
|
||||
|
||||
std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
|
||||
if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest;
|
||||
throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast<int>(format)));
|
||||
}
|
||||
|
||||
// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X).
|
||||
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
|
||||
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
|
||||
const bool chain = color.mipCount > 1;
|
||||
GuestTextureResource surface{};
|
||||
surface.baseAddress = chain ? color.surfaceAddress : color.address;
|
||||
surface.pipeBankXor = color.pipeBankXor;
|
||||
surface.width = chain ? color.surfaceExtent.width : color.extent.width;
|
||||
surface.height = chain ? color.surfaceExtent.height : color.extent.height;
|
||||
surface.depthOrLastArray = color.depth - 1u;
|
||||
surface.baseArray = 0;
|
||||
surface.mipCount = color.mipCount;
|
||||
surface.baseLevel = 0;
|
||||
surface.lastLevel = color.mipCount - 1;
|
||||
surface.tileMode = ColorTextureTileMode(color.tileMode);
|
||||
surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D;
|
||||
surface.format = GuestFormatFor(color.format, color.elementBytes);
|
||||
surface.dstSelX = 4;
|
||||
surface.dstSelY = 5;
|
||||
surface.dstSelZ = 6;
|
||||
surface.dstSelW = 7;
|
||||
surface.dccAddress = color.dccAddress;
|
||||
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
|
||||
surface.dccPipeAligned = color.dccPipeAligned;
|
||||
return surface;
|
||||
}
|
||||
|
||||
GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) {
|
||||
Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array");
|
||||
auto surface = SurfaceForTarget(color);
|
||||
|
|
@ -221,20 +189,8 @@ void proveCmask(std::uint64_t address, std::size_t bytes, std::uint64_t generati
|
|||
else cmaskProofs()[address] = {bytes, generation};
|
||||
}
|
||||
|
||||
std::string hexAddress(std::uint64_t address) {
|
||||
char text[24];
|
||||
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(address));
|
||||
return text;
|
||||
}
|
||||
|
||||
void requireCmaskCodes(std::uint32_t codes) {
|
||||
if (codes == 0) return;
|
||||
Require((codes & 1u) == 0, "a CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys, was recorded");
|
||||
std::string listed;
|
||||
for (std::uint32_t code = 1; code < 15; ++code) {
|
||||
if ((codes >> code) & 1u) listed += (listed.empty() ? "" : ", ") + std::to_string(code);
|
||||
}
|
||||
Require(false, "CMASK codes " + listed + " were recorded, which are not a single-sample fast-clear code (0 cleared, 15 expanded)");
|
||||
}
|
||||
|
||||
CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::vector<std::uint32_t>& cleared) {
|
||||
|
|
@ -264,7 +220,6 @@ CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::v
|
|||
const auto nibble = layout.Nibble(tileX, tileY);
|
||||
const auto code = (std::to_integer<std::uint32_t>(mask[nibble / 2u]) >> ((nibble % 2u) * 4u)) & 0xfu;
|
||||
if (code == 0u) cleared.push_back(tileY * layout.TilesX() + tileX);
|
||||
else Require(code == 0xfu, "CMASK code " + std::to_string(code) + " of color target " + hexAddress(color.address) + " is not a single-sample fast-clear code (0 cleared, 15 expanded)");
|
||||
}
|
||||
}
|
||||
if (cleared.empty()) return CmaskState::Expanded;
|
||||
|
|
@ -688,6 +643,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
|
|||
add(context.fragmentShaderBarycentric);
|
||||
add(context.shaderClipDistance);
|
||||
add(context.shaderCullDistance);
|
||||
add(context.viewportIndexLayer);
|
||||
add(state.paClVsOutCntl);
|
||||
add(state.stages.mesh.has_value());
|
||||
if (state.stages.mesh) {
|
||||
|
|
@ -709,6 +665,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
|
|||
add(tessellation.domain);
|
||||
add(tessellation.partitioning);
|
||||
add(tessellation.outputTopology);
|
||||
add(tessellation.patchesPerGroup);
|
||||
}
|
||||
}
|
||||
return keyed;
|
||||
|
|
@ -740,7 +697,7 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
|
|||
}
|
||||
std::set<std::uint32_t> outputs;
|
||||
try {
|
||||
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance);
|
||||
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer);
|
||||
} catch (const std::exception& error) {
|
||||
if (keyed) {
|
||||
std::lock_guard lock(validationMutex());
|
||||
|
|
@ -1080,6 +1037,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
|||
const auto& mesh = *state.stages.mesh;
|
||||
const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u;
|
||||
Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive");
|
||||
Require(((state.stages.registerValue >> 19u) & 3u) == 0 || !draw.indexed, "indexed fast-launch draws are unsupported");
|
||||
if (args == nullptr) {
|
||||
const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize;
|
||||
const auto primitives = (draw.indexCount - inputSize) / step + 1u;
|
||||
|
|
@ -1179,7 +1137,8 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
|||
const auto bytes = args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance);
|
||||
const auto& fields = attribute.resource.fields;
|
||||
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
|
||||
Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address, "vertex buffer aliases the render target");
|
||||
const auto firstByte = args == nullptr ? VertexBufferReadOffset(attribute, draw.firstVertex, draw.firstInstance) : 0;
|
||||
Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address + firstByte, "vertex buffer aliases the render target");
|
||||
const auto stride = (fields[1] >> 16u) & 0x3fffu;
|
||||
fetchOf[i] = fetches.size();
|
||||
fetches.push_back({address, address + bytes, stride, attribute.fetchIndex, DecodeVertexFormat(attribute).alignment});
|
||||
|
|
@ -1590,6 +1549,12 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const
|
|||
resources.PatchPushConstants(block);
|
||||
stages = PushConstantStages(shaders);
|
||||
}
|
||||
if (state.stages.tessellation) {
|
||||
for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels");
|
||||
const std::array<float, ShaderRecompiler::TessellationLevelPushBytes / 4> levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel};
|
||||
static_assert(ShaderRecompiler::TessellationLevelPushOffsetBytes + ShaderRecompiler::TessellationLevelPushBytes == PipelinePushSlotBytes);
|
||||
std::memcpy(block.data() + ShaderRecompiler::TessellationLevelPushOffsetBytes, levels.data(), sizeof(levels));
|
||||
}
|
||||
if (!state.stages.mesh) {
|
||||
pipeline.PushConstants(commands, stages, block);
|
||||
return;
|
||||
|
|
@ -1968,7 +1933,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
|||
if (binding.resident != nullptr) {
|
||||
timer.phase(PhaseReadTarget);
|
||||
binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED;
|
||||
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice));
|
||||
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.layers, color.arraySlice));
|
||||
continue;
|
||||
}
|
||||
materializeCmaskClear(context, color, nullptr);
|
||||
|
|
@ -2156,7 +2121,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
|||
owners.push_back(binding.resident);
|
||||
persistent.push_back(binding.multisampled ? 1u : 0u);
|
||||
}
|
||||
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent);
|
||||
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent);
|
||||
timer.phase(PhasePipeline);
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
|
||||
if (lean) {
|
||||
|
|
@ -2180,7 +2145,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
|||
// only a template the resource cache serves under this content key (reusable: no lease,
|
||||
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
|
||||
// hit's proof is the template's ProveCurrent and nothing needs completion work.
|
||||
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) {
|
||||
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) {
|
||||
auto recipe = std::make_shared<DrawRecipe>();
|
||||
recipe->device = context.device;
|
||||
recipe->templateRef = resources;
|
||||
|
|
@ -2199,6 +2164,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
|||
recipe->masked = masked;
|
||||
recipe->fragmentOutputs = inputs.fragmentOutputs;
|
||||
recipe->shaderStages = inputs.shaderStages;
|
||||
recipe->hasDepth = state.depth.has_value();
|
||||
*recipeOut = std::move(recipe);
|
||||
}
|
||||
timing.Mark("draw_and_resource_release");
|
||||
|
|
@ -2476,7 +2442,9 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
|||
Require(!draw.indirect, "a draw recipe covers direct draws only");
|
||||
auto* recorder = Recorder::Active();
|
||||
if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable);
|
||||
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state");
|
||||
if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone);
|
||||
const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u);
|
||||
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state");
|
||||
auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe);
|
||||
if (inputs.nothing) {
|
||||
result.recorded = true;
|
||||
|
|
@ -2502,6 +2470,12 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
|||
if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone);
|
||||
targets.push_back(std::move(resident));
|
||||
}
|
||||
if (recipe.hasDepth) {
|
||||
timer.phase(PhaseSetup);
|
||||
const auto depthView = DepthSurfaceView(context, *state.depth);
|
||||
timer.phase(PhaseReadTarget);
|
||||
if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone);
|
||||
}
|
||||
timer.phase(PhasePrepare);
|
||||
// The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed
|
||||
// key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps
|
||||
|
|
@ -2532,7 +2506,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
|||
auto pipeline = recipe.pipeline.lock();
|
||||
if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone);
|
||||
auto framebuffer = recipe.framebuffer.lock();
|
||||
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent);
|
||||
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent, state.renderLayers);
|
||||
timer.phase(PhasePipeline);
|
||||
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
RecordedDraw record;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) {
|
|||
pipelineInfo.stage.module = module;
|
||||
pipelineInfo.stage.pName = "main";
|
||||
pipelineInfo.layout = pipelineLayout;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
|
||||
}
|
||||
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
module = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
|
||||
|
|
@ -161,6 +162,7 @@ struct HostImports {
|
|||
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
|
||||
std::uint64_t liveBytes = 0;
|
||||
std::map<std::uint64_t, ImportFailure> failed;
|
||||
std::map<std::uint64_t, ImportFailure> failedJoins;
|
||||
std::set<std::uint64_t> unaliasedSpans;
|
||||
// Registry generation the imports were last reconciled with.
|
||||
std::uint64_t refreshedGeneration = 0;
|
||||
|
|
@ -170,6 +172,8 @@ struct HostImports {
|
|||
VkDevice watchDevice = VK_NULL_HANDLE;
|
||||
bool unwatchImports = false;
|
||||
bool unwatchDmaBufImports = false;
|
||||
std::uint64_t useCounter = 0;
|
||||
std::uint64_t buildFloor = 0;
|
||||
};
|
||||
|
||||
HostImports& Imports() {
|
||||
|
|
@ -210,12 +214,13 @@ struct RetiredImport {
|
|||
|
||||
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
|
||||
bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
|
||||
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
|
||||
|
||||
void retireImport(VkDevice device, HostImports& state, std::map<std::uint64_t, HostImport>::iterator it, const GuestAllocations::Lease& lease, bool hold = true) {
|
||||
// Results shadowed for the import reach its (old) buffer first where the memory is still a
|
||||
// readable registered range (the import retires because its registration vanished or changed
|
||||
// size); the holder below outlives the batch that copies them.
|
||||
RetireShadow(device, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end); });
|
||||
RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); });
|
||||
++state.epoch;
|
||||
state.liveBytes -= it->second.bytes;
|
||||
auto holder = std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it));
|
||||
|
|
@ -579,6 +584,13 @@ std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(const GuestAlloc
|
|||
return found != lease.end() && (*found)->address == base ? *found : nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
|
||||
const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; });
|
||||
if (found == lease.begin()) return nullptr;
|
||||
const auto& range = *std::prev(found);
|
||||
return begin >= range->address && end <= range->address + range->bytes ? range : nullptr;
|
||||
}
|
||||
|
||||
struct ImportRun {
|
||||
std::uint64_t base = 0;
|
||||
std::uint64_t bytes = 0;
|
||||
|
|
@ -630,6 +642,34 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u
|
|||
return !run.ranges.empty() && end <= run.base + run.bytes;
|
||||
}
|
||||
|
||||
ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
|
||||
auto last = leasedIndexContaining(lease, begin);
|
||||
if (last == lease.size()) return {};
|
||||
const auto first = last;
|
||||
while (lease[last]->address + lease[last]->bytes < end) {
|
||||
if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {};
|
||||
++last;
|
||||
}
|
||||
if (first == last) return {};
|
||||
ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}};
|
||||
run.ranges.assign(lease.begin() + static_cast<std::ptrdiff_t>(first), lease.begin() + static_cast<std::ptrdiff_t>(last) + 1);
|
||||
return run;
|
||||
}
|
||||
|
||||
ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
|
||||
const auto window = importWindow.load(std::memory_order_relaxed);
|
||||
if (window == 0) return {};
|
||||
auto run = crossedRun(lease, begin, end);
|
||||
if (run.ranges.empty()) return run;
|
||||
if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run;
|
||||
if (begin == run.base && end == run.base + run.bytes) return run;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
|
||||
return !crossedRun(lease, begin, end).ranges.empty();
|
||||
}
|
||||
|
||||
template<typename Entry>
|
||||
bool sameRun(const Entry& entry, const ImportRun& run) {
|
||||
if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false;
|
||||
|
|
@ -652,45 +692,173 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) {
|
|||
}
|
||||
|
||||
bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) {
|
||||
return sameRun(entry, importRun(lease, entry.base));
|
||||
if (entry.ranges.size() == 1 && !entry.joined && entry.bytes < entry.ranges.front()->bytes) {
|
||||
const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes);
|
||||
return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front());
|
||||
}
|
||||
return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base));
|
||||
}
|
||||
|
||||
bool pointerHeld(const HostImports& state, const HostImport& entry) {
|
||||
return entry.lastUse >= state.buildFloor;
|
||||
}
|
||||
|
||||
bool evictable(const HostImports& state, const HostImport& entry) {
|
||||
constexpr std::uint64_t protectedUses = 256;
|
||||
return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter;
|
||||
}
|
||||
|
||||
std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) {
|
||||
std::uint64_t freed = 0;
|
||||
while (freed < wanted) {
|
||||
auto oldest = state.imports.end();
|
||||
for (auto it = state.imports.begin(); it != state.imports.end(); ++it) {
|
||||
if (evictable(state, it->second) && (oldest == state.imports.end() || it->second.lastUse < oldest->second.lastUse)) oldest = it;
|
||||
}
|
||||
if (oldest == state.imports.end()) break;
|
||||
freed += oldest->second.bytes;
|
||||
retireImport(context.device, state, oldest, lease);
|
||||
}
|
||||
return freed;
|
||||
}
|
||||
|
||||
std::uint64_t physicalMemoryBytes() {
|
||||
#ifdef _WIN32
|
||||
MEMORYSTATUSEX status{};
|
||||
status.dwLength = sizeof(status);
|
||||
return GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0;
|
||||
#else
|
||||
const auto pages = sysconf(_SC_PHYS_PAGES);
|
||||
const auto pageSize = sysconf(_SC_PAGESIZE);
|
||||
return pages > 0 && pageSize > 0 ? static_cast<std::uint64_t>(pages) * static_cast<std::uint64_t>(pageSize) : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::atomic<std::uint64_t> budgetOverride{0};
|
||||
|
||||
// Pinned imports count against the driver's system memory heap, which imports and type-3 mirrors share; past it
|
||||
// ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and half the physical
|
||||
// memory, at most the largest host heap less 2 GiB.
|
||||
std::uint64_t importBudget(const Context& context) {
|
||||
if (const auto set = budgetOverride.load(std::memory_order_relaxed); set != 0) return set;
|
||||
static const std::uint64_t configured = []() -> std::uint64_t {
|
||||
const char* value = std::getenv("APS5_HOST_IMPORT_MIB");
|
||||
return value != nullptr ? std::strtoull(value, nullptr, 10) << 20u : 0u;
|
||||
}();
|
||||
if (configured != 0) return configured;
|
||||
std::uint64_t hostHeap = 0;
|
||||
for (std::uint32_t index = 0; index < context.memory.memoryHeapCount; ++index) {
|
||||
if ((context.memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max<std::uint64_t>(hostHeap, context.memory.memoryHeaps[index].size);
|
||||
}
|
||||
const auto fromMemory = std::max<std::uint64_t>(6144ull << 20u, physicalMemoryBytes() / 2);
|
||||
constexpr std::uint64_t reserve = 2048ull << 20u;
|
||||
return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory;
|
||||
}
|
||||
|
||||
std::atomic<std::uint64_t> rangeImportWindow{64ull << 20u};
|
||||
|
||||
// The import of an access to a registered range larger than the range window: the cells the access touches, grown to
|
||||
// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of
|
||||
// replacing each other's.
|
||||
ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) {
|
||||
const auto window = rangeImportWindow.load(std::memory_order_relaxed);
|
||||
if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run;
|
||||
const auto& range = *run.ranges.front();
|
||||
if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run;
|
||||
const auto lastCell = range.bytes / window - 1;
|
||||
const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); };
|
||||
auto windowBegin = range.address + cell(begin) * window;
|
||||
const auto endCell = cell(end - 1);
|
||||
auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window;
|
||||
for (bool grown = true; grown;) {
|
||||
grown = false;
|
||||
auto it = state.imports.upper_bound(windowBegin);
|
||||
if (it != state.imports.begin()) --it;
|
||||
for (; it != state.imports.end() && it->first < windowEnd; ++it) {
|
||||
const auto importEnd = it->second.base + it->second.bytes;
|
||||
if (importEnd <= windowBegin || it->second.base < range.address || importEnd > range.address + range.bytes) continue;
|
||||
if (it->second.base < windowBegin || importEnd > windowEnd) grown = true;
|
||||
windowBegin = std::min(windowBegin, it->second.base);
|
||||
windowEnd = std::max(windowEnd, importEnd);
|
||||
}
|
||||
}
|
||||
if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return run;
|
||||
run.base = windowBegin;
|
||||
run.bytes = windowEnd - windowBegin;
|
||||
return run;
|
||||
}
|
||||
|
||||
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end);
|
||||
bool importsStale(const Context& context, const HostImports& state);
|
||||
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict);
|
||||
|
||||
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) {
|
||||
if (!importsStale(context, state)) {
|
||||
if (const auto* current = findImport(state, address, address + 1)) return current;
|
||||
// The bytes retireLeastRecent could free outside [begin, end).
|
||||
std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) {
|
||||
std::uint64_t bytes = 0;
|
||||
for (const auto& [base, entry] : state.imports) {
|
||||
if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.bytes;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) {
|
||||
const bool fresh = !importsStale(context, state);
|
||||
if (fresh && end != 0) {
|
||||
if (const auto* current = findImport(state, address, end)) return current;
|
||||
} else if (!fresh) {
|
||||
if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current;
|
||||
}
|
||||
auto run = importRun(lease, address);
|
||||
// Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the
|
||||
// first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below.
|
||||
if (fresh && end == 0) {
|
||||
if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current;
|
||||
}
|
||||
if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment);
|
||||
if (run.ranges.empty()) return nullptr;
|
||||
return importRunAt(context, state, std::move(run), false, address, lease, end, evict);
|
||||
}
|
||||
|
||||
const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
|
||||
if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
|
||||
if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
|
||||
auto run = joinedRun(lease, begin, end);
|
||||
if (run.ranges.empty()) return nullptr;
|
||||
if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr;
|
||||
return importRunAt(context, state, std::move(run), true, begin, lease, end, true);
|
||||
}
|
||||
|
||||
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict) {
|
||||
const auto base = run.base;
|
||||
const auto bytes = run.bytes;
|
||||
if (const auto found = state.imports.find(base); found != state.imports.end()) {
|
||||
if (sameRun(found->second, run)) return &found->second;
|
||||
retireImport(context.device, state, found, lease);
|
||||
}
|
||||
for (auto it = state.imports.lower_bound(base); it != state.imports.end() && it->first < base + bytes;) {
|
||||
const auto next = std::next(it);
|
||||
retireImport(context.device, state, it, lease);
|
||||
it = next;
|
||||
}
|
||||
if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease);
|
||||
auto& failures = joined ? state.failedJoins : state.failed;
|
||||
const auto alignment = context.hostImportAlignment;
|
||||
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
|
||||
if (const auto failed = state.failed.find(base); failed != state.failed.end()) {
|
||||
if (sameRun(failed->second, run)) return nullptr;
|
||||
state.failed.erase(failed);
|
||||
if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) {
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())});
|
||||
return nullptr;
|
||||
}
|
||||
// Pinned imports count against the driver's system memory budget; past it ordinary host
|
||||
// allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the
|
||||
// registered memory of address-based shaders; past it they copy gigabytes per dispatch).
|
||||
static const std::uint64_t budget = [] {
|
||||
const char* value = std::getenv("APS5_HOST_IMPORT_MIB");
|
||||
return (value ? std::strtoull(value, nullptr, 10) : 6144ull) << 20u;
|
||||
}();
|
||||
const auto live = state.liveBytes;
|
||||
if (live + bytes > budget) {
|
||||
if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second;
|
||||
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
|
||||
if (const auto failed = failures.find(base); failed != failures.end()) {
|
||||
if (sameRun(failed->second, run)) return nullptr;
|
||||
failures.erase(failed);
|
||||
}
|
||||
// The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with
|
||||
// what may be evicted) before any of them is retired.
|
||||
auto first = state.imports.lower_bound(base);
|
||||
if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first;
|
||||
std::uint64_t replaced = 0;
|
||||
// A join may replace imports this build already looked up: it runs before any region confirms an import
|
||||
// (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the
|
||||
// hold (KeepThroughHold). Refusing it there would keep the run copied for good.
|
||||
const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); };
|
||||
for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) {
|
||||
if (held(it->second)) return nullptr;
|
||||
replaced += it->second.bytes;
|
||||
}
|
||||
const auto budget = importBudget(context);
|
||||
const auto live = state.liveBytes - replaced;
|
||||
if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) {
|
||||
// A refused import turns every later use of the range into CPU copies, so say so.
|
||||
static std::uint64_t refused = 0, refusedBytes = 0;
|
||||
static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60);
|
||||
|
|
@ -698,10 +866,16 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
|||
refusedBytes += bytes;
|
||||
if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) {
|
||||
lastReport = std::chrono::steady_clock::now();
|
||||
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast<unsigned long long>(live >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes));
|
||||
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast<unsigned long long>(state.liveBytes >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
for (auto it = first; it != state.imports.end() && it->first < base + bytes;) {
|
||||
const auto next = std::next(it);
|
||||
retireImport(context.device, state, it, lease);
|
||||
it = next;
|
||||
}
|
||||
if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget);
|
||||
HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
|
||||
entry.range = run.ranges.front();
|
||||
entry.ranges = std::move(run.ranges);
|
||||
|
|
@ -713,7 +887,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
|||
for (std::uint64_t cursor = base; cursor < base + bytes;) {
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
if (VirtualQuery(reinterpret_cast<const void*>(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
|
||||
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
return nullptr;
|
||||
}
|
||||
const auto protection = info.Protect & 0xffu;
|
||||
|
|
@ -725,7 +899,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
|||
cursor = reinterpret_cast<std::uint64_t>(info.BaseAddress) + info.RegionSize;
|
||||
}
|
||||
if (!writable && readOnly) {
|
||||
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
return nullptr;
|
||||
}
|
||||
if (!writable) {
|
||||
|
|
@ -738,13 +912,17 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
|||
entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes));
|
||||
} catch (const std::runtime_error&) {
|
||||
if (entry.ranges.size() == 1) throw;
|
||||
if (joined) {
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
return nullptr;
|
||||
}
|
||||
state.unaliasedSpans.insert(base);
|
||||
return importAllocation(context, state, address, lease);
|
||||
return importAllocation(context, state, address, lease, end, evict);
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
|
||||
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -775,7 +953,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
|||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
|
||||
#endif
|
||||
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
|
||||
std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result));
|
||||
#ifdef _WIN32
|
||||
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
|
||||
|
|
@ -805,16 +983,12 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
|||
importedBytes += bytes;
|
||||
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
|
||||
state.liveBytes += bytes;
|
||||
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0);
|
||||
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : "");
|
||||
entry.joined = joined;
|
||||
entry.lastUse = ++state.useCounter;
|
||||
return &state.imports.emplace(base, entry).first->second;
|
||||
}
|
||||
|
||||
// The lease is in address order (it is built from the registry map), so a range is found by binary search.
|
||||
const GuestAllocations::Range* leasedRangeAt(const GuestAllocations::Lease& lease, std::uint64_t base) {
|
||||
const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; });
|
||||
return found != lease.end() && (*found)->address == base ? found->get() : nullptr;
|
||||
}
|
||||
|
||||
// The readable registered range containing [begin, end), or null.
|
||||
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
|
||||
const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; });
|
||||
|
|
@ -839,6 +1013,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
|
|||
}
|
||||
state.liveBytes = 0;
|
||||
state.failed.clear();
|
||||
state.failedJoins.clear();
|
||||
state.device = context.device;
|
||||
state.destroyBuffer = context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer");
|
||||
state.freeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
|
||||
|
|
@ -851,19 +1026,19 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
|
|||
if (generation == state.refreshedGeneration) return;
|
||||
state.refreshedGeneration = generation;
|
||||
for (auto it = state.imports.begin(); it != state.imports.end();) {
|
||||
const auto* range = leasedRangeAt(lease, it->first);
|
||||
if (range != nullptr && sameRange(it->second, lease)) {
|
||||
if (sameRange(it->second, lease)) {
|
||||
if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes);
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
|
||||
if (trace && range != nullptr && range->bytes == it->second.bytes) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
|
||||
if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
|
||||
const auto next = std::next(it);
|
||||
retireImport(context.device, state, it, lease);
|
||||
it = next;
|
||||
}
|
||||
for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it);
|
||||
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
|
||||
}
|
||||
|
||||
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) {
|
||||
|
|
@ -871,7 +1046,15 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6
|
|||
if (found == state.imports.begin()) return nullptr;
|
||||
--found;
|
||||
const auto& entry = found->second;
|
||||
return begin >= entry.base && end <= entry.base + entry.bytes ? &entry : nullptr;
|
||||
if (begin < entry.base || end > entry.base + entry.bytes) return nullptr;
|
||||
entry.lastUse = ++state.useCounter;
|
||||
return &entry;
|
||||
}
|
||||
|
||||
const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
|
||||
if (!runCovers(lease, begin, end)) return findImport(state, begin, end);
|
||||
const auto* entry = importAllocation(context, state, begin, lease, end);
|
||||
return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr;
|
||||
}
|
||||
|
||||
// Whether the imports need reconciling before a lookup: the registry changed since the last walk.
|
||||
|
|
@ -1784,6 +1967,10 @@ MirrorStats MirrorCounters() {
|
|||
}
|
||||
|
||||
void ClearImageMirrors(VkDevice device) {
|
||||
auto space = Spaces().current.load();
|
||||
if (space != nullptr && space->device == device) {
|
||||
Spaces().current.compare_exchange_strong(space, std::shared_ptr<const GuestBufferMemory::AddressSpace>{});
|
||||
}
|
||||
auto& state = Mirrors();
|
||||
std::map<std::uint64_t, std::shared_ptr<ImageMirror>> entries;
|
||||
{
|
||||
|
|
@ -1794,7 +1981,6 @@ void ClearImageMirrors(VkDevice device) {
|
|||
state.heapBytes = 0;
|
||||
state.device = VK_NULL_HANDLE;
|
||||
}
|
||||
Spaces().current.store(nullptr);
|
||||
}
|
||||
|
||||
void ClearHostImports(VkDevice device) {
|
||||
|
|
@ -1812,6 +1998,7 @@ void ClearHostImports(VkDevice device) {
|
|||
state.imports.clear();
|
||||
state.liveBytes = 0;
|
||||
state.failed.clear();
|
||||
state.failedJoins.clear();
|
||||
state.device = VK_NULL_HANDLE;
|
||||
state.refreshedGeneration = 0;
|
||||
++state.epoch;
|
||||
|
|
@ -2000,6 +2187,20 @@ void SetHostImportWindow(std::uint64_t bytes) {
|
|||
importWindow.store(bytes, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void SetHostImportRangeWindow(std::uint64_t bytes) {
|
||||
rangeImportWindow.store(bytes, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void SetHostImportBudget(std::uint64_t bytes) {
|
||||
budgetOverride.store(bytes, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::uint64_t HostImportLiveBytes() {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
return state.liveBytes;
|
||||
}
|
||||
|
||||
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) {
|
||||
if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return nullptr;
|
||||
ensurePinWaiter();
|
||||
|
|
@ -2007,13 +2208,14 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s
|
|||
std::lock_guard lock(state.mutex);
|
||||
// A hit is only valid while the registry has not changed since the imports were reconciled.
|
||||
if (!importsStale(context, state)) {
|
||||
if (const auto* entry = findImport(state, address, address + bytes)) return entry;
|
||||
if (const auto* entry = findImport(state, address, address + bytes)) {
|
||||
entry->lastUse = ++state.useCounter;
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
|
||||
refreshImports(context, state, lease);
|
||||
if (!runCovers(lease, address, address + bytes)) return nullptr;
|
||||
const auto* entry = importAllocation(context, state, address, lease);
|
||||
return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr;
|
||||
return importAccess(context, state, address, address + bytes, lease);
|
||||
}
|
||||
|
||||
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) {
|
||||
|
|
@ -2036,7 +2238,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped&
|
|||
if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue;
|
||||
try {
|
||||
if (!GuestMemory::GpuMutex().HeldByThisThread() && wouldRetireImport(state, importRun(current, range->address))) return false;
|
||||
if (importAllocation(context, state, range->address, current) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base);
|
||||
if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base);
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast<unsigned long long>(range->address), range->bytes, error.what());
|
||||
}
|
||||
|
|
@ -2136,6 +2338,13 @@ void GuestBufferMemory::AcquireRegistered() {
|
|||
else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed);
|
||||
else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed);
|
||||
else hit = true;
|
||||
importFloor = state.useCounter + 1;
|
||||
if (hit) {
|
||||
const auto stamp = ++state.useCounter;
|
||||
for (const auto& region : current->base) {
|
||||
if (region.direct != nullptr) region.direct->lastUse = stamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hit) {
|
||||
for (auto* mirror : current->sparseMirrors) {
|
||||
|
|
@ -2174,6 +2383,8 @@ void GuestBufferMemory::AcquireRegistered() {
|
|||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (importable) refreshImports(context, state, lease);
|
||||
if (importFloor == 0) importFloor = state.useCounter + 1;
|
||||
state.buildFloor = importFloor;
|
||||
// Taken before the pass: the imports it takes are the registry's ones (just reconciled, so
|
||||
// none is retired by a make below), and a mirror's preparation may sync the recorder, whose
|
||||
// completions can retire an import already taken; that moves the epoch, and Upload then
|
||||
|
|
@ -2195,7 +2406,7 @@ void GuestBufferMemory::AcquireRegistered() {
|
|||
if (importable) {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
entry = importAllocation(context, state, range->address, lease);
|
||||
entry = importAllocation(context, state, range->address, lease, 0, false);
|
||||
}
|
||||
lap(timing.importsUs, at);
|
||||
if (entry != nullptr) {
|
||||
|
|
@ -2257,6 +2468,20 @@ void GuestBufferMemory::AcquireRegistered() {
|
|||
publish = false;
|
||||
spaces.unpublished.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
std::size_t kept = 0;
|
||||
for (std::size_t index = 0; index < regions.size(); ++index) {
|
||||
auto& region = regions[index];
|
||||
if (kept != 0) {
|
||||
auto& previous = regions[kept - 1];
|
||||
if (region.direct != nullptr && previous.direct == region.direct && previous.end == region.begin && previous.writable == region.writable && region.mirror == nullptr && previous.mirror == nullptr && !region.sparse && !previous.sparse && region.end <= region.direct->base + region.direct->bytes) {
|
||||
previous.end = region.end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (kept != index) regions[kept] = std::move(region);
|
||||
++kept;
|
||||
}
|
||||
regions.erase(regions.begin() + static_cast<std::ptrdiff_t>(kept), regions.end());
|
||||
for (const auto& region : regions) {
|
||||
if (region.direct != nullptr || region.mirror != nullptr) ranges.push_back(addressRange(region));
|
||||
}
|
||||
|
|
@ -2830,6 +3055,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
|
|||
if (context.hostImportAlignment != 0) {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (importFloor == 0) importFloor = state.useCounter + 1;
|
||||
const bool stale = importsStale(context, state);
|
||||
for (auto& region : regions) {
|
||||
if (region.mirror != nullptr) continue;
|
||||
|
|
@ -2904,10 +3130,19 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
|
|||
|
||||
void GuestBufferMemory::UploadFinish(bool addressable) {
|
||||
Require(prepared && !uploaded, "guest memory upload was not prepared");
|
||||
uploaded = true;
|
||||
for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "written buffer range");
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
|
||||
if (space != nullptr) {
|
||||
for (const auto& region : space->base) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
|
||||
}
|
||||
uploaded = true;
|
||||
for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "written buffer range");
|
||||
if (context.hostImportAlignment != 0) {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.buildFloor = importFloor != 0 ? importFloor : state.useCounter + 1;
|
||||
}
|
||||
// Registered ranges to import from. Address-based shaders hold their lease until write-back;
|
||||
// descriptor-only uploads acquire one only when the registry changed or an import must be made
|
||||
// (a lease copies every registered range's shared_ptr under the registry lock), so the guest can
|
||||
|
|
@ -2937,6 +3172,17 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
|||
++Mirrors().subranges;
|
||||
}
|
||||
bool importsRefreshed = false;
|
||||
if (context.hostImportAlignment != 0) {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (importsStale(context, state)) refreshImports(context, state, importRanges());
|
||||
importsRefreshed = true;
|
||||
for (const auto& region : regions) {
|
||||
if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue;
|
||||
if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue;
|
||||
static_cast<void>(importJoined(context, state, region.begin, region.end, importRanges()));
|
||||
}
|
||||
}
|
||||
if (space != nullptr && context.hostImportAlignment != 0) {
|
||||
// The space's regions were confirmed against the imports when it was built; one check that
|
||||
// none was retired since (a mirror refresh or a flush between the stages can reconcile
|
||||
|
|
@ -2994,7 +3240,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
|||
region.direct = nullptr;
|
||||
if (entry == nullptr) entry = findImport(state, region.begin, region.end);
|
||||
if (entry == nullptr) {
|
||||
if (runCovers(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, region.begin, importRanges());
|
||||
entry = importAccess(context, state, region.begin, region.end, importRanges());
|
||||
}
|
||||
// A staged region (see stagingEligible) is copied out of the import even when aligned;
|
||||
// without a recorder to record the copies it binds in place like any other.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
|
|
@ -153,6 +154,7 @@ public:
|
|||
}
|
||||
|
||||
Context context;
|
||||
std::shared_ptr<std::atomic<bool>> active = std::make_shared<std::atomic<bool>>(true);
|
||||
ColorTarget target;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
|
|
@ -175,10 +177,19 @@ struct GuardedRange {
|
|||
std::uint64_t address;
|
||||
std::uint64_t bytes;
|
||||
std::uint32_t samples;
|
||||
std::shared_ptr<std::atomic<bool>> active;
|
||||
};
|
||||
|
||||
class PrimedSharedMutex : public std::shared_mutex {
|
||||
public:
|
||||
PrimedSharedMutex() {
|
||||
lock();
|
||||
unlock();
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_mutex& guardedMutex() {
|
||||
static std::shared_mutex mutex;
|
||||
static PrimedSharedMutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +207,17 @@ void guardReads(std::uint64_t address, std::size_t bytes) {
|
|||
RequireNoMultisampleTarget(address, bytes, "a CPU read");
|
||||
}
|
||||
|
||||
void guard(const Context& context, const ColorTarget& target) {
|
||||
void retireUnmapped(std::uintptr_t address, std::size_t bytes) {
|
||||
std::unique_lock lock(guardedMutex());
|
||||
std::erase_if(guarded(), [&](const auto& range) {
|
||||
if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false;
|
||||
range.active->store(false, std::memory_order_release);
|
||||
return true;
|
||||
});
|
||||
anyGuarded().store(!guarded().empty(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void guard(const Context& context, const ColorTarget& target, const std::shared_ptr<std::atomic<bool>>& active) {
|
||||
auto bytes = std::max<std::uint64_t>(target.bytes * target.samples, static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.elementBytes * target.samples);
|
||||
if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) {
|
||||
const auto log2Samples = std::countr_zero(target.samples);
|
||||
|
|
@ -208,10 +229,13 @@ void guard(const Context& context, const ColorTarget& target) {
|
|||
bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples;
|
||||
}
|
||||
std::unique_lock lock(guardedMutex());
|
||||
guarded().push_back({context.device, target.address, bytes, target.samples});
|
||||
guarded().push_back({context.device, target.address, bytes, target.samples, active});
|
||||
anyGuarded().store(true, std::memory_order_release);
|
||||
static std::once_flag installed;
|
||||
std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); });
|
||||
std::call_once(installed, [] {
|
||||
GuestMemory::SetReadGuard(&guardReads);
|
||||
});
|
||||
GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped);
|
||||
}
|
||||
|
||||
std::mutex& targetsMutex() {
|
||||
|
|
@ -226,14 +250,15 @@ std::vector<std::unique_ptr<MultisampleTarget>>& targets() {
|
|||
|
||||
MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
|
||||
for (const auto& target : targets()) {
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
|
||||
target->cmask.CheckAddress(use.cmaskAddress);
|
||||
target->ApplyFastClear(use);
|
||||
return *target;
|
||||
}
|
||||
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
|
||||
guard(context, use);
|
||||
auto& created = *targets().back();
|
||||
guard(context, use, created.active);
|
||||
if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
|
||||
created.ApplyFastClear(use);
|
||||
return created;
|
||||
|
|
@ -380,6 +405,7 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source,
|
|||
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
for (const auto& target : targets()) {
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
target->colorFill.NoteFill(address, bytes, pattern);
|
||||
const auto before = target->cmask.Keys();
|
||||
target->cmask.NoteFill(address, bytes, pattern);
|
||||
|
|
@ -395,6 +421,7 @@ void ClearMultisampleTargets(VkDevice device) {
|
|||
std::unique_lock guardLock(guardedMutex());
|
||||
std::erase_if(guarded(), [&](const auto& range) { return range.device == device; });
|
||||
anyGuarded().store(!guarded().empty(), std::memory_order_release);
|
||||
if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr);
|
||||
}
|
||||
|
||||
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {
|
||||
|
|
|
|||
|
|
@ -69,10 +69,11 @@ bool HasStencil(VkFormat format) {
|
|||
return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_S8_UINT;
|
||||
}
|
||||
|
||||
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent) : context(context) {
|
||||
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent, std::uint32_t layers) : context(context) {
|
||||
// Cached objects outlive their device's teardown; they must not keep its buffer pool alive past it.
|
||||
this->context.bufferPool.reset();
|
||||
Require(extent.width != 0 && extent.height != 0 && extent.width <= context.limits.maxFramebufferWidth && extent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits");
|
||||
Require(layers != 0 && layers <= context.limits.maxFramebufferLayers, "framebuffer layers exceed device limits");
|
||||
views.assign(targets.begin(), targets.end());
|
||||
if (renderPass == VK_NULL_HANDLE) return;
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
|
|
@ -81,7 +82,7 @@ Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::s
|
|||
framebufferInfo.pAttachments = targets.empty() ? nullptr : targets.data();
|
||||
framebufferInfo.width = extent.width;
|
||||
framebufferInfo.height = extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
framebufferInfo.layers = layers;
|
||||
Check(context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer");
|
||||
}
|
||||
|
||||
|
|
@ -498,13 +499,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
|||
return;
|
||||
}
|
||||
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules);
|
||||
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
|
||||
else {
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
|
||||
}
|
||||
if (alternateAlpha) {
|
||||
blend.pAttachments = secondChannels.data();
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines");
|
||||
}
|
||||
if (state.depthBiasPerFace) {
|
||||
raster.cullMode = VK_CULL_MODE_FRONT_BIT;
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines");
|
||||
}
|
||||
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
|
|
@ -571,7 +577,7 @@ VkPipelineLayout Pipeline::Layout() const {
|
|||
return layout;
|
||||
}
|
||||
|
||||
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent) {
|
||||
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers, std::span<const std::uint8_t> persistent) {
|
||||
Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments");
|
||||
bool resident = true;
|
||||
for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0));
|
||||
|
|
@ -586,7 +592,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
|
|||
return false;
|
||||
});
|
||||
for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) {
|
||||
if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
|
||||
if (it->extent.width != extent.width || it->extent.height != extent.height || it->layers != layers || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
|
||||
// View handles are recycled once a StorageTexture is destroyed, so the owners must be the
|
||||
// very objects the views were made for.
|
||||
bool same = true;
|
||||
|
|
@ -596,7 +602,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
|
|||
return framebuffers.back().framebuffer;
|
||||
}
|
||||
}
|
||||
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent);
|
||||
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent, layers);
|
||||
if (!resident) return framebuffer;
|
||||
// Beyond the bound the least recently used unreferenced entry goes.
|
||||
constexpr std::size_t bound = 8;
|
||||
|
|
@ -610,6 +616,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
|
|||
entry.owners.assign(owners.begin(), owners.end());
|
||||
for (const auto& owner : owners) entry.owned.push_back(owner != nullptr);
|
||||
entry.extent = extent;
|
||||
entry.layers = layers;
|
||||
entry.framebuffer = framebuffer;
|
||||
framebuffers.push_back(std::move(entry));
|
||||
return framebuffer;
|
||||
|
|
@ -636,7 +643,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V
|
|||
}
|
||||
VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR};
|
||||
rendering.renderArea = {{0, 0}, extent};
|
||||
rendering.layerCount = 1;
|
||||
rendering.layerCount = state.renderLayers;
|
||||
rendering.colorAttachmentCount = static_cast<std::uint32_t>(colors.size());
|
||||
rendering.pColorAttachments = colors.empty() ? nullptr : colors.data();
|
||||
if (depthFormat != VK_FORMAT_UNDEFINED) rendering.pDepthAttachment = &depth;
|
||||
|
|
@ -825,6 +832,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
|
|||
append(key, tessellation.domain);
|
||||
append(key, tessellation.partitioning);
|
||||
append(key, tessellation.outputTopology);
|
||||
append(key, tessellation.patchesPerGroup);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,11 +106,15 @@ void PipelineCache::load(std::vector<std::byte>& initialData) {
|
|||
void PipelineCache::save(bool final) {
|
||||
const auto getData = context.Function<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
|
||||
std::size_t size = 0;
|
||||
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
|
||||
std::vector<std::byte> file(sizeof(FileHeader) + size);
|
||||
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
|
||||
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
|
||||
file.resize(sizeof(FileHeader) + size);
|
||||
std::vector<std::byte> file;
|
||||
{
|
||||
auto synchronizationLock = synchronization.LockForSnapshot();
|
||||
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
|
||||
file.resize(sizeof(FileHeader) + size);
|
||||
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
|
||||
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
|
||||
file.resize(sizeof(FileHeader) + size);
|
||||
}
|
||||
const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))};
|
||||
std::memcpy(file.data(), &header, sizeof(header));
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
|
|
|
|||
|
|
@ -205,7 +205,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
|||
}
|
||||
VkPipeline handle = VK_NULL_HANDLE;
|
||||
if (partsOnly) lock.unlock();
|
||||
const auto created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
|
||||
VkResult created;
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
|
||||
}
|
||||
if (partsOnly) lock.lock();
|
||||
Check(created, "vkCreateGraphicsPipelines library");
|
||||
if (const auto raced = cache.find(key); raced != cache.end()) {
|
||||
|
|
@ -234,7 +238,10 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
|||
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked};
|
||||
create.layout = info.layout;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
|
||||
}
|
||||
++device.counters.linked;
|
||||
if (optimize && optimized != nullptr) {
|
||||
auto slot = std::make_shared<OptimizedPipeline>();
|
||||
|
|
@ -243,8 +250,9 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
|||
*optimized = slot;
|
||||
const auto createPipelines = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
|
||||
const auto cache = context.pipelineCache;
|
||||
const auto cacheSynchronization = context.pipelineCacheSynchronization;
|
||||
const auto libraryLayout = fullLayout;
|
||||
enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] {
|
||||
enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] {
|
||||
if (slot->released.load()) return;
|
||||
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
|
||||
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
|
||||
|
|
@ -253,7 +261,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
|||
create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT;
|
||||
create.layout = libraryLayout;
|
||||
VkPipeline result = VK_NULL_HANDLE;
|
||||
if (createPipelines(slot->device, cache, 1, &create, nullptr, &result) != VK_SUCCESS) return;
|
||||
const auto pipelineCreationResult = [&] {
|
||||
auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
|
||||
return createPipelines(slot->device, cache, 1, &create, nullptr, &result);
|
||||
}();
|
||||
if (pipelineCreationResult != VK_SUCCESS) return;
|
||||
slot->handle.store(result);
|
||||
if (slot->released.load()) {
|
||||
if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr);
|
||||
|
|
|
|||
|
|
@ -1998,7 +1998,10 @@ bool Recorder::gpuSampleCounter() {
|
|||
pipelineInfo.stage.module = module;
|
||||
pipelineInfo.stage.pName = "main";
|
||||
pipelineInfo.layout = sampleLayout;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
|
||||
}
|
||||
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
module = VK_NULL_HANDLE;
|
||||
const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes};
|
||||
|
|
@ -2009,7 +2012,10 @@ bool Recorder::gpuSampleCounter() {
|
|||
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps");
|
||||
pipelineInfo.stage.module = module;
|
||||
pipelineInfo.layout = dumpLayout;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
|
||||
}
|
||||
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
module = VK_NULL_HANDLE;
|
||||
} catch (const std::exception& error) {
|
||||
|
|
@ -2286,7 +2292,10 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec
|
|||
pipelineInfo.stage.module = module;
|
||||
pipelineInfo.stage.pName = "main";
|
||||
pipelineInfo.layout = meshArgumentLayout;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
|
||||
}
|
||||
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
meshArgumentState = 1;
|
||||
} catch (const std::exception& error) {
|
||||
|
|
@ -3309,9 +3318,6 @@ void Recorder::SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUn
|
|||
// later batch's own ordering against this write-back is its recorder's business at record
|
||||
// time (FillBuffer and DispatchIndirect consult copiedWriters; a later copied writer's
|
||||
// write-back runs after this one in finish order; a label behind completions lands after).
|
||||
// Known gap, not introduced here: a later batch writing the same range GPU-direct in place
|
||||
// (a writable region bound in place, or a GPU copy-back) is not ordered against this
|
||||
// write-back by the resource build; it went from deterministically stale to racy.
|
||||
announcedSource = 4;
|
||||
announcedSite = nullptr;
|
||||
++holdCounters.completionSyncsSkipped;
|
||||
|
|
|
|||
|
|
@ -113,10 +113,12 @@ namespace AgcDriver::Graphics {
|
|||
return requiresNonSeamlessCube;
|
||||
}
|
||||
|
||||
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
|
||||
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
|
||||
if (IsSrgbTextureFormat(guestFormat)) return false;
|
||||
for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) {
|
||||
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented");
|
||||
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
|
|||
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
|
||||
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
|
||||
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
|
||||
const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0;
|
||||
const auto zOrder = (shaderControl >> 4u) & 0x3u;
|
||||
const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; };
|
||||
return ShaderRecompiler::ShaderPixelStageInfo{
|
||||
|
|
@ -164,7 +165,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
|
|||
.pixelKillEnable = pixelKillEnable,
|
||||
.depthExportEnable = depthExportEnable,
|
||||
.sampleMaskExportEnable = sampleMaskExportEnable,
|
||||
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable,
|
||||
.earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable),
|
||||
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
|
||||
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
|
||||
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
|
@ -110,6 +111,7 @@ struct CachedTexture {
|
|||
std::uint64_t sourceVersion = 0;
|
||||
std::uint64_t accounted = 0;
|
||||
std::uint64_t present = 0;
|
||||
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
|
||||
};
|
||||
|
||||
// Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the
|
||||
|
|
@ -553,7 +555,8 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
|||
// memory first.
|
||||
const bool depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D;
|
||||
auto source = StorageTexture::FindPending(address, guestBytes);
|
||||
if (source != nullptr && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
|
||||
const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource);
|
||||
if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
|
||||
if (source != nullptr && MetadataMoved(*source, resource)) source.reset();
|
||||
std::optional<DccKeys> keys;
|
||||
bool clearThroughKeys = false;
|
||||
|
|
@ -625,11 +628,25 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
|||
texture->RefreshDepthCopy();
|
||||
return texture;
|
||||
}
|
||||
if (source != nullptr && blockCopy) {
|
||||
auto texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{});
|
||||
texture->RefreshDepthCopy();
|
||||
return texture;
|
||||
}
|
||||
if (source != nullptr) return std::make_shared<Texture>(context, source, resource, components);
|
||||
std::vector<std::byte> snapshot(bytes);
|
||||
ReadTextureSurface(resource, *keys, snapshot);
|
||||
return std::make_shared<Texture>(context, *context.detiler, resource, components, snapshot, depthCompare);
|
||||
}
|
||||
auto snapshotGeneration = generation;
|
||||
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
|
||||
if (source == nullptr && generation == 0) {
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
|
||||
if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) {
|
||||
snapshotGeneration = partial;
|
||||
committed = std::move(ranges);
|
||||
}
|
||||
}
|
||||
auto& cache = Textures();
|
||||
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
|
||||
std::lock_guard lock(cache.mutex);
|
||||
|
|
@ -658,8 +675,13 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
|||
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
|
||||
return GuestMemory::EqualsCommitted(address, it->bytes);
|
||||
};
|
||||
if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) {
|
||||
it->generation = generation;
|
||||
const auto unchanged = [&] {
|
||||
if (it->committed != committed) return false;
|
||||
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation);
|
||||
};
|
||||
if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) {
|
||||
it->generation = snapshotGeneration;
|
||||
it->committed = std::move(committed);
|
||||
touchTexture(cache, it);
|
||||
logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr});
|
||||
reportTextureCounters();
|
||||
|
|
@ -670,13 +692,17 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
|||
eraseTexture(cache, it);
|
||||
++cache.counts.stale;
|
||||
}
|
||||
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, generation};
|
||||
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration};
|
||||
entry.committed = std::move(committed);
|
||||
if (source != nullptr) {
|
||||
entry.source = source;
|
||||
entry.sourceVersion = source->Version();
|
||||
if (depthCompare) {
|
||||
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::DepthCopyTag{});
|
||||
entry.texture->RefreshDepthCopy();
|
||||
} else if (blockCopy) {
|
||||
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{});
|
||||
entry.texture->RefreshDepthCopy();
|
||||
} else {
|
||||
entry.texture = std::make_shared<Texture>(context, source, resource, components);
|
||||
}
|
||||
|
|
@ -1616,7 +1642,7 @@ void ShaderResources::buildComplete() {
|
|||
write.pImageInfo = images.data() + images.size();
|
||||
for (const auto index : binding.imageAllocations) {
|
||||
if (index == std::numeric_limits<std::size_t>::max()) images.push_back(absentImage(binding.layout.binding));
|
||||
else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
|
||||
else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
|
||||
}
|
||||
Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding");
|
||||
break;
|
||||
|
|
@ -2288,13 +2314,39 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span
|
|||
return OwnRefreshFallback::Count;
|
||||
}
|
||||
|
||||
void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) {
|
||||
if (bytes == 0 || Recorder::InCompletion()) return;
|
||||
auto* recorder = Recorder::Active();
|
||||
if (recorder == nullptr) return;
|
||||
const auto drawWriters = DrawCopiedWriters();
|
||||
const bool labels = Recorder::PendingCompletionLabels() != 0;
|
||||
if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return;
|
||||
if (!recorder->PendingWriteOverlaps(address, bytes)) return;
|
||||
const auto overlaps = [&](const auto& writers) {
|
||||
return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); });
|
||||
};
|
||||
if ((context.copiedWriters != nullptr && overlaps(*context.copiedWriters)) || overlaps(*drawWriters) || (labels && recorder->PendingLabelIn(address, bytes))) {
|
||||
Recorder::CountSync(4);
|
||||
recorder->SyncThrough(address, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofReport* report) {
|
||||
if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other};
|
||||
if (!reusable || shaders.empty()) return false;
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
|
||||
for (const auto& range : describedRanges) {
|
||||
CompletePendingCpuWrites(context, range.address, static_cast<std::size_t>(range.bytes));
|
||||
if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes));
|
||||
}
|
||||
for (const auto& image : storageTextures) {
|
||||
CompletePendingCpuWrites(context, image->Descriptor().baseAddress, static_cast<std::size_t>(image->GuestBytes()));
|
||||
if (image->Descriptor().dccAddress != 0) CompletePendingCpuWrites(context, image->Descriptor().dccAddress, DccKeyBytes(image->GuestBytes()));
|
||||
}
|
||||
// APS5_NO_FAST_REVALIDATE=1 always repeats the lookups.
|
||||
static const bool noFast = std::getenv("APS5_NO_FAST_REVALIDATE") != nullptr;
|
||||
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
const auto finish = [&](bool fast, bool ok, ProofFailure failure = ProofFailure::Other) {
|
||||
if (profile) countRevalidate(fast, ok, start);
|
||||
if (report != nullptr) report->failure = ok ? ProofFailure::None : failure;
|
||||
|
|
@ -3241,7 +3293,7 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
|
|||
auto& cache = Textures();
|
||||
std::lock_guard lock(cache.mutex);
|
||||
const auto it = findSampledEntry(cache, context.device, record.words, record.components);
|
||||
if (it == cache.entries.end() || it->texture != record.texture) return nullptr;
|
||||
if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr;
|
||||
if (it->source == nullptr) it->generation = record.generation;
|
||||
touchTexture(cache, it);
|
||||
logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()});
|
||||
|
|
@ -3281,12 +3333,14 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
continue;
|
||||
}
|
||||
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
|
||||
RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers);
|
||||
const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers);
|
||||
RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers);
|
||||
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
|
||||
if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
|
||||
const VkComponentMapping components = ViewComponents(resource);
|
||||
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
|
||||
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
|
||||
std::shared_ptr<Texture> texture;
|
||||
const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element);
|
||||
if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture;
|
||||
|
|
@ -3306,6 +3360,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
}
|
||||
RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers);
|
||||
RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers);
|
||||
if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented");
|
||||
if (!shaderSamplers.empty()) {
|
||||
const bool singleLevel = DescriptorSingleLevel(words);
|
||||
const auto firstSampler = static_cast<std::size_t>(shaderSamplers.data() - samplers.data());
|
||||
|
|
@ -3323,6 +3378,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
textures.push_back(std::move(texture));
|
||||
textureFirstLayer.push_back(firstLayer);
|
||||
textureBaseLevel.push_back(baseLevel);
|
||||
textureSrgb.push_back(forcedSrgb);
|
||||
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
|
||||
item.imageAllocations.push_back(textures.size() - 1);
|
||||
}
|
||||
|
|
@ -3351,6 +3407,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
const auto mip = std::min(resource.baseLevel + mipOffset, resource.mipCount - 1u);
|
||||
Require(resource.minLod <= mip * 256u, "guest storage texture descriptor clamps its minimum LOD above the level it addresses, which is not implemented");
|
||||
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
|
||||
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
|
||||
// The same surface as the previous element: its image was just looked up and refreshed.
|
||||
if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back());
|
||||
else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes));
|
||||
|
|
@ -3515,7 +3573,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() {
|
|||
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
|
||||
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {};
|
||||
const auto reads = guestMemory.InPlaceReads();
|
||||
const auto maxSnapshotBytes = MaxDrawSnapshotBytes();
|
||||
const auto maxSnapshotBytes = maxDrawSnapshotBytes;
|
||||
std::size_t totalSnapshotBytes = 0;
|
||||
auto result = std::make_shared<DrawBindings>();
|
||||
std::vector<std::size_t> selected;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ struct Module {
|
|||
std::uint32_t cullDistances = 0;
|
||||
bool primitiveIndices = false;
|
||||
bool fragmentBarycentric = false;
|
||||
bool layerOutput = false;
|
||||
std::map<std::uint32_t, std::vector<std::uint32_t>> modes;
|
||||
|
||||
const std::vector<std::uint32_t>& Type(std::uint32_t id) const {
|
||||
|
|
@ -134,13 +135,14 @@ struct Module {
|
|||
return;
|
||||
}
|
||||
if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = true;
|
||||
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool"), "unsupported mesh built-in");
|
||||
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in");
|
||||
return;
|
||||
}
|
||||
const auto signature = Signature(type);
|
||||
if (vertex && storage == spv::StorageClassInput) {
|
||||
Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input");
|
||||
} else if (vertex && storage == spv::StorageClassOutput) {
|
||||
if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return;
|
||||
Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output");
|
||||
position = true;
|
||||
} else {
|
||||
|
|
@ -149,7 +151,7 @@ struct Module {
|
|||
}
|
||||
};
|
||||
|
||||
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) {
|
||||
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) {
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
Require(compiled.program != nullptr, "missing compiled shader");
|
||||
const auto& shader = *compiled.program;
|
||||
|
|
@ -199,6 +201,11 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
Require(fragmentShaderBarycentric, "device does not support enabled VK_KHR_fragment_shader_barycentric with fragmentShaderBarycentric");
|
||||
module.fragmentBarycentric = true;
|
||||
}
|
||||
const bool isLayerCapability = capability == spv::CapabilityShaderViewportIndexLayerEXT;
|
||||
if (isLayerCapability) {
|
||||
Require(viewportIndexLayer, "device does not support enabled VK_EXT_shader_viewport_index_layer");
|
||||
module.layerOutput = true;
|
||||
}
|
||||
VkSubgroupFeatureFlags subgroupOperations = 0;
|
||||
switch (capability) {
|
||||
case spv::CapabilityGroupNonUniform: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; break;
|
||||
|
|
@ -295,7 +302,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
isFeatureCapability ||
|
||||
isDescriptorIndexingCapability ||
|
||||
((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) ||
|
||||
(imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT),
|
||||
(imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT) ||
|
||||
isLayerCapability,
|
||||
std::string("SPIR-V requires unsupported device capability ") +
|
||||
std::to_string(static_cast<std::uint32_t>(capability)));
|
||||
|
||||
|
|
@ -320,7 +328,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
|
||||
break;
|
||||
}
|
||||
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
|
||||
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
|
||||
break;
|
||||
}
|
||||
case spv::OpDecorateId:
|
||||
|
|
@ -433,9 +441,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints});
|
||||
Require(module.modes.size() == 1, "unsupported tessellation-control execution mode");
|
||||
} else if (evaluation) {
|
||||
mode(state.rectList ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {});
|
||||
mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {});
|
||||
mode(spv::ExecutionModeVertexOrderCw, {});
|
||||
Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing");
|
||||
const bool quads = state.rectList || state.stages.tessellation->domain == 2u;
|
||||
const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u;
|
||||
mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {});
|
||||
mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {});
|
||||
mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {});
|
||||
Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode");
|
||||
} else if (geometry) {
|
||||
mode(spv::ExecutionModeTriangles, {});
|
||||
|
|
@ -506,7 +517,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
}
|
||||
}
|
||||
} else if (variable.storage == spv::StorageClassPushConstant) {
|
||||
Require(!push && (!shader.pushConstants.empty() || mesh) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
|
||||
Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
|
||||
push = true;
|
||||
Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member");
|
||||
const auto offset = module.offsets.find({typeId, 0});
|
||||
|
|
@ -574,7 +585,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage");
|
||||
}
|
||||
Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources");
|
||||
Require(push == (!shader.pushConstants.empty() || mesh), "recompiler push constant metadata disagrees with SPIR-V");
|
||||
Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V");
|
||||
for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable");
|
||||
if (mesh) Require(module.primitiveIndices, "mesh shader does not export primitive indices");
|
||||
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
|
||||
|
|
@ -583,7 +594,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
|
||||
}
|
||||
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) {
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) {
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
const bool tessellation = state.stages.path == ShaderPath::Tessellation;
|
||||
const bool mesh = state.stages.path == ShaderPath::Geometry;
|
||||
|
|
@ -604,7 +615,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
|
|||
Require(shaders[i].program != nullptr, "missing compiled shader");
|
||||
Require(shaders[i].stage == expected, "graphics stage order disagrees");
|
||||
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
|
||||
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance);
|
||||
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer);
|
||||
if (i != 0) {
|
||||
for (const auto& [location, signature] : current.inputs) {
|
||||
const auto output = previous.outputs.find(location);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include <algorithm>
|
||||
|
|
@ -97,10 +99,30 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt
|
|||
|
||||
// The register rules DecodeState and DrawRejection share (the precheck must reject exactly what
|
||||
// DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict.
|
||||
// Render target index, viewport index and the misc export vector that carries them are accepted but
|
||||
// not routed: color targets are single-layer, so layered draws land in layer 0.
|
||||
constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u);
|
||||
constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u);
|
||||
|
||||
std::uint32_t inactiveVertexPointSize(const QueueState& queue) {
|
||||
const auto stages = find(queue.context, 0x2d5);
|
||||
const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig);
|
||||
const auto raster = find(queue.context, 0x205);
|
||||
if (stages == queue.context.end() || primitive == queue.userConfig.end() || raster == queue.context.end()) return 0;
|
||||
if ((stages->second & 0x24u) != 0) return 0;
|
||||
if ((raster->second & 8u) != 0 && (raster->second & 0x7e0u) != 0x240u) return 0;
|
||||
switch (primitive->second) {
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
case 17:
|
||||
return 1u << 16u;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr std::uint32_t DepthControlMask = ~0x007007f0u;
|
||||
// EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader
|
||||
// to run, which it always does here.
|
||||
|
|
@ -355,9 +377,20 @@ std::string singleSampleMsaaRejection(const QueueState& queue) {
|
|||
return {};
|
||||
}
|
||||
|
||||
constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u;
|
||||
constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid);
|
||||
|
||||
bool coveredCentroidSupported(const QueueState& queue) {
|
||||
const auto config = find(queue.context, 0x2f8);
|
||||
if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true;
|
||||
const auto ena = find(queue.context, 0x1b3);
|
||||
const auto addr = find(queue.context, 0x1b4);
|
||||
return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0;
|
||||
}
|
||||
|
||||
std::uint32_t decodeSamples(const Registers& cx) {
|
||||
const auto config = read(cx, 0x2f8);
|
||||
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling");
|
||||
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling");
|
||||
const auto log2 = config & 7u;
|
||||
if (log2 == 0) {
|
||||
Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported");
|
||||
|
|
@ -613,17 +646,22 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
|
|||
validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing");
|
||||
const auto config = read(queue.context, 0x2d6);
|
||||
const auto parameters = read(queue.context, 0x2db);
|
||||
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u};
|
||||
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu};
|
||||
validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts");
|
||||
validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero");
|
||||
if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state"));
|
||||
validate(tess.domain == 1 && tess.partitioning == 2 && tess.outputTopology == 2, "only triangular, fractional-odd, clockwise tessellation is supported by the reference path");
|
||||
Require(readFloat(queue.context, 0x287) <= 1.0f && readFloat(queue.context, 0x286) >= 63.0f, "tessellation level clamps inside the fractional-odd range [1, 63] are unsupported");
|
||||
validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path");
|
||||
result.minTessellationLevel = readFloat(queue.context, 0x287);
|
||||
result.maxTessellationLevel = readFloat(queue.context, 0x286);
|
||||
Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum");
|
||||
result.tessellation = tess;
|
||||
} else {
|
||||
validate((value & ~0x0047ec30u) == 0, "unsupported geometry routing, fast launch or wave-ID state");
|
||||
const bool fastLaunch = ((value >> 19u) & 3u) == 1u;
|
||||
validate((value & ~(fastLaunch ? 0x004fec30u : 0x0047ec30u)) == 0, "unsupported geometry routing, fast launch or wave-ID state");
|
||||
const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig);
|
||||
const auto vertices = (group >> 9u) & 0x1ffu;
|
||||
const auto primitives = group & 0x1ffu;
|
||||
validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported");
|
||||
const auto maxVertices = read(queue.context, 0x1ff);
|
||||
const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce);
|
||||
validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly");
|
||||
|
|
@ -634,11 +672,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
|
|||
const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit});
|
||||
validate(groupPrimitives != 0, "geometry subgroup contains no primitives");
|
||||
const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader);
|
||||
validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
|
||||
validate(vertexWorkgroup || fastLaunch || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
|
||||
const auto esgsItemSize = read(queue.context, 0x2ab);
|
||||
validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE");
|
||||
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)});
|
||||
result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize};
|
||||
const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u;
|
||||
validate(amplification != 0, "fast launch subgroup amplifies to no primitives");
|
||||
const auto maxPrimitives = primitives * amplification;
|
||||
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives});
|
||||
result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -693,17 +734,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
|||
}
|
||||
APS5_LOG_OUT_DEBUG("Topology=%u", static_cast<unsigned>(result.topology));
|
||||
result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
|
||||
if ((read(cx, 0x207) & LayerExports) != 0) {
|
||||
static bool reported = false;
|
||||
if (!reported) {
|
||||
reported = true;
|
||||
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207));
|
||||
}
|
||||
}
|
||||
zero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports");
|
||||
zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports");
|
||||
Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported");
|
||||
result.paClVsOutCntl = read(cx, 0x207);
|
||||
result.samples = decodeSamples(cx);
|
||||
Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented");
|
||||
decodeSampleLocations(cx, result);
|
||||
{
|
||||
const auto depthControl = read(cx, 0x200);
|
||||
|
|
@ -817,6 +852,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
|||
}
|
||||
if (result.hasColorTarget) {
|
||||
result.color = result.colors.front();
|
||||
result.renderLayers = result.color.layers;
|
||||
for (const auto& color : result.colors) Require(color.layers == result.renderLayers, "color targets with different layer counts are unsupported");
|
||||
if (result.renderLayers > 1) {
|
||||
Require(!result.depth, "layered rendering with a depth target is unsupported");
|
||||
result.layerExports = read(cx, 0x207) & (1u << 18u);
|
||||
}
|
||||
if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)};
|
||||
} else if (result.depth) {
|
||||
result.renderExtent = result.depth->extent;
|
||||
|
|
@ -827,6 +868,13 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
|||
APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height);
|
||||
Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes");
|
||||
}
|
||||
if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) {
|
||||
static bool reported = false;
|
||||
if (!reported) {
|
||||
reported = true;
|
||||
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207));
|
||||
}
|
||||
}
|
||||
const auto xs = readFloat(cx, 0x10f);
|
||||
const auto xo = readFloat(cx, 0x110);
|
||||
const auto ys = readFloat(cx, 0x111);
|
||||
|
|
@ -872,11 +920,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
|||
for (std::uint32_t component = 0; component < 4; ++component) {
|
||||
if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component;
|
||||
}
|
||||
state.blendEnable = (blend >> 30u) & 1u;
|
||||
state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0;
|
||||
if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented");
|
||||
Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported");
|
||||
if (state.blendEnable) {
|
||||
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
|
||||
state.srcColorBlendFactor = blendFactor(blend & 0x1fu);
|
||||
state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu);
|
||||
state.colorBlendOp = blendOp((blend >> 5u) & 7u);
|
||||
|
|
@ -990,6 +1037,41 @@ std::uint32_t ColorWriteMask(const Registers& context) {
|
|||
return mask;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
|
||||
if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest;
|
||||
throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast<int>(format)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
|
||||
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
|
||||
const bool chain = color.mipCount > 1;
|
||||
GuestTextureResource surface{};
|
||||
surface.baseAddress = chain ? color.surfaceAddress : color.address;
|
||||
surface.pipeBankXor = color.pipeBankXor;
|
||||
surface.width = chain ? color.surfaceExtent.width : color.extent.width;
|
||||
surface.height = chain ? color.surfaceExtent.height : color.extent.height;
|
||||
surface.depthOrLastArray = color.depth - 1u;
|
||||
surface.baseArray = 0;
|
||||
surface.mipCount = color.mipCount;
|
||||
surface.baseLevel = 0;
|
||||
surface.lastLevel = color.mipCount - 1;
|
||||
surface.tileMode = ColorTextureTileMode(color.tileMode);
|
||||
surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D;
|
||||
surface.format = GuestFormatFor(color.format, color.elementBytes);
|
||||
surface.dstSelX = 4;
|
||||
surface.dstSelY = 5;
|
||||
surface.dstSelZ = 6;
|
||||
surface.dstSelW = 7;
|
||||
surface.dccAddress = color.dccAddress;
|
||||
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
|
||||
surface.dccPipeAligned = color.dccPipeAligned;
|
||||
return surface;
|
||||
}
|
||||
|
||||
ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
|
||||
const auto stride = slot * 0xfu;
|
||||
ColorTarget color{};
|
||||
|
|
@ -1026,16 +1108,16 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
|
|||
Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported");
|
||||
if (volume) {
|
||||
color.depth = (attrib3 & 0x1fffu) + 1u;
|
||||
Require(maxMip == 0 && (info & 0x10000000u) == 0, "mipmapped or DCC 3D color targets are unsupported");
|
||||
Require(slice < color.depth, "the color view slice is beyond the 3D surface");
|
||||
Require(lastSlice == slice, "color views of several 3D slices are unsupported");
|
||||
Require(maxMip == 0, "mipmapped 3D color targets are unsupported");
|
||||
Require(slice < color.depth && lastSlice >= slice, "the color view slices are outside the 3D surface");
|
||||
color.depthSlice = slice;
|
||||
color.layers = std::min(lastSlice + 1u, color.depth) - slice;
|
||||
} else if ((attrib3 & 0x1fffu) != 0) {
|
||||
Require(lastSlice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface");
|
||||
color.arrayLastSlice = attrib3 & 0x1fffu;
|
||||
color.arraySlice = slice;
|
||||
}
|
||||
if (lastSlice != slice) {
|
||||
if (!volume && lastSlice != slice) {
|
||||
const auto exports = find(cx, 0x207);
|
||||
Require(exports == cx.end() || (exports->second & (1u << 18u)) == 0, "layered draws into a color view of several array slices are unsupported");
|
||||
}
|
||||
|
|
@ -1063,7 +1145,17 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
|
|||
}
|
||||
Require(color.tileMode != ColorTileMode::D64KBX || (color.pipeBankXor & ~0xcf00u) == 0, "unsupported display color pipe/bank XOR column bits");
|
||||
color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0;
|
||||
if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1);
|
||||
if (slice != 0 && !volume) {
|
||||
color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1);
|
||||
if (color.arrayLastSlice == 0 && ColorTileModeIsXor(color.tileMode)) {
|
||||
const auto* equation = FindTextureSwizzleEquation(static_cast<std::uint32_t>(color.tileMode), color.elementBytes);
|
||||
Require(equation != nullptr, "a sliced color target has no swizzle equation");
|
||||
const auto coordinates = slice << 24u;
|
||||
for (std::uint32_t bit = 0; bit < equation->bits.size(); ++bit) {
|
||||
color.pipeBankXor ^= (std::popcount(equation->bits[bit] & coordinates) & 1u) << bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
color.address = color.surfaceAddress + mipOffset;
|
||||
color.bytes = colorLayout.Bytes();
|
||||
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
|
||||
|
|
@ -1076,7 +1168,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
|
|||
const auto clear = find(cx, 0x323 + word + stride);
|
||||
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
|
||||
}
|
||||
if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) {
|
||||
if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) {
|
||||
Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported");
|
||||
Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)");
|
||||
if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported");
|
||||
|
|
@ -1104,6 +1196,10 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (volume && (color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB)) {
|
||||
color.bytes = DescribeSurface(SurfaceForTarget(color)).guestBytes;
|
||||
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
|
|
@ -1120,7 +1216,18 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
|
|||
if (mode != 2u && mode != 6u && !resolve) return std::nullopt;
|
||||
ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt};
|
||||
Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma");
|
||||
Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work");
|
||||
const auto value = [&](std::uint32_t offset) {
|
||||
const auto found = find(cx, offset);
|
||||
return found == cx.end() ? 0u : found->second;
|
||||
};
|
||||
const bool planes = !depthPlanesAbsent(cx);
|
||||
const auto depthControl = planes ? read(cx, 0x200) : 0u;
|
||||
const bool depthWork = (depthControl & 0xfu) != 0 && ((depthControl & 8u) != 0 || !depthPassThrough(depthControl));
|
||||
if (depthWork || (planes && (read(cx, 0x0) & 0xfu) != 0)) {
|
||||
char text[192];
|
||||
std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u;
|
||||
if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
|
||||
zero(cx, 0x80, ~0u, "window offset");
|
||||
|
|
@ -1189,7 +1296,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
|
|||
if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies";
|
||||
if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported";
|
||||
}
|
||||
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
|
||||
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
|
||||
if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported");
|
||||
if (value(cx, 0x200, word)) {
|
||||
const auto renderControl = find(cx, 0x000);
|
||||
|
|
@ -1215,7 +1322,8 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
|
|||
if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) {
|
||||
if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason;
|
||||
}
|
||||
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
|
||||
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
|
||||
if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented";
|
||||
const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0;
|
||||
if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason;
|
||||
if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
|
|
@ -267,7 +268,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
|||
const auto linearBytes = sliceLinearBytes * arrayLayers;
|
||||
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u;
|
||||
imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u);
|
||||
imageInfo.imageType = ImageTypeFor(descriptor.dimension);
|
||||
imageInfo.format = vkFormat;
|
||||
imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth};
|
||||
|
|
@ -446,6 +447,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
|||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
createFirstLayerView(descriptor, viewInfo);
|
||||
prepareBaseLevelView(descriptor, viewInfo);
|
||||
prepareSrgbView(descriptor, viewInfo);
|
||||
if (profile) {
|
||||
auto& totals = Profile();
|
||||
totals.view += timer.lap();
|
||||
|
|
@ -475,6 +477,75 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR
|
|||
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount;
|
||||
}
|
||||
|
||||
bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) {
|
||||
const auto& from = source.Descriptor();
|
||||
if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false;
|
||||
if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false;
|
||||
if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false;
|
||||
for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) {
|
||||
const auto width = std::max(descriptor.width >> level, 1u);
|
||||
const auto height = std::max(descriptor.height >> level, 1u);
|
||||
if (width % 4u != 0 || height % 4u != 0 || std::max(from.width >> level, 1u) != width / 4u || std::max(from.height >> level, 1u) != height / 4u) return false;
|
||||
}
|
||||
const auto geometry = DescribeSurface(descriptor);
|
||||
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers;
|
||||
}
|
||||
|
||||
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) {
|
||||
try {
|
||||
Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture");
|
||||
blockFormat = SampledTextureFormat(context, descriptor.format);
|
||||
const auto geometry = DescribeSurface(descriptor);
|
||||
copyWidth = descriptor.width;
|
||||
copyHeight = descriptor.height;
|
||||
copyLevels = descriptor.mipCount;
|
||||
copyLayers = geometry.imageLayers;
|
||||
copyElementBytes = BytesPerElement(descriptor.format);
|
||||
APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast<int>(blockFormat));
|
||||
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
imageInfo.format = blockFormat;
|
||||
imageInfo.extent = {descriptor.width, descriptor.height, 1u};
|
||||
imageInfo.mipLevels = copyLevels;
|
||||
imageInfo.arrayLayers = copyLayers;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage block copy");
|
||||
owned = std::make_shared<OwnedImage>(context, image, VK_NULL_HANDLE);
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocationBytes = requirements.size;
|
||||
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory block copy");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy");
|
||||
|
||||
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
|
||||
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.image = image;
|
||||
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
|
||||
viewInfo.format = blockFormat;
|
||||
viewFormat = blockFormat;
|
||||
viewInfo.components = components;
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
|
||||
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
|
||||
ChainMinLod(context, descriptor, viewInfo, minLod);
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView block copy");
|
||||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
createFirstLayerView(descriptor, viewInfo);
|
||||
prepareBaseLevelView(descriptor, viewInfo);
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) {
|
||||
try {
|
||||
Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture");
|
||||
|
|
@ -537,6 +608,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
|||
bool Texture::RefreshDepthCopy() {
|
||||
if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false;
|
||||
if (copiedOnce && storageSource->Version() == copiedVersion) return false;
|
||||
if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy();
|
||||
std::uint64_t linearBytes = 0;
|
||||
for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast<std::uint64_t>(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers;
|
||||
auto linear = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
|
|
@ -608,6 +680,51 @@ bool Texture::RefreshDepthCopy() {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool Texture::refreshBlockCopy() {
|
||||
std::vector<VkImageCopy> regions;
|
||||
for (std::uint32_t level = 0; level < copyLevels; ++level) {
|
||||
VkImageCopy region{};
|
||||
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers};
|
||||
region.dstSubresource = region.srcSubresource;
|
||||
region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u};
|
||||
regions.push_back(region);
|
||||
}
|
||||
auto* recorder = Recorder::Active();
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
if (recorder != nullptr) {
|
||||
commands = recorder->Commands();
|
||||
recorder->Keep(owned);
|
||||
recorder->Keep(storageSource);
|
||||
} else {
|
||||
batch = std::make_unique<CommandBatch>(context);
|
||||
commands = batch->Handle();
|
||||
}
|
||||
const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
|
||||
const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT};
|
||||
VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransferDst.image = image;
|
||||
toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers};
|
||||
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst);
|
||||
context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
|
||||
VkImageMemoryBarrier toShaderRead = toTransferDst;
|
||||
toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead);
|
||||
if (batch) batch->SubmitAndWait();
|
||||
copiedOnce = true;
|
||||
copiedVersion = storageSource->Version();
|
||||
return true;
|
||||
}
|
||||
|
||||
Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() {
|
||||
return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)};
|
||||
}
|
||||
|
|
@ -648,6 +765,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
|||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
createFirstLayerView(descriptor, viewInfo);
|
||||
prepareBaseLevelView(descriptor, viewInfo);
|
||||
prepareSrgbView(descriptor, viewInfo);
|
||||
if (profile) {
|
||||
auto& totals = Profile();
|
||||
totals.view += timer.lap();
|
||||
|
|
@ -700,6 +818,44 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma
|
|||
baseLevelInfo = viewInfo;
|
||||
}
|
||||
|
||||
void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) {
|
||||
const auto format = SrgbTwinFormat(context, viewInfo.format);
|
||||
if (format == VK_FORMAT_UNDEFINED) return;
|
||||
viewInfo.pNext = nullptr;
|
||||
viewInfo.format = format;
|
||||
srgbInfo = viewInfo;
|
||||
srgbMinLod = EffectiveMinLod(descriptor);
|
||||
}
|
||||
|
||||
VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const {
|
||||
Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding");
|
||||
std::lock_guard lock(srgbMutex);
|
||||
auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u];
|
||||
if (slot != VK_NULL_HANDLE) return slot;
|
||||
VkImageViewCreateInfo info = *srgbInfo;
|
||||
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
|
||||
usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.pNext = &usage;
|
||||
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
|
||||
if (baseLevel) {
|
||||
info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D;
|
||||
info.subresourceRange.levelCount = 1;
|
||||
info.subresourceRange.layerCount = 1;
|
||||
} else {
|
||||
if (firstLayer) {
|
||||
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
info.subresourceRange.layerCount = 1;
|
||||
}
|
||||
if (srgbMinLod != 0.0f) {
|
||||
minLod.minLod = srgbMinLod;
|
||||
minLod.pNext = info.pNext;
|
||||
info.pNext = &minLod;
|
||||
}
|
||||
}
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB");
|
||||
return slot;
|
||||
}
|
||||
|
||||
VkImageView Texture::BaseLevelView() const {
|
||||
Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture");
|
||||
std::lock_guard lock(baseLevelMutex);
|
||||
|
|
@ -715,6 +871,10 @@ void Texture::release() noexcept {
|
|||
firstLayerView = VK_NULL_HANDLE;
|
||||
if (baseLevelView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, baseLevelView, nullptr);
|
||||
baseLevelView = VK_NULL_HANDLE;
|
||||
for (auto& srgbView : srgbViews) {
|
||||
if (srgbView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, srgbView, nullptr);
|
||||
srgbView = VK_NULL_HANDLE;
|
||||
}
|
||||
// The image and its memory go with the last holder: this texture, or the batch still uploading it.
|
||||
owned.reset();
|
||||
image = VK_NULL_HANDLE;
|
||||
|
|
@ -817,7 +977,7 @@ bool IntegerFormat(VkFormat format) {
|
|||
switch (format) {
|
||||
case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT:
|
||||
case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_B8G8R8A8_SINT:
|
||||
case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
||||
case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32:
|
||||
case VK_FORMAT_R16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT:
|
||||
case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32_SINT:
|
||||
case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_SINT:
|
||||
|
|
@ -851,6 +1011,23 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc
|
|||
return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear);
|
||||
}
|
||||
|
||||
VkFormat SrgbTwinFormat(const Context& context, VkFormat format) {
|
||||
const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; };
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB;
|
||||
case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB;
|
||||
case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB;
|
||||
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32;
|
||||
case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK;
|
||||
case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK;
|
||||
case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK;
|
||||
case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK;
|
||||
case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK;
|
||||
default: return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) {
|
||||
if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED;
|
||||
struct Table {
|
||||
|
|
@ -919,6 +1096,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
|
|||
trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers;
|
||||
layerGeneration.assign(trackedLayers, 0);
|
||||
layerPending.assign(trackedLayers, false);
|
||||
layerOriginal.assign(trackedLayers, true);
|
||||
sliceLinearBytes = geometry.sliceLinearBytes;
|
||||
const auto linearBytes = sliceLinearBytes * arrayLayers;
|
||||
|
||||
|
|
@ -1333,26 +1511,26 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t
|
|||
return created;
|
||||
}
|
||||
|
||||
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) {
|
||||
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layers, std::uint32_t layer) {
|
||||
Require(attachable, "storage image cannot be a color attachment");
|
||||
Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image");
|
||||
const bool volume = descriptor.dimension == TextureDimension::k3D;
|
||||
Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
|
||||
Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
|
||||
Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image");
|
||||
const auto found = attachmentViews.find({format, mip, depthSlice, layer});
|
||||
const auto found = attachmentViews.find({format, mip, depthSlice, layers, layer});
|
||||
if (found != attachmentViews.end()) return found->second;
|
||||
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
|
||||
usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.pNext = &usage;
|
||||
viewInfo.image = image;
|
||||
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.format = format;
|
||||
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u};
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, layers};
|
||||
VkImageView created = VK_NULL_HANDLE;
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment");
|
||||
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created);
|
||||
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layers, layer}, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
|
|
@ -1530,6 +1708,8 @@ LookupOutcomes& ThreadLookupOutcomes() {
|
|||
bool StorageTexture::Refresh() {
|
||||
const bool profile = LookupOutcomes::Profiled();
|
||||
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
|
||||
if (refreshProved()) {
|
||||
NoteProved();
|
||||
refreshesProved.fetch_add(1, std::memory_order_relaxed);
|
||||
|
|
@ -1655,7 +1835,7 @@ bool StorageTexture::Refresh() {
|
|||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (changed[layer] && layerPending[layer]) pendingChanged = true;
|
||||
}
|
||||
if (!unchanged && !keysChanged && !pendingChanged && originalValid && equalsOriginal()) {
|
||||
if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) && equalsOriginal()) {
|
||||
unchanged = true;
|
||||
stamped = false;
|
||||
}
|
||||
|
|
@ -1698,7 +1878,7 @@ bool StorageTexture::Refresh() {
|
|||
static const bool traceUpload = std::getenv("APS5_TRACE_UPLOAD") != nullptr;
|
||||
if (traceUpload) {
|
||||
const auto keys = TextureClearKeys(descriptor, guestBytes);
|
||||
std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalValid ? 1 : 0, dirty ? 1 : 0);
|
||||
std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) ? 1 : 0, dirty ? 1 : 0);
|
||||
}
|
||||
// Changed layers with results pending: the CPU wrote this memory while GPU results were
|
||||
// pending, so as with an immediate write-back followed by the CPU write, its blocks win and the
|
||||
|
|
@ -1883,8 +2063,8 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std
|
|||
}
|
||||
|
||||
void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
|
||||
original.resize(static_cast<std::size_t>(guestBytes));
|
||||
comparedGuestBytes = {};
|
||||
const bool complete = layers == nullptr || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; });
|
||||
std::vector<bool> selected(trackedLayers, true);
|
||||
if (layers != nullptr) selected = *layers;
|
||||
for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
|
||||
|
|
@ -1892,12 +2072,19 @@ void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
|
|||
if (!selected[layer]) continue;
|
||||
const auto offset = static_cast<std::size_t>(layerBegin(layer) - descriptor.baseAddress);
|
||||
GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast<std::size_t>(layerBytes(layer))));
|
||||
layerOriginal[layer] = true;
|
||||
}
|
||||
originalValid = originalValid || complete;
|
||||
}
|
||||
|
||||
bool StorageTexture::originalHolds(std::uint64_t address, std::size_t bytes) const {
|
||||
for (auto layer = (address - descriptor.baseAddress) / trackedLayerBytes; layer < trackedLayers && layer * trackedLayerBytes < address - descriptor.baseAddress + bytes; ++layer) {
|
||||
if (!layerOriginal[static_cast<std::size_t>(layer)]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize) const {
|
||||
if (!originalValid) return false;
|
||||
if (!originalHolds(address, bytes)) return false;
|
||||
struct Exempt {
|
||||
const StorageTexture* previous;
|
||||
~Exempt() { refreshing = previous; }
|
||||
|
|
@ -1906,7 +2093,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
|
|||
Require(address >= descriptor.baseAddress && bytes <= guestBytes && address - descriptor.baseAddress <= guestBytes - bytes, "texture comparison exceeds its snapshot");
|
||||
Require(original.size() == guestBytes, "texture comparison has no complete snapshot");
|
||||
GuestMemory::FlushGpuWrites(address, bytes);
|
||||
if (!originalValid) return false;
|
||||
if (!originalHolds(address, bytes)) return false;
|
||||
constexpr std::uint64_t blockBytes = 65536;
|
||||
const auto end = address + bytes;
|
||||
const auto first = address / blockBytes;
|
||||
|
|
@ -1937,12 +2124,13 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
|
|||
constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20;
|
||||
|
||||
void StorageTexture::upload(const std::vector<bool>* layers) {
|
||||
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
|
||||
const bool profile = LookupOutcomes::Profiled();
|
||||
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
|
||||
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
|
||||
const auto elementBytes = BytesPerElement(descriptor.format);
|
||||
const auto linearBytes = sliceLinearBytes * arrayLayers;
|
||||
original.resize(static_cast<std::size_t>(guestBytes));
|
||||
// A layer selection applies to the direct path alone (the others upload everything); it names
|
||||
// array layers, which the tracked layers are when there are several.
|
||||
if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr;
|
||||
|
|
@ -1972,7 +2160,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
|
|||
// batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms
|
||||
// under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before.
|
||||
static_cast<void>(HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
|
||||
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) originalValid = false;
|
||||
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
|
||||
else captureGuestBytes(nullptr);
|
||||
forgetBorrowed(0, trackedLayers);
|
||||
stampLayers(false);
|
||||
|
|
@ -2021,7 +2209,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
|
|||
return;
|
||||
}
|
||||
if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) : nullptr) {
|
||||
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) originalValid = false;
|
||||
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
|
||||
else captureGuestBytes(layers);
|
||||
stampLayers(true);
|
||||
// A whole-surface upload of a block-unit image goes through the windows too when a unit
|
||||
|
|
@ -2143,9 +2331,10 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
|
|||
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start);
|
||||
return;
|
||||
}
|
||||
originalValid = true;
|
||||
layerOriginal.assign(trackedLayers, true);
|
||||
forgetBorrowed(0, trackedLayers);
|
||||
stampLayers(false);
|
||||
original.resize(static_cast<std::size_t>(guestBytes));
|
||||
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
|
||||
{
|
||||
Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
|
|
@ -3223,9 +3412,10 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s
|
|||
// As the empty write-back leaves the layers: no results pending, the generation unchanged (the
|
||||
// stamps make their next Refresh re-upload), `original` no longer vouching for the bytes.
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (selected(layer)) layerPending[layer] = false;
|
||||
if (!selected(layer)) continue;
|
||||
layerPending[layer] = false;
|
||||
layerOriginal[layer] = false;
|
||||
}
|
||||
originalValid = false;
|
||||
if (!anyLayerPending()) {
|
||||
++dropped;
|
||||
staleEvicted.fetch_add(1, std::memory_order_relaxed);
|
||||
|
|
@ -3285,7 +3475,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
|
|||
// touched the bytes a CPU-path upload copied).
|
||||
texture->dirty = false;
|
||||
texture->layerPending.assign(texture->trackedLayers, false);
|
||||
texture->originalValid = false;
|
||||
texture->layerOriginal.assign(texture->trackedLayers, false);
|
||||
it = pending.textures.erase(it);
|
||||
++discarded;
|
||||
} else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) {
|
||||
|
|
@ -3297,13 +3487,11 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
|
|||
const auto unitBegin = texture->layerBegin(unit);
|
||||
if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue;
|
||||
texture->layerPending[unit] = false;
|
||||
texture->layerOriginal[unit] = false;
|
||||
unitsDropped.fetch_add(1, std::memory_order_relaxed);
|
||||
dropped = true;
|
||||
}
|
||||
if (dropped) {
|
||||
texture->originalValid = false;
|
||||
++discarded;
|
||||
}
|
||||
if (dropped) ++discarded;
|
||||
if (dropped && !texture->anyLayerPending()) {
|
||||
texture->dirty = false;
|
||||
it = pending.textures.erase(it);
|
||||
|
|
@ -3501,7 +3689,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) {
|
|||
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
|
||||
recorder->EndGpuTiming(timing, guestBytes);
|
||||
originalValid = false;
|
||||
layerOriginal.assign(trackedLayers, false);
|
||||
uploadedKeys = keys;
|
||||
filledKeys = DccKeys::Uncompressed;
|
||||
keyProof = {};
|
||||
|
|
@ -3594,7 +3782,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
|
|||
std::lock_guard lock(live.mutex);
|
||||
for (auto* texture : live.textures) {
|
||||
if (texture == this || texture->released || !texture->overlaps(begin, bytes)) continue;
|
||||
texture->originalValid = false;
|
||||
texture->layerOriginal.assign(texture->trackedLayers, false);
|
||||
if (texture->descriptor.dccAddress != 0) aliasedKeys = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -3643,13 +3831,19 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
|
|||
// memo hit of this submission's epoch would not consume pages the game dirtied since that
|
||||
// walk, the write-back's own walk would stamp them above this generation and keep their
|
||||
// blocks, and the fill's pattern would be lost in them.
|
||||
originalValid = false;
|
||||
const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (watched) layerOriginal.assign(trackedLayers, false);
|
||||
uploadedKeys = DccKeys::Uncompressed;
|
||||
if (layer != WholeImage && trackedLayers > 1) {
|
||||
// The cleared layer's units alone: their stamps above lie below this generation, the other
|
||||
// units keep theirs (their pages are consumed by their own collects).
|
||||
const auto first = blockUnits ? static_cast<std::uint32_t>((begin - descriptor.baseAddress) / trackedLayerBytes) : layer;
|
||||
const auto count = blockUnits ? static_cast<std::uint32_t>(bytes / trackedLayerBytes) : 1u;
|
||||
if (!watched) {
|
||||
std::vector<bool> filled(trackedLayers, false);
|
||||
std::fill_n(filled.begin() + first, count, true);
|
||||
captureGuestBytes(&filled);
|
||||
}
|
||||
const auto now = GuestMemory::CollectWritesUncached(begin, bytes);
|
||||
for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = now;
|
||||
refreshGeneration();
|
||||
|
|
@ -3657,6 +3851,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
|
|||
markLayersPending(first, count);
|
||||
return true;
|
||||
}
|
||||
if (!watched) captureGuestBytes(nullptr);
|
||||
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
|
||||
refreshGeneration();
|
||||
forgetBorrowed(0, trackedLayers);
|
||||
|
|
@ -3721,7 +3916,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
|
|||
auto& live = Live();
|
||||
std::lock_guard lock(live.mutex);
|
||||
for (auto* texture : live.textures) {
|
||||
if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) texture->originalValid = false;
|
||||
if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) texture->layerOriginal.assign(texture->trackedLayers, false);
|
||||
}
|
||||
}
|
||||
GuestMemory::MarkWritten(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
|
|
@ -3772,7 +3967,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
|
|||
// As after a clear: the image holds the surface, guest memory follows at the write-back (which
|
||||
// keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason
|
||||
// given in FillClear).
|
||||
originalValid = false;
|
||||
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
|
||||
else captureGuestBytes(nullptr);
|
||||
uploadedKeys = DccKeys::Uncompressed;
|
||||
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
|
||||
refreshGeneration();
|
||||
|
|
@ -3999,7 +4195,7 @@ bool StorageTexture::guestBytesSettled() const {
|
|||
}
|
||||
|
||||
bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const {
|
||||
const auto& baseline = originalValid ? original : generationBaseline;
|
||||
const auto& baseline = originalHolds(from, static_cast<std::size_t>(to - from)) ? original : generationBaseline;
|
||||
if (baseline.size() != guestBytes) return false;
|
||||
const auto offset = static_cast<std::size_t>(from - descriptor.baseAddress);
|
||||
return GuestMemory::EqualsCommittedUnsynced(from, std::span<const std::byte>(baseline).subspan(offset, static_cast<std::size_t>(to - from)));
|
||||
|
|
@ -4052,12 +4248,29 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
|||
}
|
||||
std::vector<std::uint8_t> changedBlocks(spanBlocks);
|
||||
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks);
|
||||
const bool compared = !tracked && compareUntracked(firstStored, static_cast<std::size_t>(lastStored - firstStored), changedBlocks);
|
||||
std::vector<bool> compared(trackedLayers, false);
|
||||
if (!tracked) {
|
||||
std::vector<bool> saved(trackedLayers, false);
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) saved[layer] = layers[layer] && layerOriginal[layer];
|
||||
for (const auto& [begin, end] : unitRuns(saved)) {
|
||||
const auto from = descriptor.baseAddress + begin;
|
||||
const auto last = descriptor.baseAddress + end - 1;
|
||||
if (!compareUntracked(from, static_cast<std::size_t>(end - begin), std::span(changedBlocks).subspan(static_cast<std::size_t>((from - spanBegin) / block), static_cast<std::size_t>(last / block - from / block + 1)))) continue;
|
||||
for (auto layer = static_cast<std::uint32_t>(begin / trackedLayerBytes); layer < trackedLayers && layerBegin(layer) <= last; ++layer) compared[layer] = true;
|
||||
}
|
||||
}
|
||||
const auto forgetStored = [&] {
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (layers[layer]) layerOriginal[layer] = false;
|
||||
}
|
||||
};
|
||||
bool unsavedOutsideWatch = false;
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (!layers[layer]) continue;
|
||||
const auto begin = layerBegin(layer);
|
||||
const auto end = begin + layerBytes(layer);
|
||||
if (!compared && (!tracked || layerGeneration[layer] == 0)) {
|
||||
if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) {
|
||||
unsavedOutsideWatch = unsavedOutsideWatch || !tracked;
|
||||
keep.emplace_back(begin, end);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -4066,7 +4279,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
|||
const auto to = std::min(at + block, end);
|
||||
if (from >= to) continue;
|
||||
const bool edge = from != at || to != at + block;
|
||||
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) {
|
||||
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) {
|
||||
skippedAny = true;
|
||||
skippedLayer[layer] = true;
|
||||
++skipped;
|
||||
|
|
@ -4118,18 +4331,19 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
|||
// store, the keys stay as they are, and the image is stale there (re-uploaded at its next
|
||||
// use, never from a matching `original`).
|
||||
emptyWriteBacks.fetch_add(1, std::memory_order_relaxed);
|
||||
originalValid = false;
|
||||
forgetStored();
|
||||
settle(true);
|
||||
return;
|
||||
}
|
||||
if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) {
|
||||
Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented");
|
||||
if (blockUnits) {
|
||||
// The kept blocks alone pass through the retiler (windows of their slices).
|
||||
shadowImport = import;
|
||||
const auto storedBytes = writeBackWindows(*import, keep, firstStored, lastStored, shadowed, imported);
|
||||
countStorageWriteBack(storedBytes, true);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
originalValid = false;
|
||||
forgetStored();
|
||||
traceKeyStore("block write-back", descriptor, guestBytes);
|
||||
if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
|
||||
uploadedKeys = DccKeys::Uncompressed;
|
||||
|
|
@ -4234,7 +4448,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
|||
countStorageWriteBack(storedBytes, true);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
// The guest bytes now differ from `original`; other caches of the range see the write.
|
||||
originalValid = false;
|
||||
forgetStored();
|
||||
// The keys go the same way as the texels: a fill recorded behind the retile when the
|
||||
// metadata is host-imported (no CPU wait for the title's key-writing kernels), else a CPU
|
||||
// store (APS5_CPU_DCC_KEYS=1 keeps the CPU store; see DccMetadata.hpp).
|
||||
|
|
@ -4258,18 +4472,21 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
|||
static std::atomic<int> reports{0};
|
||||
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), keep.size());
|
||||
unregisteredDropped.fetch_add(1, std::memory_order_relaxed);
|
||||
originalValid = false;
|
||||
forgetStored();
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (layers[layer]) layerPending[layer] = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
original.resize(static_cast<std::size_t>(guestBytes));
|
||||
// The store below compares against the guest bytes, read now when `original` does not hold
|
||||
// them (a GPU clear, a dropped or kept unit). Reading them says nothing about the image: a
|
||||
// stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for
|
||||
// the content again only where it did before, or once every unit is stored.
|
||||
const bool wasValid = originalValid;
|
||||
if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original);
|
||||
Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented");
|
||||
std::vector<bool> unsaved = layerOriginal;
|
||||
unsaved.flip();
|
||||
for (const auto& [begin, end] : unitRuns(unsaved)) GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
|
||||
DeviceBuffer linear(context, static_cast<std::size_t>(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
Buffer host(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
|
|
@ -4353,7 +4570,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
|||
uploadedKeys = DccKeys::Uncompressed;
|
||||
// The store above is the only write to these pages, so `original` is current at a fresh
|
||||
// generation, unless blocks were kept for the CPU: then the image is stale there.
|
||||
originalValid = !skippedAny && (wasValid || std::all_of(layers.begin(), layers.end(), [](bool selected) { return selected; }));
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (layers[layer]) layerOriginal[layer] = !skippedLayer[layer];
|
||||
}
|
||||
// As in the GPU-direct path, but the store's memcpy dirtied this surface's pages in the write
|
||||
// watch: a memoized collect would leave them for the next walk to stamp newer than this value
|
||||
// (the shared edge block included, undoing the advance), so the walk is made here and
|
||||
|
|
|
|||
|
|
@ -181,7 +181,10 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
|
|||
createInfo.stage = stage;
|
||||
createInfo.layout = image ? imagePipelineLayout : pipelineLayout;
|
||||
VkPipeline result = VK_NULL_HANDLE;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
|
||||
}
|
||||
pipelines.emplace_back(key, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -299,7 +302,11 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask
|
|||
pipelineInfo.stage.module = cmaskModule;
|
||||
pipelineInfo.stage.pName = "main";
|
||||
pipelineInfo.layout = cmaskPipelineLayout;
|
||||
const auto created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
|
||||
VkResult created;
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
|
||||
}
|
||||
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, cmaskModule, nullptr);
|
||||
Check(created, "vkCreateComputePipelines CMASK clear");
|
||||
cmaskErrors = std::make_unique<Buffer>(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ constexpr FormatEntry kFormatLookup[] = {
|
|||
{29, VK_FORMAT_R16G16_SFLOAT, 4, false},
|
||||
{36, VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4, false},
|
||||
{50, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false},
|
||||
{51, VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4, false},
|
||||
{54, VK_FORMAT_A2B10G10R10_UINT_PACK32, 4, false},
|
||||
{55, VK_FORMAT_A2B10G10R10_SINT_PACK32, 4, false},
|
||||
{56, VK_FORMAT_R8G8B8A8_UNORM, 4, false},
|
||||
{57, VK_FORMAT_R8G8B8A8_SNORM, 4, false},
|
||||
{60, VK_FORMAT_R8G8B8A8_UINT, 4, false},
|
||||
|
|
|
|||
|
|
@ -10,25 +10,25 @@ add_custom_command(
|
|||
VERBATIM
|
||||
)
|
||||
|
||||
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests)
|
||||
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
|
||||
if(TARGET ${agcTarget})
|
||||
target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_mesh_tests)
|
||||
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
|
||||
if(TARGET ${agcTarget})
|
||||
target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests)
|
||||
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
|
||||
if(TARGET ${agcTarget})
|
||||
target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/TextureTiling.cpp Graphics/src/TextureFormat.cpp Execution/src/GuestMemory.cpp)
|
||||
target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp)
|
||||
target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot})
|
||||
|
||||
if(TARGET agc_driver_visual_test)
|
||||
|
|
|
|||
|
|
@ -175,6 +175,42 @@ void failedImportRemapTests(const Context& context, const BdaTestAccess& access)
|
|||
::operator delete(block, std::align_val_t{piece});
|
||||
}
|
||||
|
||||
void spaceImportRunTests(const Context& context) {
|
||||
constexpr std::size_t piece = 65536;
|
||||
void* block = ::operator new(2 * piece, std::align_val_t{piece});
|
||||
const auto first = reinterpret_cast<std::uintptr_t>(block);
|
||||
std::memset(block, 0x3c, 2 * piece);
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = piece;
|
||||
const auto change = [&](std::uint8_t* range, bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
auto* guest = static_cast<std::uint8_t*>(block);
|
||||
change(guest, true);
|
||||
change(guest + piece, true);
|
||||
for (int build = 0; build < 2; ++build) {
|
||||
const auto before = AddressSpaceCounters();
|
||||
GuestBufferMemory leased(importing);
|
||||
leased.AcquireRegistered();
|
||||
leased.AddReadable(first + piece - 256, 512);
|
||||
leased.Upload(true);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = leased.Descriptor(first + piece - 256, 512, adjustment);
|
||||
Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a V# across one import's ranges has no descriptor");
|
||||
const auto ranges = leased.AddressRanges();
|
||||
Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + 2 * piece <= range.end; }), "one import's adjacent ranges are not one BDA table entry");
|
||||
leased.WriteBack();
|
||||
const auto after = AddressSpaceCounters();
|
||||
if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a V# across one import's ranges dissolved the cached address space");
|
||||
}
|
||||
change(guest, false);
|
||||
change(guest + piece, false);
|
||||
::operator delete(block, std::align_val_t{piece});
|
||||
}
|
||||
|
||||
void importWindowTests(const Context& context) {
|
||||
constexpr std::size_t piece = 65536;
|
||||
constexpr std::size_t window = 2 * piece;
|
||||
|
|
@ -207,6 +243,237 @@ void importWindowTests(const Context& context) {
|
|||
::operator delete(block, std::align_val_t{window});
|
||||
}
|
||||
|
||||
void importJoinTests(const Context& context) {
|
||||
constexpr std::size_t piece = 65536;
|
||||
constexpr std::size_t window = 2 * piece;
|
||||
constexpr std::size_t large = 3 * piece;
|
||||
void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window});
|
||||
auto* guest = static_cast<std::uint8_t*>(block);
|
||||
std::memset(guest, 0x11, 3 * large);
|
||||
const auto first = reinterpret_cast<std::uintptr_t>(block);
|
||||
const auto second = first + large;
|
||||
const auto third = second + large;
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = piece;
|
||||
SetHostImportWindow(window);
|
||||
const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
const auto bind = [&](std::uintptr_t address, std::size_t bytes = 32) {
|
||||
GuestBufferMemory plain(importing);
|
||||
plain.AddReadable(address, bytes);
|
||||
plain.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = plain.Descriptor(address, bytes, adjustment);
|
||||
const auto copied = plain.CopiedBytes();
|
||||
plain.WriteBack();
|
||||
return std::pair{view.buffer, copied};
|
||||
};
|
||||
change(guest, large, true);
|
||||
change(guest + large, large, true);
|
||||
const auto* low = HostImportFor(importing, first, 16);
|
||||
Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone");
|
||||
const auto* high = HostImportFor(importing, second, 16);
|
||||
Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone");
|
||||
Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound");
|
||||
{
|
||||
GuestBufferMemory leased(importing);
|
||||
leased.AcquireRegistered();
|
||||
leased.AddReadable(second - 16, 32);
|
||||
leased.Upload(true);
|
||||
const auto* joined = HostImportFor(importing, second - 16, 32);
|
||||
Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one");
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto crossing = leased.Descriptor(second - 16, 32, adjustment);
|
||||
Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place");
|
||||
leased.WriteBack();
|
||||
}
|
||||
const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer;
|
||||
const auto [again, copied] = bind(second - 16);
|
||||
Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import");
|
||||
auto* heap = guest + 3 * large + 2 * piece;
|
||||
change(heap, piece, true);
|
||||
change(heap + piece, piece, true);
|
||||
Require(bind(reinterpret_cast<std::uintptr_t>(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast<std::uintptr_t>(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined");
|
||||
const auto heapBase = reinterpret_cast<std::uintptr_t>(heap);
|
||||
const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece);
|
||||
const auto* pieces = HostImportFor(importing, heapBase, 2 * piece);
|
||||
Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one");
|
||||
change(heap + 2 * piece, piece, true);
|
||||
static_cast<void>(bind(heapBase + 2 * piece - 16));
|
||||
const auto* kept = HostImportFor(importing, first, 16);
|
||||
Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges");
|
||||
change(guest + 2 * large, large, true);
|
||||
Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them");
|
||||
const auto [grownBuffer, grownCopied] = bind(third - 16);
|
||||
const auto* grown = HostImportFor(importing, third - 16, 32);
|
||||
Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import");
|
||||
Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other");
|
||||
change(heap, piece, false);
|
||||
change(heap + piece, piece, false);
|
||||
change(heap + 2 * piece, piece, false);
|
||||
change(guest + large, large, false);
|
||||
const auto* alone = HostImportFor(importing, first, 16);
|
||||
Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges");
|
||||
change(guest, large, false);
|
||||
change(guest + 2 * large, large, false);
|
||||
Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges");
|
||||
SetHostImportWindow(0);
|
||||
::operator delete(block, std::align_val_t{window});
|
||||
}
|
||||
|
||||
// One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the
|
||||
// test and reset after it.
|
||||
class WindowedRange {
|
||||
public:
|
||||
WindowedRange(const Context& context, std::size_t window, std::size_t cells, std::uint64_t budget = 0) : window(window), bytes(window * cells), importing(context) {
|
||||
block = ::operator new(bytes, std::align_val_t{piece});
|
||||
std::memset(block, 0x33, bytes);
|
||||
importing.hostImportAlignment = piece;
|
||||
SetHostImportRangeWindow(window);
|
||||
SetHostImportBudget(budget);
|
||||
change(true);
|
||||
}
|
||||
~WindowedRange() {
|
||||
change(false);
|
||||
SetHostImportBudget(0);
|
||||
SetHostImportRangeWindow(std::uint64_t{64} << 20u);
|
||||
::operator delete(block, std::align_val_t{piece});
|
||||
}
|
||||
WindowedRange(const WindowedRange&) = delete;
|
||||
WindowedRange& operator=(const WindowedRange&) = delete;
|
||||
|
||||
std::uintptr_t First() const { return reinterpret_cast<std::uintptr_t>(block); }
|
||||
|
||||
static constexpr std::size_t piece = 65536;
|
||||
const std::size_t window;
|
||||
const std::size_t bytes;
|
||||
Context importing;
|
||||
|
||||
private:
|
||||
void change(bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
}
|
||||
|
||||
void* block = nullptr;
|
||||
};
|
||||
|
||||
// A registered range larger than the import window, its first window imported by an earlier access: an address-based
|
||||
// build (AcquireRegistered) must import the whole range, so a view in its last window binds (Demon's Souls threw
|
||||
// "guest buffer view ... exceeds its GPU owner" with the 64 MiB windows).
|
||||
void importWholeRangeOverWindowTests(const Context& context) {
|
||||
WindowedRange range(context, 2 * WindowedRange::piece, 2);
|
||||
const auto first = range.First();
|
||||
const auto* windowed = HostImportFor(range.importing, first + 16, 32);
|
||||
Require(windowed != nullptr && windowed->base == first && windowed->bytes == range.window, "an access to the first window of a large range was not imported as that window");
|
||||
GuestBufferMemory leased(range.importing);
|
||||
leased.AcquireRegistered();
|
||||
leased.Upload(true);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = leased.Descriptor(first + range.bytes - WindowedRange::piece + 16, 32, adjustment);
|
||||
Require(view.range == 32 + adjustment, "a view in the last window of a large registered range has no descriptor");
|
||||
const auto* whole = HostImportFor(range.importing, first, range.bytes);
|
||||
Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && view.buffer == whole->buffer, "an address-based build was served by the range's first window, not an import of the whole range");
|
||||
leased.WriteBack();
|
||||
}
|
||||
|
||||
// An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before,
|
||||
// every such build retired the window and the next access imported it again.
|
||||
void importRefusedWholeRangeTests(const Context& context) {
|
||||
WindowedRange range(context, 2 * WindowedRange::piece, 4, 3 * 2 * WindowedRange::piece);
|
||||
const auto first = range.First();
|
||||
const auto* windowed = HostImportFor(range.importing, first + 16, 32);
|
||||
Require(windowed != nullptr && windowed->bytes == range.window, "the first window of a range over the budget was not imported");
|
||||
const auto buffer = windowed->buffer;
|
||||
for (int build = 0; build < 3; ++build) {
|
||||
GuestBufferMemory leased(range.importing);
|
||||
leased.AcquireRegistered();
|
||||
leased.Upload(true);
|
||||
leased.WriteBack();
|
||||
Require(HostImportCovers(range.importing, first + 16, 32), "an address-based build refused its whole-range import and retired the range's window (build " + std::to_string(build) + ")");
|
||||
}
|
||||
const auto* kept = HostImportFor(range.importing, first + 16, 32);
|
||||
Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import");
|
||||
}
|
||||
|
||||
// Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows
|
||||
// the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at
|
||||
// every use.
|
||||
void importNeighbouringEdgesTests(const Context& context) {
|
||||
WindowedRange range(context, 2 * WindowedRange::piece, 3);
|
||||
const auto first = range.First();
|
||||
const auto use = [&](std::uintptr_t address) {
|
||||
GuestBufferMemory leased(range.importing);
|
||||
leased.AddReadable(address, 32);
|
||||
leased.Upload(false);
|
||||
leased.WriteBack();
|
||||
};
|
||||
const auto acrossFirst = first + range.window - 16;
|
||||
const auto acrossSecond = first + 2 * range.window - 16;
|
||||
use(acrossFirst);
|
||||
Require(HostImportCovers(range.importing, acrossFirst, 32), "a use across the first cell edge was not imported");
|
||||
use(acrossSecond);
|
||||
Require(HostImportCovers(range.importing, first, range.bytes), "a use across the second cell edge did not grow the first use's window to their union");
|
||||
const auto serial = HostImportSerial(range.importing, first, range.bytes, false);
|
||||
for (int round = 0; round < 10; ++round) {
|
||||
use(acrossFirst);
|
||||
use(acrossSecond);
|
||||
Require(HostImportSerial(range.importing, first, range.bytes, false) == serial, "a use across a cell edge replaced the shared window (round " + std::to_string(round) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// A range mapped while the imports are at the budget is refused at map time without retiring an import: the map
|
||||
// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit).
|
||||
void importAtMapOverBudgetTests(const Context& context) {
|
||||
constexpr std::size_t bytes = 65536;
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = bytes;
|
||||
void* kept = ::operator new(bytes, std::align_val_t{bytes});
|
||||
void* used = ::operator new(bytes, std::align_val_t{bytes});
|
||||
void* mapped = ::operator new(bytes, std::align_val_t{bytes});
|
||||
const auto change = [&](void* block, bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
change(kept, true);
|
||||
change(used, true);
|
||||
const auto keptAddress = reinterpret_cast<std::uintptr_t>(kept);
|
||||
const auto usedAddress = reinterpret_cast<std::uintptr_t>(used);
|
||||
Require(HostImportFor(importing, keptAddress, 16) != nullptr, "the import to keep was not made");
|
||||
for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, usedAddress, 16) != nullptr, "the import in use was not made");
|
||||
{
|
||||
GuestBufferMemory leased(importing);
|
||||
leased.AddReadable(usedAddress, 16);
|
||||
leased.Upload(false);
|
||||
leased.WriteBack();
|
||||
}
|
||||
const auto live = HostImportLiveBytes();
|
||||
SetHostImportBudget(live + bytes - 1);
|
||||
change(mapped, true);
|
||||
GuestAllocations::Mapped ranges;
|
||||
{
|
||||
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
|
||||
for (const auto& range : lease) {
|
||||
if (range->address == reinterpret_cast<std::uintptr_t>(mapped)) ranges.push_back(range);
|
||||
}
|
||||
}
|
||||
Require(ranges.size() == 1, "the mapped range is not registered");
|
||||
static_cast<void>(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true));
|
||||
SetHostImportBudget(0);
|
||||
Require(HostImportLiveBytes() == live, "a range mapped at the budget retired an import at map time (an eviction without the GPU lock)");
|
||||
Require(HostImportCovers(importing, keptAddress, 16) && !HostImportCovers(importing, reinterpret_cast<std::uintptr_t>(mapped), 16), "a range mapped at the budget was imported, or the import to keep was retired");
|
||||
for (auto* block : {kept, used, mapped}) change(block, false);
|
||||
for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes});
|
||||
}
|
||||
|
||||
ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) {
|
||||
return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false};
|
||||
}
|
||||
|
|
@ -1202,7 +1469,13 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
|||
importCrossingTests(context, access);
|
||||
importSpanTests(context);
|
||||
failedImportRemapTests(context, access);
|
||||
spaceImportRunTests(context);
|
||||
importWindowTests(context);
|
||||
importJoinTests(context);
|
||||
importWholeRangeOverWindowTests(context);
|
||||
importRefusedWholeRangeTests(context);
|
||||
importNeighbouringEdgesTests(context);
|
||||
importAtMapOverBudgetTests(context);
|
||||
heapMirrorTests(context, access);
|
||||
importedHeapMirrorTests(context, access);
|
||||
importWatchTests();
|
||||
|
|
|
|||
|
|
@ -145,22 +145,14 @@ void run(AgcDriver::VulkanDevice& device, ColorTileMode mode, const std::string&
|
|||
expect(device, color, [](std::uint32_t x, std::uint32_t y) { return y < 4 ? Drawn : y >= 8 && x >= 24 ? Clear : Stale; }, name + " fast-cleared bottom edge tiles under a draw of the top rows");
|
||||
}
|
||||
|
||||
void invalidCode(AgcDriver::VulkanDevice& device) {
|
||||
void unknownCode(AgcDriver::VulkanDevice& device) {
|
||||
const auto color = target(ColorTileMode::RenderTarget);
|
||||
prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 2 && tileY == 0; }, 3);
|
||||
eliminate(device, color);
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.WaitIdle();
|
||||
}
|
||||
expect(device, color, [](std::uint32_t, std::uint32_t) { return Stale; }, "a tile with CMASK code 3 eliminated");
|
||||
prepare(color, [](std::uint32_t, std::uint32_t) { return true; });
|
||||
try {
|
||||
eliminate(device, color);
|
||||
} catch (const std::runtime_error& error) {
|
||||
Require(std::string(error.what()).find("were recorded, which are not a single-sample fast-clear code") != std::string::npos, std::string("unexpected failure: ") + error.what());
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("a CMASK code 3 read by the GPU pass did not throw");
|
||||
eliminate(device, color);
|
||||
expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "fast-cleared target eliminated after a CMASK code 3");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -182,7 +174,7 @@ int main() {
|
|||
}
|
||||
run(*device, ColorTileMode::Linear, "linear");
|
||||
run(*device, ColorTileMode::RenderTarget, "SW_64KB_R_X");
|
||||
invalidCode(*device);
|
||||
unknownCode(*device);
|
||||
device->WaitIdle();
|
||||
device.reset();
|
||||
{
|
||||
|
|
|
|||
|
|
@ -150,6 +150,66 @@ int main() {
|
|||
return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses();
|
||||
});
|
||||
|
||||
passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] {
|
||||
Builder b;
|
||||
const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); };
|
||||
const auto flushedResult = [&](IrOpcode opcode, std::initializer_list<IrValue*> arguments) -> IrValue& {
|
||||
return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)}));
|
||||
};
|
||||
auto& x = value(b.flush(b.unknown()));
|
||||
auto& y = value(b.flush(b.unknown()));
|
||||
auto& z = value(b.flush(b.unknown()));
|
||||
auto& raw = value(b.unknown());
|
||||
IrValue* removed[] = {
|
||||
&flushedResult(IrOpcode::FPMin32, {&x, &y}),
|
||||
&flushedResult(IrOpcode::FPMax32, {&y, &x}),
|
||||
&flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}),
|
||||
&flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}),
|
||||
&flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}),
|
||||
};
|
||||
auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw});
|
||||
auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw});
|
||||
for (auto* flushed : removed) b.keep(*flushed);
|
||||
b.keep(mixedPair);
|
||||
b.keep(mixedMedian);
|
||||
const bool counted = b.eliminate() == 5u;
|
||||
bool kept = false;
|
||||
for (auto* flushed : removed) kept |= flushed->HasUses();
|
||||
return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses();
|
||||
});
|
||||
|
||||
passed &= run("a floor or ceil needs no flush, even of an unknown value", [] {
|
||||
Builder b;
|
||||
auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
|
||||
auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})}));
|
||||
auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})}));
|
||||
b.keep(floored);
|
||||
b.keep(ceiled);
|
||||
return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses();
|
||||
});
|
||||
|
||||
passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] {
|
||||
Builder b;
|
||||
IrBlock& head = *b.block;
|
||||
IrBlock& body = b.newBlock();
|
||||
IrBlock& exit = b.newBlock();
|
||||
head.AddBranch(&body);
|
||||
body.AddBranch(&body);
|
||||
body.AddBranch(&exit);
|
||||
auto& initial = b.flush(b.unknown());
|
||||
b.block = &body;
|
||||
auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32);
|
||||
body.AppendInstruction(&carried);
|
||||
auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())});
|
||||
auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})});
|
||||
carried.AddPhiOperand(&head, &initial);
|
||||
carried.AddPhiOperand(&body, &maximum);
|
||||
auto& flushed = b.flush(maximum);
|
||||
b.block = &exit;
|
||||
b.keep(flushed);
|
||||
return b.eliminate() == 0u && flushed.HasUses();
|
||||
});
|
||||
|
||||
passed &= run("a phi of flushed values needs no flush", [] {
|
||||
Builder b;
|
||||
IrBlock& head = *b.block;
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ void RunDepthSurfaceReuseTests() {
|
|||
context.memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
context.limits.maxFramebufferWidth = 16384;
|
||||
context.limits.maxFramebufferHeight = 16384;
|
||||
context.limits.maxFramebufferLayers = 1;
|
||||
TestD16StencilFallback(context);
|
||||
constexpr std::uint64_t depth32 = 0x40000000;
|
||||
constexpr std::uint64_t depth16 = 0x50000000;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
#include <cstdio>
|
||||
|
||||
int main() {
|
||||
std::puts("skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS");
|
||||
return 77;
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
#include "BdaAbi.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <span>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
void Check(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
SpirvTarget Target(bool byteAccess) {
|
||||
static constexpr std::array<std::uint32_t, 3> withBytes{
|
||||
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess,
|
||||
};
|
||||
static constexpr std::array<std::uint32_t, 2> withoutBytes{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses};
|
||||
static constexpr std::array<std::string_view, 4> extensions{
|
||||
"SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer",
|
||||
};
|
||||
return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, byteAccess ? std::span<const std::uint32_t>(withBytes) : std::span<const std::uint32_t>(withoutBytes), extensions, false,
|
||||
{1024, 1024, 64}, 1024, 32768, {}, {}};
|
||||
}
|
||||
|
||||
struct Counts {
|
||||
std::size_t compareExchanges = 0;
|
||||
std::size_t byteStores = 0;
|
||||
std::size_t byteTypes = 0;
|
||||
bool byteCapability = false;
|
||||
bool byteBuffers = false;
|
||||
};
|
||||
|
||||
Counts Count(std::span<const std::uint32_t> words) {
|
||||
Check(words.size() >= 5 && words[0] == spv::MagicNumber, "formatted buffer byte stores: missing SPIR-V module");
|
||||
Counts counts;
|
||||
std::uint32_t byteType = 0;
|
||||
std::vector<std::uint32_t> bytePointers;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> spv::WordCountShift;
|
||||
Check(count != 0 && count <= words.size() - offset, "formatted buffer byte stores: malformed SPIR-V instruction");
|
||||
const auto opcode = words[offset] & spv::OpCodeMask;
|
||||
if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges;
|
||||
if (opcode == spv::OpCapability && words[offset + 1] == spv::CapabilityStorageBuffer8BitAccess) counts.byteCapability = true;
|
||||
if (opcode == spv::OpTypeInt && words[offset + 2] == 8u) {
|
||||
++counts.byteTypes;
|
||||
byteType = words[offset + 1];
|
||||
}
|
||||
if (opcode == spv::OpTypePointer && words[offset + 2] == spv::StorageClassStorageBuffer && words[offset + 3] == byteType && byteType != 0) bytePointers.push_back(words[offset + 1]);
|
||||
if (opcode == spv::OpName) {
|
||||
const auto name = std::string_view(reinterpret_cast<const char*>(&words[offset + 2]));
|
||||
if (name == "buffers_u8") counts.byteBuffers = true;
|
||||
}
|
||||
offset += count;
|
||||
}
|
||||
std::vector<std::uint32_t> byteChains;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> spv::WordCountShift;
|
||||
const auto opcode = words[offset] & spv::OpCodeMask;
|
||||
if (opcode == spv::OpAccessChain) {
|
||||
for (const auto pointer : bytePointers) {
|
||||
if (words[offset + 1] == pointer) byteChains.push_back(words[offset + 2]);
|
||||
}
|
||||
}
|
||||
if (opcode == spv::OpStore) {
|
||||
for (const auto chain : byteChains) {
|
||||
if (words[offset + 1] == chain) ++counts.byteStores;
|
||||
}
|
||||
}
|
||||
offset += count;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
Counts Compile(bool byteAccess, bool vertex = false) {
|
||||
std::vector<std::uint32_t> code{0xe0000000u, 0x80020a00u, 0xbf8c3f70u, 0xe0100000u, 0x80020a00u};
|
||||
if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u});
|
||||
code.push_back(0xbf810000u);
|
||||
const std::span<const std::uint32_t> words(code);
|
||||
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(words)}}};
|
||||
const std::array<std::uint32_t, 12> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu};
|
||||
RecompileRequest request{};
|
||||
request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), words, 0, {}};
|
||||
request.target = Target(byteAccess);
|
||||
request.layout = {0, 0, 0, 128};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
if (vertex) request.context.vertex = ShaderVertexStageInfo{};
|
||||
else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.context.memory = memory;
|
||||
request.useCache = false;
|
||||
return Count(Recompile(request).spirv.Words());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto bytes = Compile(true);
|
||||
Check(bytes.compareExchanges == 0, "formatted buffer byte stores: a store still patches its dword with " + std::to_string(bytes.compareExchanges) + " compare-exchange loops");
|
||||
Check(bytes.byteBuffers && bytes.byteCapability && bytes.byteTypes == 1, "formatted buffer byte stores: the byte view of the buffers is not declared under StorageBuffer8BitAccess");
|
||||
Check(bytes.byteStores >= 4, "formatted buffer byte stores: the 8-bit components are stored with " + std::to_string(bytes.byteStores) + " byte stores, expected at least 4");
|
||||
const auto dwords = Compile(false);
|
||||
Check(dwords.compareExchanges > 0 && !dwords.byteBuffers && !dwords.byteCapability, "formatted buffer byte stores: without StorageBuffer8BitAccess the stores do not fall back to the dword patch");
|
||||
std::printf("formatted buffer byte stores tests passed (%zu byte stores, %zu compare-exchange loops without the capability)\n", bytes.byteStores, dwords.compareExchanges);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
#include "BdaAbi.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libc/include/GuestHeap.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
|
@ -51,6 +53,7 @@ using AgcDriver::Graphics::Require;
|
|||
|
||||
alignas(256) std::array<std::byte, 1024> colorMemory{};
|
||||
alignas(256) std::array<std::byte, 2048> sliceMemory{};
|
||||
alignas(256) std::array<std::uint8_t, 4> volumeKeyMemory{};
|
||||
|
||||
AgcDriver::QueueState makeState() {
|
||||
AgcDriver::QueueState queue;
|
||||
|
|
@ -98,6 +101,90 @@ void expectFailure(TAction action, std::string_view reason) {
|
|||
throw std::runtime_error("expected graphics rejection: " + std::string(reason));
|
||||
}
|
||||
|
||||
void inactiveVertexPointSizeTests() {
|
||||
const auto pointSizeState = [] {
|
||||
auto queue = makeState();
|
||||
queue.context[0x1b3] = queue.context[0x1b4] = 2;
|
||||
queue.context[0x207] = 0x01210000;
|
||||
return queue;
|
||||
};
|
||||
for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.userConfig[0x242] = primitive;
|
||||
for (const auto raster : {0u, 0x240u, 0x248u}) {
|
||||
queue.context[0x205] = raster;
|
||||
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection);
|
||||
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
|
||||
}
|
||||
}
|
||||
for (const auto raster : {0u, 0x240u, 0x248u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.userConfig[0x242] = 3;
|
||||
queue.context[0x205] = raster;
|
||||
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection);
|
||||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding");
|
||||
Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class");
|
||||
queue.userConfig[0x24b] = 1;
|
||||
queue.context[0x103] = 0xffffffffu;
|
||||
const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true);
|
||||
Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection);
|
||||
const auto indexedState = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding");
|
||||
}
|
||||
const auto rejected = [](AgcDriver::QueueState queue) {
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck");
|
||||
try {
|
||||
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
|
||||
} catch (const std::runtime_error&) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("potentially active point size passed the state decoder");
|
||||
};
|
||||
for (const auto primitive : {1u, 9u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.userConfig[0x242] = primitive;
|
||||
rejected(queue);
|
||||
}
|
||||
for (const auto stages : {0x2004u, 0x2020u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.context[0x2d5] = stages;
|
||||
rejected(queue);
|
||||
}
|
||||
for (const auto raster : {8u, 0x208u, 0x48u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.context[0x205] = raster;
|
||||
rejected(queue);
|
||||
}
|
||||
for (const auto flag : {1u << 17u, 1u << 20u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.context[0x207] = 0x01210000 | flag;
|
||||
rejected(queue);
|
||||
}
|
||||
for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.context[0x207] |= distances;
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances");
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state");
|
||||
}
|
||||
{
|
||||
auto queue = pointSizeState();
|
||||
queue.context[0x207] |= 0x004001ffu;
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit");
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances");
|
||||
}
|
||||
for (const auto offset : {0x2d5u, 0x205u}) {
|
||||
auto queue = pointSizeState();
|
||||
queue.context.erase(offset);
|
||||
rejected(queue);
|
||||
}
|
||||
auto queue = pointSizeState();
|
||||
queue.userConfig.erase(0x242);
|
||||
rejected(queue);
|
||||
}
|
||||
|
||||
void hdrComponentSwapTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
for (const auto number : {0u, 1u, 4u, 7u}) {
|
||||
|
|
@ -451,6 +538,58 @@ void hardwareScreenOffsetTests() {
|
|||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
||||
}
|
||||
|
||||
void UnwrittenSampleLocationTests() {
|
||||
using AgcDriver::Graphics::DecodeState;
|
||||
using AgcDriver::Graphics::DrawRejection;
|
||||
using AgcDriver::Graphics::SampleLocations;
|
||||
const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) {
|
||||
auto written = makeState();
|
||||
written.context[0x2f8] = config;
|
||||
written.context[0x292] = scanMode;
|
||||
written.context[0x3b8] = 0x0dc00000u;
|
||||
written.context[0x31d] = attributes;
|
||||
written.context[0x1b3] = 2;
|
||||
written.context[0x1b4] = 2;
|
||||
AgcDriver::QueueState queue;
|
||||
queue.shader = written.shader;
|
||||
queue.userConfig = written.userConfig;
|
||||
written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; });
|
||||
return queue;
|
||||
};
|
||||
AgcDriver::QueueState initial;
|
||||
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0");
|
||||
for (const auto samples : {2u, 4u, 8u}) {
|
||||
const auto log2 = static_cast<std::uint32_t>(std::countr_zero(samples));
|
||||
auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u));
|
||||
const auto state = DecodeState(queue);
|
||||
Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count");
|
||||
Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern");
|
||||
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center");
|
||||
const auto centered = SampleLocations(state);
|
||||
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center");
|
||||
const auto rejection = DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection);
|
||||
}
|
||||
auto queue = fresh(0x00200002, 0x12000);
|
||||
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae;
|
||||
auto state = DecodeState(queue);
|
||||
Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed");
|
||||
queue = fresh(0x00200002, 0x12000);
|
||||
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a;
|
||||
state = DecodeState(queue);
|
||||
Require(state.samples == 4 && state.customSampleLocations, "explicit custom 4x sample locations changed");
|
||||
const auto custom = SampleLocations(state);
|
||||
const std::array<VkSampleLocationEXT, 4> expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}};
|
||||
for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed");
|
||||
queue.ClearContext();
|
||||
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS");
|
||||
expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples");
|
||||
expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples");
|
||||
queue = fresh(0, 0, 3);
|
||||
state = DecodeState(queue);
|
||||
Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused");
|
||||
}
|
||||
|
||||
void MsaaEnableSingleSampleTests() {
|
||||
using AgcDriver::Graphics::DecodeState;
|
||||
using AgcDriver::Graphics::DrawRejection;
|
||||
|
|
@ -572,6 +711,34 @@ void ShaderStageTests() {
|
|||
fan.userConfig[0x242] = 3;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly");
|
||||
}
|
||||
{
|
||||
auto fast = makeState();
|
||||
fast.userConfig[0x242] = 1;
|
||||
fast.context[0x2d5] = 0x482030;
|
||||
fast.userConfig[0x25b] = 0x201;
|
||||
fast.context[0x1ff] = 32;
|
||||
fast.context[0x2ce] = 32;
|
||||
fast.context[0x2d3] = 64;
|
||||
fast.context[0x29b] = 2;
|
||||
fast.context[0x2ab] = 4;
|
||||
fast.shader[0x8a] = 0x022c0245;
|
||||
fast.shader[0x8b] = 0x9002a;
|
||||
const auto state = AgcDriver::Graphics::DecodeState(fast);
|
||||
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry");
|
||||
const auto& mesh = *state.stages.mesh;
|
||||
Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR");
|
||||
fast.context[0x2d5] = 0x502030;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state");
|
||||
fast.context[0x2d5] = 0x482030;
|
||||
fast.userConfig[0x25b] = 0x402;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
|
||||
fast.userConfig[0x25b] = 0x201;
|
||||
fast.userConfig[0x242] = 4;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
|
||||
fast.userConfig[0x242] = 1;
|
||||
fast.context[0x2d3] = 0;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives");
|
||||
}
|
||||
queue.context[0x2ab] = 0;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE");
|
||||
queue.context[0x2ab] = 4;
|
||||
|
|
@ -598,17 +765,28 @@ void ShaderStageTests() {
|
|||
}
|
||||
Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed");
|
||||
const auto& tessellation = *stages.tessellation;
|
||||
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2, "the passthrough triangle-patch configuration decoded differently");
|
||||
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "only triangular"}, std::pair{0x40069u, "only triangular"}, std::pair{0x4006au, "only triangular"}}) {
|
||||
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently");
|
||||
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) {
|
||||
queue.context[0x2db] = parameters;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
|
||||
}
|
||||
queue.context[0x2db] = 0x40049;
|
||||
for (const auto [maximum, minimum] : {std::pair{0x41100000u, 0x3f800000u}, std::pair{0x42800000u, 0x40000000u}}) {
|
||||
queue.context[0x2db] = 0x40041;
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently");
|
||||
queue.context[0x2d6] = 0x1043f;
|
||||
queue.context[0x2db] = 0x4006a;
|
||||
queue.context[0x286] = 0x41100000;
|
||||
const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages;
|
||||
const auto& quads = quadStages.tessellation;
|
||||
Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently");
|
||||
queue.context[0x2db] = 0x40069;
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently");
|
||||
for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) {
|
||||
queue.context[0x286] = maximum;
|
||||
queue.context[0x287] = minimum;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "tessellation level clamps");
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
|
||||
}
|
||||
queue.context[0x2d6] = 0xc355;
|
||||
queue.context[0x2db] = 0x40049;
|
||||
queue.context[0x286] = 0x42800000;
|
||||
queue.context[0x287] = 0x3f800000;
|
||||
queue.context[0x2d5] = 0x202d;
|
||||
|
|
@ -616,6 +794,8 @@ void ShaderStageTests() {
|
|||
queue.context[0x2d5] = 0x0200210d;
|
||||
queue.context[0x2d6] = 0;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts");
|
||||
queue.context[0x2d6] = 0xc300;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero");
|
||||
queue = makeState();
|
||||
for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) {
|
||||
queue.context[0x2d5] = value;
|
||||
|
|
@ -964,6 +1144,16 @@ void PixelInputLayoutTests() {
|
|||
Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded");
|
||||
}
|
||||
|
||||
void BlendBypassTests() {
|
||||
auto queue = makeState();
|
||||
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
|
||||
queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u);
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded");
|
||||
queue.context[0x31c] |= 0x10000u;
|
||||
const auto bypassed = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends");
|
||||
}
|
||||
|
||||
void OpaqueDestinationAlphaTests() {
|
||||
auto queue = makeState();
|
||||
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
|
||||
|
|
@ -1332,6 +1522,12 @@ void DepthStencilTests() {
|
|||
Require(plainRange.color.address == plain.color.address && plainRange.color.bytes == plain.color.bytes && plainRange.color.arrayLastSlice == plain.color.arrayLastSlice, "a color view of slices 0 to 35 of a target with no array slices did not render into its only slice");
|
||||
queue.context[0x31b] = 1u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
|
||||
queue.context[0x31b] = 1u << 13u;
|
||||
const auto firstSlice = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(firstSlice.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()), "a color view of several slices did not retain its first slice");
|
||||
queue.context[0x207] = 1u << 18u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices");
|
||||
queue.context[0x207] = 0;
|
||||
const auto sliced = reinterpret_cast<std::uintptr_t>(sliceMemory.data());
|
||||
queue.context[0x318] = static_cast<std::uint32_t>(sliced >> 8u);
|
||||
queue.context[0x390] = static_cast<std::uint32_t>(sliced >> 40u);
|
||||
|
|
@ -1355,13 +1551,35 @@ void DepthStencilTests() {
|
|||
queue.context[0x31b] = 2u | (2u << 13u);
|
||||
const auto volume = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice");
|
||||
Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered");
|
||||
queue.context[0x31b] = 4u | (4u << 13u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the 3D surface");
|
||||
queue.context[0x31b] = 2u | (3u << 13u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "several 3D slices");
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface");
|
||||
queue.context[0x31b] = 2u | (1u << 13u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
|
||||
queue.context[0x31b] = 1u | (3u << 13u);
|
||||
queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u);
|
||||
const auto layered = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export");
|
||||
{
|
||||
auto pointSize = queue;
|
||||
pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2;
|
||||
Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck");
|
||||
pointSize.context[0x207] |= 1u << 16u;
|
||||
const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false);
|
||||
Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection);
|
||||
const auto decoded = AgcDriver::Graphics::DecodeState(pointSize);
|
||||
Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing");
|
||||
Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state");
|
||||
}
|
||||
queue.context[0x31b] = 1u | (8u << 13u);
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice");
|
||||
queue.context[0x207] = 0;
|
||||
queue.context[0x31b] = 0;
|
||||
queue.context[0x31c] |= 0x10000000;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets");
|
||||
const auto volumeKeys = reinterpret_cast<std::uintptr_t>(volumeKeyMemory.data());
|
||||
queue.context[0x325] = static_cast<std::uint32_t>(volumeKeys >> 8u);
|
||||
queue.context[0x3a8] = static_cast<std::uint32_t>(volumeKeys >> 40u);
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == volumeKeys, "a DCC 3D color target lost its keys");
|
||||
}
|
||||
|
||||
void OneDimensionalColorTests() {
|
||||
|
|
@ -1660,6 +1878,15 @@ void metadataPassTests() {
|
|||
queue.context[0x0] = 0;
|
||||
queue.context[0x200] = 0x70;
|
||||
Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass");
|
||||
auto bound = queue;
|
||||
bound.context[0x010] = 0x3;
|
||||
bound.context[0x011] = 0x1;
|
||||
bound.context[0x200] = 0x72;
|
||||
Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass");
|
||||
bound.context[0x200] = 0x76;
|
||||
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
|
||||
bound.context[0x200] = 0x7a;
|
||||
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
|
||||
queue.context[0x91] = 0x40020;
|
||||
expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target");
|
||||
queue.context[0x91] = 0x40040;
|
||||
|
|
@ -1825,6 +2052,16 @@ void cmaskTests() {
|
|||
auto mipmapped = queue;
|
||||
mipmapped.context[0x3b0] |= 1u << 28u;
|
||||
expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array");
|
||||
constexpr std::size_t blockBytes = 65536u;
|
||||
static std::vector<std::byte> mipStorage(16u * blockBytes);
|
||||
const auto mipBase = (reinterpret_cast<std::uintptr_t>(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes;
|
||||
auto higherMip = queue;
|
||||
higherMip.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u);
|
||||
higherMip.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u);
|
||||
higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u;
|
||||
higherMip.context[0x31b] = 1u << 26u;
|
||||
const auto higherTarget = DecodeColorBuffer(higherMip.context, 0);
|
||||
Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK");
|
||||
auto unaddressed = queue;
|
||||
unaddressed.context[0x31f] = 0;
|
||||
unaddressed.context[0x398] = 0;
|
||||
|
|
@ -1896,8 +2133,8 @@ void cmaskTests() {
|
|||
cmaskMemory.fill(0xff);
|
||||
setTile(2, 0);
|
||||
setTile(3, 3);
|
||||
expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not a single-sample fast-clear code");
|
||||
Require(texels(0x5a5a5a5au), "a refused pass changed the texels");
|
||||
AgcDriver::Graphics::RunColorMetadataPass(context, *pass);
|
||||
Require(tiles([](std::uint32_t tile) { return tile == 2; }) && cmaskIs(0xff), "a tile with a CMASK code other than 0 or 15 did not keep its texels");
|
||||
|
||||
queue.context[0x31c] |= 0x10000000;
|
||||
const auto keysAddress = reinterpret_cast<std::uintptr_t>(dccKeys.data());
|
||||
|
|
@ -2073,6 +2310,54 @@ void multisampleTests() {
|
|||
state = DecodeState(queue);
|
||||
Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard");
|
||||
AgcDriver::Graphics::ValidateSampleLocations({}, state);
|
||||
for (const auto log2 : {0u, 1u, 2u, 3u}) {
|
||||
queue = log2 == 0 ? makeState() : multisampled();
|
||||
queue.context[0x1b3] = 2u;
|
||||
queue.context[0x1b4] = 2u;
|
||||
queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u;
|
||||
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
|
||||
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
|
||||
queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u;
|
||||
queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u;
|
||||
}
|
||||
std::vector<AgcDriver::Graphics::RegisterRead> sampleReads;
|
||||
AgcDriver::Graphics::RegisterReadLog() = &sampleReads;
|
||||
const auto coveredCenter = DecodeState(queue);
|
||||
const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
||||
AgcDriver::Graphics::RegisterReadLog() = nullptr;
|
||||
Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs");
|
||||
for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register");
|
||||
Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection);
|
||||
for (const auto centroid : {4u, 64u, 68u}) {
|
||||
queue.context[0x1b3] = 2u | centroid;
|
||||
queue.context[0x1b4] = 2u | centroid;
|
||||
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation");
|
||||
queue.context[0x1b4] = 2u;
|
||||
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode");
|
||||
queue.context[0x1b3] = 2u;
|
||||
queue.context[0x1b4] = 2u | centroid;
|
||||
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode");
|
||||
}
|
||||
queue.context[0x1b3] = 2u;
|
||||
queue.context[0x1b4] = 2u;
|
||||
for (const auto offset : {0x1b3u, 0x1b4u}) {
|
||||
queue.context.erase(offset);
|
||||
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs");
|
||||
queue.context[offset] = 2u;
|
||||
}
|
||||
const auto withProgram = queue;
|
||||
queue.shader[0x008] = 0;
|
||||
queue.context[0x8e] = 0;
|
||||
queue.context.erase(0x1b3);
|
||||
queue.context.erase(0x1b4);
|
||||
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program");
|
||||
queue = withProgram;
|
||||
queue.context[0x2f8] |= 0x10u;
|
||||
expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits");
|
||||
}
|
||||
queue = multisampled();
|
||||
queue.context[0x31d] = 0x1000;
|
||||
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
|
||||
|
|
@ -2262,6 +2547,18 @@ void clipDistanceTests() {
|
|||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck");
|
||||
}
|
||||
|
||||
void framebufferLayerTests() {
|
||||
AgcDriver::Graphics::Context context{};
|
||||
context.limits.maxFramebufferWidth = 64;
|
||||
context.limits.maxFramebufferHeight = 64;
|
||||
context.limits.maxFramebufferLayers = 3;
|
||||
AgcDriver::Graphics::Framebuffer accepted(context, VK_NULL_HANDLE, {}, {64, 64}, 3);
|
||||
for (const VkRenderPass pass : std::array<VkRenderPass, 2>{VK_NULL_HANDLE, reinterpret_cast<VkRenderPass>(1)}) {
|
||||
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 0); }, "framebuffer layers exceed device limits");
|
||||
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 4); }, "framebuffer layers exceed device limits");
|
||||
}
|
||||
}
|
||||
|
||||
void DepthClipTests() {
|
||||
auto queue = makeState();
|
||||
const auto direct = AgcDriver::Graphics::DecodeState(queue);
|
||||
|
|
@ -2339,7 +2636,7 @@ void AlphaToCoverageTests() {
|
|||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage");
|
||||
}
|
||||
queue.context[0x2dc] = 0xaa01;
|
||||
const auto exportFormat = queue.context.at(0x1c5);
|
||||
const std::uint32_t exportFormat = queue.context.at(0x1c5);
|
||||
for (const auto format : {4u, 5u, 6u, 9u}) {
|
||||
queue.context[0x1c5] = (exportFormat & ~0xfu) | format;
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha");
|
||||
|
|
@ -2739,6 +3036,32 @@ AgcDriver::Graphics::Context mockContext() {
|
|||
return context;
|
||||
}
|
||||
|
||||
void addressSpaceTeardownTests() {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_NO_LEASE_MIRROR", "1");
|
||||
#else
|
||||
setenv("APS5_NO_LEASE_MIRROR", "1", 1);
|
||||
#endif
|
||||
auto context = mockContext();
|
||||
context.device = makeHandle<VkDevice>();
|
||||
context.bufferDeviceAddress = true;
|
||||
context.limits.maxStorageBufferRange = 1u << 27;
|
||||
void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64);
|
||||
const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops;
|
||||
{
|
||||
AgcDriver::Graphics::GuestBufferMemory memory(context);
|
||||
memory.AcquireRegistered();
|
||||
Require(memory.HoldsLease(), "address-based build holds no lease before device teardown");
|
||||
memory.Upload(true);
|
||||
memory.WriteBack();
|
||||
Require(!memory.HoldsLease(), "completed address-based build kept its own lease");
|
||||
}
|
||||
AgcDriver::Graphics::ClearImageMirrors(context.device);
|
||||
GuestHeap::GuestHeapFree_nid_postfix(block);
|
||||
Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations");
|
||||
Require(mock.live == 0, "address-space teardown test leaked Vulkan objects");
|
||||
}
|
||||
|
||||
using Role = ShaderRecompiler::DescriptorRole;
|
||||
using Kind = ShaderRecompiler::DescriptorKind;
|
||||
|
||||
|
|
@ -3372,6 +3695,7 @@ struct ModuleShape {
|
|||
std::uint32_t floatControlsWidth = 0;
|
||||
std::uint32_t clipDistanceLength = 0;
|
||||
std::uint32_t cullDistanceLength = 0;
|
||||
bool layerOutput = false;
|
||||
};
|
||||
|
||||
void emit(std::vector<std::uint32_t>& out, spv::Op op, std::initializer_list<std::uint32_t> operands) {
|
||||
|
|
@ -3410,6 +3734,14 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
|
|||
emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, shape.parameterLocation});
|
||||
extraInterface.push_back(parameter);
|
||||
}
|
||||
if (shape.layerOutput) {
|
||||
const auto pointer = id();
|
||||
const auto layer = id();
|
||||
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, uintType});
|
||||
emit(declarations, spv::OpVariable, {pointer, layer, spv::StorageClassOutput});
|
||||
emit(annotations, spv::OpDecorate, {layer, spv::DecorationBuiltIn, spv::BuiltInLayer});
|
||||
extraInterface.push_back(layer);
|
||||
}
|
||||
if (shape.secondTarget) {
|
||||
const auto target = id();
|
||||
emit(declarations, spv::OpVariable, {outputPointer, target, spv::StorageClassOutput});
|
||||
|
|
@ -3566,6 +3898,15 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
|
|||
if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry});
|
||||
if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance});
|
||||
if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance});
|
||||
if (shape.layerOutput) {
|
||||
emit(words, spv::OpCapability, {spv::CapabilityShaderViewportIndexLayerEXT});
|
||||
const std::string extension = "SPV_EXT_shader_viewport_index_layer";
|
||||
const auto count = (extension.size() + 4) / 4;
|
||||
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
|
||||
const auto start = words.size();
|
||||
words.resize(start + count, 0);
|
||||
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
|
||||
}
|
||||
if (shape.barycentric) {
|
||||
emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR});
|
||||
const std::string extension = "SPV_KHR_fragment_shader_barycentric";
|
||||
|
|
@ -4048,6 +4389,41 @@ void conservativeZExportTests() {
|
|||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode");
|
||||
}
|
||||
|
||||
bool declaresEarlyFragmentTests(const ShaderRecompiler::RecompileRequest& request) {
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void earlyFragmentTestsTests() {
|
||||
const std::array<std::pair<std::uint32_t, bool>, 9> cases{{
|
||||
{0x0000u, false},
|
||||
{0x0020u, false},
|
||||
{0x0010u, true},
|
||||
{0x0030u, true},
|
||||
{0x0050u, false},
|
||||
{0x0011u, false},
|
||||
{0x1000u, true},
|
||||
{0x1050u, true},
|
||||
{0x1031u, true}
|
||||
}};
|
||||
const ShaderRecompiler::RequestSerializer serializer;
|
||||
for (const auto& [shaderControl, expected] : cases) {
|
||||
const auto request = depthExportRequest(shaderControl);
|
||||
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
|
||||
Require(declaresEarlyFragmentTests(request) == expected && declaresEarlyFragmentTests(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + (expected ? " did not declare" : " declared") + " early fragment tests");
|
||||
}
|
||||
for (const auto shaderControl : {0x0010u, 0x1000u}) {
|
||||
auto request = depthExportRequest(shaderControl);
|
||||
request.context.pixel->alphaToCoverage = true;
|
||||
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
|
||||
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests");
|
||||
}
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 5> OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
||||
constexpr std::array<std::uint32_t, 12> OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
||||
|
||||
|
|
@ -4251,6 +4627,7 @@ void stencilRefExportTests() {
|
|||
Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl));
|
||||
}
|
||||
Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference");
|
||||
Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests");
|
||||
const std::array<std::uint32_t, 2> stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT};
|
||||
const std::array<std::uint32_t, 1> plain{spv::CapabilityShader};
|
||||
const ShaderRecompiler::RequestSerializer serializer;
|
||||
|
|
@ -4920,6 +5297,7 @@ void validationTests() {
|
|||
vertex.vertexAttributes[0].components = 2;
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 64, "vertex range includes records before the first vertex");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch");
|
||||
{
|
||||
auto wider = attribute;
|
||||
|
|
@ -4942,6 +5320,7 @@ void validationTests() {
|
|||
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed");
|
||||
}
|
||||
attribute.fetchIndex = 1;
|
||||
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 224, "instance range includes records before the first instance");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2) == 112, "a first instance past the records still spans its fetch");
|
||||
|
|
@ -4959,6 +5338,7 @@ void validationTests() {
|
|||
attribute.resource.fields[1] = 0;
|
||||
attribute.resource.fields[2] = 16;
|
||||
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value");
|
||||
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value");
|
||||
attribute.resource.fields[2] = 8;
|
||||
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
|
||||
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range");
|
||||
|
|
@ -5034,6 +5414,14 @@ void validationTests() {
|
|||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12");
|
||||
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true);
|
||||
}
|
||||
{
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.spirv = makeModule({.layerOutput = true});
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
||||
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "VK_EXT_shader_viewport_index_layer");
|
||||
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true);
|
||||
}
|
||||
for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) {
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.spirv = makeModule({});
|
||||
|
|
@ -5389,6 +5777,21 @@ int main(int argc, char** argv) {
|
|||
std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n';
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") {
|
||||
addressSpaceTeardownTests();
|
||||
std::cout << "address-space device teardown releases cached guest allocations\n";
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
|
||||
auto pipelineUse = synchronization.LockForPipelineUse();
|
||||
bool snapshotAcquired = false;
|
||||
std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); });
|
||||
snapshot.join();
|
||||
Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation");
|
||||
pipelineUse.unlock();
|
||||
Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished");
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") {
|
||||
descriptorLayoutFlagTests();
|
||||
std::cout << "Descriptor layout native and captured binding flags passed\n";
|
||||
|
|
@ -5399,6 +5802,11 @@ int main(int argc, char** argv) {
|
|||
std::cerr << "descriptor pool reset failure did not terminate\n";
|
||||
return 1;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") {
|
||||
multisampleTests();
|
||||
std::cout << "Covered-centroid and multisample state checks passed\n";
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
const AgcDriver::Graphics::Context context{};
|
||||
const AgcDriver::Graphics::State state{};
|
||||
|
|
@ -5445,6 +5853,7 @@ int main(int argc, char** argv) {
|
|||
unorm10_11_11TargetTests();
|
||||
DepthBoundsBiasTests();
|
||||
conservativeZExportTests();
|
||||
earlyFragmentTestsTests();
|
||||
orderedPixelShaderTests();
|
||||
stencilRefExportTests();
|
||||
aliasedBufferViewTests();
|
||||
|
|
@ -5455,6 +5864,7 @@ int main(int argc, char** argv) {
|
|||
DisabledColorTests();
|
||||
OpaqueDestinationAlphaTests();
|
||||
Color1555TargetTests();
|
||||
BlendBypassTests();
|
||||
CompactedExportTests();
|
||||
ReversedComponentOrderTests();
|
||||
ShadersWriteMemoryTests();
|
||||
|
|
@ -5466,8 +5876,11 @@ int main(int argc, char** argv) {
|
|||
multisampleTests();
|
||||
MsaaEnableSingleSampleTests();
|
||||
clipDistanceTests();
|
||||
framebufferLayerTests();
|
||||
UnwrittenSampleLocationTests();
|
||||
ShaderStageTests();
|
||||
NggVertexWorkgroupTests();
|
||||
inactiveVertexPointSizeTests();
|
||||
TuningFieldTests();
|
||||
PixelInputLayoutTests();
|
||||
UnexportedPixelInputTests();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
#include "IntermediateRepresentation/IrBuilder.hpp"
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "Optimization/DeadCodeEliminator.hpp"
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
IrProgram program;
|
||||
auto& block = program.CreateBlock();
|
||||
program.SetEntryBlock(block);
|
||||
program.BlockOrder().push_back(&block);
|
||||
IrBuilder builder(program);
|
||||
builder.SetInsertionPoint(block);
|
||||
auto& value = builder.Constant(42u);
|
||||
IrSsaValues<NumScalarRegs> table;
|
||||
const auto& readTable = table;
|
||||
if (readTable[0] != nullptr || table.HasStorage()) throw std::runtime_error("an empty SSA table allocated storage during a read");
|
||||
for (IrValue* entry : readTable) if (entry != nullptr) throw std::runtime_error("an empty SSA table contained a value");
|
||||
if (table.HasStorage()) throw std::runtime_error("iterating an empty SSA table allocated storage");
|
||||
table[3] = &value;
|
||||
if (!table.HasStorage() || readTable[3] != &value || readTable[2] != nullptr) throw std::runtime_error("an SSA write lost zero initialization or its value");
|
||||
table.fill(nullptr);
|
||||
if (table.HasStorage() || readTable[3] != nullptr) throw std::runtime_error("clearing an SSA table retained construction storage");
|
||||
const auto id = value.Id();
|
||||
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&value}));
|
||||
auto& identity = program.CreateValue(IrOpcode::Identity, IrType::U32);
|
||||
identity.AddArgument(&value);
|
||||
block.AppendInstruction(&identity);
|
||||
DeadCodeEliminator{}.RemoveIdentities(program);
|
||||
auto& first = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
|
||||
auto& second = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
|
||||
first.AddArgument(&second);
|
||||
second.AddArgument(&first);
|
||||
std::vector<IrValue*> pinned;
|
||||
for (std::uint32_t number = 100u; number < 112u; ++number) pinned.push_back(&builder.Constant(number));
|
||||
block.ssaScalarValues[0] = pinned[0];
|
||||
block.ssaVectorValues[0] = pinned[1];
|
||||
block.ssaThreadBitScalarValues[0] = pinned[2];
|
||||
block.ssaScalarMaskTags[0] = pinned[3];
|
||||
program.Metadata().dynamicReads.push_back(pinned[4]);
|
||||
program.Metadata().blockInfo.emplace_back();
|
||||
program.Metadata().blockInfo.back().condition = pinned[5];
|
||||
program.Metadata().blockInfo.back().indirectTarget = pinned[6];
|
||||
auto& resources = program.Resources();
|
||||
resources.descriptorSources.emplace_back();
|
||||
resources.descriptorSources.back().dwords[0] = pinned[7];
|
||||
resources.srtReads.push_back({pinned[8], 0u});
|
||||
resources.controlFlow.emplace_back();
|
||||
resources.controlFlow.back().condition = pinned[9];
|
||||
resources.uniformFill.values[0] = pinned[10];
|
||||
auto external = std::make_unique<IrValue>(IrOpcode::Identity, IrType::U32, value.Id());
|
||||
external->AddArgument(pinned[11]);
|
||||
resources.valueStorage.push_back(std::move(external));
|
||||
const auto removed = program.ReclaimUnusedValues();
|
||||
if (removed != 3u) throw std::runtime_error("detached identity and cyclic garbage were not reclaimed");
|
||||
if (value.Id() != id || value.ImmediateU32() != 42u || value.Uses().size() != 1u) throw std::runtime_error("a live instruction lost its value or retained an unused backlink");
|
||||
for (std::size_t index = 0; index < pinned.size(); ++index) {
|
||||
if (pinned[index]->ImmediateU32() != 100u + index) throw std::runtime_error("a metadata or resource-owned value was reclaimed");
|
||||
}
|
||||
if (program.ReclaimUnusedValues() != 0u) throw std::runtime_error("reclaiming an unchanged graph was not idempotent");
|
||||
auto& unused = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
|
||||
IrValue untracked(IrOpcode::Identity, IrType::U32, value.Id());
|
||||
untracked.AddArgument(&unused);
|
||||
bool rejected = false;
|
||||
try {
|
||||
static_cast<void>(program.ReclaimUnusedValues());
|
||||
} catch (const std::runtime_error&) {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) throw std::runtime_error("an untracked external owner was accepted");
|
||||
untracked.ReplaceArgument(0, nullptr);
|
||||
program.Metadata().dynamicReads.clear();
|
||||
if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection");
|
||||
if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID");
|
||||
IrProgram moved = std::move(program);
|
||||
if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner");
|
||||
IrProgram assigned;
|
||||
static_cast<void>(assigned.CreateValue(IrOpcode::IAdd32, IrType::U32));
|
||||
assigned = std::move(moved);
|
||||
if (value.Id() != id || value.ImmediateU32() != 42u || assigned.ReclaimUnusedValues() != 0u) throw std::runtime_error("move assignment invalidated a live allocation owner");
|
||||
if (assigned.CreateValue(IrOpcode::IAdd32, IrType::U32).Id() <= pinned.back()->Id()) throw std::runtime_error("a transferred allocation owner reused a value ID");
|
||||
std::puts("IR reclamation ownership tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <new>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -46,6 +47,13 @@ alignas(256) constexpr auto Wave32GeometryCode = [] {
|
|||
return code;
|
||||
}();
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 25> FastLaunchCode{
|
||||
0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010,
|
||||
0x80020601, 0xb07c1003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302,
|
||||
0xf800020f, 0x09080706, 0x7d840080, 0xbefe036a, 0x7e1402ff, 0x00200400, 0xf8000941, 0x0000000a,
|
||||
0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> PixelCode{
|
||||
0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
|
@ -132,6 +140,8 @@ struct MeshDraw {
|
|||
bool restart = false;
|
||||
std::uint32_t waveSize = 64u;
|
||||
VkCullModeFlags cull = VK_CULL_MODE_NONE;
|
||||
std::span<const std::uint32_t> code = {};
|
||||
std::uint32_t stagesRegister = 0x20u;
|
||||
};
|
||||
|
||||
bool ReadsNumWorkgroups(const std::vector<std::uint32_t>& words) {
|
||||
|
|
@ -160,7 +170,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
|
|||
const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw);
|
||||
std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
|
||||
std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8);
|
||||
const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode;
|
||||
const std::span<const std::uint32_t> code = !setup.code.empty() ? setup.code : setup.waveSize == 32u ? std::span<const std::uint32_t>(Wave32GeometryCode) : std::span<const std::uint32_t>(GeometryCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> geometryMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span(code))}}};
|
||||
ShaderRecompiler::RecompileRequest geometry{
|
||||
{ShaderStage::Mesh, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
|
|
@ -194,7 +204,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
|
|||
}};
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, setup.waveSize, 64, setup.mesh, std::nullopt};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt};
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
|
|
@ -215,6 +225,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
|
|||
|
||||
constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u};
|
||||
constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u};
|
||||
constexpr ShaderRecompiler::MeshConfiguration FastLaunchSubgroup{1u, 1u, 1u, 3u, 1u, 32u, 128u, 0u, 4u};
|
||||
|
||||
std::size_t FanPixel(float angle, float radius) {
|
||||
const auto x = static_cast<std::uint32_t>((radius * std::cos(angle) + 1.0f) * 0.5f * Width);
|
||||
|
|
@ -344,6 +355,24 @@ int main() {
|
|||
CheckTriangles("non-indexed wave32 triangle list");
|
||||
}
|
||||
|
||||
ClearPixels();
|
||||
DrawMesh(device, {FastLaunchSubgroup, {0, Triangles, 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
|
||||
CheckTriangles("fast-launch subgroups");
|
||||
ClearPixels();
|
||||
DrawMesh(device, {FastLaunchSubgroup, {0, Triangles / 2u, 0, 1, 0, false, Triangles / 4u}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
|
||||
for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) {
|
||||
const bool drawn = triangle >= Triangles / 4u && triangle < Triangles / 4u + Triangles / 2u;
|
||||
Require(PixelIs(CellPixel(triangle), drawn ? TriangleColor(triangle) : Background), "fast-launch subgroups from a start vertex: triangle " + std::to_string(triangle) + " pixel " + PixelText(CellPixel(triangle)));
|
||||
}
|
||||
bool fastLaunchIndexedRefused = false;
|
||||
try {
|
||||
DrawMesh(device, {FastLaunchSubgroup, {reinterpret_cast<std::uintptr_t>(Indices.data()), Triangles, 2, 1, 0, true}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
|
||||
} catch (const std::runtime_error& error) {
|
||||
fastLaunchIndexedRefused = std::string(error.what()).find("indexed fast-launch draws are unsupported") != std::string::npos;
|
||||
if (!fastLaunchIndexedRefused) throw;
|
||||
}
|
||||
Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn");
|
||||
|
||||
constexpr std::size_t recordBlockBytes = 65536;
|
||||
auto* record = static_cast<std::uint32_t*>(::operator new(recordBlockBytes, std::align_val_t{65536}));
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
|
@ -50,8 +51,10 @@ std::uint32_t low(const void* pointer) { return static_cast<std::uint32_t>(reint
|
|||
std::uint32_t high(const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); }
|
||||
|
||||
void execute(AgcDriver::QueueState& state, const std::vector<std::uint32_t>& packet) {
|
||||
AgcDriver::DriverDetail::RegisterStateKey(state);
|
||||
AgcDriver::Pm4::Validate(packet, 0);
|
||||
AgcDriver::Pm4::Execute(packet, state);
|
||||
check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint");
|
||||
}
|
||||
|
||||
void testCatalog() {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,13 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "ResidentPresent.hpp"
|
||||
#include "SampleLod_spv.h"
|
||||
#include "BufferUpdate_spv.h"
|
||||
#include "SampleArray_spv.h"
|
||||
#include "Cover_vert_spv.h"
|
||||
#include "Cover_frag_spv.h"
|
||||
#include "VertexRange_vert_spv.h"
|
||||
#include "VertexRangeProducer_frag_spv.h"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
|
||||
#include <SDL_loadso.h>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
|
|
@ -306,6 +310,240 @@ private:
|
|||
|
||||
using Kind = Recorder::ReadKind;
|
||||
|
||||
void copiedDirectOrderingTests(const Device& device, Recorder& recorder) {
|
||||
auto context = device.GetContext();
|
||||
context.nullDescriptors = device.NullDescriptors();
|
||||
if (context.hostImportAlignment == 0 || !context.nullDescriptors || !AgcDriver::GuestMemory::WriteWatched()) {
|
||||
std::cout << "Host imports, null descriptors or write watching unavailable: copied/direct ordering test skipped\n";
|
||||
return;
|
||||
}
|
||||
recorder.Sync();
|
||||
const auto bytes = static_cast<std::size_t>(std::max<VkDeviceSize>(131072, context.hostImportAlignment));
|
||||
auto* block = static_cast<std::uint32_t*>(AllocateWatched(bytes, bytes));
|
||||
Require(block != nullptr, "allocate copied/direct ordering memory");
|
||||
const auto address = reinterpret_cast<std::uint64_t>(block);
|
||||
std::vector<std::shared_ptr<ShaderResources>> writers;
|
||||
context.copiedWriters = &writers;
|
||||
struct Release {
|
||||
const Context& context;
|
||||
Recorder& recorder;
|
||||
std::uint32_t* block;
|
||||
std::size_t bytes;
|
||||
std::vector<std::shared_ptr<ShaderResources>>& writers;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkShaderModule module = VK_NULL_HANDLE;
|
||||
std::shared_ptr<ShaderResources> layoutOwner;
|
||||
~Release() {
|
||||
recorder.Sync();
|
||||
writers.clear();
|
||||
DrawCopiedWriters()->clear();
|
||||
if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
|
||||
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
|
||||
if (module) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(block);
|
||||
}
|
||||
static_cast<void>(HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes));
|
||||
recorder.Sync();
|
||||
ReleaseWatched(block, bytes);
|
||||
}
|
||||
} release{context, recorder, block, bytes, writers};
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(block, bytes, true, true, true);
|
||||
}
|
||||
std::memset(block, 0, bytes);
|
||||
Require(HostImportFor(context, address, bytes) != nullptr, "import copied/direct ordering memory");
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
ShaderRecompiler::DescriptorBinding binding;
|
||||
binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
|
||||
binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
|
||||
binding.descriptorSet = 0;
|
||||
binding.binding = 0;
|
||||
binding.count = 1;
|
||||
binding.guestDescriptor = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, 1024, 0x31000000u};
|
||||
binding.bufferWritten = {true};
|
||||
program.bindings.push_back(binding);
|
||||
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
||||
auto direct = std::make_shared<ShaderResources>(context, compute);
|
||||
release.layoutOwner = direct;
|
||||
Require(!direct->HasCopiedWrites(), "ordering consumer did not bind directly");
|
||||
const auto setLayout = direct->Layout();
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(std::uint32_t)};
|
||||
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
layoutInfo.setLayoutCount = 1;
|
||||
layoutInfo.pSetLayouts = &setLayout;
|
||||
layoutInfo.pushConstantRangeCount = 1;
|
||||
layoutInfo.pPushConstantRanges = &push;
|
||||
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &release.layout), "ordering pipeline layout");
|
||||
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
moduleInfo.codeSize = sizeof(BUFFER_UPDATE_SPV);
|
||||
moduleInfo.pCode = BUFFER_UPDATE_SPV;
|
||||
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &release.module), "ordering shader module");
|
||||
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, release.module, "main", nullptr};
|
||||
pipelineInfo.layout = release.layout;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &release.pipeline), "ordering pipeline");
|
||||
const auto update = [&](const std::shared_ptr<ShaderResources>& resources, std::uint32_t delta, std::uint32_t count = 1) {
|
||||
const auto commands = recorder.Commands();
|
||||
recorder.Keep(resources);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.pipeline);
|
||||
resources->Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.layout);
|
||||
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, release.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(delta), &delta);
|
||||
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, count, 1, 1);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_READ_BIT);
|
||||
resources->MarkGpuWrites(recorder);
|
||||
};
|
||||
for (int mode = 0; mode < 3; ++mode) {
|
||||
*block = 10;
|
||||
auto copiedContext = context;
|
||||
copiedContext.hostImportAlignment = 0;
|
||||
copiedContext.dmaBufImport = false;
|
||||
auto copied = std::make_shared<ShaderResources>(copiedContext, compute);
|
||||
Require(copied->HasCopiedWrites(), "ordering producer did not take the copied path");
|
||||
auto& pending = mode == 2 ? *DrawCopiedWriters() : writers;
|
||||
pending.push_back(copied);
|
||||
update(copied, 1);
|
||||
recorder.OnComplete([copied, &pending] {
|
||||
copied->WriteBackBuffers();
|
||||
std::erase(pending, copied);
|
||||
});
|
||||
Require(!recorder.Idle() && *block == 10, "copied producer was completed before the consumer test");
|
||||
if (mode == 0) direct = std::make_shared<ShaderResources>(context, compute);
|
||||
else Require(direct->Revalidate(std::span(&compute, 1)), "the imported template could not be revalidated");
|
||||
update(direct, 2);
|
||||
recorder.Sync();
|
||||
Require(*block == 13, mode == 0 ? "fresh direct consumer ran before copied write-back" : "reused direct consumer ran before copied write-back");
|
||||
}
|
||||
*block = 20;
|
||||
block[1024] = 30;
|
||||
auto copiedContext = context;
|
||||
copiedContext.hostImportAlignment = 0;
|
||||
copiedContext.dmaBufImport = false;
|
||||
auto copied = std::make_shared<ShaderResources>(copiedContext, compute);
|
||||
writers.push_back(copied);
|
||||
update(copied, 1);
|
||||
recorder.OnComplete([copied, &writers] {
|
||||
copied->WriteBackBuffers();
|
||||
std::erase(writers, copied);
|
||||
});
|
||||
auto separateProgram = program;
|
||||
separateProgram.bindings[0].guestDescriptor[0] += 4096;
|
||||
const CompiledShader separateCompute{ShaderRecompiler::ShaderStage::Compute, &separateProgram, 0};
|
||||
auto separate = std::make_shared<ShaderResources>(context, separateCompute);
|
||||
Require(!writers.empty() && *block == 20, "a disjoint consumer waited for copied writes");
|
||||
update(separate, 2);
|
||||
recorder.Sync();
|
||||
Require(*block == 21 && block[1024] == 32, "disjoint copied and direct writes changed each other");
|
||||
const std::uint32_t label = 40;
|
||||
static_cast<void>(recorder.Commands());
|
||||
recorder.AfterCompletions(address, std::as_bytes(std::span(&label, 1)), 1000, 0, false);
|
||||
Require(direct->Revalidate(std::span(&compute, 1)), "template reuse after a CPU completion label failed");
|
||||
update(direct, 2);
|
||||
recorder.Sync();
|
||||
Require(*block == 42, "direct consumer preceded an overlapping CPU completion label");
|
||||
update(direct, 1);
|
||||
const auto submissions = recorder.Submissions();
|
||||
Require(direct->Revalidate(std::span(&compute, 1)) && recorder.Submissions() == submissions, "ordinary direct work incurred a CPU write-back wait");
|
||||
update(direct, 2);
|
||||
recorder.Sync();
|
||||
Require(*block == 45, "direct consumers lost GPU-ordered updates");
|
||||
{
|
||||
TextureDetiler detiler(context);
|
||||
auto imageContext = context;
|
||||
imageContext.detiler = &detiler;
|
||||
TextureCache textureCache(imageContext);
|
||||
imageContext.textureCache = &textureCache;
|
||||
Buffer readback(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
struct Clear {
|
||||
const Context& context;
|
||||
Recorder& recorder;
|
||||
~Clear() { recorder.Sync(); ClearCachedTextures(context.device); }
|
||||
} clear{context, recorder};
|
||||
ShaderRecompiler::RecompileResult imageProgram;
|
||||
ShaderRecompiler::DescriptorBinding imageBinding;
|
||||
imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage;
|
||||
imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages;
|
||||
imageBinding.descriptorSet = 0;
|
||||
imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding;
|
||||
imageBinding.count = 1;
|
||||
imageBinding.imageSamplers = {0};
|
||||
imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
|
||||
imageBinding.guestDescriptor = {static_cast<std::uint32_t>(address >> 8), static_cast<std::uint32_t>(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0};
|
||||
imageProgram.bindings.push_back(imageBinding);
|
||||
const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0};
|
||||
auto sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
|
||||
recorder.Sync();
|
||||
for (int reuse = 0; reuse < 2; ++reuse) {
|
||||
auto producer = std::make_shared<ShaderResources>(copiedContext, compute);
|
||||
writers.push_back(producer);
|
||||
update(producer, 1);
|
||||
recorder.OnComplete([producer, &writers] {
|
||||
producer->WriteBackBuffers();
|
||||
std::erase(writers, producer);
|
||||
});
|
||||
if (reuse == 0) sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
|
||||
else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "sampled image template did not revalidate");
|
||||
const auto images = sampled->StorageImages();
|
||||
Require(images.size() == 1, "sampled image did not use imported texture storage");
|
||||
const auto commands = recorder.Commands();
|
||||
recorder.Keep(sampled);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {1, 1, 1};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
recorder.Sync();
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, readback.Bytes().data(), sizeof(value));
|
||||
Require(value == static_cast<std::uint32_t>(46 + reuse), "cached sampled image preceded a copied producer");
|
||||
}
|
||||
sampled.reset();
|
||||
ClearCachedTextures(context.device);
|
||||
const auto keys = address + 65536;
|
||||
const auto keyBytes = DccKeyBytes(65536);
|
||||
imageProgram.bindings[0].guestDescriptor[6] = (1u << 21) | (static_cast<std::uint32_t>((keys >> 8) & 0xffu) << 24);
|
||||
imageProgram.bindings[0].guestDescriptor[7] = static_cast<std::uint32_t>(keys >> 16);
|
||||
auto keyProgram = program;
|
||||
keyProgram.bindings[0].guestDescriptor = {static_cast<std::uint32_t>(keys), static_cast<std::uint32_t>(keys >> 32u) & 0xffffu, static_cast<std::uint32_t>(keyBytes), 0x31000000u};
|
||||
const CompiledShader keyCompute{ShaderRecompiler::ShaderStage::Compute, &keyProgram, 0};
|
||||
for (int reuse = 0; reuse < 2; ++reuse) {
|
||||
std::memset(reinterpret_cast<void*>(keys), 0, keyBytes);
|
||||
AgcDriver::GuestMemory::MarkWritten(keys, keyBytes);
|
||||
sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
|
||||
recorder.Sync();
|
||||
auto producer = std::make_shared<ShaderResources>(copiedContext, keyCompute);
|
||||
writers.push_back(producer);
|
||||
update(producer, 0x40404040, static_cast<std::uint32_t>(keyBytes / 4));
|
||||
recorder.OnComplete([producer, &writers] {
|
||||
producer->WriteBackBuffers();
|
||||
std::erase(writers, producer);
|
||||
});
|
||||
if (reuse == 0) sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
|
||||
else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "metadata-only producer invalidated the sampled template");
|
||||
const auto images = sampled->StorageImages();
|
||||
Require(images.size() == 1, "metadata texture is not resident");
|
||||
const auto commands = recorder.Commands();
|
||||
recorder.Keep(sampled);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {1, 1, 1};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
recorder.Sync();
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, readback.Bytes().data(), sizeof(value));
|
||||
Require(value == 0xff000000, "cached sampled image ignored copied compression-metadata writes");
|
||||
}
|
||||
}
|
||||
std::cout << "Copied producers precede fresh and reused direct consumers\n";
|
||||
}
|
||||
|
||||
void readTrackingTests(const Device& device, Recorder& recorder) {
|
||||
Require(Recorder::ReadTracking(), "read tracking is off (APS5_COPY_READ_TRACKING=0 set?)");
|
||||
Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x10000, 16), "a fresh recorder reports a read");
|
||||
|
|
@ -1072,6 +1310,41 @@ void hostImportRetirementTests(const Device& device) {
|
|||
Require(preservedEveryRecorder, "unlocked mapping changed recorder batches");
|
||||
std::cout << "Host import retirement locking and GPU readback tests passed\n";
|
||||
}
|
||||
#ifdef _WIN32
|
||||
void secondMappingImportTests(const Device& device) {
|
||||
const auto& context = device.GetContext();
|
||||
if (context.hostImportAlignment == 0) {
|
||||
std::cout << "host imports unavailable: imports after a second mapping not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::size_t bytes = 65536;
|
||||
constexpr std::size_t page = 16384;
|
||||
struct Release {
|
||||
void* mapping = nullptr;
|
||||
void* alias = nullptr;
|
||||
std::int64_t physical = -1;
|
||||
~Release() {
|
||||
if (alias != nullptr) sceKernelMunmap(alias, page);
|
||||
if (mapping != nullptr) sceKernelMunmap(mapping, bytes);
|
||||
if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes);
|
||||
}
|
||||
} release;
|
||||
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing");
|
||||
Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing");
|
||||
const auto address = reinterpret_cast<std::uint64_t>(release.mapping);
|
||||
if (HostImportFor(context, address, bytes) == nullptr) {
|
||||
std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n";
|
||||
return;
|
||||
}
|
||||
const auto first = HostImportSerial(context, address, bytes, true);
|
||||
Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import");
|
||||
Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time");
|
||||
Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page");
|
||||
const auto second = HostImportSerial(context, address, bytes, true);
|
||||
Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept");
|
||||
HostImportFor(context, address, bytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
void movedMetadataTests(const Device& device, Recorder& recorder) {
|
||||
const auto& base = device.GetContext();
|
||||
|
|
@ -1996,6 +2269,19 @@ void drawSnapshotCapTests(const Device& device, Recorder& recorder) {
|
|||
Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write");
|
||||
std::memset(block, 0x11, bytes);
|
||||
snapshotRecorder.Sync();
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "64") == 0, "cannot raise snapshot cap");
|
||||
#else
|
||||
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "64", 1) == 0, "cannot raise snapshot cap");
|
||||
#endif
|
||||
const auto again = resources.PrepareDrawBindings(snapshotRecorder);
|
||||
Require(again != nullptr && again->snapshots.size() == 1 && again->snapshots[0].address == smallAddress, "built resources read the draw snapshot cap again instead of keeping the one they were built with");
|
||||
snapshotRecorder.Sync();
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot restore snapshot cap");
|
||||
#else
|
||||
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot restore snapshot cap");
|
||||
#endif
|
||||
}
|
||||
const auto firstAddress = address + 4096;
|
||||
constexpr std::size_t firstBytes = 600 * 1024;
|
||||
|
|
@ -4817,6 +5103,12 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
|||
context.nullDescriptors = true;
|
||||
TextureCache cache(context);
|
||||
context.textureCache = &cache;
|
||||
const auto clearTargets = [&] {
|
||||
recorder.Submit();
|
||||
device.WaitQueue();
|
||||
recorder.Sync();
|
||||
ClearMultisampleTargets(context.device);
|
||||
};
|
||||
const auto refused = [](const std::function<void()>& access, const std::string& expected, const std::string& what) {
|
||||
std::string message;
|
||||
try {
|
||||
|
|
@ -4873,7 +5165,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
|||
}, "a texture read", "a texture read");
|
||||
refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane");
|
||||
AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels);
|
||||
ClearMultisampleTargets(context.device);
|
||||
clearTargets();
|
||||
target.tileMode = ColorTileMode::RenderTarget;
|
||||
{
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
|
|
@ -4884,7 +5176,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
|||
}
|
||||
refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target");
|
||||
AgcDriver::GuestMemory::Read(address + targetBytes, texels);
|
||||
ClearMultisampleTargets(context.device);
|
||||
clearTargets();
|
||||
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) {
|
||||
target.extent = {128, 64};
|
||||
target.surfaceExtent = target.extent;
|
||||
|
|
@ -4898,7 +5190,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
|||
}
|
||||
refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding");
|
||||
AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels);
|
||||
ClearMultisampleTargets(context.device);
|
||||
clearTargets();
|
||||
}
|
||||
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) {
|
||||
target.extent = {64, 32};
|
||||
|
|
@ -4913,9 +5205,48 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
|||
}
|
||||
refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding");
|
||||
AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels);
|
||||
ClearMultisampleTargets(context.device);
|
||||
clearTargets();
|
||||
}
|
||||
AgcDriver::GuestMemory::Read(address + 256u, texels);
|
||||
target.extent = {side, side};
|
||||
target.surfaceExtent = target.extent;
|
||||
target.samples = 4;
|
||||
VkImageView previous = VK_NULL_HANDLE;
|
||||
for (const bool removeAllocation : {false, true}) {
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
{
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
view = MultisampleTargetView(context, target);
|
||||
Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image");
|
||||
recorder.Submit();
|
||||
device.WaitQueue();
|
||||
recorder.Sync();
|
||||
}
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Protect(block, bytes, true, true, true, [] {});
|
||||
}
|
||||
refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed");
|
||||
bool failed = false;
|
||||
try {
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); });
|
||||
} catch (const std::runtime_error&) {
|
||||
failed = true;
|
||||
}
|
||||
Require(failed, "the failed host unmap did not throw");
|
||||
refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
if (removeAllocation) mutation.Remove(block);
|
||||
else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {});
|
||||
mutation.Add(block, bytes, true, true, true);
|
||||
}
|
||||
RequireNoMultisampleTarget(address, bytes, "a texture read");
|
||||
AgcDriver::GuestMemory::Read(address + 256u, texels);
|
||||
previous = view;
|
||||
}
|
||||
clearTargets();
|
||||
std::cout << "Multisampled target guest access tests passed\n";
|
||||
}
|
||||
|
||||
|
|
@ -5284,6 +5615,165 @@ void atomicViewTests(const Device& device, Recorder& recorder) {
|
|||
}
|
||||
}
|
||||
|
||||
bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool imported) {
|
||||
if (!device.Graphics() || (imported && device.GetContext().hostImportAlignment == 0)) return false;
|
||||
constexpr std::size_t unit = 65536;
|
||||
constexpr std::size_t bytes = unit * 3;
|
||||
void* watched = AllocateWatched(bytes, unit);
|
||||
std::vector<std::byte> fallback(watched == nullptr ? bytes + unit : 0);
|
||||
auto* block = watched != nullptr ? static_cast<std::byte*>(watched) : reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(fallback.data()) + unit - 1) & ~(unit - 1));
|
||||
struct Release {
|
||||
void* watched;
|
||||
~Release() { if (watched != nullptr) ReleaseWatched(watched, bytes); }
|
||||
} release{watched};
|
||||
std::fill_n(block, bytes, std::byte{0});
|
||||
const auto address = reinterpret_cast<std::uint64_t>(block);
|
||||
auto context = device.GetContext();
|
||||
context.recorder = &recorder;
|
||||
if (!imported) {
|
||||
context.hostImportAlignment = 0;
|
||||
context.dmaBufImport = false;
|
||||
}
|
||||
TextureDetiler detiler(context);
|
||||
context.detiler = &detiler;
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(block, bytes, true, true, true);
|
||||
}
|
||||
struct Cleanup {
|
||||
const Context& context;
|
||||
Recorder& recorder;
|
||||
void* block;
|
||||
~Cleanup() {
|
||||
recorder.Sync();
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
SharedResourceCache().Clear();
|
||||
ClearCachedTextures(context.device);
|
||||
recorder.Sync();
|
||||
ClearCachedPipelines(context.device);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(block);
|
||||
}
|
||||
HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes);
|
||||
}
|
||||
} cleanup{context, recorder, block};
|
||||
if (imported && HostImportFor(context, address, bytes) == nullptr) return false;
|
||||
const auto shader = [](auto& words, std::uint64_t variant) {
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
program.spirv = std::vector<std::uint32_t>(std::begin(words), std::end(words));
|
||||
program.variantId = variant;
|
||||
return program;
|
||||
};
|
||||
auto producerVertex = shader(COVER_VERT_SPV, 101);
|
||||
auto producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102);
|
||||
auto vertex = shader(VERTEXRANGE_VERT_SPV, 103);
|
||||
auto pixel = shader(COVER_FRAG_SPV, 104);
|
||||
const std::array producerShaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &producerVertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &producerPixel, 0}};
|
||||
const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}};
|
||||
const auto stateFor = [](std::uint64_t base, VkFormat format, std::uint32_t elementBytes) {
|
||||
State state{};
|
||||
state.stages.path = ShaderPath::Vertex;
|
||||
state.color = {base, {64, 64}, format, unit, 0xe4u, ColorTileMode::RenderTarget, elementBytes};
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {64, 64};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, 64, 64, -64, 0, 1};
|
||||
state.scissor = {{0, 0}, {64, 64}};
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends = {state.blend};
|
||||
return state;
|
||||
};
|
||||
const auto producer = stateFor(address + unit, VK_FORMAT_R32G32B32A32_SFLOAT, 16);
|
||||
const auto consumer = stateFor(address, VK_FORMAT_R8G8B8A8_UNORM, 4);
|
||||
const AgcDriver::Pm4::DrawParameters triangle{0, 3, 0, 1, 0, false};
|
||||
const auto produce = [&] {
|
||||
Draw(context, producer, triangle, producerShaders);
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
const std::array range{std::pair{address + unit, address + 2 * unit}};
|
||||
Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending");
|
||||
};
|
||||
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) {
|
||||
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), 77u << 12u}}, fetchIndex};
|
||||
};
|
||||
const auto check = [&](std::uint32_t edge) {
|
||||
std::vector<std::byte> texels(64 * 64 * 4);
|
||||
ReadColorTarget(consumer.color, texels);
|
||||
for (std::size_t y = 0; y < 64; ++y) {
|
||||
for (std::size_t x = 0; x < 64; ++x) {
|
||||
const auto expected = x < edge ? std::byte{0} : std::byte{255};
|
||||
for (std::size_t channel = 0; channel < 4; ++channel) Require(texels[(y * 64 + x) * 4 + channel] == expected, "the draw did not consume the GPU-produced vertex offset");
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto reject = [&](auto action, const char* expected) {
|
||||
try { action(); }
|
||||
catch (const std::runtime_error& error) {
|
||||
Require(std::string(error.what()).find(expected) != std::string::npos, error.what());
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("an actual render-target alias was accepted");
|
||||
};
|
||||
for (const std::uint32_t fetchIndex : {0u, 1u}) {
|
||||
producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102);
|
||||
vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)};
|
||||
auto draw = triangle;
|
||||
if (fetchIndex == 0) draw.firstVertex = unit / 16;
|
||||
else draw.firstInstance = unit / 16;
|
||||
produce();
|
||||
std::shared_ptr<const AgcDriver::DrawRecipe> recipe;
|
||||
Draw(context, consumer, draw, shaders, {}, &recipe);
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
const auto read = recorder.DescribePendingRead(address + unit, 16);
|
||||
Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput), "the vertex regression used the wrong copied/imported path");
|
||||
check(16);
|
||||
Require(recipe != nullptr, "the range regression did not create a draw recipe");
|
||||
std::vector<std::byte> cleared(unit);
|
||||
AgcDriver::GuestMemory::Write(address, cleared);
|
||||
for (std::size_t offset = 5; offset < producerPixel.spirv.size();) {
|
||||
const auto count = producerPixel.spirv[offset] >> 16u;
|
||||
Require(count != 0, "invalid producer SPIR-V");
|
||||
if ((producerPixel.spirv[offset] & 0xffffu) == 43u && count == 4 && producerPixel.spirv[offset + 3] == 0x3f000000u) producerPixel.spirv[offset + 3] = 0x3f400000u;
|
||||
offset += count;
|
||||
}
|
||||
producerPixel.variantId = 105;
|
||||
produce();
|
||||
const auto hit = DrawWithRecipe(context, consumer, draw, shaders, {}, *recipe);
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
Require(hit.recorded, "the vertex range regression did not reuse its draw recipe");
|
||||
check(24);
|
||||
vertex.vertexAttributes = {attributeFor(address + unit, 16, fetchIndex)};
|
||||
Draw(context, consumer, triangle, shaders);
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
check(24);
|
||||
vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)};
|
||||
auto indirect = draw;
|
||||
indirect.indirect.emplace();
|
||||
reject([&] { Draw(context, consumer, indirect, shaders); }, "vertex buffer aliases the render target");
|
||||
if (fetchIndex == 0) {
|
||||
const std::array<std::uint16_t, 3> indices{0, 1, 2};
|
||||
AgcDriver::GuestMemory::Write(address + 2 * unit, std::as_bytes(std::span(indices)));
|
||||
auto indexed = draw;
|
||||
indexed.indexed = true;
|
||||
indexed.indexSize = 2;
|
||||
indexed.indexAddress = address + 2 * unit;
|
||||
produce();
|
||||
Draw(context, consumer, indexed, shaders);
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
check(24);
|
||||
indexed.indexAddress = address;
|
||||
reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target");
|
||||
}
|
||||
reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target");
|
||||
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)};
|
||||
reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target");
|
||||
}
|
||||
std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void pipelineCacheTests(const Device& device, bool benchmark) {
|
||||
std::thread establishThreadedProcess([] {});
|
||||
establishThreadedProcess.join();
|
||||
|
|
@ -5716,6 +6206,148 @@ void pipelineLibraryTests(const Device& device) {
|
|||
std::cout << "Pipeline library reuse tests passed\n";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace StorageCpuSnapshotTest {
|
||||
constexpr std::uint32_t Side = 256;
|
||||
constexpr std::uint32_t Format32UInt = 20;
|
||||
constexpr std::uint32_t TileR64KBX = 0x1b;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u;
|
||||
constexpr std::size_t BlockBytes = 12 * SurfaceBytes;
|
||||
|
||||
class Block {
|
||||
public:
|
||||
explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) {
|
||||
if (watched) {
|
||||
memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536);
|
||||
const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr);
|
||||
Require(section != nullptr, "cannot allocate the shared texture section");
|
||||
try { GuestArena::GuestArenaMap_nid_postfix(memory, BlockBytes, section, 0, PAGE_READWRITE); }
|
||||
catch (...) { CloseHandle(section); throw; }
|
||||
CloseHandle(section);
|
||||
} else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
Require(memory != nullptr, "cannot allocate the texture block");
|
||||
std::memset(memory, 0x5a, BlockBytes);
|
||||
GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
~Block() {
|
||||
ClearCachedTextures(context.device);
|
||||
GuestAllocations::Mutation().Remove(memory);
|
||||
HostImportFor(context, Surface(0), BlockBytes);
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes);
|
||||
} else VirtualFree(memory, 0, MEM_RELEASE);
|
||||
}
|
||||
|
||||
std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast<std::uint64_t>(memory) + surface * SurfaceBytes; }
|
||||
|
||||
private:
|
||||
const Context& context;
|
||||
bool watched;
|
||||
void* memory = nullptr;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side) {
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u),
|
||||
((side - 1u) >> 2u) | ((side - 1u) << 14u),
|
||||
0xfacu | (TileR64KBX << 20u) | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) {
|
||||
const auto words = Descriptor(address, side);
|
||||
return DecodeTextureResource(words);
|
||||
}
|
||||
|
||||
void Run(Context context) {
|
||||
TextureDetiler detiler(context);
|
||||
context.detiler = &detiler;
|
||||
for (const bool watched : {true, false}) {
|
||||
Block block(context, watched);
|
||||
const auto address = block.Surface(0);
|
||||
Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking");
|
||||
Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import");
|
||||
Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking");
|
||||
auto image = StorageTexture::Create(context, *context.detiler, Resource(address), 0);
|
||||
const auto expectedBytes = watched ? 0u : SurfaceBytes;
|
||||
Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot");
|
||||
const auto readPixel = [&] {
|
||||
Buffer result(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
CommandBatch batch(context);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {1, 1, 1};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, result.Handle(), 1, ©);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
batch.SubmitAndWait();
|
||||
result.Invalidate();
|
||||
std::uint32_t pixel = 0;
|
||||
std::memcpy(&pixel, result.Bytes().data(), sizeof(pixel));
|
||||
return pixel;
|
||||
};
|
||||
Require(readPixel() == 0x5a5a5a5au, "snapshot upload returned incorrect GPU texels");
|
||||
std::memset(reinterpret_cast<void*>(address), 0x6b, SurfaceBytes);
|
||||
image->Refresh();
|
||||
Require(readPixel() == 0x6b6b6b6bu, "snapshot refresh missed a CPU write");
|
||||
Require(image->OriginalSnapshotBytes() == expectedBytes, "watched refresh allocated a CPU surface snapshot");
|
||||
{
|
||||
CommandBatch batch(context);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
VkClearColorValue clear{};
|
||||
clear.uint32[0] = 0x7c7c7c7cu;
|
||||
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
image->MarkDirty();
|
||||
image->WriteBack();
|
||||
std::uint32_t pixel = 0;
|
||||
AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1)));
|
||||
Require(pixel == 0x7c7c7c7cu, "snapshot write-back lost GPU texels");
|
||||
Require(image->OriginalSnapshotBytes() == expectedBytes, "GPU-direct write-back allocated a CPU surface snapshot");
|
||||
if (watched) {
|
||||
AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "snapshot fixture is still write-watched");
|
||||
std::memset(reinterpret_cast<void*>(address), 0x8d, SurfaceBytes);
|
||||
image->Refresh();
|
||||
Require(image->OriginalSnapshotBytes() == SurfaceBytes, "unwatched refresh did not allocate its comparison snapshot");
|
||||
Require(readPixel() == 0x8d8d8d8du, "unwatched refresh returned incorrect GPU texels");
|
||||
}
|
||||
image.reset();
|
||||
Context cpu = context;
|
||||
cpu.hostImportAlignment = 0;
|
||||
image = StorageTexture::Create(cpu, *context.detiler, Resource(address), 0);
|
||||
Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot");
|
||||
const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu;
|
||||
Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels");
|
||||
{
|
||||
CommandBatch batch(context);
|
||||
VkClearColorValue clear{};
|
||||
clear.uint32[0] = 0x9e9e9e9eu;
|
||||
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
image->MarkDirty();
|
||||
image->WriteBack();
|
||||
AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1)));
|
||||
Require(pixel == 0x9e9e9e9eu, "CPU write-back lost guest texels");
|
||||
std::printf("%s snapshot fixture: direct %zu bytes, CPU fallback %zu bytes\n", watched ? "watched" : "unwatched", expectedBytes, image->OriginalSnapshotBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
std::unique_ptr<Device> created;
|
||||
|
|
@ -5750,9 +6382,25 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
std::unique_lock gpu(GpuMutex());
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") {
|
||||
if (device.GetContext().hostImportAlignment == 0) {
|
||||
Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "storage snapshot test requires host imports");
|
||||
std::cout << "skipped, host imports unavailable\n";
|
||||
return 77;
|
||||
}
|
||||
StorageCpuSnapshotTest::Run(device.GetContext());
|
||||
std::cout << "Storage CPU snapshot tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n';
|
||||
Recorder recorder(device.GetContext());
|
||||
recorder.Activate();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") {
|
||||
copiedDirectOrderingTests(device, recorder);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) {
|
||||
storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only");
|
||||
return 0;
|
||||
|
|
@ -5832,6 +6480,16 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) {
|
||||
return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") {
|
||||
secondMappingImportTests(device);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
readTrackingTests(device, recorder);
|
||||
writeSettledTests(device, recorder);
|
||||
completionCountTests(device, recorder);
|
||||
|
|
@ -5862,6 +6520,9 @@ int main(int argc, char** argv) {
|
|||
storeRunTests(device, recorder);
|
||||
queuedWriteGroupTests();
|
||||
remappedImportTests(device);
|
||||
#ifdef _WIN32
|
||||
secondMappingImportTests(device);
|
||||
#endif
|
||||
movedMetadataTests(device, recorder);
|
||||
viewPastLastMipTests(device, recorder);
|
||||
keysFillTests(device, recorder);
|
||||
|
|
@ -5891,6 +6552,7 @@ int main(int argc, char** argv) {
|
|||
cmaskPassTests(device, recorder);
|
||||
pendingKeyStoreTests(device, recorder);
|
||||
sampleDumpTests(device, recorder);
|
||||
copiedDirectOrderingTests(device, recorder);
|
||||
gpu.unlock();
|
||||
hostImportUnmapTests(device, recorder);
|
||||
drawSnapshotPatchTests(device);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
|
||||
using namespace AgcDriver;
|
||||
using namespace AgcDriver::DriverDetail;
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWords = false) {
|
||||
std::uint64_t key = 0xcbf29ce484222325ull;
|
||||
const auto mix = [&](std::uint64_t value) {
|
||||
key ^= value;
|
||||
key *= 0x100000001b3ull;
|
||||
};
|
||||
const auto userEnd = [&](std::uint32_t base) {
|
||||
if (allUserWords) return base + 32u;
|
||||
const auto resources = queue.shader.find(base - 1);
|
||||
if (resources == queue.shader.end()) return base;
|
||||
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
|
||||
return base + std::min(count, 32u);
|
||||
};
|
||||
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
|
||||
const auto unread = [&](std::uint32_t offset) {
|
||||
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
|
||||
};
|
||||
for (const auto& range : Graphics::DrawKeyRegisters) {
|
||||
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
|
||||
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
|
||||
const auto end = range.first + range.count;
|
||||
const bool shader = range.bank == Graphics::RegisterBank::Shader;
|
||||
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
|
||||
if (shader && unread(it->first)) continue;
|
||||
mix(it->first);
|
||||
mix(it->second);
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
void testFingerprints() {
|
||||
static_assert(!std::is_convertible_v<Registers::Reference, std::uint32_t&>);
|
||||
std::mt19937 random(42);
|
||||
QueueState queue;
|
||||
auto reference = baselineRegisterStateKey(queue);
|
||||
auto tracked = RegisterStateKey(queue);
|
||||
const auto compare = [&] {
|
||||
const auto expected = baselineRegisterStateKey(queue);
|
||||
const auto actual = RegisterStateKey(queue);
|
||||
check(actual == RegisterStateKey(queue, false, false), "incremental fingerprint missed a mutation");
|
||||
check(RegisterStateKey(queue, true) == RegisterStateKey(queue, true, false), "all-user fingerprint missed a mutation");
|
||||
check((expected == reference) == (actual == tracked), "exact key changed its dependency set");
|
||||
reference = expected;
|
||||
tracked = actual;
|
||||
};
|
||||
for (const auto& range : Graphics::DrawKeyRegisters) {
|
||||
auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
|
||||
for (auto offset = range.first; offset < range.first + range.count; ++offset) {
|
||||
bank.erase(offset); compare();
|
||||
bank.emplace(offset, 0); compare();
|
||||
bank.insert_or_assign(offset, 0); compare();
|
||||
bank[offset] = random(); compare();
|
||||
bank.at(offset) ^= 1; compare();
|
||||
}
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 30000; ++i) {
|
||||
auto& bank = i % 3 == 0 ? queue.context : i % 3 == 1 ? queue.shader : queue.userConfig;
|
||||
const auto offset = random() % 0x500;
|
||||
switch (i % 11) {
|
||||
case 0: bank.erase(offset); break;
|
||||
case 1: bank.emplace(offset, random()); break;
|
||||
case 2: bank[offset] |= random(); break;
|
||||
case 3: bank[offset] &= random(); break;
|
||||
case 6: bank[offset] += random(); break;
|
||||
case 4: {
|
||||
auto retained = bank[offset];
|
||||
bank.insert_or_assign(0x1000, random());
|
||||
compare();
|
||||
retained = random();
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
auto copy = bank;
|
||||
copy.insert_or_assign(offset, random());
|
||||
compare();
|
||||
bank = copy;
|
||||
break;
|
||||
}
|
||||
default: bank.insert_or_assign(offset, random()); break;
|
||||
}
|
||||
compare();
|
||||
}
|
||||
queue.shader[0x8b] = 0; compare();
|
||||
queue.shader[0x8c] = random(); compare();
|
||||
queue.shader[0x8b] = 2; compare();
|
||||
queue.shader[0x8b] = 1u << 27u; compare();
|
||||
queue.shader.erase(0x8b); compare();
|
||||
queue.savedContext = queue.context;
|
||||
queue.ClearContext(); compare();
|
||||
queue.context = *queue.savedContext; compare();
|
||||
auto moved = std::move(queue.context);
|
||||
check(queue.context.empty(), "moved register bank retained its population");
|
||||
queue.context.emplace(0x200, 1); compare();
|
||||
queue.context = std::move(moved); compare();
|
||||
queue.context = queue.context; compare();
|
||||
queue.context.clear(); compare();
|
||||
queue.context.emplace(0x200, 0); compare();
|
||||
queue = QueueState{}; compare();
|
||||
Registers registers{{0, 7}, {64, 9}};
|
||||
auto low = std::make_shared<const Registers::Mask>(std::vector<std::uint64_t>{1});
|
||||
auto high = std::make_shared<const Registers::Mask>(std::vector<std::uint64_t>{0, 1});
|
||||
const auto lowHash = registers.Fingerprint(low);
|
||||
check(registers.Fingerprint(high) != lowHash && registers.Fingerprint(low) == lowHash, "projection switch reused another mask's hash");
|
||||
registers[64] = 8;
|
||||
check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testFingerprints();
|
||||
std::puts("Register fingerprint dependencies and mutations passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@
|
|||
#include "Optimization/DescriptorBindingBuilder.hpp"
|
||||
#include "ShaderCacheDirectory.hpp"
|
||||
#include "ControlFlow/RequestSerializer.hpp"
|
||||
#include "Optimization/RequestMemoryView.hpp"
|
||||
#include "Optimization/SrtWalker.hpp"
|
||||
#include "IntermediateRepresentation/IrBlock.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
|
|
@ -18,7 +21,10 @@
|
|||
#include <future>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
|
@ -247,6 +253,7 @@ CompiledVariant sampleVariant() {
|
|||
info.info.usesDma = true;
|
||||
info.info.usesFaultBuffer = true;
|
||||
info.info.rasterPrimitive = true;
|
||||
info.info.layerExported = true;
|
||||
info.bindings.pushDataStartDword = 2;
|
||||
info.bindings.memoryOffsetDword = 1;
|
||||
info.bindings.memoryOffsetCount = 5;
|
||||
|
|
@ -415,6 +422,7 @@ void verifyKeySensitivity() {
|
|||
changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; });
|
||||
changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; });
|
||||
changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; });
|
||||
changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; });
|
||||
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
|
||||
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
|
||||
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
|
||||
|
|
@ -1081,6 +1089,743 @@ void verifySpecializationLiveness() {
|
|||
require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable");
|
||||
}
|
||||
|
||||
constexpr std::uint64_t PlanSrtAddress = 0x7f0000010000ull;
|
||||
constexpr std::uint64_t PlanTableAddress = 0x7f0000020000ull;
|
||||
constexpr std::uint64_t PlanMaterialAddress = 0x7f0000030000ull;
|
||||
constexpr std::uint64_t PlanPayloadAddress = 0x7f0000040000ull;
|
||||
constexpr std::uint64_t PlanPointerAddress = 0x7f0000048000ull;
|
||||
constexpr std::uint64_t PlanOutputAddress = 0x7f0000050000ull;
|
||||
constexpr std::uint64_t PlanTextureAddress = 0x7f0000060000ull;
|
||||
constexpr std::uint64_t PlanUnmappedAddress = 0x7f00000f0000ull;
|
||||
|
||||
struct PlanFixture {
|
||||
std::string name;
|
||||
ShaderStage stage = ShaderStage::Compute;
|
||||
std::uint64_t codeAddress = 0;
|
||||
std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> code;
|
||||
std::vector<std::uint32_t> userData;
|
||||
std::vector<std::pair<std::uint64_t, std::vector<std::uint32_t>>> regions;
|
||||
std::optional<ShaderPixelStageInfo> pixel;
|
||||
std::vector<MemoryRegion> memory;
|
||||
|
||||
RecompileRequest Request(bool useCache) {
|
||||
static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilitySampledImageArrayDynamicIndexing, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance};
|
||||
static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
memory.clear();
|
||||
for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))});
|
||||
RecompileRequest request{};
|
||||
request.shader = {stage, codeAddress, code, 0, {}};
|
||||
request.context.waveSize = waveSize;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
if (stage == ShaderStage::Compute) request.context.compute = ShaderComputeStageInfo{{waveSize, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.context.pixel = pixel;
|
||||
request.context.memory = memory;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.bdaAbiVersion = 1u;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = useCache;
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> imageWords(std::uint64_t base) {
|
||||
return {static_cast<std::uint32_t>(base >> 8u), static_cast<std::uint32_t>((base >> 40u) & 0xffu) | (56u << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> bufferWords(std::uint64_t base, std::uint32_t stride, std::uint32_t records, std::uint32_t format = 0xfacu) {
|
||||
return {static_cast<std::uint32_t>(base), static_cast<std::uint32_t>((base >> 32u) & 0xffffu) | (stride << 16u), records, format};
|
||||
}
|
||||
|
||||
void place(std::vector<std::uint32_t>& words, std::size_t offset, const std::vector<std::uint32_t>& values) {
|
||||
std::copy(values.begin(), values.end(), words.begin() + static_cast<std::ptrdiff_t>(offset));
|
||||
}
|
||||
|
||||
std::vector<PlanFixture> planFixtures() {
|
||||
std::vector<PlanFixture> fixtures;
|
||||
const std::vector<std::uint32_t> srtPointer{static_cast<std::uint32_t>(PlanSrtAddress), static_cast<std::uint32_t>(PlanSrtAddress >> 32u)};
|
||||
const auto add = [&](std::string name, std::uint32_t waveSize, std::vector<std::uint32_t> code, std::vector<std::uint32_t> userData, std::vector<std::pair<std::uint64_t, std::vector<std::uint32_t>>> regions) {
|
||||
PlanFixture fixture;
|
||||
fixture.name = std::move(name);
|
||||
fixture.codeAddress = 0x100000u + static_cast<std::uint64_t>(fixtures.size()) * 0x1000u;
|
||||
fixture.waveSize = waveSize;
|
||||
fixture.code = std::move(code);
|
||||
fixture.userData = std::move(userData);
|
||||
fixture.regions = std::move(regions);
|
||||
fixtures.push_back(std::move(fixture));
|
||||
};
|
||||
add("user data buffer", 64, {0xe0700000u, 0x80000000u, 0xbf810000u}, bufferWords(PlanOutputAddress, 0u, 0x40u, 0x00027facu), {});
|
||||
|
||||
std::vector<std::uint32_t> guarded(32, 0u);
|
||||
place(guarded, 0, imageWords(PlanTextureAddress));
|
||||
place(guarded, 16, bufferWords(PlanOutputAddress, 0u, 16u));
|
||||
guarded[20] = 1u;
|
||||
add("guarded image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}});
|
||||
add("guarded image stores", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}});
|
||||
|
||||
std::vector<std::uint32_t> heap(32, 0u);
|
||||
place(heap, 0, imageWords(PlanTextureAddress));
|
||||
place(heap, 8, imageWords(PlanTextureAddress + 0x100u));
|
||||
place(heap, 24, imageWords(PlanTextureAddress));
|
||||
const std::vector<std::uint32_t> materials{0u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 3u, 0u, 0u};
|
||||
std::vector<std::uint32_t> tables(16, 0u);
|
||||
place(tables, 0, bufferWords(PlanTableAddress, 32u, 4u));
|
||||
place(tables, 8, bufferWords(PlanMaterialAddress, 16u, 3u));
|
||||
place(tables, 12, bufferWords(PlanOutputAddress, 0u, 16u));
|
||||
add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
|
||||
std::vector<std::uint32_t> phis(32, 0u);
|
||||
place(phis, 0, imageWords(PlanTextureAddress));
|
||||
place(phis, 12, {0x49u, 0u, 0x00500000u, 0u});
|
||||
place(phis, 16, bufferWords(PlanOutputAddress, 0u, 16u));
|
||||
phis[20] = 1u;
|
||||
place(phis, 24, imageWords(PlanTextureAddress + 0x100u));
|
||||
add("descriptor phi sampler", 32, {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
|
||||
add("descriptor phi image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
|
||||
add("descriptor phi loop", 32, {0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
|
||||
add("descriptor phi in sequence", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
|
||||
|
||||
std::vector<std::uint32_t> root(16, 0u);
|
||||
place(root, 0, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu));
|
||||
root[4] = 1u;
|
||||
root[6] = static_cast<std::uint32_t>(PlanPayloadAddress);
|
||||
root[7] = static_cast<std::uint32_t>(PlanPayloadAddress >> 32u);
|
||||
const std::vector<std::uint32_t> payload{0x3f800000u};
|
||||
add("guarded nested read", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, root}, {PlanPayloadAddress, payload}});
|
||||
auto chained = root;
|
||||
chained[6] = static_cast<std::uint32_t>(PlanPointerAddress);
|
||||
chained[7] = static_cast<std::uint32_t>(PlanPointerAddress >> 32u);
|
||||
add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast<std::uint32_t>(PlanPayloadAddress), static_cast<std::uint32_t>(PlanPayloadAddress >> 32u)}}});
|
||||
add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}});
|
||||
add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
|
||||
|
||||
PlanFixture fragment;
|
||||
fragment.name = "fragment parameters";
|
||||
fragment.stage = ShaderStage::Fragment;
|
||||
fragment.codeAddress = 0x100000u + static_cast<std::uint64_t>(fixtures.size()) * 0x1000u;
|
||||
fragment.code = {0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u};
|
||||
ShaderPixelStageInfo pixel{};
|
||||
pixel.interpolatorCount = 2u;
|
||||
pixel.interpolatorSettings[1] = 1u;
|
||||
pixel.wave32 = true;
|
||||
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter);
|
||||
pixel.hasPerspectiveCenterVgpr = true;
|
||||
pixel.noPerspective = true;
|
||||
pixel.targetOutputMode[0] = 9u;
|
||||
pixel.targetExportMapping[0] = 0xe4u;
|
||||
fragment.pixel = pixel;
|
||||
fixtures.push_back(std::move(fragment));
|
||||
return fixtures;
|
||||
}
|
||||
|
||||
IrResourcePlan handBuiltPlan() {
|
||||
IrResourcePlan plan;
|
||||
std::uint32_t ids = 0;
|
||||
const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& {
|
||||
plan.valueStorage.push_back(std::make_unique<IrValue>(opcode, type, ids++));
|
||||
return *plan.valueStorage.back();
|
||||
};
|
||||
const auto constant = [&](std::uint32_t value) -> IrValue& {
|
||||
auto& immediate = make(IrOpcode::Void, IrType::U32);
|
||||
immediate.SetImmediateU32(value);
|
||||
return immediate;
|
||||
};
|
||||
auto& resource = make(IrOpcode::GetSrtResource, IrType::SrtResource);
|
||||
const auto readConst = [&](std::uint32_t slot) -> IrValue& {
|
||||
auto& read = make(IrOpcode::ReadConst, IrType::U32);
|
||||
read.AddArgument(&resource);
|
||||
read.AddArgument(&constant(slot));
|
||||
return read;
|
||||
};
|
||||
const auto userData = [&](std::uint32_t index) -> IrValue& {
|
||||
auto& reg = make(IrOpcode::Void, IrType::ScalarReg);
|
||||
reg.SetRegister({RegisterBank::Scalar, index});
|
||||
auto& read = make(IrOpcode::GetUserData, IrType::U32);
|
||||
read.AddArgument(®);
|
||||
return read;
|
||||
};
|
||||
auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64);
|
||||
handle.AddArgument(&userData(0));
|
||||
handle.AddArgument(&userData(1));
|
||||
auto& pointer = make(IrOpcode::LoadAddressU32, IrType::U32);
|
||||
pointer.AddArgument(&handle);
|
||||
pointer.AddArgument(&constant(8));
|
||||
plan.blockStorage.push_back(std::make_unique<IrBlock>(3u));
|
||||
plan.blockStorage.push_back(std::make_unique<IrBlock>(7u));
|
||||
auto& phi = make(IrOpcode::Phi, IrType::U32);
|
||||
phi.AddPhiOperand(plan.blockStorage[0].get(), &readConst(0));
|
||||
phi.AddPhiOperand(plan.blockStorage[1].get(), &constant(0));
|
||||
auto& condition = make(IrOpcode::Void, IrType::Bool);
|
||||
condition.SetImmediateU64(1u);
|
||||
condition.SetFlags(std::uint64_t{0x1234u});
|
||||
plan.stage = IrShaderStage::Compute;
|
||||
plan.shaderHash = 0x123456789abcull;
|
||||
plan.userDataBase = 2u;
|
||||
plan.userDataCount = 4u;
|
||||
plan.srgbDecodeFormats = 5u;
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.resource = 1u;
|
||||
memory.dmask = 0xfu;
|
||||
memory.imageDimension = RdnaImageDimension::Dim2D;
|
||||
memory.gpuDescriptor = true;
|
||||
memory.flushDenormals = true;
|
||||
MemoryInfo every;
|
||||
every.kind = ResourceKind::Buffer;
|
||||
every.resource = 2u;
|
||||
every.sampler = 3u;
|
||||
every.offset = 0x14u;
|
||||
every.secondaryOffset = 0x28u;
|
||||
every.dmask = 0x5u;
|
||||
every.dataDwords = 3u;
|
||||
every.dataBits = 16u;
|
||||
every.componentIndex = 1u;
|
||||
every.componentCount = 2u;
|
||||
every.dataFormat = 10u;
|
||||
every.numberFormat = 7u;
|
||||
every.imageSampleFlags = 0x21u;
|
||||
every.imageDimension = RdnaImageDimension::Dim3D;
|
||||
every.imageAddressComponents = 3u;
|
||||
every.addressIsFull = true;
|
||||
every.dataSigned = true;
|
||||
every.typed = true;
|
||||
every.formatted = true;
|
||||
every.d16 = true;
|
||||
every.imageHasMip = true;
|
||||
every.imageR128 = true;
|
||||
every.imagePacked = true;
|
||||
every.imageByElements = 2u;
|
||||
every.idxen = true;
|
||||
every.offen = true;
|
||||
every.planningOnly = true;
|
||||
every.coherent = true;
|
||||
every.gpuDescriptor = true;
|
||||
every.flushDenormals = true;
|
||||
plan.memoryInfo = {memory, every, MemoryInfo{}};
|
||||
DescriptorSource buffer;
|
||||
buffer.dwordCount = 4u;
|
||||
buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer};
|
||||
DescriptorSource table;
|
||||
table.dwordCount = 8u;
|
||||
table.dwords[0] = &readConst(3);
|
||||
table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true};
|
||||
DescriptorSource heap;
|
||||
heap.dwordCount = 4u;
|
||||
heap.dwords[1] = &readConst(6);
|
||||
DescriptorSource material;
|
||||
material.dwordCount = 4u;
|
||||
material.dwords[2] = &readConst(7);
|
||||
auto& wide = make(IrOpcode::Void, IrType::U64);
|
||||
wide.SetImmediateU64(0x8000000000000001ull);
|
||||
material.dwords[3] = &wide;
|
||||
plan.descriptorSources = {buffer, table, heap, material};
|
||||
plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}};
|
||||
plan.materializationSources = {0u, 2u};
|
||||
plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}};
|
||||
plan.guardedSrtSlots = {1u};
|
||||
plan.srtGuardOffset = 6u;
|
||||
plan.cleanFlatSlots = {0u, 1u};
|
||||
plan.pureFlatSlots = {1u, 0u};
|
||||
plan.requiresSpecializationMemory = true;
|
||||
plan.srtPlanComplete = true;
|
||||
plan.resourceTrackingComplete = true;
|
||||
plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}};
|
||||
ImageResource image{};
|
||||
image.source = 1u;
|
||||
image.resourceClass = ImageResourceClass::Sampled;
|
||||
image.dimension = RdnaImageDimension::Dim2D;
|
||||
plan.info.images = {image};
|
||||
ResourceMaterializer::PrepareImageModes(plan.info);
|
||||
plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u};
|
||||
auto& fillValue = readConst(5);
|
||||
plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr};
|
||||
return plan;
|
||||
}
|
||||
|
||||
std::vector<std::byte> encodedPlan(const IrResourcePlan& plan) {
|
||||
std::vector<std::byte> bytes;
|
||||
require(ShaderDiskCache::EncodePlan(plan, bytes), "a self-contained plan does not encode");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) {
|
||||
if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false;
|
||||
std::map<const IrValue*, std::size_t> leftValues;
|
||||
std::map<const IrValue*, std::size_t> rightValues;
|
||||
std::map<const IrBlock*, std::size_t> leftBlocks;
|
||||
std::map<const IrBlock*, std::size_t> rightBlocks;
|
||||
for (std::size_t index = 0; index < left.valueStorage.size(); ++index) {
|
||||
leftValues.emplace(left.valueStorage[index].get(), index);
|
||||
rightValues.emplace(right.valueStorage[index].get(), index);
|
||||
}
|
||||
for (std::size_t index = 0; index < left.blockStorage.size(); ++index) {
|
||||
leftBlocks.emplace(left.blockStorage[index].get(), index);
|
||||
rightBlocks.emplace(right.blockStorage[index].get(), index);
|
||||
}
|
||||
const auto sameValue = [&](const IrValue* a, const IrValue* b) {
|
||||
if (a == nullptr || b == nullptr) return a == b;
|
||||
const auto foundA = leftValues.find(a);
|
||||
const auto foundB = rightValues.find(b);
|
||||
return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second;
|
||||
};
|
||||
const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) {
|
||||
const auto foundA = leftBlocks.find(a);
|
||||
const auto foundB = rightBlocks.find(b);
|
||||
return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second;
|
||||
};
|
||||
for (std::size_t index = 0; index < left.blockStorage.size(); ++index) {
|
||||
if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < left.valueStorage.size(); ++index) {
|
||||
const auto& a = *left.valueStorage[index];
|
||||
const auto& b = *right.valueStorage[index];
|
||||
if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags<std::uint64_t>() != b.Flags<std::uint64_t>() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false;
|
||||
if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false;
|
||||
for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) {
|
||||
if (!sameValue(a.Argument(operand), b.Argument(operand))) return false;
|
||||
}
|
||||
for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) {
|
||||
if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false;
|
||||
}
|
||||
}
|
||||
if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false;
|
||||
if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false;
|
||||
for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) {
|
||||
const auto& a = left.descriptorSources[index];
|
||||
const auto& b = right.descriptorSources[index];
|
||||
if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false;
|
||||
for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) {
|
||||
if (!sameValue(a.dwords[dword], b.dwords[dword])) return false;
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0; index < left.controlFlow.size(); ++index) {
|
||||
const auto& a = left.controlFlow[index];
|
||||
const auto& b = right.controlFlow[index];
|
||||
if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < left.srtReads.size(); ++index) {
|
||||
if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) {
|
||||
if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false;
|
||||
}
|
||||
return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots &&
|
||||
left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill;
|
||||
}
|
||||
|
||||
struct MaterializedPlan {
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
ResourceSnapshot snapshot;
|
||||
SrtReadTrace trace;
|
||||
std::size_t leaf = 0;
|
||||
};
|
||||
|
||||
MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) {
|
||||
MaterializedPlan outcome;
|
||||
runtime.readTrace = &outcome.trace;
|
||||
try {
|
||||
ResourceMaterializer{}.Materialize(plan, runtime, outcome.snapshot);
|
||||
outcome.ok = true;
|
||||
} catch (const std::exception& error) {
|
||||
outcome.error = error.what();
|
||||
}
|
||||
outcome.leaf = static_cast<std::size_t>(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin());
|
||||
return outcome;
|
||||
}
|
||||
|
||||
bool sameMaterialization(const MaterializedPlan& left, const MaterializedPlan& right) {
|
||||
const auto& a = left.snapshot;
|
||||
const auto& b = right.snapshot;
|
||||
return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads &&
|
||||
left.leaf == right.leaf && left.trace.leafSlot == right.trace.leafSlot && left.trace.leaves == right.trace.leaves && left.trace.otherReads == right.trace.otherReads;
|
||||
}
|
||||
|
||||
void mutateWords(std::vector<std::uint32_t>& words, std::mt19937& random) {
|
||||
static constexpr std::array<std::uint64_t, 8> addresses{PlanSrtAddress, PlanTableAddress, PlanMaterialAddress, PlanPayloadAddress, PlanPointerAddress, PlanOutputAddress, PlanTextureAddress, PlanUnmappedAddress};
|
||||
for (auto& word : words) {
|
||||
switch (random() % 16u) {
|
||||
case 0: word = static_cast<std::uint32_t>(random()); break;
|
||||
case 1: word = 0u; break;
|
||||
case 2: {
|
||||
const auto address = addresses[random() % addresses.size()] + (random() % 8u) * 4u;
|
||||
word = random() % 2u == 0u ? static_cast<std::uint32_t>(address) : static_cast<std::uint32_t>(address >> 32u);
|
||||
break;
|
||||
}
|
||||
case 3: word ^= 1u << (random() % 32u); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t verifyRoundTripRuntimes(const IrResourcePlan& plan, const IrResourcePlan& decoded, const PlanFixture& fixture, std::uint32_t seed, const std::string& what) {
|
||||
std::mt19937 random(seed);
|
||||
std::size_t succeeded = 0;
|
||||
for (std::uint32_t run = 0; run < 1000u; ++run) {
|
||||
auto userData = fixture.userData;
|
||||
auto regions = fixture.regions;
|
||||
if (userData.size() < plan.userDataCount) userData.resize(plan.userDataCount, 0u);
|
||||
if (run != 0u) {
|
||||
mutateWords(userData, random);
|
||||
for (auto& region : regions) mutateWords(region.second, random);
|
||||
}
|
||||
std::vector<MemoryRegion> memory;
|
||||
for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))});
|
||||
RequestMemoryView view(memory);
|
||||
const auto runtime = view.MakeRuntime(userData, fixture.codeAddress);
|
||||
const auto original = materializePlan(plan, runtime);
|
||||
const auto loaded = materializePlan(decoded, runtime);
|
||||
require(sameMaterialization(original, loaded), what + ": runtime " + std::to_string(run) + " materialized differently from the decoded plan (" + original.error + " / " + loaded.error + ")");
|
||||
if (original.ok) ++succeeded;
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
void verifyPlanEntryDamage(const std::vector<std::byte>& key, const IrResourcePlan& plan, const std::string& what) {
|
||||
const auto file = ShaderDiskCache::EncodePlanEntry(key, plan);
|
||||
require(!file.empty(), what + ": the plan entry was not encoded");
|
||||
IrResourcePlan decoded;
|
||||
require(ShaderDiskCache::DecodePlanEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded && encodedPlan(decoded) == encodedPlan(plan), what + ": the plan entry does not decode to the plan");
|
||||
for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 37) {
|
||||
IrResourcePlan partial;
|
||||
require(ShaderDiskCache::DecodePlanEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry truncated to " + std::to_string(size) + " bytes is not rejected");
|
||||
}
|
||||
for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 29) {
|
||||
for (const auto bit : {std::byte{0x01}, std::byte{0x80}}) {
|
||||
auto damaged = file;
|
||||
damaged[offset] ^= bit;
|
||||
IrResourcePlan partial;
|
||||
require(ShaderDiskCache::DecodePlanEntry(damaged, key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with byte " + std::to_string(offset) + " flipped is not rejected");
|
||||
}
|
||||
}
|
||||
auto longer = file;
|
||||
longer.push_back(std::byte{0});
|
||||
require(ShaderDiskCache::DecodePlanEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with a trailing byte is not rejected");
|
||||
auto otherKey = key;
|
||||
otherKey.back() ^= std::byte{1};
|
||||
require(ShaderDiskCache::DecodePlanEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, what + ": a plan entry for another key loads");
|
||||
auto previousFormat = file;
|
||||
previousFormat[4] = static_cast<std::byte>(ShaderDiskCache::FormatVersion - 1u);
|
||||
require(ShaderDiskCache::DecodePlanEntry(previousFormat, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of the previous format loads");
|
||||
auto otherSource = file;
|
||||
otherSource[8] ^= std::byte{1};
|
||||
require(ShaderDiskCache::DecodePlanEntry(otherSource, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of another source version loads");
|
||||
CompiledVariant variant;
|
||||
require(ShaderDiskCache::DecodeEntry(file, key, variant) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry decodes as a compiled variant");
|
||||
}
|
||||
|
||||
void verifyPlanPayloadDamage(const IrResourcePlan& plan, const std::string& what) {
|
||||
const auto bytes = encodedPlan(plan);
|
||||
for (std::size_t size = 0; size < bytes.size(); ++size) {
|
||||
IrResourcePlan partial;
|
||||
require(!ShaderDiskCache::DecodePlan(std::span(bytes).first(size), partial), what + ": a plan truncated to " + std::to_string(size) + " bytes decodes");
|
||||
}
|
||||
std::size_t rejected = 0;
|
||||
for (std::size_t offset = 0; offset < bytes.size(); ++offset) {
|
||||
for (const auto bit : {std::byte{0x01}, std::byte{0x40}}) {
|
||||
auto damaged = bytes;
|
||||
damaged[offset] ^= bit;
|
||||
IrResourcePlan decoded;
|
||||
if (!ShaderDiskCache::DecodePlan(damaged, decoded)) {
|
||||
++rejected;
|
||||
continue;
|
||||
}
|
||||
std::vector<std::byte> again;
|
||||
require(ShaderDiskCache::EncodePlan(decoded, again), what + ": a plan accepted with byte " + std::to_string(offset) + " flipped is not self-contained");
|
||||
}
|
||||
}
|
||||
require(rejected != 0u, what + ": no damaged plan was rejected");
|
||||
}
|
||||
|
||||
void verifyMalformedPlans() {
|
||||
const auto rejects = [](const std::string& what, const std::function<void(IrResourcePlan&)>& damage) {
|
||||
auto plan = handBuiltPlan();
|
||||
damage(plan);
|
||||
std::vector<std::byte> bytes;
|
||||
require(ShaderDiskCache::EncodePlan(plan, bytes), "malformed plan: " + what + " did not encode");
|
||||
IrResourcePlan decoded;
|
||||
require(!ShaderDiskCache::DecodePlan(bytes, decoded), "malformed plan: " + what + " decodes");
|
||||
};
|
||||
rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); });
|
||||
rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; });
|
||||
rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; });
|
||||
rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; });
|
||||
rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); });
|
||||
rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; });
|
||||
rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; });
|
||||
rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; });
|
||||
rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; });
|
||||
rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->heapSource = 4u; });
|
||||
rejects("an image table material past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->materialSource = 4u; });
|
||||
auto foreign = handBuiltPlan();
|
||||
IrValue stray(IrOpcode::Void, IrType::U32, 99u);
|
||||
foreign.srtReads[0].value = &stray;
|
||||
std::vector<std::byte> bytes;
|
||||
require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded");
|
||||
auto unblocked = handBuiltPlan();
|
||||
auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique<IrValue>(IrOpcode::Phi, IrType::U32, 100u));
|
||||
phi.AddArgument(unblocked.valueStorage[0].get());
|
||||
unblocked.srtReads[0].value = φ
|
||||
bytes.clear();
|
||||
require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded");
|
||||
auto emptied = handBuiltPlan();
|
||||
emptied.valueStorage.push_back(nullptr);
|
||||
bytes.clear();
|
||||
require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded");
|
||||
}
|
||||
|
||||
void verifyPlanRoundTrip() {
|
||||
const auto handBuilt = handBuiltPlan();
|
||||
const auto handBytes = encodedPlan(handBuilt);
|
||||
IrResourcePlan handDecoded;
|
||||
require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode");
|
||||
require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip");
|
||||
const auto& phi = *handDecoded.descriptorSources[0].dwords[2];
|
||||
require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands");
|
||||
require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values");
|
||||
require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value");
|
||||
PlanFixture handFixture;
|
||||
handFixture.userData = {static_cast<std::uint32_t>(PlanSrtAddress), static_cast<std::uint32_t>(PlanSrtAddress >> 32u), 0u, 0u};
|
||||
std::vector<std::uint32_t> handSrt(16, 0u);
|
||||
place(handSrt, 0, bufferWords(PlanOutputAddress, 0u, 16u));
|
||||
handFixture.regions = {{PlanSrtAddress, handSrt}, {PlanPayloadAddress, {0x3f800000u}}};
|
||||
static_cast<void>(verifyRoundTripRuntimes(handBuilt, handDecoded, handFixture, 7u, "hand-built plan"));
|
||||
verifyPlanPayloadDamage(handBuilt, "hand-built plan");
|
||||
verifyMalformedPlans();
|
||||
|
||||
auto fixtures = planFixtures();
|
||||
std::vector<std::shared_ptr<const IrResourcePlan>> plans;
|
||||
std::size_t index = 0;
|
||||
for (auto& fixture : fixtures) {
|
||||
auto request = fixture.Request(false);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
plans.push_back(plan);
|
||||
const auto bytes = encodedPlan(*plan);
|
||||
IrResourcePlan decoded;
|
||||
require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode");
|
||||
require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip");
|
||||
require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value");
|
||||
require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph");
|
||||
const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast<std::uint32_t>(index), fixture.name);
|
||||
require(succeeded != 0u, fixture.name + ": no runtime materialized");
|
||||
std::vector<std::byte> key;
|
||||
ShaderDiskCache::BuildPlanKey(request, 32u, key);
|
||||
if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name);
|
||||
std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n";
|
||||
++index;
|
||||
}
|
||||
const auto covers = [&](const std::function<bool(const IrResourcePlan&)>& predicate) {
|
||||
return std::any_of(plans.begin(), plans.end(), [&](const std::shared_ptr<const IrResourcePlan>& plan) { return predicate(*plan); });
|
||||
};
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }); }), "no fixture plans an indirect image table");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value->IsPhi(); }); }), "no fixture plans a phi");
|
||||
require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot");
|
||||
}
|
||||
|
||||
void verifyPlanKey() {
|
||||
auto fixtures = planFixtures();
|
||||
auto& fixture = fixtures[1];
|
||||
const auto planKey = [&](const std::function<void(RecompileRequest&)>& change, std::uint32_t hostSubgroupSize = 32u) {
|
||||
auto request = fixture.Request(true);
|
||||
change(request);
|
||||
std::vector<std::byte> key;
|
||||
ShaderDiskCache::BuildPlanKey(request, hostSubgroupSize, key);
|
||||
return key;
|
||||
};
|
||||
const auto key = planKey([](RecompileRequest&) {});
|
||||
require(planKey([](RecompileRequest&) {}) == key, "the plan key is not deterministic");
|
||||
std::vector<std::byte> variantKey;
|
||||
ShaderDiskCache::BuildKey(fixture.Request(true), 32u, variantKey);
|
||||
require(variantKey != key && ShaderDiskCache::EntryName(variantKey) != ShaderDiskCache::EntryName(key), "the plan and the compiled variant share an entry");
|
||||
require(planKey([](RecompileRequest& request) { request.layout = {1u, 4u, 16u, 64u}; }) == key, "the binding layout changed the plan key");
|
||||
require(planKey([](RecompileRequest& request) { request.shader.codeAddress += 0x1000u; request.context.memory = {}; }) == key, "the code address or the memory changed the plan key");
|
||||
auto userData = fixture.userData;
|
||||
userData[0] ^= 0x100u;
|
||||
require(planKey([&](RecompileRequest& request) { request.context.userData = userData; }) == key, "user data values changed the plan key");
|
||||
const auto original = fixture.code;
|
||||
for (std::size_t word = 0; word < original.size(); ++word) {
|
||||
for (std::uint32_t bit = 0; bit < 32u; ++bit) {
|
||||
fixture.code = original;
|
||||
fixture.code[word] ^= 1u << bit;
|
||||
require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores code word " + std::to_string(word) + " bit " + std::to_string(bit));
|
||||
}
|
||||
}
|
||||
fixture.code = original;
|
||||
fixture.code.push_back(0xbf810000u);
|
||||
require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores an appended code word");
|
||||
fixture.code = original;
|
||||
require(planKey([](RecompileRequest&) {}, 64u) != key, "the plan key ignores the host subgroup size");
|
||||
require(planKey([](RecompileRequest& request) { request.target.subgroupSize = 64u; }) != key, "the plan key ignores the target");
|
||||
require(planKey([](RecompileRequest& request) { request.target.srgbDecodeFormats = 2u; }) != key, "the plan key ignores the sRGB formats decoded in the shader");
|
||||
require(planKey([](RecompileRequest& request) { request.context.waveSize = 64u; request.context.compute->numThreads[0] = 64u; }) != key, "the plan key ignores the wave size");
|
||||
require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode");
|
||||
}
|
||||
|
||||
void appendWords(std::vector<std::byte>& out, std::span<const std::uint32_t> words) {
|
||||
const auto count = static_cast<std::uint64_t>(words.size());
|
||||
const auto countBytes = std::as_bytes(std::span(&count, 1));
|
||||
out.insert(out.end(), countBytes.begin(), countBytes.end());
|
||||
const auto bytes = std::as_bytes(words);
|
||||
out.insert(out.end(), bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
void appendSnapshot(std::vector<std::byte>& out, const ResourceCapture& capture) {
|
||||
const auto& snapshot = capture.snapshot;
|
||||
for (const auto* values : {&snapshot.buffers, &snapshot.images, &snapshot.samplers}) {
|
||||
std::vector<std::uint32_t> words;
|
||||
for (const auto& value : *values) {
|
||||
words.push_back(value.dwordCount);
|
||||
words.insert(words.end(), value.dwords.begin(), value.dwords.end());
|
||||
}
|
||||
appendWords(out, words);
|
||||
}
|
||||
appendWords(out, snapshot.flattenedSrt);
|
||||
appendWords(out, snapshot.userData);
|
||||
const auto& fill = snapshot.uniformFill;
|
||||
appendWords(out, std::vector<std::uint32_t>{static_cast<std::uint32_t>(fill.kind), fill.resource, fill.groupStride[0], fill.groupStride[1], fill.groupStride[2], fill.words, fill.value, snapshot.nullRootReads});
|
||||
std::vector<std::uint32_t> poison;
|
||||
for (const auto& entry : snapshot.srtPoison) poison.insert(poison.end(), {entry.slot, entry.pc, static_cast<std::uint32_t>(entry.address), static_cast<std::uint32_t>(entry.address >> 32u)});
|
||||
appendWords(out, poison);
|
||||
std::vector<std::uint32_t> leaves{capture.readTrace.leafSlot};
|
||||
for (const auto& [slot, address] : capture.readTrace.leaves) leaves.insert(leaves.end(), {slot, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)});
|
||||
appendWords(out, leaves);
|
||||
std::vector<std::uint32_t> reads;
|
||||
for (const auto address : capture.readTrace.otherReads) reads.insert(reads.end(), {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)});
|
||||
appendWords(out, reads);
|
||||
}
|
||||
|
||||
std::vector<std::vector<std::byte>> preparedOutputs(std::vector<PlanFixture>& fixtures, std::vector<std::shared_ptr<const IrResourcePlan>>* plans = nullptr) {
|
||||
std::vector<std::vector<std::byte>> outputs;
|
||||
for (auto& fixture : fixtures) {
|
||||
const auto request = fixture.Request(true);
|
||||
const auto handle = PrepareShader(request);
|
||||
RequestMemoryView view(request.context.memory);
|
||||
const auto capture = CaptureResources(request, view.MakeRuntime(request.context.userData, request.shader.codeAddress), *handle);
|
||||
const auto result = MaterializeShader(request, *capture, *handle);
|
||||
std::vector<std::byte> bytes;
|
||||
appendWords(bytes, GetPreparedArtifact(*handle).spirv.Words());
|
||||
ShaderDiskCache::EncodeResult(*result, bytes);
|
||||
appendSnapshot(bytes, *capture);
|
||||
require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode");
|
||||
if (plans != nullptr) plans->push_back(capture->plan);
|
||||
outputs.push_back(std::move(bytes));
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
std::filesystem::path planOutputPath(std::size_t index) {
|
||||
return ShaderCacheDirectory() / "plan-outputs" / ("fixture-" + std::to_string(index) + ".bin");
|
||||
}
|
||||
|
||||
std::filesystem::path planEntryPath(PlanFixture& fixture) {
|
||||
std::vector<std::byte> key;
|
||||
ShaderDiskCache::BuildPlanKey(fixture.Request(true), 32u, key);
|
||||
return ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key);
|
||||
}
|
||||
|
||||
int runPlanLoadingProcess(std::string_view mode) {
|
||||
if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1");
|
||||
if (mode == "--load-plan-off") setEnvironment("APS5_NO_PERF_DISK_PLANS", "1");
|
||||
auto fixtures = planFixtures();
|
||||
const auto count = fixtures.size();
|
||||
std::vector<std::shared_ptr<const IrResourcePlan>> plans;
|
||||
const auto outputs = preparedOutputs(fixtures, &plans);
|
||||
ShaderDiskCache::Flush();
|
||||
const auto totals = ShaderDiskCache::Totals();
|
||||
const auto builds = FrontEndBuilds();
|
||||
const auto counts = " (front end builds " + std::to_string(builds) + ", plan hits " + std::to_string(totals.planHits) + ", plan misses " + std::to_string(totals.planMisses) + ", plan load failures " + std::to_string(totals.planLoadFailures) + ", variant hits " + std::to_string(totals.hits) + ", variant misses " + std::to_string(totals.misses) + ")";
|
||||
if (mode == "--load-plan-switch") {
|
||||
require(builds == count && totals.planHits == 0u && totals.planMisses == count && totals.misses == count, "another switch value reused stored plans or variants" + counts);
|
||||
std::cout << "a process with another switch value rebuilt all " << count << " plans\n";
|
||||
return 0;
|
||||
}
|
||||
if (mode == "--load-plan") {
|
||||
require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts);
|
||||
} else if (mode == "--load-plan-off") {
|
||||
require(builds == count && totals.planHits == 0u && totals.planMisses == 0u && totals.planWrites == 0u && totals.hits == count && totals.misses == 0u, "APS5_NO_PERF_DISK_PLANS=1 used the stored plans or missed the stored variants" + counts);
|
||||
} else {
|
||||
require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts);
|
||||
}
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
std::vector<std::byte> expected;
|
||||
require(ReadWholeFile(planOutputPath(index), expected), "cannot read the first process's output for " + fixtures[index].name);
|
||||
require(outputs[index] == expected, fixtures[index].name + ": the SPIR-V, bindings, push constants, snapshot or plan differ from the first process");
|
||||
}
|
||||
if (mode == "--load-plan") {
|
||||
auto changed = planFixtures();
|
||||
auto& fixture = changed.front();
|
||||
fixture.code.insert(fixture.code.end() - 1, 0xbf800000u);
|
||||
const auto request = fixture.Request(true);
|
||||
static_cast<void>(PrepareShader(request));
|
||||
const auto after = ShaderDiskCache::Totals();
|
||||
require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan");
|
||||
std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n";
|
||||
} else if (mode == "--load-plan-off") {
|
||||
std::cout << "APS5_NO_PERF_DISK_PLANS=1 ran the front end for all " << count << " shaders with the same output\n";
|
||||
} else {
|
||||
std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n";
|
||||
}
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
const auto fresh = GetResourcePlan(fixtures[index].Request(false));
|
||||
require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds");
|
||||
}
|
||||
ShaderDiskCache::Flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void verifyPlanAcrossProcesses(const char* self) {
|
||||
auto fixtures = planFixtures();
|
||||
const auto builds = FrontEndBuilds();
|
||||
const auto before = ShaderDiskCache::Totals();
|
||||
const auto outputs = preparedOutputs(fixtures);
|
||||
ShaderDiskCache::Flush();
|
||||
const auto after = ShaderDiskCache::Totals();
|
||||
require(FrontEndBuilds() - builds == fixtures.size() && after.planMisses - before.planMisses == fixtures.size() && after.planWrites - before.planWrites == fixtures.size(), "the first process did not build and store every plan");
|
||||
std::filesystem::create_directories(planOutputPath(0).parent_path());
|
||||
for (std::size_t index = 0; index < outputs.size(); ++index) require(WriteFileAtomically(planOutputPath(index), outputs[index]), "cannot write the first process's output");
|
||||
auto refused = fixtures[1];
|
||||
refused.codeAddress = 0x200000u;
|
||||
refused.code = {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u};
|
||||
bool threw = false;
|
||||
try {
|
||||
static_cast<void>(PrepareShader(refused.Request(true)));
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
ShaderDiskCache::Flush();
|
||||
require(threw && ShaderDiskCache::Totals().planWrites == after.planWrites && !std::filesystem::exists(planEntryPath(refused)), "a plan the front end refused was stored");
|
||||
std::uintmax_t planBytes = 0;
|
||||
for (auto& fixture : fixtures) planBytes += std::filesystem::file_size(planEntryPath(fixture));
|
||||
std::cout << fixtures.size() << " plan entries, " << planBytes / fixtures.size() << " bytes per entry on average\n";
|
||||
const auto run = [&](const char* mode) {
|
||||
const std::string command = "\"" + std::string(self) + "\" " + mode;
|
||||
require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed");
|
||||
};
|
||||
run("--load-plan");
|
||||
run("--load-plan-off");
|
||||
std::vector<std::byte> entry;
|
||||
const auto damaged = planEntryPath(fixtures.front());
|
||||
require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry");
|
||||
entry[entry.size() / 2] ^= std::byte{0x20};
|
||||
require(WriteFileAtomically(damaged, entry), "cannot damage a stored plan entry");
|
||||
run("--load-plan-corrupt");
|
||||
std::vector<std::byte> rewritten;
|
||||
std::vector<std::byte> key;
|
||||
ShaderDiskCache::BuildPlanKey(fixtures.front().Request(true), 32u, key);
|
||||
IrResourcePlan plan;
|
||||
require(ReadWholeFile(damaged, rewritten) && ShaderDiskCache::DecodePlanEntry(rewritten, key, plan) == ShaderDiskCache::LoadStatus::Loaded, "the rebuilt plan was not stored again");
|
||||
run("--load-plan-switch");
|
||||
}
|
||||
|
||||
void verifyDefaultDirectory(const char* self) {
|
||||
setEnvironment("ANYPS5_SHADER_CACHE_DIR", "");
|
||||
setEnvironment("ANYPS5_NO_SHADER_CACHE", "1");
|
||||
|
|
@ -1098,6 +1843,7 @@ int main(int argc, char** argv) {
|
|||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
|
||||
if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]);
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::remove_all(directory);
|
||||
verifyDefaultDirectory(argv[0]);
|
||||
|
|
@ -1109,8 +1855,11 @@ int main(int argc, char** argv) {
|
|||
verifyArtifactStorageIsolation();
|
||||
verifyKeySensitivity();
|
||||
verifyFloatModeSerialization();
|
||||
verifyPlanRoundTrip();
|
||||
verifyPlanKey();
|
||||
verifyStore();
|
||||
verifyAcrossProcesses(argv[0]);
|
||||
verifyPlanAcrossProcesses(argv[0]);
|
||||
verifyEmissionFailureMemo();
|
||||
verifyFailureMemoSwitch(argv[0]);
|
||||
verifyInvocationIsolation();
|
||||
|
|
|
|||
|
|
@ -1686,6 +1686,23 @@ void verifyMeshConfiguration() {
|
|||
other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}};
|
||||
RecompileCacheKey::Build(other, key);
|
||||
require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration");
|
||||
RecompileRequest hull{};
|
||||
hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}};
|
||||
hull.context.waveSize = 64;
|
||||
hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}};
|
||||
const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull));
|
||||
const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr;
|
||||
require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization");
|
||||
auto grouped = hull;
|
||||
grouped.graphics->tessellation->patchesPerGroup = 62u;
|
||||
for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) {
|
||||
hull.shader.stage = stage;
|
||||
grouped.shader.stage = stage;
|
||||
RecompileCacheKey::Build(hull, key);
|
||||
const auto patchKey = key;
|
||||
RecompileCacheKey::Build(grouped, key);
|
||||
require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index");
|
||||
}
|
||||
const std::array<std::uint32_t, 8> users{};
|
||||
request.context.waveSize = 32u;
|
||||
request.context.userData = users;
|
||||
|
|
@ -1825,14 +1842,16 @@ void verifyPixelRequestSerialization() {
|
|||
minimal.context.waveSize = 64;
|
||||
minimal.context.pixel = ShaderPixelStageInfo{};
|
||||
minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
|
||||
minimal.context.pixel->layerExported = true;
|
||||
const auto encoded = serializer.Serialize(minimal);
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQRQAAAA=", "new requests did not use serialization version 20");
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQRcAAAA=", "new requests did not use serialization version 23");
|
||||
require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip");
|
||||
require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip");
|
||||
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
|
||||
for (std::size_t bytes = 0; bytes < 20u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted");
|
||||
for (std::size_t bytes = 0; bytes < 22u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class or layer export flag was accepted");
|
||||
}
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRUAAAA="}) {
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
|
||||
}
|
||||
static constexpr std::string_view version18Request{
|
||||
|
|
@ -1921,6 +1940,14 @@ void verifyVertexInfoVersion19() {
|
|||
while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset;
|
||||
require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds");
|
||||
require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it");
|
||||
auto version20 = flagged;
|
||||
version20[4] = 20u;
|
||||
const auto replay20 = serializer.Deserialize(encodeBase64(version20));
|
||||
require(replay20.request.context.vertex.has_value() && replay20.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a version 20 vertex info lost its position export layout");
|
||||
require(serializer.Serialize(replay20.request) == marked, "a version 20 vertex info misread the fields after its position export layout");
|
||||
vertex.context.vertex->paClVsOutCntl = (1u << 18u) | (1u << 21u) | 0xfu;
|
||||
const auto layered = serializer.Deserialize(serializer.Serialize(vertex));
|
||||
require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index");
|
||||
auto legacy = plain;
|
||||
legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u);
|
||||
for (const auto version : {16u, 17u, 18u, 19u}) {
|
||||
|
|
@ -1935,11 +1962,20 @@ void verifyVertexInfoVersion19() {
|
|||
pixel.context.pixel = ShaderPixelStageInfo{};
|
||||
pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
|
||||
const auto encodedPixel = serializer.Serialize(pixel);
|
||||
auto exportedPixel = pixel;
|
||||
exportedPixel.context.pixel->layerExported = true;
|
||||
const auto exportedBytes = decodeBase64(serializer.Serialize(exportedPixel));
|
||||
auto legacyPixel = decodeBase64(encodedPixel);
|
||||
legacyPixel[4] = 19u;
|
||||
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
|
||||
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "a main version 19 request lost its pixel primitive class");
|
||||
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 request misread the fields after its pixel primitive class");
|
||||
std::size_t flagOffset = 0;
|
||||
while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset;
|
||||
require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized");
|
||||
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset));
|
||||
for (const auto version : {19u, 20u, 21u, 22u}) {
|
||||
legacyPixel[4] = static_cast<std::uint8_t>(version);
|
||||
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
|
||||
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported, "a main version 19 to 22 request lost its pixel primitive class or read a layer export flag");
|
||||
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyLegacyPixelRequests() {
|
||||
|
|
@ -2034,7 +2070,7 @@ void verifyLegacyPixelRequests() {
|
|||
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");
|
||||
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
|
||||
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
|
||||
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread");
|
||||
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve, "legacy request trailer was misread");
|
||||
const auto upgraded = serializer.Deserialize(serializer.Serialize(request));
|
||||
require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping");
|
||||
}
|
||||
|
|
@ -2455,6 +2491,40 @@ void verifySpecializedBufferAtomicBindings() {
|
|||
expectFailure([&] { static_cast<void>(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64");
|
||||
}
|
||||
|
||||
void verifyCoherentGds() {
|
||||
using namespace ShaderRecompiler;
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
const std::vector<std::uint32_t> code{0x34000082u, 0xbefc03ffu, 0x0000ffffu, 0xd8da0000u, 0x01000000u, 0xbf8cc07fu, 0xd8360080u, 0x00000100u, 0xbf810000u};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x5c000u, code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = BdaAbi::Version;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.fragmentShaderBarycentricEnabled = false;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
const auto words = Recompile(request).spirv;
|
||||
std::uint32_t gds = 0u;
|
||||
std::map<std::uint32_t, std::vector<std::uint32_t>> decorations;
|
||||
for (std::size_t cursor = 5; cursor < words.size();) {
|
||||
const auto count = words[cursor] >> 16u;
|
||||
const auto opcode = words[cursor] & 0xffffu;
|
||||
require(count != 0 && count <= words.size() - cursor, "coherent GDS: truncated SPIR-V instruction");
|
||||
if (opcode == spv::OpName && std::string_view(reinterpret_cast<const char*>(&words[cursor + 2])) == "gds") gds = words[cursor + 1];
|
||||
if (opcode == spv::OpDecorate) decorations[words[cursor + 1]].push_back(words[cursor + 2]);
|
||||
cursor += count;
|
||||
}
|
||||
require(gds != 0u, "coherent GDS: a ds_read_b32 and ds_write_b32 on GDS declare no gds buffer");
|
||||
require(std::ranges::find(decorations[gds], static_cast<std::uint32_t>(spv::DecorationCoherent)) != decorations[gds].end(), "coherent GDS: the gds buffer is not Coherent");
|
||||
}
|
||||
|
||||
void verifyCoherentBufferAccesses() {
|
||||
using namespace ShaderRecompiler;
|
||||
alignas(256) static std::array<std::uint32_t, 64> input{};
|
||||
|
|
@ -3529,6 +3599,7 @@ int main(int argc, char** argv) {
|
|||
verifyGuardedNullPointers();
|
||||
verifyCubeDepthCompareMode();
|
||||
verifySrgbDepthCompareModes();
|
||||
verifyCoherentGds();
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
const std::vector<std::uint32_t> minimalSpirv{
|
||||
0x07230203u, 0x00010000u, 0u, 5u, 0u,
|
||||
|
|
@ -3642,6 +3713,9 @@ int main(int argc, char** argv) {
|
|||
RecompileCacheKey::Build(uncached, plainKey);
|
||||
RecompileCacheKey::Build(registrationModules, registrationKey);
|
||||
require(plainKey != registrationKey, "registration modules did not change the cache key");
|
||||
auto float16Denormals = uncached;
|
||||
float16Denormals.target.float16DenormPreserve = true;
|
||||
require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float16Denormals)).request.target.float16DenormPreserve, "f16 denormal preservation was lost in serialization");
|
||||
auto changedLayout = request;
|
||||
changedLayout.layout.pushConstantSizeBytes = 64;
|
||||
require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "SpirvBackend/SpirvSpecialization.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
|
@ -285,8 +286,21 @@ void testFoldingLeavesNoDeadBlocksOrConstants() {
|
|||
checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids");
|
||||
}
|
||||
|
||||
void testIdsBeyondTheBoundAreRejected() {
|
||||
auto words = foldedSwitchModule(13u, {1u, 1u, 1u});
|
||||
words[3] = 12u;
|
||||
bool rejected = false;
|
||||
try {
|
||||
static_cast<void>(ShaderRecompiler::SpecializeSpirv(words));
|
||||
} catch (const std::exception&) {
|
||||
rejected = true;
|
||||
}
|
||||
check(rejected, "a module whose ids reach its bound was specialized");
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFoldingLeavesNoDeadBlocksOrConstants();
|
||||
testIdsBeyondTheBoundAreRejected();
|
||||
testStructExtractsAreNotShuffled();
|
||||
testRepeatedTypesShareOneId();
|
||||
testOperandShapesDoNotCollide();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Query.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include "prx/libkernel/Equeue/Equeue.hpp"
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
|
|
@ -211,6 +212,22 @@ void testEndOfPipeInterrupts() {
|
|||
check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt");
|
||||
struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this<Output> {
|
||||
std::atomic<int> ready{0};
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
|
||||
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ready.fetch_add(1); }
|
||||
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
|
||||
};
|
||||
const auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
|
||||
std::array<std::uint32_t, 6> flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0};
|
||||
Packet flipPacket{flip.data(), static_cast<std::uint32_t>(flip.size()), 0, {}};
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == 1, "flip was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
|
||||
alignas(8) static volatile std::uint64_t label = 0;
|
||||
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&label);
|
||||
words = {0xc0064900, 0x528, (3u << 29u) | (3u << 24u) | (1u << 16u), static_cast<std::uint32_t>(labelAddress), static_cast<std::uint32_t>(static_cast<std::uint64_t>(labelAddress) >> 32u), 0x89abcdefu, 0x01234567u, 0};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "GraphicsTests.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
|
||||
#include <array>
|
||||
|
|
@ -48,6 +49,20 @@ void srgbDecodeTests() {
|
|||
Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views");
|
||||
}
|
||||
|
||||
void srgbTwinTests() {
|
||||
constexpr std::uint32_t srgb8 = 1u;
|
||||
constexpr std::uint32_t srgb8_8 = 2u;
|
||||
Context context{};
|
||||
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins");
|
||||
context.srgbDecodeFormats = srgb8_8;
|
||||
Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin");
|
||||
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader");
|
||||
context.srgbDecodeFormats = srgb8 | srgb8_8;
|
||||
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin");
|
||||
Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins");
|
||||
Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin");
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
void reject(TAction action, std::string_view reason) {
|
||||
try {
|
||||
|
|
@ -125,10 +140,28 @@ void singleDccKeysTests() {
|
|||
Require(read(56) == DccKeys::Uncompressed, "comp-to-single keys mixed with uncompressed ones must read as stored");
|
||||
}
|
||||
|
||||
void packedFloatDccClearTests() {
|
||||
for (const bool alphaOnMsb : {false, true}) {
|
||||
for (const auto keys : {DccKeys::Clear0000, DccKeys::Clear0001, DccKeys::Clear1110, DccKeys::Clear1111}) {
|
||||
std::array<std::uint32_t, 4> texels{};
|
||||
texels.fill(0xdeadbeefu);
|
||||
Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, keys, alphaOnMsb, std::as_writable_bytes(std::span(texels))), "packed float fast clear was not encoded");
|
||||
const std::uint32_t expected = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 ? 0x781e03c0u : 0u;
|
||||
for (const auto texel : texels) Require(texel == expected, "packed float DCC clear did not encode RGB zero or one");
|
||||
}
|
||||
}
|
||||
alignas(64) std::array<std::uint8_t, 4> keys{0xc0, 0xc0, 0xc0, 0xc0};
|
||||
GuestTextureResource resource{};
|
||||
resource.format = 36;
|
||||
resource.dccAddress = reinterpret_cast<std::uint64_t>(keys.data());
|
||||
Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RunTextureFormatTests() {
|
||||
srgbDecodeTests();
|
||||
srgbTwinTests();
|
||||
packedChannelTests();
|
||||
Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM");
|
||||
Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM");
|
||||
|
|
@ -136,6 +169,15 @@ void RunTextureFormatTests() {
|
|||
Require(!IsBlockCompressed(1), "format 1 must not be block compressed");
|
||||
Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block");
|
||||
|
||||
Require(ResolveTextureFormat(50) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "format 50 must resolve to A2B10G10R10_UNORM");
|
||||
Require(ResolveTextureFormat(51) == VK_FORMAT_A2B10G10R10_SNORM_PACK32, "format 51 must resolve to A2B10G10R10_SNORM");
|
||||
Require(ResolveTextureFormat(54) == VK_FORMAT_A2B10G10R10_UINT_PACK32, "format 54 must resolve to A2B10G10R10_UINT");
|
||||
Require(ResolveTextureFormat(55) == VK_FORMAT_A2B10G10R10_SINT_PACK32, "format 55 must resolve to A2B10G10R10_SINT");
|
||||
for (const std::uint32_t format : {50u, 51u, 54u, 55u}) Require(BytesPerElement(format) == 4u && !IsBlockCompressed(format), "packed 10-bit format " + std::to_string(format) + " must be four bytes and not block compressed");
|
||||
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SNorm) == ShaderRecompiler::IrTextureNumericClass::Float, "format 51 must sample as float");
|
||||
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SInt) == ShaderRecompiler::IrTextureNumericClass::Sint, "format 55 must sample as signed integer");
|
||||
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2UInt) == ShaderRecompiler::IrTextureNumericClass::Uint, "format 54 must sample as unsigned integer");
|
||||
|
||||
Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM");
|
||||
Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide");
|
||||
|
||||
|
|
@ -175,4 +217,5 @@ void RunTextureFormatTests() {
|
|||
for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format");
|
||||
convertedDccClearTests();
|
||||
singleDccKeysTests();
|
||||
packedFloatDccClearTests();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
#include "Recompiler.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Module {
|
||||
std::vector<std::uint32_t> capabilities;
|
||||
std::vector<std::string> extensions;
|
||||
bool layerOutput = false;
|
||||
std::uint32_t version = 0;
|
||||
};
|
||||
|
||||
Module inspect(const std::vector<std::uint32_t>& words) {
|
||||
Module module;
|
||||
if (words.size() > 1) module.version = words[1];
|
||||
std::vector<std::uint32_t> layerIds;
|
||||
for (std::size_t at = 5; at < words.size();) {
|
||||
const auto length = words[at] >> 16u;
|
||||
const auto opcode = words[at] & 0xffffu;
|
||||
if (length == 0 || at + length > words.size()) break;
|
||||
if (opcode == 17u && length == 2) module.capabilities.push_back(words[at + 1]);
|
||||
if (opcode == 10u && length >= 2) module.extensions.emplace_back(reinterpret_cast<const char*>(&words[at + 1]));
|
||||
if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) module.layerOutput = true;
|
||||
at += length;
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
bool has(const std::vector<std::uint32_t>& values, std::uint32_t value) {
|
||||
for (const auto entry : values) {
|
||||
if (entry == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool named(const std::vector<std::string>& values, std::string_view value) {
|
||||
for (const auto entry : values) {
|
||||
if (entry == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Module recompile(std::uint32_t control, bool layerExtension) {
|
||||
using namespace ShaderRecompiler;
|
||||
static const std::array<std::uint32_t, 5> code{0xf80000cfu, 0x00000000u, 0xf80008dfu, 0x01010101u, 0xbf810000u};
|
||||
static const std::array<std::uint32_t, 3> withExtension{1u, 32u, 5254u};
|
||||
static const std::array<std::uint32_t, 2> without{1u, 32u};
|
||||
ShaderVertexStageInfo vertex{};
|
||||
vertex.paClVsOutCntl = control;
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Vertex, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.vertex = vertex;
|
||||
request.target.vulkanVersion = layerExtension ? 0x00401000u : 0x00402000u;
|
||||
request.target.spirvVersion = layerExtension ? 0x00010300u : 0x00010500u;
|
||||
request.target.subgroupSize = 64;
|
||||
if (layerExtension) request.target.supportedCapabilities = withExtension;
|
||||
else request.target.supportedCapabilities = without;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
return inspect(Recompile(request).spirv);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
constexpr std::uint32_t layer = (1u << 18u) | (1u << 21u);
|
||||
const auto extension = recompile(layer, true);
|
||||
if (!extension.layerOutput || !has(extension.capabilities, 5254u) || has(extension.capabilities, 69u) || !named(extension.extensions, "SPV_EXT_shader_viewport_index_layer") || extension.version != 0x00010300u) {
|
||||
std::fprintf(stderr, "a vertex layer export with VK_EXT_shader_viewport_index_layer: layer %d, version 0x%x\n", extension.layerOutput ? 1 : 0, extension.version);
|
||||
return 1;
|
||||
}
|
||||
const auto core = recompile(layer, false);
|
||||
if (!core.layerOutput || !has(core.capabilities, 69u) || has(core.capabilities, 5254u) || core.version != 0x00010500u) {
|
||||
std::fprintf(stderr, "a vertex layer export without the extension: layer %d, version 0x%x\n", core.layerOutput ? 1 : 0, core.version);
|
||||
return 1;
|
||||
}
|
||||
const auto ignored = recompile(0u, true);
|
||||
if (ignored.layerOutput || has(ignored.capabilities, 5254u)) {
|
||||
std::fprintf(stderr, "a misc export without USE_VTX_RENDER_TARGET_INDX wrote gl_Layer\n");
|
||||
return 1;
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "unexpected error: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -562,6 +562,142 @@ void CheckUnwatchedImport() {
|
|||
#endif
|
||||
}
|
||||
|
||||
class PartlyCommitted {
|
||||
public:
|
||||
static constexpr std::size_t Bytes = 4 * Block;
|
||||
|
||||
PartlyCommitted() {
|
||||
#ifdef _WIN32
|
||||
memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block));
|
||||
#else
|
||||
void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot reserve the partly committed range");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast<void*>(aligned + Bytes), begin + Block - aligned);
|
||||
memory = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
#endif
|
||||
Require(memory != nullptr, "cannot reserve the partly committed range");
|
||||
}
|
||||
|
||||
~PartlyCommitted() {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(memory, Bytes);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes);
|
||||
munmap(memory, Bytes);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
PartlyCommitted(const PartlyCommitted&) = delete;
|
||||
PartlyCommitted& operator=(const PartlyCommitted&) = delete;
|
||||
|
||||
void Commit(std::size_t block) {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block);
|
||||
#else
|
||||
Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range");
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block);
|
||||
#endif
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block);
|
||||
}
|
||||
|
||||
void Decommit(std::size_t block) {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block);
|
||||
Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range");
|
||||
#endif
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block);
|
||||
}
|
||||
|
||||
std::uint8_t* Data() const { return memory; }
|
||||
std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast<std::uint64_t>(memory) + block * Block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* memory = nullptr;
|
||||
};
|
||||
|
||||
using Runs = std::vector<std::pair<std::uint64_t, std::uint64_t>>;
|
||||
|
||||
std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) {
|
||||
BumpCollectEpoch();
|
||||
return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs);
|
||||
}
|
||||
|
||||
void CheckCommittedRanges() {
|
||||
PartlyCommitted range;
|
||||
const auto base = range.Address();
|
||||
range.Commit(0);
|
||||
std::memset(range.Data(), 0x11, Block);
|
||||
Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole");
|
||||
Runs runs;
|
||||
const Runs first{{base, base + Block}};
|
||||
const auto synced = CollectCommitted(range, runs);
|
||||
Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected");
|
||||
Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed");
|
||||
|
||||
range.Data()[8] = 0x22;
|
||||
const auto written = CollectCommitted(range, runs);
|
||||
Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed");
|
||||
Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed");
|
||||
Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed");
|
||||
|
||||
const auto beforeCommit = CollectCommitted(range, runs);
|
||||
range.Commit(2);
|
||||
const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}};
|
||||
const auto grown = CollectCommitted(range, runs);
|
||||
Require(grown != 0 && runs == both, "a block committed inside the range is not described");
|
||||
Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit");
|
||||
Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect");
|
||||
|
||||
range.Decommit(2);
|
||||
range.Commit(2);
|
||||
const auto recommitted = CollectCommitted(range, runs);
|
||||
Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged");
|
||||
|
||||
range.Decommit(2);
|
||||
Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed");
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr);
|
||||
HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr);
|
||||
struct Sections {
|
||||
HANDLE first;
|
||||
HANDLE second;
|
||||
~Sections() {
|
||||
if (first != nullptr) CloseHandle(first);
|
||||
if (second != nullptr) CloseHandle(second);
|
||||
}
|
||||
} sections{firstSection, secondSection};
|
||||
Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range");
|
||||
const auto beforeMap = CollectCommitted(range, runs);
|
||||
GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
|
||||
const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}};
|
||||
const auto mapped = CollectCommitted(range, runs);
|
||||
Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described");
|
||||
Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping");
|
||||
Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed");
|
||||
GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block);
|
||||
GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
|
||||
Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged");
|
||||
GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
|
||||
Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed");
|
||||
#endif
|
||||
|
||||
range.Decommit(0);
|
||||
Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty");
|
||||
Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts");
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
if (!WriteWatched()) {
|
||||
|
|
@ -587,6 +723,7 @@ int main() {
|
|||
CheckCollectBoundaries();
|
||||
CheckImportWindow();
|
||||
CheckUnwatchedImport();
|
||||
CheckCommittedRanges();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "write tracking test failed: " << error.what() << "\n";
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -165,22 +165,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin
|
|||
}
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode) {
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
||||
cx[0x31b] = slice | (slice << 13u);
|
||||
cx[0x31c] = Color32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u);
|
||||
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (Layers - 1u);
|
||||
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u);
|
||||
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
||||
const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array");
|
||||
Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
|
||||
if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array");
|
||||
else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
|
||||
return color;
|
||||
}
|
||||
|
||||
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode) {
|
||||
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::size_t at = 0;
|
||||
|
|
@ -207,7 +208,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
|
|||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
const auto color = DecodeTarget(surface, slice, swizzleMode);
|
||||
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed);
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
|
|
@ -246,29 +247,31 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
|
|||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool collapsed = false, std::uint32_t slice = 1) {
|
||||
const auto surface = AddressOf(texels);
|
||||
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / 4u, Initial);
|
||||
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
|
||||
AgcDriver::GuestMemory::BumpCollectEpoch();
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
|
||||
DrawSlice(device, surface, 1, swizzleMode);
|
||||
const auto pending = StorageTexture::FindPending(surface, SurfaceBytes);
|
||||
Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array");
|
||||
LoadLayer(device, surface, 1, swizzleMode);
|
||||
RequireOutput(drawn, Initial, "slice 1 read through the array after the draw");
|
||||
for (const std::uint32_t other : {0u, 2u, 3u}) {
|
||||
DrawSlice(device, surface, slice, swizzleMode, collapsed);
|
||||
const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes);
|
||||
if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface");
|
||||
else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array");
|
||||
LoadLayer(device, surface, slice, swizzleMode);
|
||||
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw");
|
||||
for (std::uint32_t other = 0; other < Layers; ++other) {
|
||||
if (other == slice) continue;
|
||||
LoadLayer(device, surface, other, swizzleMode);
|
||||
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1");
|
||||
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice " + std::to_string(slice));
|
||||
}
|
||||
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::ClearCachedTextures(device.Device());
|
||||
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
|
||||
AgcDriver::GuestMemory::BumpCollectEpoch();
|
||||
LoadLayer(device, surface, 1, swizzleMode);
|
||||
RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory");
|
||||
LoadLayer(device, surface, slice, swizzleMode);
|
||||
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory");
|
||||
AgcDriver::Graphics::ClearCachedTextures(device.Device());
|
||||
}
|
||||
|
||||
|
|
@ -281,6 +284,7 @@ int main() {
|
|||
if (!device) return VulkanTestSkipped;
|
||||
Run(*device, block.Data(), TileR64KBX);
|
||||
Run(*device, block.Data(), static_cast<std::uint32_t>(AgcDriver::Graphics::ColorTileMode::D64KBX));
|
||||
for (std::uint32_t slice = 1; slice < Layers; ++slice) Run(*device, block.Data(), TileR64KBX, true, slice);
|
||||
std::puts("array layer target tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue