mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-11 17:19:31 -10:00
Compare commits
754
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f787306fce | ||
|
|
d8bf54d1e6 | ||
|
|
f165e7fa29 | ||
|
|
9d73f6a26a | ||
|
|
4a60f12112 | ||
|
|
192be7194f | ||
|
|
5f3caf9be4 | ||
|
|
571ef142c5 | ||
|
|
21845f7949 | ||
|
|
27a4dc65c9 | ||
|
|
deaab60ba9 | ||
|
|
c2c3cea8e3 | ||
|
|
a5dbd2c5c1 | ||
|
|
4c4de96fa5 | ||
|
|
c19aaf98e5 | ||
|
|
30a7b86bc8 | ||
|
|
0e0f95efa6 | ||
|
|
483497ef1c | ||
|
|
f7ba023dc6 | ||
|
|
cad46c5ff5 | ||
|
|
654bed3a7f | ||
|
|
6154ce2282 | ||
|
|
5e331b277a | ||
|
|
96712da3cb | ||
|
|
755ba0b9a5 | ||
|
|
29223492fc | ||
|
|
d5f9e06dac | ||
|
|
26d4f3f273 | ||
|
|
fc71cbfeed | ||
|
|
0d55f3075b | ||
|
|
e72b69caca | ||
|
|
24b778c3b4 | ||
|
|
e1895229b8 | ||
|
|
cf79b811ac | ||
|
|
2b86defaa7 | ||
|
|
0da40f990f | ||
|
|
d63ae80019 | ||
|
|
cd5c829460 | ||
|
|
14ff11b96b | ||
|
|
12bbd34fe5 | ||
|
|
4a015bfb7c | ||
|
|
aaad7c57c1 | ||
|
|
82a17c59ae | ||
|
|
646722c466 | ||
|
|
02f9ee4629 | ||
|
|
902c147f8e | ||
|
|
92166e6236 | ||
|
|
eb6536a832 | ||
|
|
cf26b92d45 | ||
|
|
d9d89df9d8 | ||
|
|
687caa60c4 | ||
|
|
9503016e93 | ||
|
|
cce82d001c | ||
|
|
f9d2eddb83 | ||
|
|
d66d48d9a3 | ||
|
|
aab654d4e8 | ||
|
|
b7eeb79a33 | ||
|
|
db7b03ffda | ||
|
|
e27a62210c | ||
|
|
06e9cfe6da | ||
|
|
b6941b78f2 | ||
|
|
d64883cef3 | ||
|
|
7ab3a23f95 | ||
|
|
07050a34bd | ||
|
|
9054341b05 | ||
|
|
30db6aa7fd | ||
|
|
663e7457ab | ||
|
|
2b375bf38c | ||
|
|
172cec105a | ||
|
|
5b28083ef3 | ||
|
|
3072b90817 | ||
|
|
02c7045ff6 | ||
|
|
4c29ce57c7 | ||
|
|
291b2c2b03 | ||
|
|
9fa96cab6f | ||
|
|
892c64cc69 | ||
|
|
f8e59c3e3b | ||
|
|
6fad4fb984 | ||
|
|
32ed49b6d8 | ||
|
|
35992267b9 | ||
|
|
95d025f4c2 | ||
|
|
40b9d160c9 | ||
|
|
984872a0dd | ||
|
|
4d00052e33 | ||
|
|
51b9a663bd | ||
|
|
76557beb69 | ||
|
|
bf955abf30 | ||
|
|
2d2ebb8fa5 | ||
|
|
4aec48949f | ||
|
|
929928d3e6 | ||
|
|
1dcb48e1ec | ||
|
|
80570541d0 | ||
|
|
a792e58186 | ||
|
|
cc15269fb6 | ||
|
|
1177119180 | ||
|
|
2bfb755693 | ||
|
|
44699b4c51 | ||
|
|
3f287372f4 | ||
|
|
d3e0272348 | ||
|
|
818e0a229e | ||
|
|
09fa6403c5 | ||
|
|
cad3d55f76 | ||
|
|
0d85f3f65f | ||
|
|
39046b18e8 | ||
|
|
370854a657 | ||
|
|
46d26691cf | ||
|
|
713fe9176d | ||
|
|
66319c12e6 | ||
|
|
31b0e68219 | ||
|
|
8bf7dd6dfe | ||
|
|
5814b1cb5f | ||
|
|
59cd7697be | ||
|
|
063632d5f5 | ||
|
|
8424af6894 | ||
|
|
a0d2e7d935 | ||
|
|
7e779ceb8b | ||
|
|
5481445402 | ||
|
|
2c62ea5aaa | ||
|
|
0bd5635d74 | ||
|
|
53ef669745 | ||
|
|
44d13ad5d4 | ||
|
|
f66e1a8d91 | ||
|
|
83703c28b2 | ||
|
|
634df8744a | ||
|
|
e8b23d06ed | ||
|
|
c1894da34b | ||
|
|
1b016ba22e | ||
|
|
0482839c1b | ||
|
|
023cef202f | ||
|
|
97f41ca90f | ||
|
|
fdc97df507 | ||
|
|
09fdaa392a | ||
|
|
6e5270105b | ||
|
|
7c9db3e706 | ||
|
|
e8b01b7282 | ||
|
|
f22a814607 | ||
|
|
fd7c423476 | ||
|
|
731699144c | ||
|
|
b14027d065 | ||
|
|
5133e20195 | ||
|
|
1fc67d4d10 | ||
|
|
1511bd7a23 | ||
|
|
3e75afe983 | ||
|
|
29df2938b1 | ||
|
|
87949f0a24 | ||
|
|
c5e63f9f06 | ||
|
|
09de8c7bbb | ||
|
|
bc6dbfe589 | ||
|
|
0808d1f189 | ||
|
|
5a4d8c11e6 | ||
|
|
54ea398de4 | ||
|
|
219d6b6b4e | ||
|
|
71d150b028 | ||
|
|
251c800717 | ||
|
|
6c58b77fed | ||
|
|
02b3da3eec | ||
|
|
4676650694 | ||
|
|
9aae4a6671 | ||
|
|
660288e50b | ||
|
|
53ffc05840 | ||
|
|
5b275cd324 | ||
|
|
05c0a3942f | ||
|
|
fa34419e4b | ||
|
|
a640a5a91a | ||
|
|
7475a27b7b | ||
|
|
721a16e17b | ||
|
|
20c3b24dab | ||
|
|
8d60a65c6c | ||
|
|
72d6a5b383 | ||
|
|
b1104143ef | ||
|
|
1c98b67b44 | ||
|
|
1299432e7a | ||
|
|
271bace8e1 | ||
|
|
195dceeb94 | ||
|
|
dec1f7bd51 | ||
|
|
3943570b53 | ||
|
|
a5b58a242b | ||
|
|
8dc176f4e6 | ||
|
|
6494dc92e0 | ||
|
|
13fe0905ba | ||
|
|
7cae9ff3c4 | ||
|
|
d9d0c7ad13 | ||
|
|
d60a13d0d3 | ||
|
|
38e2c2522a | ||
|
|
8c46ceea46 | ||
|
|
7d7fa98a44 | ||
|
|
fd2f881a32 | ||
|
|
0f209ac875 | ||
|
|
de7ceb78d7 | ||
|
|
3298a6dcd5 | ||
|
|
efa58613aa | ||
|
|
2ddce27f40 | ||
|
|
48cef8f361 | ||
|
|
ec56b95628 | ||
|
|
8d44c49cd3 | ||
|
|
1d1d79ebc1 | ||
|
|
f7453ff16f | ||
|
|
28f0c9802e | ||
|
|
e1a97b5783 | ||
|
|
cb5de34db6 | ||
|
|
9f72537d21 | ||
|
|
06f24ae718 | ||
|
|
fa68c60740 | ||
|
|
04d109da79 | ||
|
|
936e9e0cba | ||
|
|
373c49af50 | ||
|
|
1ec82f9a9f | ||
|
|
45c080011c | ||
|
|
0fa6d4d68e | ||
|
|
6e344a9fd6 | ||
|
|
1d3ed915df | ||
|
|
f5ccf953c4 | ||
|
|
0179d86295 | ||
|
|
5b03edb56e | ||
|
|
d62d4787ab | ||
|
|
7e2abf5fc4 | ||
|
|
17273e22ba | ||
|
|
81d944457c | ||
|
|
f2b210b1f1 | ||
|
|
d77c945717 | ||
|
|
b4ce8a563b | ||
|
|
c9df2c9f93 | ||
|
|
62a8e1f9fa | ||
|
|
c0cfcda90e | ||
|
|
68d5cd7114 | ||
|
|
37d6df3219 | ||
|
|
59b2aed66c | ||
|
|
b9abf293aa | ||
|
|
cb8761a18c | ||
|
|
6b734b015b | ||
|
|
bd4cf5bf17 | ||
|
|
f5dbdcadba | ||
|
|
ebcccff515 | ||
|
|
d361eaf394 | ||
|
|
bae784326f | ||
|
|
bea5186176 | ||
|
|
fe4d3a2e94 | ||
|
|
f9a650f3ac | ||
|
|
e3ba3ae992 | ||
|
|
5a7076f3ea | ||
|
|
e1eb285cc7 | ||
|
|
786b48499c | ||
|
|
f782dd4326 | ||
|
|
7955d4c512 | ||
|
|
6858deccf1 | ||
|
|
d16d9b6d15 | ||
|
|
855f1f9043 | ||
|
|
16b7843619 | ||
|
|
9c306d6f5b | ||
|
|
2a095d62d5 | ||
|
|
60ffd2a342 | ||
|
|
449ebe1366 | ||
|
|
55a7086663 | ||
|
|
51094095b2 | ||
|
|
b3b496a449 | ||
|
|
b7db4654ee | ||
|
|
af4217724b | ||
|
|
27f8cff837 | ||
|
|
1b5be38573 | ||
|
|
1ebd5f32ab | ||
|
|
51ca66993a | ||
|
|
3af468a074 | ||
|
|
f0ccc014ad | ||
|
|
de5b2f94db | ||
|
|
8edbc6760d | ||
|
|
d47d019d27 | ||
|
|
5c03db3283 | ||
|
|
33f8d2f80c | ||
|
|
5d0aeb8197 | ||
|
|
91a474ae11 | ||
|
|
827b18454f | ||
|
|
6edd7b88a9 | ||
|
|
44316071c3 | ||
|
|
d2cf911a6d | ||
|
|
2b4656e70f | ||
|
|
548a41afa2 | ||
|
|
f15d4a5845 | ||
|
|
be7e745e48 | ||
|
|
b67388bf60 | ||
|
|
f738f99adb | ||
|
|
b94b07ff5e | ||
|
|
2106e9b21e | ||
|
|
f5a3990fbf | ||
|
|
b1cdc91bf4 | ||
|
|
e7cdaa120a | ||
|
|
58a9cbddca | ||
|
|
56d067048f | ||
|
|
175c1a69a6 | ||
|
|
fa5f2c393e | ||
|
|
d53c507d5e | ||
|
|
b5c00e8040 | ||
|
|
23ae7d42c2 | ||
|
|
1aa3ad9441 | ||
|
|
fc67c07589 | ||
|
|
190aa00a63 | ||
|
|
a4266531f2 | ||
|
|
bb6b34d530 | ||
|
|
be2f80a4cf | ||
|
|
12e1d88d71 | ||
|
|
1d2b14b01d | ||
|
|
1a9bf76f69 | ||
|
|
715402eb80 | ||
|
|
18e26a3e4d | ||
|
|
95756af921 | ||
|
|
72e4d61b57 | ||
|
|
6604778b58 | ||
|
|
ce25b504c8 | ||
|
|
5a281cec36 | ||
|
|
33a70e3fa4 | ||
|
|
f5ea4bde3a | ||
|
|
251976843a | ||
|
|
8e17b81335 | ||
|
|
1bcc5ec3f9 | ||
|
|
9ac80b72b7 | ||
|
|
a8d3ffaf2e | ||
|
|
884fc3f5b1 | ||
|
|
da035b00ec | ||
|
|
f2b07381ab | ||
|
|
bee546c085 | ||
|
|
80392718df | ||
|
|
122a7650ad | ||
|
|
c4c5649ce9 | ||
|
|
da7c9d05b3 | ||
|
|
b61cff5625 | ||
|
|
938c04c32e | ||
|
|
37f6d169d7 | ||
|
|
ebcd7beedb | ||
|
|
1e45ee6ed3 | ||
|
|
8bf3aa3982 | ||
|
|
1ae7d9053c | ||
|
|
d6dd30c7ce | ||
|
|
7c723354eb | ||
|
|
c9675c1ba5 | ||
|
|
debbbf6585 | ||
|
|
ac4204f395 | ||
|
|
8215f3afef | ||
|
|
7714b1d37a | ||
|
|
9e701221fd | ||
|
|
02b49472ef | ||
|
|
5a76ab5f51 | ||
|
|
ef218f6ced | ||
|
|
8e4d76ef04 | ||
|
|
a8f543b1cc | ||
|
|
5f61926401 | ||
|
|
6cd362ab78 | ||
|
|
39f876471f | ||
|
|
ad8dd2f57e | ||
|
|
98b40e8e5e | ||
|
|
018ca6a76e | ||
|
|
242fba5b5a | ||
|
|
03decca8b2 | ||
|
|
d2a9fcd3f3 | ||
|
|
937cadf658 | ||
|
|
3e2b1d6d19 | ||
|
|
dc8f30b7e8 | ||
|
|
e5dedb5239 | ||
|
|
f19afb61ad | ||
|
|
d9eab08f7f | ||
|
|
d404575740 | ||
|
|
c6f61bbcc4 | ||
|
|
70865d77e8 | ||
|
|
d526f569a0 | ||
|
|
957ba89eac | ||
|
|
c4146a25af | ||
|
|
c59deaa19b | ||
|
|
fb6b8c54e2 | ||
|
|
d1a243289c | ||
|
|
eb18b83652 | ||
|
|
c4889805f3 | ||
|
|
57e1e16ca2 | ||
|
|
b52d83f71e | ||
|
|
7aef48e484 | ||
|
|
72c066552e | ||
|
|
3f254035ec | ||
|
|
5644d16a61 | ||
|
|
d3da93ba30 | ||
|
|
545b8f9e8d | ||
|
|
906b9143f3 | ||
|
|
8c893c55f0 | ||
|
|
c2e172a8a3 | ||
|
|
9147b0893b | ||
|
|
004628230c | ||
|
|
4d8fbac921 | ||
|
|
4e43f7a14f | ||
|
|
75cfb4cc28 | ||
|
|
dd60b729ee | ||
|
|
651abefe2b | ||
|
|
8858d0c7e0 | ||
|
|
df589fa039 | ||
|
|
2c8c04518d | ||
|
|
226d535e77 | ||
|
|
b0ba94929e | ||
|
|
e592fc3af0 | ||
|
|
ddb70f7a42 | ||
|
|
505c938cad | ||
|
|
adbb0b4578 | ||
|
|
2aeaeb2495 | ||
|
|
782331b499 | ||
|
|
0f1921c196 | ||
|
|
30e4e488b2 | ||
|
|
e2ee8ad7d9 | ||
|
|
c59a4d6ffd | ||
|
|
4256dec408 | ||
|
|
959c3b6180 | ||
|
|
e00864ba59 | ||
|
|
9062522b9d | ||
|
|
fa0e2b01cd | ||
|
|
f663c83f98 | ||
|
|
4740691c04 | ||
|
|
853ec54bd3 | ||
|
|
ee8d4c4a72 | ||
|
|
4fcb17840a | ||
|
|
fb05c1acfc | ||
|
|
8e4e46fdd6 | ||
|
|
9351cd381f | ||
|
|
724b70e606 | ||
|
|
72e3ec1924 | ||
|
|
278dee9871 | ||
|
|
08eed8f8a7 | ||
|
|
7a8364a0f7 | ||
|
|
8eb87f05ff | ||
|
|
c5406b0bf0 | ||
|
|
2f3a3e302d | ||
|
|
17acc701da | ||
|
|
c96f20ac6c | ||
|
|
fb6707ec92 | ||
|
|
dfc7fdd786 | ||
|
|
306dca0a21 | ||
|
|
541bc4b519 | ||
|
|
3c17bd7960 | ||
|
|
bdef01a46c | ||
|
|
a005b741f2 | ||
|
|
4f699a47f4 | ||
|
|
26d32147ea | ||
|
|
b5c4b2f84b | ||
|
|
0b2ed50a68 | ||
|
|
75ef2801f5 | ||
|
|
699cf1101f | ||
|
|
386d0560a1 | ||
|
|
22f000349e | ||
|
|
8101d02e21 | ||
|
|
07af125581 | ||
|
|
4d6923514e | ||
|
|
5e5258f410 | ||
|
|
d9672a3587 | ||
|
|
e9198b0004 | ||
|
|
bf26fb0d64 | ||
|
|
e1255c4465 | ||
|
|
2a94e35372 | ||
|
|
8b16965108 | ||
|
|
f306365eb8 | ||
|
|
68a4fb2261 | ||
|
|
27b18c07f0 | ||
|
|
94279ded10 | ||
|
|
ad43388bb5 | ||
|
|
02ae7bfa26 | ||
|
|
0ba1641e9c | ||
|
|
7bf8f4b226 | ||
|
|
a06a1853e2 | ||
|
|
ba8b8cda4a | ||
|
|
b972efd9fa | ||
|
|
d9cf87a5a8 | ||
|
|
c635024f69 | ||
|
|
8a4ca5e7de | ||
|
|
2760a81580 | ||
|
|
1ab052ca3c | ||
|
|
67e0a30ea8 | ||
|
|
02d449ced2 | ||
|
|
e8e1971422 | ||
|
|
20ea6e500e | ||
|
|
5cc88b517b | ||
|
|
4591f41aeb | ||
|
|
761650fe14 | ||
|
|
2e8cc55201 | ||
|
|
316e0ee2b0 | ||
|
|
e725b49629 | ||
|
|
fd0574f5cf | ||
|
|
41a579d428 | ||
|
|
753df1c63a | ||
|
|
a47aa92bcd | ||
|
|
f790048622 | ||
|
|
0b21fcbf60 | ||
|
|
cda60d91ae | ||
|
|
3beead5ef0 | ||
|
|
9298d7961c | ||
|
|
1ee6ac1ed9 | ||
|
|
c4ec5dfd57 | ||
|
|
407bc867f4 | ||
|
|
c3f9c44e15 | ||
|
|
1110d526f2 | ||
|
|
dc226c105c | ||
|
|
976dd1cc06 | ||
|
|
e1e8ea3822 | ||
|
|
f2a4074c91 | ||
|
|
f3a6fbbdbf | ||
|
|
c793eeaf45 | ||
|
|
cc76626978 | ||
|
|
ee3e2699ca | ||
|
|
9c702e54fe | ||
|
|
b6a0f352eb | ||
|
|
a0b9b67f4a | ||
|
|
0d242f4125 | ||
|
|
67aab17c4e | ||
|
|
4d0178d549 | ||
|
|
1e13e7a7e2 | ||
|
|
e61aa0e8b0 | ||
|
|
fd4d917b90 | ||
|
|
8ef9fdfb0c | ||
|
|
94a37b8562 | ||
|
|
c9b853dda2 | ||
|
|
b5906ff6d5 | ||
|
|
e8d176ca8d | ||
|
|
0f157e4f3a | ||
|
|
eb8de82c63 | ||
|
|
394455b62e | ||
|
|
79e5720a76 | ||
|
|
6d91dd0c76 | ||
|
|
a5fe3eea40 | ||
|
|
d9146d02b5 | ||
|
|
fca27e4c63 | ||
|
|
c1aba1d3f4 | ||
|
|
f61130508b | ||
|
|
4e7219047b | ||
|
|
7ab5bfd238 | ||
|
|
e347c6f99f | ||
|
|
5d5e2712ad | ||
|
|
f03a2ffe21 | ||
|
|
34a9b0144c | ||
|
|
4a42d89978 | ||
|
|
76178bacf0 | ||
|
|
3e4dce841c | ||
|
|
bd11bddc68 | ||
|
|
55e719d3b7 | ||
|
|
1f230b80a9 | ||
|
|
4bdf605756 | ||
|
|
4e3e68b758 | ||
|
|
a98cde40f8 | ||
|
|
afb34764ec | ||
|
|
4afaf25272 | ||
|
|
445b1bbd4c | ||
|
|
db693c8f36 | ||
|
|
f39838d4b2 | ||
|
|
33a0458cf2 | ||
|
|
d46bea7851 | ||
|
|
a25182804a | ||
|
|
dd178e638f | ||
|
|
ea435acad4 | ||
|
|
71eb66aa68 | ||
|
|
92ce70e08a | ||
|
|
2d9d67e048 | ||
|
|
b689c295dc | ||
|
|
a77e5ec7f3 | ||
|
|
ebe873fb5e | ||
|
|
aef2b895df | ||
|
|
cbe92651a1 | ||
|
|
d51f612f54 | ||
|
|
f7ff9d2410 | ||
|
|
4b8f61ed0e | ||
|
|
309da9f821 | ||
|
|
d81eb7ffe9 | ||
|
|
922c047e22 | ||
|
|
175998d817 | ||
|
|
1f7d2adcbe | ||
|
|
a445b69634 | ||
|
|
5b0d6e7047 | ||
|
|
2d7f975701 | ||
|
|
ce5491d573 | ||
|
|
2f0e97de09 | ||
|
|
d5adb5fc37 | ||
|
|
5895774d7e | ||
|
|
827ff272ab | ||
|
|
134eb9f0a6 | ||
|
|
fc83f106f2 | ||
|
|
4471b929a3 | ||
|
|
89e9f69c8c | ||
|
|
80ae207954 | ||
|
|
69a156f587 | ||
|
|
d1d35697a1 | ||
|
|
6a9154801f | ||
|
|
ded3d99728 | ||
|
|
72c06622be | ||
|
|
72fe5b13ba | ||
|
|
aaf5aa6fcf | ||
|
|
eaa027179b | ||
|
|
bbaa73ec43 | ||
|
|
d3c8eb00ea | ||
|
|
d42021f1d2 | ||
|
|
2b3062609c | ||
|
|
9b8c6dc0b0 | ||
|
|
1ff33fd853 | ||
|
|
c9a064bbc5 | ||
|
|
3831c9df9f | ||
|
|
8f96b0b6bb | ||
|
|
3365857605 | ||
|
|
c98211b51b | ||
|
|
21023c478e | ||
|
|
71709ecb3f | ||
|
|
7cc8eae1d1 | ||
|
|
5885feff36 | ||
|
|
3e6842009d | ||
|
|
2b01dbcd5e | ||
|
|
ca8cc5a43b | ||
|
|
dbef6e3e14 | ||
|
|
5f61de11d9 | ||
|
|
1ef6e26dfa | ||
|
|
2c9aa0ac86 | ||
|
|
605f1ead0c | ||
|
|
92dab6cafc | ||
|
|
3adf1bbe97 | ||
|
|
7fad4b662d | ||
|
|
2139b4464a | ||
|
|
c37356308c | ||
|
|
7d0ab4d38a | ||
|
|
ac2a95ab1d | ||
|
|
faef0581d1 | ||
|
|
4df6ddc3f3 | ||
|
|
34d493fb45 | ||
|
|
b4a5943911 | ||
|
|
86f629b61e | ||
|
|
dc10ffbf02 | ||
|
|
8b7a7d5cfe | ||
|
|
e96b2ff99a | ||
|
|
5f234b1b99 | ||
|
|
f284d61b06 | ||
|
|
09de0cc427 | ||
|
|
f23a03f6c7 | ||
|
|
644d598685 | ||
|
|
afd4df1cd9 | ||
|
|
3606b9ea41 | ||
|
|
de91e0b39d | ||
|
|
2ffd4f31c2 | ||
|
|
41b4268ace | ||
|
|
6d6dd49139 | ||
|
|
37249efd34 | ||
|
|
98498da9aa | ||
|
|
536c0f938b | ||
|
|
a2b63b3e3b | ||
|
|
1f3f7cebae | ||
|
|
66013b7277 | ||
|
|
316bef4484 | ||
|
|
a203a7255b | ||
|
|
6260f30e4e | ||
|
|
cbbb38b654 | ||
|
|
f4ddbb4327 | ||
|
|
29968525ac | ||
|
|
7cb62d72e4 | ||
|
|
a912cb9c47 | ||
|
|
fe962aa5f6 | ||
|
|
7c4cdcea29 | ||
|
|
9972a26864 | ||
|
|
6e73d47b54 | ||
|
|
4802e85471 | ||
|
|
218816fbf6 | ||
|
|
a5b8c780bf | ||
|
|
1159d8f197 | ||
|
|
0817fddb83 | ||
|
|
c1bbcf1535 | ||
|
|
398bec3d6e | ||
|
|
3c11bce138 | ||
|
|
8057b16916 | ||
|
|
3750bd11b1 | ||
|
|
07c4f9364b | ||
|
|
fb6b997e15 | ||
|
|
73b311e9d1 | ||
|
|
72f206a80c | ||
|
|
f3fdefa0ec | ||
|
|
1e4f136720 | ||
|
|
a8aca2ba12 | ||
|
|
125308948a | ||
|
|
9ec09e4714 | ||
|
|
57e9e2b751 | ||
|
|
7837d91790 | ||
|
|
53b7a6768d | ||
|
|
9d025d7e3c | ||
|
|
1a4e885e2c | ||
|
|
945c667281 | ||
|
|
8d7267f7f7 | ||
|
|
bdc0092803 | ||
|
|
c3834bb29d | ||
|
|
9e82a7acd1 | ||
|
|
0eb2196f99 | ||
|
|
2b579a4067 | ||
|
|
4eee03985c | ||
|
|
10b318cd6b | ||
|
|
1414d6350f | ||
|
|
fe35609011 | ||
|
|
ef090d751b | ||
|
|
26afbb999c | ||
|
|
d906242b0e | ||
|
|
43851ba7e7 | ||
|
|
0317c86476 | ||
|
|
dbf99fe456 | ||
|
|
7842fca4da | ||
|
|
ef01959fb1 | ||
|
|
ec2cd929ab | ||
|
|
6f79c55c2a | ||
|
|
529cd743a0 | ||
|
|
3877ae52d8 | ||
|
|
e364221b4b | ||
|
|
b23b5a810f | ||
|
|
89faf5b548 | ||
|
|
a1d48cf2c0 | ||
|
|
a9f390ee3b | ||
|
|
956eb692ea | ||
|
|
0f6227abba | ||
|
|
166073d4bc | ||
|
|
5a589fb0f0 | ||
|
|
a9ce60dde5 | ||
|
|
e632dda2b7 | ||
|
|
d22f313e9e | ||
|
|
71e2401edc | ||
|
|
7c94c21638 | ||
|
|
e957a4bd85 | ||
|
|
89f90f0b65 | ||
|
|
f965ae85f3 | ||
|
|
983d9a3235 | ||
|
|
87c473666a | ||
|
|
1c9f8a4359 | ||
|
|
d0850614ba | ||
|
|
f4d10e7099 | ||
|
|
1b029a08f6 | ||
|
|
8a5211f260 | ||
|
|
0831968204 | ||
|
|
1e8745416d | ||
|
|
9fc9722501 | ||
|
|
adfd8e43c8 | ||
|
|
f7267efa1b | ||
|
|
d288db6b6e | ||
|
|
e8327d7baa | ||
|
|
51ff5618de | ||
|
|
35fc116bd3 | ||
|
|
21f90219c6 | ||
|
|
ede2ada183 | ||
|
|
2a95475e8f | ||
|
|
732dfb1cb0 | ||
|
|
2d6f393adb | ||
|
|
713f1bd2b1 | ||
|
|
47894e831f | ||
|
|
5e735bd030 | ||
|
|
da4591bc88 | ||
|
|
a6a7b85f9e | ||
|
|
69cb922604 | ||
|
|
ee9bae684c | ||
|
|
da4e938c99 | ||
|
|
57a3d9bb51 | ||
|
|
b2d8cfa7f1 | ||
|
|
366d731251 | ||
|
|
c3a16de36d | ||
|
|
fd7dfc59df | ||
|
|
507006a08d | ||
|
|
ec468766cd | ||
|
|
7e176a3b9e | ||
|
|
9dee3221a3 | ||
|
|
ab335d9ebb |
No files matched your search
@@ -36,8 +36,9 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.target }}
|
||||
- name: The aarch64 Linux linker
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
|
||||
+16
-12
@@ -29,16 +29,18 @@ jobs:
|
||||
# One job, not one per check: each job takes a runner from the organisation's shared pool, and with every pull request
|
||||
# and every merge to main starting this workflow, three jobs a run left the pool's queue tens of minutes deep.
|
||||
#
|
||||
# Where (docs/ci-runners.md): our checks runners (label horizon-checks: the desktop's, an isolated user, so a fork's
|
||||
# pull request runs there too) take the pull requests whose number ends in 0 or 1, about what their three runners
|
||||
# can do; GitHub's runners take the others and main's pushes (which also save the Rust cache GitHub's pull request
|
||||
# runs read), so both pools work at once. The repository variable SELF_HOSTED_CI moves them: `all` every job to
|
||||
# ours, `light` (the desktop paused) or `off` every job to GitHub's. On ours the build folder persists
|
||||
# Where (docs/ci-runners.md): our checks runners (label horizon-checks: the desktop's) take this repository's own
|
||||
# pull requests whose number ends in 0 or 1, about what their three runners can do; GitHub's runners take the others,
|
||||
# forks' pull requests and main's pushes (which also save the Rust cache GitHub's pull request runs read), so both
|
||||
# pools work at once. The repository variable SELF_HOSTED_CI moves them: `all` every job to ours, `light` (the
|
||||
# desktop paused) or `off` every job to GitHub's. A fork's pull request sees no repository variable, so no switch
|
||||
# could move it off a machine that is off: it always runs on GitHub's. On ours the build folder persists
|
||||
# (CARGO_TARGET_DIR, outside the checkout), so the apt packages, Python and the Rust cache steps are GitHub's only.
|
||||
verify:
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
|
||||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
(endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 45
|
||||
@@ -110,7 +112,7 @@ jobs:
|
||||
fi
|
||||
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: steps.rust.outputs.required != 'false' && runner.environment == 'github-hosted'
|
||||
with:
|
||||
shared-key: verify
|
||||
@@ -141,7 +143,8 @@ jobs:
|
||||
launcher:
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
|
||||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
(endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 45
|
||||
@@ -166,7 +169,7 @@ jobs:
|
||||
libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev libudev-dev
|
||||
fi
|
||||
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: steps.launcher.outputs.required != 'false' && runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: launcher
|
||||
@@ -199,13 +202,14 @@ jobs:
|
||||
run: |
|
||||
$cores = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
|
||||
"CARGO_BUILD_JOBS=$cores" >> $env:GITHUB_ENV
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: aarch64-pc-windows-msvc
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
with:
|
||||
shared-key: windows-arm64-game
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
with:
|
||||
workspaces: launcher
|
||||
shared-key: windows-arm64-launcher
|
||||
|
||||
@@ -50,7 +50,9 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
clean: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
- run: bash tools/ci/runner-env.sh
|
||||
env:
|
||||
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
|
||||
@@ -156,10 +158,11 @@ jobs:
|
||||
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
|
||||
# keeps its preparations outside the checkout.
|
||||
clean: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: runner.environment == 'github-hosted' && !matrix.musl
|
||||
with:
|
||||
# A release restores only what a release saved: an untrusted build has its own key and saves nothing.
|
||||
|
||||
@@ -173,10 +173,11 @@ jobs:
|
||||
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
|
||||
# parity jobs, and nothing they leave may end up in a launcher release (target/ is rebuilt from scratch).
|
||||
clean: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: launcher
|
||||
|
||||
@@ -20,8 +20,12 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
check:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's. A fork's
|
||||
# pull request sees no repository variable, so it always runs on GitHub's (it would wait for the desktop when off).
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' &&
|
||||
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
@@ -30,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Ours keep their build folder between jobs.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: crates/horizon-mod-registry
|
||||
|
||||
@@ -55,8 +55,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
# Keeps .local/ (parity state, probe builds, the previous audit) and target/.
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
if: steps.when.outputs.run == 'true'
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Tools from the runner
|
||||
if: steps.when.outputs.run == 'true'
|
||||
run: bash tools/ci/runner-env.sh
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
fi
|
||||
rustup toolchain install stable --profile minimal
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
shared-key: pack-bench
|
||||
|
||||
@@ -37,7 +37,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Tools from the runner
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Vanilla data, and resources prepared by this preparer
|
||||
|
||||
@@ -71,7 +71,9 @@ jobs:
|
||||
echo "::warning::head ${HEAD_SHA:0:9} conflicts with main ${main:0:9}: tested alone, without main's fixes"
|
||||
echo "Tested: head ${HEAD_SHA:0:9} alone (it conflicts with main ${main:0:9})" | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable, 2026-11
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Tools from the runner
|
||||
run: bash tools/ci/runner-env.sh
|
||||
- name: Vanilla data, and resources prepared by this preparer
|
||||
|
||||
@@ -20,8 +20,12 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
check:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's. A fork's
|
||||
# pull request sees no repository variable, so it always runs on GitHub's (it would wait for the desktop when off).
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' &&
|
||||
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
@@ -30,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Ours keep their build folder between jobs.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: crates/horizon-telemetry-server
|
||||
|
||||
Generated
+46
-2
@@ -706,6 +706,25 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.14"
|
||||
@@ -926,6 +945,12 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
@@ -1523,7 +1548,6 @@ dependencies = [
|
||||
"horizon-anticheat",
|
||||
"horizon-assets",
|
||||
"horizon-bench",
|
||||
"horizon-chat-filter",
|
||||
"horizon-client-core",
|
||||
"horizon-client-input",
|
||||
"horizon-client-render",
|
||||
@@ -1554,6 +1578,7 @@ dependencies = [
|
||||
"open",
|
||||
"png",
|
||||
"pollster",
|
||||
"rayon",
|
||||
"rustix 1.1.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1584,7 +1609,6 @@ dependencies = [
|
||||
"horizon-client-input",
|
||||
"horizon-command",
|
||||
"horizon-core",
|
||||
"horizon-cosmetics",
|
||||
"horizon-hdr",
|
||||
"horizon-hooks",
|
||||
"horizon-items",
|
||||
@@ -3665,6 +3689,26 @@ dependencies = [
|
||||
"objc2-quartz-core 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rc2"
|
||||
version = "0.8.1"
|
||||
|
||||
@@ -1764,14 +1764,13 @@ fn stack_from_template(template: &Value) -> Option<StackView> {
|
||||
|
||||
/// `RangeSelectItemModel.lastIndexLessOrEqual`.
|
||||
fn last_index_less_or_equal(entries: &[(f32, ItemModelNode)], needle: f32) -> Option<usize> {
|
||||
let haystack: Vec<f32> = entries.iter().map(|e| e.0).collect();
|
||||
let found = if haystack.len() < 16 {
|
||||
haystack
|
||||
let found = if entries.len() < 16 {
|
||||
entries
|
||||
.iter()
|
||||
.position(|h| *h > needle)
|
||||
.map_or(haystack.len() as i64 - 1, |i| i as i64 - 1)
|
||||
.position(|e| e.0 > needle)
|
||||
.map_or(entries.len() as i64 - 1, |i| i as i64 - 1)
|
||||
} else {
|
||||
match binary_search(&haystack, needle) {
|
||||
match binary_search(entries, needle) {
|
||||
Ok(index) => index as i64,
|
||||
Err(insertion) => insertion as i64 - 1,
|
||||
}
|
||||
@@ -1780,8 +1779,8 @@ fn last_index_less_or_equal(entries: &[(f32, ItemModelNode)], needle: f32) -> Op
|
||||
}
|
||||
/// `Arrays.binarySearch(float[], float)`, including its handling of equal
|
||||
/// values (-0.0 below 0.0, NaN above everything).
|
||||
fn binary_search(a: &[f32], key: f32) -> std::result::Result<usize, usize> {
|
||||
let (mut low, mut high) = (0i64, a.len() as i64 - 1);
|
||||
fn binary_search(entries: &[(f32, ItemModelNode)], key: f32) -> std::result::Result<usize, usize> {
|
||||
let (mut low, mut high) = (0i64, entries.len() as i64 - 1);
|
||||
let bits = |v: f32| -> i32 {
|
||||
if v.is_nan() {
|
||||
0x7fc0_0000
|
||||
@@ -1791,7 +1790,7 @@ fn binary_search(a: &[f32], key: f32) -> std::result::Result<usize, usize> {
|
||||
};
|
||||
while low <= high {
|
||||
let mid = (low + high) >> 1;
|
||||
let value = a[mid as usize];
|
||||
let value = entries[mid as usize].0;
|
||||
if value < key {
|
||||
low = mid + 1;
|
||||
} else if value > key {
|
||||
|
||||
@@ -158,3 +158,61 @@ fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
|
||||
}
|
||||
assert_eq!(view.to_map(), *merged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_index_less_or_equal_linear_and_binary() {
|
||||
use super::last_index_less_or_equal;
|
||||
let make = |thresholds: &[f32]| -> Vec<(f32, ItemModelNode)> {
|
||||
thresholds
|
||||
.iter()
|
||||
.map(|&t| (t, ItemModelNode::Empty))
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Empty
|
||||
assert_eq!(last_index_less_or_equal(&[], 0.0), None);
|
||||
|
||||
// Short (< 16): linear search
|
||||
let short = make(&[-1.0, 0.0, 1.0, 2.5, 5.0]);
|
||||
assert_eq!(last_index_less_or_equal(&short, -2.0), None);
|
||||
assert_eq!(last_index_less_or_equal(&short, -1.0), Some(0));
|
||||
assert_eq!(last_index_less_or_equal(&short, -0.5), Some(0));
|
||||
assert_eq!(last_index_less_or_equal(&short, 0.0), Some(1));
|
||||
assert_eq!(last_index_less_or_equal(&short, 2.5), Some(3));
|
||||
assert_eq!(last_index_less_or_equal(&short, 3.0), Some(3));
|
||||
assert_eq!(last_index_less_or_equal(&short, 5.0), Some(4));
|
||||
assert_eq!(last_index_less_or_equal(&short, 10.0), Some(4));
|
||||
|
||||
// Long (>= 16): binary search
|
||||
let thresholds: Vec<f32> = (0..32).map(|i| i as f32 * 10.0).collect();
|
||||
let long = make(&thresholds);
|
||||
assert_eq!(last_index_less_or_equal(&long, -5.0), None);
|
||||
assert_eq!(last_index_less_or_equal(&long, 0.0), Some(0));
|
||||
assert_eq!(last_index_less_or_equal(&long, 5.0), Some(0));
|
||||
assert_eq!(last_index_less_or_equal(&long, 10.0), Some(1));
|
||||
assert_eq!(last_index_less_or_equal(&long, 150.0), Some(15));
|
||||
assert_eq!(last_index_less_or_equal(&long, 155.0), Some(15));
|
||||
assert_eq!(last_index_less_or_equal(&long, 310.0), Some(31));
|
||||
assert_eq!(last_index_less_or_equal(&long, 1000.0), Some(31));
|
||||
|
||||
// Short lists use upper-bound tie behavior, including signed zero.
|
||||
let duplicates = make(&[1.0, 1.0, 2.0]);
|
||||
assert_eq!(last_index_less_or_equal(&duplicates, 1.0), Some(1));
|
||||
|
||||
let zeroes = make(&[-0.0, 0.0]);
|
||||
assert_eq!(last_index_less_or_equal(&zeroes, -0.0), Some(1));
|
||||
assert_eq!(last_index_less_or_equal(&zeroes, 0.0), Some(1));
|
||||
|
||||
// At 16 entries, vanilla returns Arrays.binarySearch's hit, not the last tie.
|
||||
// Expected indices were checked on the JVM.
|
||||
let duplicates = make(&[
|
||||
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 7.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0,
|
||||
]);
|
||||
assert_eq!(last_index_less_or_equal(&duplicates, 7.0), Some(7));
|
||||
|
||||
let zeroes = make(&[
|
||||
-8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -0.0, -0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0,
|
||||
]);
|
||||
assert_eq!(last_index_less_or_equal(&zeroes, -0.0), Some(7));
|
||||
assert_eq!(last_index_less_or_equal(&zeroes, 0.0), Some(9));
|
||||
}
|
||||
@@ -7,7 +7,7 @@ rust-version.workspace = true
|
||||
description = "The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
|
||||
|
||||
[dependencies]
|
||||
# The moderation durations, shared with `/tempban`.
|
||||
# The hook the hub asks (`chat_guard`), and the moderation durations.
|
||||
horizon-server-files = { path = "../horizon-server-files" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The dedicated server's chat filter (docs/chat-filter.md), without the game: `server.toml`'s
|
||||
//! `[chat_filter]`, the matcher for the slur list (`data/slurs.txt`, or the owner's `words_file`), and
|
||||
//! the escalation (a refused message, then a mute, then a temporary ban), with the mutes kept in a file
|
||||
//! so that they outlive a restart. The server's hub (`horizon_client::lan`) asks it before it relays a
|
||||
//! player's text and carries out what it decides.
|
||||
//! so that they outlive a restart. The server's hub (`horizon_client::lan`) asks it, through the engine's
|
||||
//! `horizon_server_files::chat_guard` hook, before it relays a player's text, and carries out what it decides.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -10,6 +10,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
mod normalise;
|
||||
|
||||
use horizon_server_files::chat_guard::{self, ChatGuard, Loaded, Place};
|
||||
pub use horizon_server_files::chat_guard::{Action, Mute};
|
||||
pub use horizon_server_files::duration::{
|
||||
DurationUnit, duration_parts, duration_text, now_millis, parse_duration,
|
||||
};
|
||||
@@ -221,49 +223,59 @@ fn fits(entry: &[Run], runs: &[Run], whole: bool) -> bool {
|
||||
.all(|(e, r)| e.letter == r.letter && r.count >= e.count)
|
||||
}
|
||||
|
||||
/// What a hit leads to.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
/// The text is refused, nothing more.
|
||||
Refused,
|
||||
/// Refused, and the player can't chat until then (epoch milliseconds).
|
||||
Muted { until: i64 },
|
||||
/// Refused, and the player is banned until then.
|
||||
Banned { until: i64 },
|
||||
/// A mute as the mutes file keeps it.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SavedMute {
|
||||
uuid: String,
|
||||
name: String,
|
||||
until: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
by: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Action::Refused => "refused",
|
||||
Action::Muted { .. } => "muted",
|
||||
Action::Banned { .. } => "banned",
|
||||
impl From<SavedMute> for Mute {
|
||||
fn from(m: SavedMute) -> Self {
|
||||
let SavedMute {
|
||||
uuid,
|
||||
name,
|
||||
until,
|
||||
by,
|
||||
reason,
|
||||
} = m;
|
||||
Mute {
|
||||
uuid,
|
||||
name,
|
||||
until,
|
||||
by,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Mute {
|
||||
pub uuid: String,
|
||||
pub name: String,
|
||||
/// Epoch milliseconds; `None`: until `/unmute`.
|
||||
pub until: Option<i64>,
|
||||
/// The operator who muted them (`/mute`, `/tempmute`); `None`: the filter.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub by: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
impl Mute {
|
||||
pub fn active(&self, now: i64) -> bool {
|
||||
self.until.is_none_or(|until| until > now)
|
||||
impl From<Mute> for SavedMute {
|
||||
fn from(m: Mute) -> Self {
|
||||
let Mute {
|
||||
uuid,
|
||||
name,
|
||||
until,
|
||||
by,
|
||||
reason,
|
||||
} = m;
|
||||
SavedMute {
|
||||
uuid,
|
||||
name,
|
||||
until,
|
||||
by,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
struct MutesFile {
|
||||
mutes: Vec<Mute>,
|
||||
mutes: Vec<SavedMute>,
|
||||
}
|
||||
|
||||
/// One player's hits: how many, and when the last one stops counting.
|
||||
@@ -293,7 +305,7 @@ impl Guard {
|
||||
{
|
||||
for m in saved.mutes {
|
||||
if let Some(uuid) = parse_uuid(&m.uuid) {
|
||||
mutes.insert(uuid, m);
|
||||
mutes.insert(uuid, m.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,7 +443,7 @@ impl Guard {
|
||||
};
|
||||
let now = now_millis();
|
||||
self.mutes.retain(|_, m| m.active(now));
|
||||
let mut mutes: Vec<Mute> = self.mutes.values().cloned().collect();
|
||||
let mut mutes: Vec<SavedMute> = self.mutes.values().cloned().map(Into::into).collect();
|
||||
mutes.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
if let Ok(text) = serde_json::to_string_pretty(&MutesFile { mutes }) {
|
||||
let tmp = file.with_extension("json.tmp");
|
||||
@@ -442,6 +454,94 @@ impl Guard {
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatGuard for Guard {
|
||||
fn matches(&self, text: &str) -> bool {
|
||||
self.filter.matches(text)
|
||||
}
|
||||
|
||||
fn muted(&self, uuid: u128, now: i64) -> Option<&Mute> {
|
||||
Guard::muted(self, uuid, now)
|
||||
}
|
||||
|
||||
fn mutes(&self, now: i64) -> Vec<&Mute> {
|
||||
Guard::mutes(self, now)
|
||||
}
|
||||
|
||||
fn mute(
|
||||
&mut self,
|
||||
uuid: u128,
|
||||
name: &str,
|
||||
until: Option<i64>,
|
||||
by: &str,
|
||||
reason: Option<String>,
|
||||
) {
|
||||
Guard::mute(self, uuid, name, until, by, reason);
|
||||
}
|
||||
|
||||
fn strike(&mut self, uuid: u128, name: &str, now: i64) -> (u32, Action) {
|
||||
Guard::strike(self, uuid, name, now)
|
||||
}
|
||||
|
||||
fn unmute(&mut self, name: &str) -> Option<String> {
|
||||
Guard::unmute(self, name)
|
||||
}
|
||||
|
||||
fn log(
|
||||
&mut self,
|
||||
now: i64,
|
||||
name: &str,
|
||||
uuid: u128,
|
||||
place: Place,
|
||||
count: u32,
|
||||
action: Action,
|
||||
text: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = &self.config.log;
|
||||
if path.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let line = log_line(now, name, uuid, place.name(), count, action, text);
|
||||
append_log(Path::new(path), &line).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// `server.toml`'s `[chat_filter]` for the dedicated server ([`chat_guard::install`] it at startup): on
|
||||
/// by default, off (with an error logged) when the section is wrong. The mutes are kept in `dir`.
|
||||
pub fn load(server_toml: &str, dir: &Path) -> Loaded {
|
||||
let mut loaded = Loaded::default();
|
||||
let config = match Config::from_server_toml(server_toml) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
loaded.log.push((
|
||||
"ERROR",
|
||||
format!("server.toml: {e}; the chat filter stays off"),
|
||||
));
|
||||
return loaded;
|
||||
}
|
||||
};
|
||||
if !config.enabled {
|
||||
return loaded;
|
||||
}
|
||||
let (guard, problem) = Guard::load(config, dir);
|
||||
if let Some(problem) = problem {
|
||||
loaded.log.push(("WARN", problem));
|
||||
}
|
||||
loaded.log.push((
|
||||
"INFO",
|
||||
format!(
|
||||
"Chat filter on: {} words (server.toml [chat_filter]; enabled = false turns it off)",
|
||||
guard.filter.len()
|
||||
),
|
||||
));
|
||||
loaded.guard = Some(Box::new(guard));
|
||||
loaded
|
||||
}
|
||||
|
||||
/// Gives the dedicated server this chat filter.
|
||||
pub fn install() {
|
||||
chat_guard::install(load);
|
||||
}
|
||||
|
||||
/// One line of `logs/chat-filter.log`, without its newline.
|
||||
pub fn log_line(
|
||||
now: i64,
|
||||
|
||||
@@ -51,8 +51,14 @@ fn float(tag: Option<&Tag>) -> Option<f32> {
|
||||
other => int(Some(other))? as f32,
|
||||
})
|
||||
}
|
||||
/// `TagValueInput.getBooleanOr`: a number is true when its byte value is not 0.
|
||||
fn flag(tag: Option<&Tag>) -> Option<bool> {
|
||||
Some(int(tag)? != 0)
|
||||
Some(int(tag)? as i8 != 0)
|
||||
}
|
||||
/// `Codec.BOOL` through `NbtOps.getBooleanValue`: a number is true when it
|
||||
/// is not 0 as a whole (`doubleValue() != 0`), unlike `getBooleanOr`.
|
||||
fn codec_flag(tag: Option<&Tag>) -> Option<bool> {
|
||||
Some(float(tag)? != 0.0)
|
||||
}
|
||||
fn boolean(v: bool) -> Tag {
|
||||
Tag::Byte(i8::from(v))
|
||||
@@ -308,7 +314,7 @@ pub fn sign_side(tag: Option<&Tag>) -> Tag {
|
||||
(
|
||||
"has_glowing_text",
|
||||
boolean(
|
||||
tag.and_then(|t| flag(t.get("has_glowing_text")))
|
||||
tag.and_then(|t| codec_flag(t.get("has_glowing_text")))
|
||||
.unwrap_or(false),
|
||||
),
|
||||
),
|
||||
@@ -868,6 +874,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `getBooleanOr` tests a number's byte value: an int 256 is false and
|
||||
/// 257 true, for a sign's wax and a piston's flags alike.
|
||||
#[test]
|
||||
fn booleans_are_read_as_bytes() {
|
||||
let sign = |waxed: &str| {
|
||||
canon(&format!(
|
||||
"{{id:\"minecraft:sign\",x:1,y:2,z:3,is_waxed:{waxed}}}"
|
||||
))
|
||||
.get("is_waxed")
|
||||
.cloned()
|
||||
};
|
||||
assert_eq!(sign("1b"), Some(Tag::Byte(1)));
|
||||
assert_eq!(sign("256"), Some(Tag::Byte(0)));
|
||||
assert_eq!(sign("257"), Some(Tag::Byte(1)));
|
||||
// `SignText` reads `has_glowing_text` through `Codec.BOOL`, which tests
|
||||
// the whole number: 256 is true there.
|
||||
let glowing = canon(
|
||||
r#"{id:"minecraft:sign",x:1,y:2,z:3,front_text:{messages:["","","",""],has_glowing_text:256}}"#,
|
||||
);
|
||||
let front = glowing.get("front_text").unwrap();
|
||||
assert_eq!(front.get("has_glowing_text"), Some(&Tag::Byte(1)));
|
||||
let piston = canon(r#"{id:"minecraft:piston",x:1,y:2,z:3,extending:256,source:513}"#);
|
||||
assert_eq!(piston.get("extending"), Some(&Tag::Byte(0)));
|
||||
assert_eq!(piston.get("source"), Some(&Tag::Byte(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_type_has_defaults() {
|
||||
for id in TYPES.iter().chain(NEWEST.iter()) {
|
||||
|
||||
@@ -27,6 +27,9 @@ horizon-mcp = { path = "../horizon-mcp" }
|
||||
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
|
||||
horizon-optifine = { path = "../horizon-optifine" }
|
||||
horizon-threads = { path = "../horizon-threads" }
|
||||
# One pool of helper threads for the server tick's parallel phases (`parallel.rs`): threads made
|
||||
# once, not for every phase of every tick.
|
||||
rayon = "1.10"
|
||||
horizon-hdr = { path = "../horizon-hdr" }
|
||||
horizon-world = { path = "../horizon-world" }
|
||||
horizon-protocol = { path = "../horizon-protocol" }
|
||||
@@ -36,8 +39,6 @@ horizon-server-files = { path = "../horizon-server-files" }
|
||||
getrandom = "0.3"
|
||||
# The dedicated server's anticheat, off unless server.toml's [anticheat] turns it on (docs/anticheat.md).
|
||||
horizon-anticheat = { path = "../horizon-anticheat" }
|
||||
# The dedicated server's chat filter: [chat_filter], the slur list, mutes (docs/chat-filter.md).
|
||||
horizon-chat-filter = { path = "../horizon-chat-filter" }
|
||||
# Shader packs: reading them and turning their GLSL into naga modules.
|
||||
horizon-shaders = { path = "../horizon-shaders" }
|
||||
# Horizon's own shader packs: the format, the stages and the check (docs/shaders.md).
|
||||
|
||||
@@ -9,7 +9,6 @@ description = "The client's world data, rendering and screens: chunks, meshing,
|
||||
[dependencies]
|
||||
# The settings stores of the crates above the engine (docs/layers.md, "Platform registries").
|
||||
horizon-hooks = { path = "../horizon-hooks" }
|
||||
horizon-cosmetics = { path = "../horizon-cosmetics" }
|
||||
# The GUI atlas's pixels, kept compressed once uploaded (hud.rs `pack_pixels`).
|
||||
lz4_flex = { version = "0.14", default-features = false, features = ["std", "safe-encode", "safe-decode", "checked-decode"] }
|
||||
horizon-assets = { path = "../horizon-assets" }
|
||||
|
||||
@@ -92,6 +92,9 @@ pub struct BlockEntities {
|
||||
/// instance blocks, `shows_box`), so a frame looks at them without scanning the map.
|
||||
/// Like `mods`, one taken out stays listed until `put` or `remove`.
|
||||
boxes: FxHashSet<Pos>,
|
||||
/// The signs a player's editor is on (`SignBlockEntity.playerWhoMayEdit`
|
||||
/// set), so the level's tick checks those alone instead of every sign.
|
||||
sign_editors: FxHashSet<Pos>,
|
||||
asleep: FxHashSet<Pos>,
|
||||
}
|
||||
|
||||
@@ -312,6 +315,44 @@ impl BlockEntities {
|
||||
.filter(move |((cx, cz), _)| !far(*cx, x) && !far(*cz, z))
|
||||
.flat_map(|(_, set)| set)
|
||||
}
|
||||
/// `SignBlockEntity.playerWhoMayEdit` of the sign at `pos`.
|
||||
pub fn sign_editor(&self, pos: &Pos) -> Option<u128> {
|
||||
match self.map.get(pos) {
|
||||
Some(BlockEntity::Sign(sign)) => sign.allowed_editor,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// `SignBlockEntity.setAllowedPlayerEditor`: the one player whose editor
|
||||
/// is on the sign at `pos`, or `None` for nobody's.
|
||||
pub fn set_sign_editor(&mut self, pos: Pos, who: Option<u128>) {
|
||||
let Some(BlockEntity::Sign(sign)) = self.map.get_mut(&pos) else {
|
||||
return;
|
||||
};
|
||||
sign.allowed_editor = who;
|
||||
if who.is_some() {
|
||||
self.sign_editors.insert(pos);
|
||||
} else {
|
||||
self.sign_editors.remove(&pos);
|
||||
}
|
||||
}
|
||||
/// The positions of the signs a player's editor is on, in no order.
|
||||
pub fn sign_editors(&self) -> impl Iterator<Item = &Pos> {
|
||||
self.sign_editors.iter()
|
||||
}
|
||||
/// Drops `pos` from the signs an editor is on, for a sign that is gone or
|
||||
/// no longer holds one. `set_sign_editor` keeps the list itself.
|
||||
pub fn forget_sign_editor(&mut self, pos: Pos) {
|
||||
self.sign_editors.remove(&pos);
|
||||
}
|
||||
/// Keeps `pos` listed in `sign_editors` exactly while `entity` holds an
|
||||
/// editor, as `note_box` does for the boxes.
|
||||
fn note_sign_editor(&mut self, pos: Pos, entity: &BlockEntity) {
|
||||
if matches!(entity, BlockEntity::Sign(sign) if sign.allowed_editor.is_some()) {
|
||||
self.sign_editors.insert(pos);
|
||||
} else {
|
||||
self.sign_editors.remove(&pos);
|
||||
}
|
||||
}
|
||||
/// `LevelChunk.getBlockEntity`: the block entity at `pos`, if any.
|
||||
pub fn get(&self, pos: &Pos) -> Option<&BlockEntity> {
|
||||
self.map.get(pos)
|
||||
@@ -351,6 +392,7 @@ impl BlockEntities {
|
||||
self.attachments.remove(&pos);
|
||||
}
|
||||
let new_box = shows_box(&entity);
|
||||
self.note_sign_editor(pos, &entity);
|
||||
self.asleep.remove(&pos);
|
||||
let old = self.map.insert(pos, entity);
|
||||
if old.is_none() {
|
||||
@@ -398,6 +440,7 @@ impl BlockEntities {
|
||||
forget_mod(&mut self.mods, *pos);
|
||||
}
|
||||
self.boxes.remove(pos);
|
||||
self.sign_editors.remove(pos);
|
||||
removed
|
||||
}
|
||||
/// The block entity at `pos`, out of the map for a while: its slot stays
|
||||
@@ -418,6 +461,7 @@ impl BlockEntities {
|
||||
self.mods.entry(chunk_of(pos)).or_default().insert(pos);
|
||||
}
|
||||
self.note_box(pos, Some(&entity));
|
||||
self.note_sign_editor(pos, &entity);
|
||||
if self.map.insert(pos, entity).is_none()
|
||||
&& !self.live.contains_key(&pos)
|
||||
&& !self.pending.contains_key(&pos)
|
||||
@@ -445,6 +489,7 @@ impl BlockEntities {
|
||||
&mut self.attachments,
|
||||
);
|
||||
let (mods, boxes) = (&mut self.mods, &mut self.boxes);
|
||||
let sign_editors = &mut self.sign_editors;
|
||||
self.asleep.clear();
|
||||
self.map.retain(|p, e| {
|
||||
let kept = keep(p, e);
|
||||
@@ -454,6 +499,7 @@ impl BlockEntities {
|
||||
pending.remove(p);
|
||||
forget_mod(mods, *p);
|
||||
boxes.remove(p);
|
||||
sign_editors.remove(p);
|
||||
}
|
||||
kept
|
||||
});
|
||||
@@ -463,6 +509,7 @@ impl BlockEntities {
|
||||
self.map.clear();
|
||||
self.mods.clear();
|
||||
self.boxes.clear();
|
||||
self.sign_editors.clear();
|
||||
self.asleep.clear();
|
||||
self.tickers = Arc::default();
|
||||
self.live.clear();
|
||||
@@ -484,6 +531,7 @@ impl BlockEntities {
|
||||
self.asleep.remove(&p);
|
||||
let new_is_mod = matches!(e, BlockEntity::Mod(_));
|
||||
self.note_box(p, Some(&e));
|
||||
self.note_sign_editor(p, &e);
|
||||
let old = self.map.insert(p, e);
|
||||
if old.is_some() {
|
||||
self.replaced.insert(p);
|
||||
|
||||
@@ -24,7 +24,7 @@ struct Out {
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_border(in: Out) -> @location(0) vec4<f32> {
|
||||
return border_color(in);
|
||||
return frame_stored(border_color(in));
|
||||
}
|
||||
fn border_color(in: Out) -> vec4<f32> {
|
||||
let color = textureSample(forcefield, forcefield_sampler, in.uv);
|
||||
|
||||
@@ -568,6 +568,8 @@ impl Chat {
|
||||
pub fn set_qol(&mut self, atlas: &HudAtlas, qol: ChatQol) {
|
||||
if self.qol != qol {
|
||||
self.qol = qol;
|
||||
self.scroll = 0;
|
||||
self.new_since_scroll = false;
|
||||
self.rewrap(&atlas.font);
|
||||
}
|
||||
}
|
||||
@@ -593,20 +595,25 @@ impl Chat {
|
||||
.filter(|q| !q.trim().is_empty());
|
||||
if self.search != query {
|
||||
self.search = query;
|
||||
self.scroll = 0;
|
||||
self.new_since_scroll = false;
|
||||
self.rewrap(font);
|
||||
}
|
||||
}
|
||||
pub fn searching(&self) -> bool {
|
||||
self.search.is_some()
|
||||
}
|
||||
/// Re-wrap after a width or scale change (ChatComponent.rescaleChat),
|
||||
/// keeping only the messages the Chat option shows.
|
||||
/// Re-wrap after a width or scale change (`ChatComponent.rescaleChat`):
|
||||
/// `resetChatScroll`, then the messages the Chat option shows.
|
||||
pub fn rescale(&mut self, atlas: &HudAtlas) {
|
||||
self.rewrap(&atlas.font);
|
||||
}
|
||||
fn rewrap(&mut self, font: &crate::font::Font) {
|
||||
self.scroll = 0;
|
||||
self.new_since_scroll = false;
|
||||
self.rewrap(&atlas.font);
|
||||
}
|
||||
/// `ChatComponent.refreshTrimmedMessages`: the lines are laid out again
|
||||
/// where the reader was (`chatScrollbarPos` survives a filter change);
|
||||
/// `rescale` resets the scroll itself, as `rescaleChat` does.
|
||||
fn rewrap(&mut self, font: &crate::font::Font) {
|
||||
self.lines.clear();
|
||||
let mut messages = std::mem::take(&mut self.messages);
|
||||
for m in messages.iter_mut().rev() {
|
||||
|
||||
@@ -1067,6 +1067,34 @@ fn a_server_filter_mask_hides_or_masks_the_message() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `ChatComponent.refreshTrimmedMessages`: a filter change lays the lines out
|
||||
/// again but keeps `chatScrollbarPos` and `newMessageSinceScroll`; only
|
||||
/// `rescaleChat` resets them, after `resetChatScroll`.
|
||||
#[test]
|
||||
fn a_chat_option_change_keeps_the_readers_place_but_a_rescale_does_not() {
|
||||
let font = font();
|
||||
let mut chat = Chat::default();
|
||||
for i in 0..60 {
|
||||
chat.add_message(
|
||||
&font,
|
||||
Text::literal(format!("line {i}")),
|
||||
None,
|
||||
ChatSource::Server,
|
||||
0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
chat.scroll = 3;
|
||||
chat.new_since_scroll = true;
|
||||
chat.set_abilities(&font, ChatAbilities::new(ChatVisibility::System));
|
||||
assert_eq!(chat.scroll, 3);
|
||||
assert!(chat.new_since_scroll);
|
||||
let atlas = crate::hud::HudAtlas::build(Path::new("/nonexistent/horizon-test-assets")).unwrap();
|
||||
chat.rescale(&atlas);
|
||||
assert_eq!(chat.scroll, 0);
|
||||
assert!(!chat.new_since_scroll);
|
||||
}
|
||||
|
||||
/// `SuggestionsList.extractRenderState`: the list's rectangle asks for the
|
||||
/// pointing hand; outside it the cursor stays the arrow.
|
||||
#[test]
|
||||
|
||||
@@ -196,7 +196,7 @@ impl ColumnLayout {
|
||||
}
|
||||
/// The index of the section holding block `y`, if the column holds it.
|
||||
pub fn section_index(self, y: i32) -> Option<usize> {
|
||||
usize::try_from((y - self.min_y) >> 4)
|
||||
usize::try_from((i64::from(y) - i64::from(self.min_y)) >> 4)
|
||||
.ok()
|
||||
.filter(|&s| s < self.sections)
|
||||
}
|
||||
@@ -700,7 +700,7 @@ impl Chunk {
|
||||
/// The index of the section holding block `y`, if the column holds it.
|
||||
#[inline]
|
||||
pub fn section_index(&self, y: i32) -> Option<usize> {
|
||||
usize::try_from((y - self.min_y) >> 4)
|
||||
usize::try_from((i64::from(y) - i64::from(self.min_y)) >> 4)
|
||||
.ok()
|
||||
.filter(|&s| s < self.sections.len())
|
||||
}
|
||||
@@ -984,6 +984,24 @@ pub struct ChunkExtras {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn extreme_y_queries_are_air_and_writes_leave_the_column_unchanged() {
|
||||
for layout in [ColumnLayout::VANILLA, ColumnLayout::new(0, 256).unwrap()] {
|
||||
let mut chunk = Chunk::with_layout(0, 0, layout);
|
||||
chunk.set(0, layout.min_y(), 0, 7);
|
||||
for y in [i32::MIN, i32::MIN + 63, -2_146_769_275, i32::MAX] {
|
||||
assert_eq!(layout.section_index(y), None);
|
||||
assert_eq!(chunk.section_index(y), None);
|
||||
assert_eq!(chunk.get(0, y, 0), 0);
|
||||
assert_eq!(
|
||||
chunk.light(0, y, 0),
|
||||
if y < layout.min_y() { 0 } else { 0xf0 }
|
||||
);
|
||||
chunk.set(0, y, 0, 9);
|
||||
}
|
||||
assert_eq!(chunk.get(0, layout.min_y(), 0), 7);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn each_light_form_keeps_every_value_and_run() {
|
||||
// Sky varies alone, block varies alone, both, neither.
|
||||
let cases: [(fn(usize) -> u8, &str); 4] = [
|
||||
|
||||
@@ -513,6 +513,10 @@ pub struct SignData {
|
||||
pub waxed: bool,
|
||||
/// `allow_op_features`: the sign's click events may run commands.
|
||||
pub allow_op_features: bool,
|
||||
/// `playerWhoMayEdit`: the one player whose editor is on this sign, as a
|
||||
/// UUID. Every other player's click, write and applicator is refused
|
||||
/// while it is set. Not saved (vanilla keeps it out of the NBT).
|
||||
pub allowed_editor: Option<u128>,
|
||||
}
|
||||
/// `VibrationInfo`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
||||
@@ -419,8 +419,10 @@ impl SceneCollisions<'_, '_> {
|
||||
let hi = BlockPos::containing(Vec3::new(max.x + 1.0e-7, max.y + 1.0e-7, max.z + 1.0e-7))
|
||||
.offset(1, 1, 1);
|
||||
let size = |a: i32, b: i32| i64::from(b) - i64::from(a) + 1;
|
||||
let cells = size(lo.x, hi.x) * size(lo.y, hi.y) * size(lo.z, hi.z);
|
||||
if lo.x > hi.x || lo.y > hi.y || lo.z > hi.z || cells > 4096 {
|
||||
let cells = [size(lo.x, hi.x), size(lo.y, hi.y), size(lo.z, hi.z)]
|
||||
.into_iter()
|
||||
.try_fold(1i64, |n, size| n.checked_mul(size));
|
||||
if lo.x > hi.x || lo.y > hi.y || lo.z > hi.z || cells.is_none_or(|n| n > 4096) {
|
||||
return false;
|
||||
}
|
||||
// A column and section at a time. Most boxes lie in a few sections whose palette says that no
|
||||
@@ -1111,6 +1113,13 @@ mod section_tests {
|
||||
binding: &binding,
|
||||
subject: None,
|
||||
};
|
||||
// The fast path must decline an enormous volume without overflowing
|
||||
// its size calculation or beginning to walk it.
|
||||
let enormous = Aabb::new(
|
||||
Vec3::new(-1.0e9, -1.0e9, -1.0e9),
|
||||
Vec3::new(1.0e9, 1.0e9, 1.0e9),
|
||||
);
|
||||
assert!(!scene.cells_without_collision(enormous));
|
||||
// Under the fence: its section's outer cells may hold a shape reaching in.
|
||||
let under = Aabb::new(Vec3::new(5.4, 31.6, 5.4), Vec3::new(5.6, 31.8, 5.6));
|
||||
assert!(!scene.cells_without_collision(under.expand_towards(Vec3::new(0.0, 0.1, 0.0))));
|
||||
|
||||
@@ -56,6 +56,8 @@ pub struct Overlay {
|
||||
struct LoadedRule {
|
||||
rule: Rule,
|
||||
tiles: Vec<TileLayer>,
|
||||
/// By tile: the rules whose `matchTiles` name it (a range of `lists`), which chain on it.
|
||||
chain: Vec<(u32, u32)>,
|
||||
overlay: OverlayJoin,
|
||||
}
|
||||
|
||||
@@ -188,6 +190,7 @@ impl ConnectedTextures {
|
||||
Ok(tiles) => Some(LoadedRule {
|
||||
rule,
|
||||
tiles,
|
||||
chain: Vec::new(),
|
||||
overlay: OverlayJoin::default(),
|
||||
}),
|
||||
Err(e) => {
|
||||
@@ -292,10 +295,23 @@ impl ConnectedTextures {
|
||||
let mut by_layer: Vec<Vec<u16>> = Vec::new();
|
||||
// Block rules by the block each entry names, so a state tests only its own.
|
||||
let mut by_block: HashMap<&str, Vec<u16>> = HashMap::new();
|
||||
// Tile rules by the sprite or tile file each names, for chaining on a picked tile.
|
||||
let mut by_name: HashMap<String, Vec<u16>> = HashMap::new();
|
||||
for (i, r) in out.rules.iter().enumerate() {
|
||||
let i = i as u16;
|
||||
for tile in &r.rule.match_tiles {
|
||||
if let Some(l) = layer(&sprite_name(tile)) {
|
||||
let name = sprite_name(tile);
|
||||
let list = by_name.entry(name.clone()).or_default();
|
||||
if list.last() != Some(&i) {
|
||||
list.push(i);
|
||||
}
|
||||
// A tile file (`./1.png`) is no model's sprite: only chaining reaches it, by
|
||||
// name. By layer it would match every face whose sprite has the same pixels
|
||||
// (identical stills share a layer: Conquest's blank placeholders, dozens).
|
||||
if !name.contains(':') {
|
||||
continue;
|
||||
}
|
||||
if let Some(l) = layer(&name) {
|
||||
let l = l as usize;
|
||||
if by_layer.len() <= l {
|
||||
by_layer.resize(l + 1, Vec::new());
|
||||
@@ -316,6 +332,26 @@ impl ConnectedTextures {
|
||||
let range = push_list(&mut out.lists, &list);
|
||||
out.by_layer.push(range);
|
||||
}
|
||||
let by_name: HashMap<String, (u32, u32)> = by_name
|
||||
.into_iter()
|
||||
.map(|(name, list)| (name, push_list(&mut out.lists, &list)))
|
||||
.collect();
|
||||
// Set at the end: `by_block` borrows the rules' names.
|
||||
let chains: Vec<Vec<(u32, u32)>> = out
|
||||
.rules
|
||||
.iter()
|
||||
.map(|r| {
|
||||
r.rule
|
||||
.tiles
|
||||
.iter()
|
||||
.map(|t| match t {
|
||||
Tile::Texture(path) => by_name.get(&sprite_name(path)).copied(),
|
||||
_ => None,
|
||||
})
|
||||
.map(Option::unwrap_or_default)
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let mut list = Vec::new();
|
||||
for state in states {
|
||||
let Some((name, props)) = state else {
|
||||
@@ -355,6 +391,9 @@ impl ConnectedTextures {
|
||||
let range = push_list(&mut out.lists, &list);
|
||||
out.by_state.push(range);
|
||||
}
|
||||
for (r, chain) in out.rules.iter_mut().zip(chains) {
|
||||
r.chain = chain;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -392,9 +431,11 @@ impl ConnectedTextures {
|
||||
}
|
||||
|
||||
fn list(&self, ranges: &[(u32, u32)], i: usize) -> &[u16] {
|
||||
ranges.get(i).map_or(&[], |&(start, len)| {
|
||||
&self.lists[start as usize..(start + len) as usize]
|
||||
})
|
||||
ranges.get(i).map_or(&[], |&range| self.range(range))
|
||||
}
|
||||
|
||||
fn range(&self, (start, len): (u32, u32)) -> &[u16] {
|
||||
&self.lists[start as usize..(start + len) as usize]
|
||||
}
|
||||
|
||||
/// What the `face` of the block `state` at `pos` draws, for a quad with sprite `layer`.
|
||||
@@ -441,7 +482,7 @@ impl ConnectedTextures {
|
||||
let r = &self.rules[usize::from(i)];
|
||||
if (block || r.rule.match_blocks.is_empty())
|
||||
&& (tile || r.rule.match_tiles.is_empty())
|
||||
&& let Some(pick) = self.apply(
|
||||
&& let Some((pick, index)) = self.apply(
|
||||
r,
|
||||
state,
|
||||
layer,
|
||||
@@ -454,7 +495,7 @@ impl ConnectedTextures {
|
||||
)
|
||||
{
|
||||
// "Tiles output by CTM rules can also be matched by another rule": once, by the
|
||||
// tile rules naming the picked tile (and this block, if they name blocks).
|
||||
// tile rules naming the picked tile by name (and this block, if they name blocks).
|
||||
// A tile on the face's own sprite (`tiles=stone`) is no new tile.
|
||||
let Pick::Tile(TileLayer::Layer { layer: picked, .. }) = pick else {
|
||||
return pick;
|
||||
@@ -467,7 +508,7 @@ impl ConnectedTextures {
|
||||
// again: an overlay would draw twice.
|
||||
let first = self.list(&self.by_layer, layer as usize);
|
||||
let again = self
|
||||
.list(&self.by_layer, picked as usize)
|
||||
.range(r.chain.get(index).copied().unwrap_or_default())
|
||||
.iter()
|
||||
.map(|&j| (j, &self.rules[usize::from(j)]))
|
||||
.filter(|&(j, r)| {
|
||||
@@ -488,8 +529,8 @@ impl ConnectedTextures {
|
||||
&texture,
|
||||
&mut overlay,
|
||||
) {
|
||||
Some(Pick::Keep) => return pick,
|
||||
Some(chained) => return chained,
|
||||
Some((Pick::Keep, _)) => return pick,
|
||||
Some((chained, _)) => return chained,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
@@ -512,7 +553,7 @@ impl ConnectedTextures {
|
||||
biome: &impl Fn() -> Option<&'b str>,
|
||||
texture: &impl Fn() -> Option<Axes>,
|
||||
overlay: &mut impl FnMut(Overlay),
|
||||
) -> Option<Pick> {
|
||||
) -> Option<(Pick, usize)> {
|
||||
let rule = &r.rule;
|
||||
// `Rule::applies_at`, with the biome looked up only for rules that test one. Rules with
|
||||
// a custom name are not loaded.
|
||||
@@ -602,7 +643,7 @@ impl ConnectedTextures {
|
||||
return if quarters.contains(&TileLayer::Skip) {
|
||||
None
|
||||
} else {
|
||||
Some(Pick::Quarters(quarters))
|
||||
Some((Pick::Quarters(quarters), 0))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -617,7 +658,7 @@ impl ConnectedTextures {
|
||||
return None;
|
||||
}
|
||||
if !joins([0, 1, 0]) {
|
||||
return Some(Pick::Keep);
|
||||
return Some((Pick::Keep, 0));
|
||||
}
|
||||
0
|
||||
}
|
||||
@@ -644,8 +685,8 @@ impl ConnectedTextures {
|
||||
}
|
||||
match tile(index) {
|
||||
TileLayer::Skip => None,
|
||||
TileLayer::Default => Some(Pick::Keep),
|
||||
t => Some(Pick::Tile(t)),
|
||||
TileLayer::Default => Some((Pick::Keep, index)),
|
||||
t => Some((Pick::Tile(t), index)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -675,6 +716,7 @@ mod tests {
|
||||
.enumerate()
|
||||
.map(|(n, rule)| LoadedRule {
|
||||
overlay: OverlayJoin::default(),
|
||||
chain: Vec::new(),
|
||||
tiles: rule
|
||||
.tiles
|
||||
.iter()
|
||||
@@ -722,6 +764,10 @@ mod tests {
|
||||
"assets/minecraft/optifine/ctm/b0.png" => Some(2000),
|
||||
// And `a0` of the first rule.
|
||||
"assets/minecraft/optifine/ctm/a0.png" => Some(1000),
|
||||
// Two blank placeholders with the same pixels, so one layer: the tile `grass/1`
|
||||
// of the third rule (`chaining_follows_the_tile_not_its_pixels`).
|
||||
"assets/minecraft/optifine/ctm/grass/1.png"
|
||||
| "assets/minecraft/optifine/ctm/aaa/1.png" => Some(3000),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
@@ -1176,6 +1222,31 @@ mod tests {
|
||||
assert_eq!(pick_in(&ctm, 1, 3, Face::Up, &[]), tile(2000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chaining_follows_the_tile_not_its_pixels() {
|
||||
// Conquest's grass top: a placeholder tile that a rule beside it replaces. Another
|
||||
// folder's placeholder has the same pixels, hence the same layer, and its rule sorts
|
||||
// first; it must not draw on the grass.
|
||||
let ctm = world(&[
|
||||
(
|
||||
"grass/top.properties",
|
||||
"matchTiles=glass\nmethod=fixed\ntiles=1",
|
||||
),
|
||||
(
|
||||
"grass/random_1.properties",
|
||||
"matchTiles=./1.png\nmethod=fixed\ntiles=17\nfaces=top",
|
||||
),
|
||||
(
|
||||
"aaa/random_1.properties",
|
||||
"matchTiles=./1.png\nmethod=fixed\ntiles=5\nfaces=top",
|
||||
),
|
||||
]);
|
||||
// Rules in path order: aaa/random_1 (1000), grass/random_1 (2000), grass/top (3000).
|
||||
assert_eq!(pick_in(&ctm, 1, 3, Face::Up, &[]), tile(2000));
|
||||
// A face whose own sprite has a placeholder's pixels is no placeholder.
|
||||
assert_eq!(pick_in(&ctm, 2, 3000, Face::Up, &[]), Pick::Keep);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orient_texture_follows_the_quads_uvs() {
|
||||
let rule = |orient: &str| {
|
||||
|
||||
@@ -56,6 +56,15 @@ pub struct Glue<'a> {
|
||||
pub struct CoreProgram {
|
||||
pub vertex: naga::Module,
|
||||
pub fragment: naga::Module,
|
||||
/// The blocks and samplers that are none of vanilla's, in binding order: the `i`th is at
|
||||
/// [`other_binding`]`(i)` (a sampler's object one after its texture; a block leaves that
|
||||
/// binding unused).
|
||||
pub other: Vec<String>,
|
||||
}
|
||||
|
||||
/// The binding of the `index`th block or sampler of [`CoreProgram::other`].
|
||||
pub fn other_binding(index: usize) -> u32 {
|
||||
OTHER_BINDINGS + 2 * index as u32
|
||||
}
|
||||
|
||||
/// The shaders of one pipeline (`text.vsh`, `text.fsh`) with vanilla's `defines` for it.
|
||||
@@ -66,6 +75,20 @@ pub fn translate_core(
|
||||
defines: &[&str],
|
||||
include: &dyn Fn(&str) -> Option<String>,
|
||||
glue: &Glue,
|
||||
) -> Result<CoreProgram, String> {
|
||||
translate_core_with(vertex, fragment, defines, include, glue, &|_| false)
|
||||
}
|
||||
|
||||
/// [`translate_core`] for a pipeline that also gives the blocks and samplers `provided` says
|
||||
/// yes to, at their [`CoreProgram::other`] bindings; the others are still an error. `provided`
|
||||
/// answers a block's name and a sampler's alike: the caller knows which kind each one is.
|
||||
pub fn translate_core_with(
|
||||
vertex: &str,
|
||||
fragment: &str,
|
||||
defines: &[&str],
|
||||
include: &dyn Fn(&str) -> Option<String>,
|
||||
glue: &Glue,
|
||||
provided: &dyn Fn(&str) -> bool,
|
||||
) -> Result<CoreProgram, String> {
|
||||
const CORE: &str = "minecraft:shaders/core/";
|
||||
let vertex_text = glsl_fixups::fixups(&strip_comments(&expand(vertex, CORE, include)?));
|
||||
@@ -75,10 +98,16 @@ pub fn translate_core(
|
||||
.map_err(|e| format!("vertex shader: {e}"))?;
|
||||
let fragment_code = rewrite(&fragment_text, ShaderStage::Fragment, glue, &mut names)
|
||||
.map_err(|e| format!("fragment shader: {e}"))?;
|
||||
let vertex = compile(ShaderStage::Vertex, &vertex_code, defines)
|
||||
let vertex = compile(ShaderStage::Vertex, &vertex_code, defines, &names, provided)
|
||||
.map_err(|e| format!("vertex shader: {e}"))?;
|
||||
let fragment = compile(ShaderStage::Fragment, &fragment_code, defines)
|
||||
.map_err(|e| format!("fragment shader: {e}"))?;
|
||||
let fragment = compile(
|
||||
ShaderStage::Fragment,
|
||||
&fragment_code,
|
||||
defines,
|
||||
&names,
|
||||
provided,
|
||||
)
|
||||
.map_err(|e| format!("fragment shader: {e}"))?;
|
||||
let given = locations(&vertex, false);
|
||||
if let Some(missing) = locations(&fragment, true)
|
||||
.into_iter()
|
||||
@@ -88,11 +117,21 @@ pub fn translate_core(
|
||||
"the fragment shader reads location {missing}, which the vertex shader doesn't write"
|
||||
));
|
||||
}
|
||||
Ok(CoreProgram { vertex, fragment })
|
||||
Ok(CoreProgram {
|
||||
vertex,
|
||||
fragment,
|
||||
other: names.other,
|
||||
})
|
||||
}
|
||||
|
||||
/// A bound on a pack's includes, which are pasted every time.
|
||||
const MAX_PASTES: u32 = 1024;
|
||||
const MAX_EXPANDED: usize = 1 << 20;
|
||||
|
||||
/// The file with its includes inlined (`#include <ns:path>`, `#include "relative"` and the same
|
||||
/// with `#moj_import`), each file once, `#version` and `#extension` lines dropped.
|
||||
/// with `#moj_import`), `#version` and `#extension` lines dropped. An `#include` is pasted every
|
||||
/// time, as vanilla's shaderc does (the files' `#ifndef` guards keep one copy); a `#moj_import`
|
||||
/// once.
|
||||
fn expand(
|
||||
text: &str,
|
||||
dir: &str,
|
||||
@@ -103,6 +142,7 @@ fn expand(
|
||||
dir: &str,
|
||||
include: &dyn Fn(&str) -> Option<String>,
|
||||
seen: &mut HashSet<String>,
|
||||
pastes: &mut u32,
|
||||
depth: u32,
|
||||
out: &mut String,
|
||||
) -> Result<(), String> {
|
||||
@@ -113,15 +153,19 @@ fn expand(
|
||||
let directive = line.trim_start().strip_prefix('#').map(str::trim_start);
|
||||
let target = directive.and_then(|d| {
|
||||
d.strip_prefix("include")
|
||||
.or_else(|| d.strip_prefix("moj_import"))
|
||||
.map(str::trim)
|
||||
.map(|t| (t.trim(), false))
|
||||
.or_else(|| d.strip_prefix("moj_import").map(|t| (t.trim(), true)))
|
||||
});
|
||||
if let Some(target) = target {
|
||||
if let Some((target, once)) = target {
|
||||
let id = include_id(target, dir).ok_or_else(|| format!("bad include {target}"))?;
|
||||
if seen.insert(id.clone()) {
|
||||
if seen.insert(id.clone()) || !once {
|
||||
*pastes += 1;
|
||||
if *pastes > MAX_PASTES {
|
||||
return Err(format!("includes paste more than {MAX_PASTES} files"));
|
||||
}
|
||||
let file = include(&id).ok_or_else(|| format!("{id} not found"))?;
|
||||
let parent = &id[..id.rfind('/').map_or(id.len(), |i| i + 1)];
|
||||
walk(&file, parent, include, seen, depth + 1, out)?;
|
||||
walk(&file, parent, include, seen, pastes, depth + 1, out)?;
|
||||
}
|
||||
out.push('\n');
|
||||
} else if directive
|
||||
@@ -132,11 +176,16 @@ fn expand(
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
if out.len() > MAX_EXPANDED {
|
||||
return Err(format!(
|
||||
"includes grow the shader past {MAX_EXPANDED} bytes"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let mut out = String::new();
|
||||
walk(text, dir, include, &mut HashSet::new(), 0, &mut out)?;
|
||||
walk(text, dir, include, &mut HashSet::new(), &mut 0, 0, &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -293,14 +342,16 @@ fn block_name(line: &str) -> Option<&str> {
|
||||
let rest = line[at + "uniform".len()..].trim();
|
||||
let name_end = rest.find(|c: char| !is_ident(c)).unwrap_or(rest.len());
|
||||
let (name, tail) = rest.split_at(name_end);
|
||||
(!name.is_empty() && matches!(tail.trim(), "" | "{")).then_some(name)
|
||||
// `{` alone, or the whole block on the line (`uniform Day { float daylight; };`).
|
||||
(!name.is_empty() && (tail.trim().is_empty() || tail.trim_start().starts_with('{')))
|
||||
.then_some(name)
|
||||
}
|
||||
|
||||
/// What the two stages share: the vertex stage's outputs in location order, and the names of
|
||||
/// What the two stages share: the vertex stage's outputs and their locations, and the names of
|
||||
/// the blocks and samplers of other pipelines in binding order.
|
||||
#[derive(Default)]
|
||||
struct Names {
|
||||
varyings: Vec<String>,
|
||||
varyings: Vec<(String, u32)>,
|
||||
other: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -308,8 +359,8 @@ impl Names {
|
||||
fn location(&self, varying: &str) -> Option<u32> {
|
||||
self.varyings
|
||||
.iter()
|
||||
.position(|v| v == varying)
|
||||
.map(|i| i as u32)
|
||||
.find(|(v, _)| v == varying)
|
||||
.map(|(_, location)| *location)
|
||||
}
|
||||
|
||||
/// A block or sampler of another pipeline: its binding (two for a sampler).
|
||||
@@ -321,7 +372,7 @@ impl Names {
|
||||
self.other.len() - 1
|
||||
}
|
||||
};
|
||||
OTHER_BINDINGS + 2 * index as u32
|
||||
other_binding(index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +395,16 @@ fn rewrite(
|
||||
// Where the pack's own code starts: the glue above it is left as it is.
|
||||
let pack_start = out.len();
|
||||
let mut outputs: Vec<&str> = Vec::new();
|
||||
let explicit: Vec<u32> = if vertex {
|
||||
text.lines()
|
||||
.flat_map(declaration_lines)
|
||||
.filter_map(declaration)
|
||||
.filter(|d| d.storage == "out")
|
||||
.filter_map(|d| d.location)
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
for source_line in text.lines() {
|
||||
for line in declaration_lines(source_line) {
|
||||
if let Some(name) = block_name(line) {
|
||||
@@ -423,10 +484,21 @@ fn rewrite(
|
||||
}
|
||||
"out" if vertex => {
|
||||
// One location each: naga takes scalars and vectors only.
|
||||
let location = names.location(decl.name).unwrap_or_else(|| {
|
||||
names.varyings.push(decl.name.to_owned());
|
||||
names.varyings.len() as u32 - 1
|
||||
});
|
||||
let location = decl
|
||||
.location
|
||||
.or_else(|| names.location(decl.name))
|
||||
.unwrap_or_else(|| {
|
||||
(0..)
|
||||
.find(|l| {
|
||||
!explicit.contains(l)
|
||||
&& names.varyings.iter().all(|(_, v)| v != l)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let varying = (decl.name.to_owned(), location);
|
||||
if !names.varyings.contains(&varying) {
|
||||
names.varyings.push(varying);
|
||||
}
|
||||
writeln!(
|
||||
out,
|
||||
"layout(location = {location}) {qualifiers} out {} {};",
|
||||
@@ -435,9 +507,12 @@ fn rewrite(
|
||||
.unwrap();
|
||||
}
|
||||
"in" => {
|
||||
let location = names.location(decl.name).ok_or_else(|| {
|
||||
format!("input {} isn't written by the vertex shader", decl.name)
|
||||
})?;
|
||||
let location = decl
|
||||
.location
|
||||
.or_else(|| names.location(decl.name))
|
||||
.ok_or_else(|| {
|
||||
format!("input {} isn't written by the vertex shader", decl.name)
|
||||
})?;
|
||||
writeln!(
|
||||
out,
|
||||
"layout(location = {location}) {qualifiers} in {} {};",
|
||||
@@ -511,10 +586,17 @@ fn rename_main(text: &str) -> String {
|
||||
}
|
||||
|
||||
/// Parses and validates one stage; errors name the line of the rewritten code.
|
||||
fn compile(stage: ShaderStage, code: &str, defines: &[&str]) -> Result<naga::Module, String> {
|
||||
fn compile(
|
||||
stage: ShaderStage,
|
||||
code: &str,
|
||||
defines: &[&str],
|
||||
names: &Names,
|
||||
provided: &dyn Fn(&str) -> bool,
|
||||
) -> Result<naga::Module, String> {
|
||||
let mut options = Options::from(stage);
|
||||
for define in defines {
|
||||
options.defines.insert((*define).to_owned(), String::new());
|
||||
let (name, value) = define.split_once(' ').unwrap_or((define, ""));
|
||||
options.defines.insert(name.to_owned(), value.to_owned());
|
||||
}
|
||||
let module = Frontend::default().parse(&options, code).map_err(|e| {
|
||||
e.errors
|
||||
@@ -542,16 +624,16 @@ fn compile(stage: ShaderStage, code: &str, defines: &[&str]) -> Result<naga::Mod
|
||||
})?;
|
||||
let entry = info.get_entry_point(0);
|
||||
for (handle, global) in module.global_variables.iter() {
|
||||
let other = global
|
||||
.binding
|
||||
.as_ref()
|
||||
.is_some_and(|b| b.binding >= OTHER_BINDINGS);
|
||||
if other && !entry[handle].is_empty() {
|
||||
let name = global.name.as_deref().unwrap_or("?");
|
||||
let name = name
|
||||
.strip_prefix("hz_tex_")
|
||||
.or_else(|| name.strip_prefix("hz_smp_"))
|
||||
.unwrap_or(name);
|
||||
let Some(binding) = global.binding.as_ref().map(|b| b.binding) else {
|
||||
continue;
|
||||
};
|
||||
if binding >= OTHER_BINDINGS && !entry[handle].is_empty() {
|
||||
// The name it was declared with (a block's instance may have none).
|
||||
let index = ((binding - OTHER_BINDINGS) / 2) as usize;
|
||||
let name = names.other.get(index).map_or("?", String::as_str);
|
||||
if provided(name) {
|
||||
continue;
|
||||
}
|
||||
return Err(format!("{name} is not a block or sampler of this pipeline"));
|
||||
}
|
||||
}
|
||||
@@ -652,7 +734,8 @@ mod tests {
|
||||
#[test]
|
||||
fn a_packs_gl_vertex_index_reads_the_glues_vertex_number() {
|
||||
let vsh = "#version 330\n#include <minecraft:dynamictransforms.glsl>\n#include <minecraft:projection.glsl>\n\
|
||||
in vec3 Position;\nin vec4 Color;\nin vec2 UV0;\nout vec4 vertexColor;\nout vec2 texCoord0;\n\
|
||||
layout(location = 0) in vec3 Position;\nlayout(location = 1) in vec4 Color;\nlayout(location = 2) in vec2 UV0;\n\
|
||||
layout(location = 2) out vec4 vertexColor;\nlayout(location = 3) out vec2 texCoord0;\n\
|
||||
void main() {\n int corner = gl_VertexIndex % 4;\n\
|
||||
gl_Position = ProjMat * ModelViewMat * vec4(Position + vec3(float(corner), 0.0, 0.0), 1.0);\n\
|
||||
vertexColor = Color;\n texCoord0 = UV0;\n}\n";
|
||||
@@ -716,6 +799,96 @@ mod tests {
|
||||
assert_eq!(read, [0, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_varying_locations_are_kept() {
|
||||
let vsh = "#version 330\nin vec3 Position;\n\
|
||||
#ifdef IS_GUI\nlayout(location = 4) out float gui;\n#else\nlayout(location = 4) out float world;\n#endif\n\
|
||||
layout(location = 13) out vec4 a;\nout float b;\n\
|
||||
void main() { gl_Position = vec4(Position, 1.0); a = vec4(1.0); b = 1.0;\n\
|
||||
#ifdef IS_GUI\ngui = 1.0;\n#else\nworld = 1.0;\n#endif\n}\n";
|
||||
let fsh = "#version 330\n#ifdef IS_GUI\nlayout(location = 4) in float gui;\n#endif\n\
|
||||
layout(location = 13) in vec4 a;\nin float b;\nout vec4 fragColor;\n\
|
||||
void main() { fragColor = a * b;\n#ifdef IS_GUI\nfragColor *= gui;\n#endif\n}\n";
|
||||
let program = translate_core(vsh, fsh, &["IS_GUI"], &include, &GLUE).unwrap();
|
||||
let mut read = locations(&program.fragment, true);
|
||||
read.sort_unstable();
|
||||
assert_eq!(read, [0, 4, 13]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_include_is_pasted_every_time_and_a_moj_import_once() {
|
||||
let file = |id: &str| {
|
||||
(id == "minecraft:shaders/include/g.glsl")
|
||||
.then(|| "#ifndef G\n#define G\nconst float g = 1.0;\n#endif\n".to_owned())
|
||||
};
|
||||
let twice =
|
||||
"#ifdef A\n#include <minecraft:g.glsl>\n#else\n#include <minecraft:g.glsl>\n#endif\n";
|
||||
let expanded = expand(twice, "minecraft:shaders/core/", &file).unwrap();
|
||||
assert_eq!(expanded.matches("const float g").count(), 2);
|
||||
let imported = expand(
|
||||
&twice.replace("#include", "#moj_import"),
|
||||
"minecraft:shaders/core/",
|
||||
&file,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(imported.matches("const float g").count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_location_another_branch_gave_the_same_name_isnt_reused() {
|
||||
let vsh = "#version 330\nin vec3 Position;\n\
|
||||
#ifdef IS_GUI\nlayout(location = 0) out vec4 vertexColor;\n\
|
||||
#else\nlayout(location = 1) out vec4 vertexColor;\nlayout(location = 0) out vec4 worldOther;\n#endif\n\
|
||||
void main() { gl_Position = vec4(Position, 1.0); vertexColor = vec4(1.0);\n\
|
||||
#ifndef IS_GUI\nworldOther = vec4(0.5);\n#endif\n}\n";
|
||||
let fsh = "#version 330\n\
|
||||
#ifdef IS_GUI\nlayout(location = 0) in vec4 vertexColor;\n\
|
||||
#else\nlayout(location = 1) in vec4 vertexColor;\nlayout(location = 0) in vec4 worldOther;\n#endif\n\
|
||||
out vec4 fragColor;\nvoid main() { fragColor = vertexColor;\n\
|
||||
#ifndef IS_GUI\nfragColor *= worldOther;\n#endif\n}\n";
|
||||
let world = translate_core(vsh, fsh, &[], &include, &GLUE).unwrap();
|
||||
let mut written = locations(&world.vertex, false);
|
||||
written.sort_unstable();
|
||||
assert_eq!(written, [0, 1]);
|
||||
let mut read = locations(&world.fragment, true);
|
||||
read.sort_unstable();
|
||||
assert_eq!(read, [0, 1]);
|
||||
let gui = translate_core(vsh, fsh, &["IS_GUI"], &include, &GLUE).unwrap();
|
||||
assert_eq!(locations(&gui.vertex, false), [0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn includes_that_multiply_are_refused_once_over_budget() {
|
||||
let file = |id: &str| {
|
||||
let level: u32 = id
|
||||
.strip_prefix("minecraft:shaders/include/l")?
|
||||
.strip_suffix(".glsl")?
|
||||
.parse()
|
||||
.ok()?;
|
||||
let next = format!("#include <minecraft:l{}.glsl>\n", level + 1).repeat(4);
|
||||
Some(format!(
|
||||
"#ifndef L{level}\n#define L{level}\n{}#endif\n",
|
||||
if level < 15 { next.as_str() } else { "" }
|
||||
))
|
||||
};
|
||||
let error = expand(
|
||||
"#include <minecraft:l0.glsl>\n",
|
||||
"minecraft:shaders/core/",
|
||||
&file,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("paste more than"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_define_with_a_value_is_defined_to_it() {
|
||||
let vsh = "#version 330\nin vec3 Position;\n\
|
||||
void main() { gl_Position = vec4(Position, float(COUNT)); }\n";
|
||||
let fsh =
|
||||
"#version 330\nout vec4 fragColor;\nvoid main() { fragColor = vec4(COUNT / 4); }\n";
|
||||
assert!(translate_core(vsh, fsh, &["COUNT 8"], &include, &GLUE).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minified_varying_declarations_are_rewritten() {
|
||||
let vsh = "#version 330\nout float sphericalVertexDistance;out float cylindricalVertexDistance;\
|
||||
@@ -736,6 +909,63 @@ mod tests {
|
||||
translate_core(vsh, fsh, &[], &include, &GLUE).unwrap();
|
||||
}
|
||||
|
||||
/// A block declared on one line, as packs write a one-float uniform, is bound like one
|
||||
/// over several lines; the blocks and samplers the caller provides are accepted, in binding
|
||||
/// order, and one it doesn't provide is not.
|
||||
#[test]
|
||||
fn a_block_on_one_line_is_bound_and_provided_blocks_are_accepted() {
|
||||
let vsh = "#version 330\nlayout(std140) uniform Day { float daylight; };\nout vec4 vertexColor;\n\
|
||||
void main() { vertexColor = vec4(daylight); gl_Position = vec4(0.0); }\n";
|
||||
let fsh = "#version 330\nuniform sampler2D Bounds;\nin vec4 vertexColor;\nout vec4 fragColor;\n\
|
||||
void main() { fragColor = vertexColor * texture(Bounds, vec2(0.5)); }\n";
|
||||
let both = |name: &str| name == "Day" || name == "Bounds";
|
||||
let program = translate_core_with(vsh, fsh, &[], &include, &GLUE, &both).unwrap();
|
||||
assert_eq!(program.other, ["Day", "Bounds"]);
|
||||
// The bindings the caller gives them at.
|
||||
let bindings = |module: &naga::Module| -> Vec<(Option<String>, u32)> {
|
||||
module
|
||||
.global_variables
|
||||
.iter()
|
||||
.filter_map(|(_, g)| Some((g.name.clone(), g.binding.as_ref()?.binding)))
|
||||
.collect()
|
||||
};
|
||||
assert!(
|
||||
bindings(&program.vertex)
|
||||
.iter()
|
||||
.any(|(_, b)| *b == other_binding(0)),
|
||||
"{:?}",
|
||||
bindings(&program.vertex)
|
||||
);
|
||||
let fragment = bindings(&program.fragment);
|
||||
assert!(
|
||||
fragment.contains(&(Some("hz_tex_Bounds".into()), other_binding(1))),
|
||||
"{fragment:?}"
|
||||
);
|
||||
assert!(
|
||||
fragment.contains(&(Some("hz_smp_Bounds".into()), other_binding(1) + 1)),
|
||||
"{fragment:?}"
|
||||
);
|
||||
let error = translate_core(vsh, fsh, &[], &include, &GLUE).unwrap_err();
|
||||
assert!(error.contains("Day is not a block or sampler"), "{error}");
|
||||
let only_day = |name: &str| name == "Day";
|
||||
let error = translate_core_with(vsh, fsh, &[], &include, &GLUE, &only_day).unwrap_err();
|
||||
assert!(
|
||||
error.contains("Bounds is not a block or sampler"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Only a uniform block's declaration names a block, written over several lines or on one.
|
||||
#[test]
|
||||
fn block_names_are_read_from_block_declarations_only() {
|
||||
assert_eq!(block_name("layout(std140) uniform Day {"), Some("Day"));
|
||||
assert_eq!(block_name("uniform Day { float daylight; };"), Some("Day"));
|
||||
assert_eq!(block_name("uniform Foo{"), Some("Foo"));
|
||||
assert_eq!(block_name("uniform Globals"), Some("Globals"));
|
||||
assert_eq!(block_name("uniform float x;"), None);
|
||||
assert_eq!(block_name("uniform sampler2D Sampler0;"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_horizon_does_not_provide_is_an_error() {
|
||||
let fsh = "#version 330\nin vec4 vertexColor;\nout vec4 fragColor;\nvoid main() { fragColor = vertexColor; }\n";
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Dynamic Resolution: the world's resolution scale follows the GPU's frame time toward a target
|
||||
//! frame rate, between the Resolution Scale setting (the floor) and 100%.
|
||||
//!
|
||||
//! The setting is the lowest scale the player accepts; where the GPU has room for more, the
|
||||
//! scale rises toward 100% a step at a time, and falls back toward the floor where frames take
|
||||
//! longer than the target allows. A verdict is the median GPU time of a window of frames, which
|
||||
//! a few slow frames (a chunk load) don't move, and a step up is taken only when the frame would
|
||||
//! still fit if its cost grew with the pixels: most of a frame doesn't, so it errs toward
|
||||
//! staying.
|
||||
//!
|
||||
//! Each change has a price: new render targets, and history that starts over (TAA, and a
|
||||
//! shader pack's own: its denoised light, reflections and accumulation). So a shader pack gets
|
||||
//! the gentle pace (long windows, big steps, a long rest after each change): walking from a
|
||||
//! heavy scene to a light one changes the scale once, not every few frames. Without a pack the
|
||||
//! quick pace follows the scene within a second.
|
||||
//!
|
||||
//! Nothing here draws or reads the GPU: the caller feeds it the frames and applies the scale it
|
||||
//! answers.
|
||||
|
||||
/// How fast the scale follows the frame.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Pace {
|
||||
/// A shader pack: its history is expensive to start over.
|
||||
Gentle,
|
||||
/// Horizon's own renderer.
|
||||
Quick,
|
||||
}
|
||||
|
||||
impl Pace {
|
||||
/// Milliseconds of frames in one verdict.
|
||||
fn window_ms(self) -> f64 {
|
||||
match self {
|
||||
Pace::Gentle => 3_000.0,
|
||||
Pace::Quick => 500.0,
|
||||
}
|
||||
}
|
||||
/// Milliseconds of frames after a change before the next verdict counts, while the new
|
||||
/// targets and the restarted history settle.
|
||||
fn settle_ms(self) -> f64 {
|
||||
match self {
|
||||
Pace::Gentle => 8_000.0,
|
||||
Pace::Quick => 1_000.0,
|
||||
}
|
||||
}
|
||||
/// Percentage points per change.
|
||||
fn step(self) -> u32 {
|
||||
match self {
|
||||
Pace::Gentle => 10,
|
||||
Pace::Quick => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame is too slow when its GPU time passes the target's frame time by this share.
|
||||
const DOWN_ABOVE: f32 = 1.05;
|
||||
/// A step up is taken when the frame, its cost grown with the pixels, stays under this share.
|
||||
const UP_BELOW: f32 = 0.95;
|
||||
/// Timed frames a window needs for a verdict (a GPU without timestamps gives none).
|
||||
const MIN_TIMED: usize = 8;
|
||||
|
||||
/// The scale in use and what it has measured since the last verdict.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DynamicResolution {
|
||||
gpu: Vec<f32>,
|
||||
window_ms: f64,
|
||||
since_change_ms: f64,
|
||||
/// The scale in use, in percent; `None` until the first change (the floor).
|
||||
scale: Option<u32>,
|
||||
}
|
||||
|
||||
impl DynamicResolution {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Back to the floor, with nothing measured (turned off, or the world changed).
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::new();
|
||||
}
|
||||
|
||||
/// The scale in use, in percent, for a Resolution Scale of `floor`: never below it, never
|
||||
/// above 100% unless the floor itself is (then it is the floor).
|
||||
pub fn scale(&self, floor: u32) -> u32 {
|
||||
self.scale.unwrap_or(floor).clamp(floor, floor.max(100))
|
||||
}
|
||||
|
||||
/// One frame: its GPU time where its timestamps have arrived, and its whole time, in
|
||||
/// milliseconds. Answers the new scale, in percent, when this frame completed a window that
|
||||
/// changed it.
|
||||
pub fn observe(
|
||||
&mut self,
|
||||
gpu_ms: Option<f32>,
|
||||
frame_ms: f32,
|
||||
floor: u32,
|
||||
target_fps: u32,
|
||||
pace: Pace,
|
||||
) -> Option<u32> {
|
||||
self.window_ms += f64::from(frame_ms);
|
||||
self.since_change_ms += f64::from(frame_ms);
|
||||
if let Some(gpu) = gpu_ms {
|
||||
self.gpu.push(gpu);
|
||||
}
|
||||
// A window lasts its time and holds enough timed frames: a slow scene (or timestamps that
|
||||
// arrive with gaps) keeps gathering past the time rather than starting over with too few,
|
||||
// which would never judge the scenes that most need a lower scale. A GPU that can't be
|
||||
// timed gathers nothing and so leaves the scale alone.
|
||||
if self.window_ms < pace.window_ms() || self.gpu.len() < MIN_TIMED {
|
||||
return None;
|
||||
}
|
||||
self.window_ms = 0.0;
|
||||
let timed = std::mem::take(&mut self.gpu);
|
||||
// At or above 100% there is nothing to vary; a change still settling isn't judged yet.
|
||||
if floor >= 100 || self.since_change_ms < pace.settle_ms() {
|
||||
return None;
|
||||
}
|
||||
let gpu = median(timed);
|
||||
let target = 1000.0 / target_fps.max(1) as f32;
|
||||
let now = self.scale(floor);
|
||||
let next = if gpu > target * DOWN_ABOVE && now > floor {
|
||||
now.saturating_sub(pace.step()).max(floor)
|
||||
} else if now < 100 {
|
||||
let up = (now + pace.step()).min(100);
|
||||
let grown = (up as f32 / now as f32).powi(2);
|
||||
if gpu * grown < target * UP_BELOW {
|
||||
up
|
||||
} else {
|
||||
now
|
||||
}
|
||||
} else {
|
||||
now
|
||||
};
|
||||
if next == now {
|
||||
return None;
|
||||
}
|
||||
self.scale = Some(next);
|
||||
self.since_change_ms = 0.0;
|
||||
Some(next)
|
||||
}
|
||||
}
|
||||
|
||||
fn median(mut values: Vec<f32>) -> f32 {
|
||||
values.sort_by(f32::total_cmp);
|
||||
values[values.len() / 2]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `ms` milliseconds of frames, each `gpu` long on the GPU and `frame` long in all; the last
|
||||
/// answer.
|
||||
fn run(
|
||||
d: &mut DynamicResolution,
|
||||
ms: f64,
|
||||
gpu: f32,
|
||||
frame: f32,
|
||||
floor: u32,
|
||||
pace: Pace,
|
||||
) -> Option<u32> {
|
||||
let mut answer = None;
|
||||
let mut t = 0.0;
|
||||
while t < ms {
|
||||
if let Some(scale) = d.observe(Some(gpu), frame, floor, 60, pace) {
|
||||
answer = Some(scale);
|
||||
}
|
||||
t += f64::from(frame);
|
||||
}
|
||||
answer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_starts_at_the_floor_and_climbs_while_there_is_room() {
|
||||
let mut d = DynamicResolution::new();
|
||||
assert_eq!(d.scale(70), 70);
|
||||
// 8 ms of a 16.7 ms budget: room to grow, one step per window and settle.
|
||||
assert_eq!(run(&mut d, 1_200.0, 8.0, 8.0, 70, Pace::Quick), Some(75));
|
||||
assert_eq!(run(&mut d, 30_000.0, 8.0, 8.0, 70, Pace::Quick), Some(100));
|
||||
assert_eq!(d.scale(70), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_slow_frame_steps_down_but_never_below_the_floor() {
|
||||
let mut d = DynamicResolution::new();
|
||||
run(&mut d, 30_000.0, 8.0, 8.0, 70, Pace::Quick);
|
||||
assert_eq!(d.scale(70), 100);
|
||||
// 25 ms against 16.7: down a step at a time, to the floor and no further.
|
||||
run(&mut d, 60_000.0, 25.0, 25.0, 70, Pace::Quick);
|
||||
assert_eq!(d.scale(70), 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_slow_scene_still_steps_down() {
|
||||
let mut d = DynamicResolution::new();
|
||||
run(&mut d, 30_000.0, 8.0, 8.0, 70, Pace::Quick);
|
||||
assert_eq!(d.scale(70), 100);
|
||||
// 10 FPS: five frames in a 500 ms window, fewer than a verdict needs; the window keeps
|
||||
// gathering until it has them.
|
||||
run(&mut d, 60_000.0, 100.0, 100.0, 70, Pace::Quick);
|
||||
assert_eq!(d.scale(70), 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_near_the_target_stays_put() {
|
||||
let mut d = DynamicResolution::new();
|
||||
// 15 ms: under the target, but a step up (75 -> 80%) would make it 17 ms.
|
||||
assert_eq!(run(&mut d, 60_000.0, 15.0, 16.0, 75, Pace::Quick), None);
|
||||
assert_eq!(d.scale(75), 75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gentle_pace_takes_big_steps_and_long_rests() {
|
||||
let mut d = DynamicResolution::new();
|
||||
// The first window (3 s) can't count yet: a change rests 8 s, and the first counts
|
||||
// from the start.
|
||||
assert_eq!(run(&mut d, 7_000.0, 8.0, 8.0, 70, Pace::Gentle), None);
|
||||
assert_eq!(run(&mut d, 3_000.0, 8.0, 8.0, 70, Pace::Gentle), Some(80));
|
||||
// Then nothing for the 8 s rest.
|
||||
assert_eq!(run(&mut d, 7_000.0, 8.0, 8.0, 70, Pace::Gentle), None);
|
||||
assert_eq!(d.scale(70), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_changes_at_or_above_100_or_without_gpu_times() {
|
||||
let mut d = DynamicResolution::new();
|
||||
assert_eq!(run(&mut d, 30_000.0, 30.0, 30.0, 100, Pace::Quick), None);
|
||||
assert_eq!(d.scale(120), 120);
|
||||
let mut d = DynamicResolution::new();
|
||||
for _ in 0..1_000 {
|
||||
assert_eq!(d.observe(None, 8.0, 70, 60, Pace::Quick), None);
|
||||
}
|
||||
assert_eq!(d.scale(70), 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_goes_back_to_the_floor() {
|
||||
let mut d = DynamicResolution::new();
|
||||
run(&mut d, 30_000.0, 8.0, 8.0, 70, Pace::Quick);
|
||||
d.reset();
|
||||
assert_eq!(d.scale(70), 70);
|
||||
}
|
||||
}
|
||||
@@ -294,10 +294,15 @@ fn entity_written_color(in: Out, color: vec4<f32>) -> vec4<f32> {
|
||||
if in.overlay == 4u { mark = hdr_scene_alpha(globals.hdr_light.y); }
|
||||
return vec4(hdr_written(color.rgb, mark, globals.hdr_settings, globals.hdr_shares), color.a);
|
||||
}
|
||||
return color;
|
||||
if FRAME_LINEAR && (in.overlay == 11u || in.overlay == 12u) {
|
||||
// The cracks scale what is under them by twice their colour (CRUMBLING_BLEND). Vanilla's
|
||||
// product is in gamma, so under light it is (2c)^2.2: they give half of that.
|
||||
return vec4(pow(2.0 * max(color.rgb, vec3(0.0)), vec3(2.2)) * 0.5, color.a);
|
||||
}
|
||||
return frame_stored(color);
|
||||
}
|
||||
@fragment fn fs_entity_unmarked(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
return entity_color(in, front);
|
||||
return frame_stored(entity_color(in, front));
|
||||
}
|
||||
fn entity_color(in: Out, front: bool) -> vec4<f32> {
|
||||
var uv = in.uv;
|
||||
@@ -355,7 +360,7 @@ fn entity_color(in: Out, front: bool) -> vec4<f32> {
|
||||
// rendertype_entity_shadow.fsh: the shadow texture times the vertex colour,
|
||||
// then fog, with no alpha cutout, so a faint shadow in dim light still draws.
|
||||
@fragment fn fs_entity_shadow(in: Out) -> @location(0) vec4<f32> {
|
||||
return shadow_color(in);
|
||||
return frame_stored(shadow_color(in));
|
||||
}
|
||||
fn shadow_color(in: Out) -> vec4<f32> {
|
||||
var color = textureSample(atlas, atlas_sampler, in.uv) * in.color;
|
||||
@@ -448,7 +453,7 @@ fn celestial_color(in: Out) -> vec4<f32> {
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(color.rgb, hdr_entity_mark(in), globals.hdr_settings, globals.hdr_shares), color.a);
|
||||
}
|
||||
return color;
|
||||
return frame_stored(color);
|
||||
}
|
||||
|
||||
// The sky's celestial pass (sun, moon, the End sky): fs_celestial's light,
|
||||
@@ -469,7 +474,8 @@ fn celestial_color(in: Out) -> vec4<f32> {
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(added, select(1.0, hdr_mark(tier, y), full), globals.hdr_settings, globals.hdr_shares), color.a);
|
||||
}
|
||||
if HDR_WRITE_LIGHT { return vec4(added, color.a); }
|
||||
// Premultiplied after the decode: the light the texel adds, at its coverage.
|
||||
if HDR_WRITE_LIGHT { return vec4(frame_stored(color).rgb * color.a, color.a); }
|
||||
if !full { return vec4(added, 0.0); }
|
||||
return vec4(added, 1.0 - hdr_mark(tier, y));
|
||||
}
|
||||
@@ -492,7 +498,7 @@ fn energy_swirl_fog(in: Out) -> f32 {
|
||||
}
|
||||
@fragment fn fs_energy_swirl(in: Out) -> @location(0) vec4<f32> {
|
||||
let color = energy_swirl_base(in);
|
||||
return vec4(mix(color.rgb, globals.fog_color.rgb, energy_swirl_fog(in)), color.a);
|
||||
return frame_stored(vec4(mix(color.rgb, globals.fog_color.rgb, energy_swirl_fog(in)), color.a));
|
||||
}
|
||||
|
||||
// RenderPipelines.WATER_MASK: depth only (the pipeline masks the colour off).
|
||||
@@ -621,7 +627,7 @@ struct WeatherTime { values: vec4<f32> }
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_weather(in: Out) -> @location(0) vec4<f32> {
|
||||
return weather_color(in);
|
||||
return frame_stored(weather_color(in));
|
||||
}
|
||||
fn weather_color(in: Out) -> vec4<f32> {
|
||||
// Repeat within this sprite, never across the atlas. The atlas uses nearest
|
||||
|
||||
@@ -166,24 +166,6 @@ pub struct Extras {
|
||||
/// built-in track and leaving the off ones out (`musicPage`, off by
|
||||
/// default: vanilla plays exactly as before).
|
||||
pub music_page: bool,
|
||||
/// Cosmetics (`horizon_cosmetics`, off unless enabled), the lines `horizon_cosmetics::prefs`
|
||||
/// shares with the launcher's Wardrobe: Show my cosmetics (the account's, sent to the service;
|
||||
/// on by default), how much of other players' to show (no line until chosen:
|
||||
/// `effective_others`), and whether their trails and auras are hidden (off by default).
|
||||
pub cosmetics_mine: bool,
|
||||
pub cosmetics_others: Option<horizon_cosmetics::prefs::Others>,
|
||||
pub cosmetics_hide_fx: bool,
|
||||
/// My cosmetics in single player too (`cosmeticsSolo`, off by default): the window's own server
|
||||
/// names the player by their offline profile, which the service doesn't know, so their own
|
||||
/// cosmetics and name tag style show only with this on.
|
||||
pub cosmetics_solo: bool,
|
||||
/// Whether the signed-in account owns a cosmetic, as the service said (never saved; the game
|
||||
/// sets it): with no choice made, others' cosmetics show only then.
|
||||
pub cosmetics_owner: bool,
|
||||
/// Horizon's mark before the names of other Horizon players (name tags, player list), and
|
||||
/// whether the service may say this player plays Horizon: both on by default.
|
||||
pub horizon_marks: bool,
|
||||
pub horizon_listed: bool,
|
||||
/// 3D skin layers (`threeDSkinLayers`, Options → Skin Customization): each
|
||||
/// painted texel of a player's outer layer drawn as a slab
|
||||
/// (`horizon_client::mobs::skin_layers`).
|
||||
@@ -238,13 +220,6 @@ impl Default for Extras {
|
||||
wheel_cycles_settings: false,
|
||||
music_off: Vec::new(),
|
||||
music_page: false,
|
||||
cosmetics_mine: true,
|
||||
cosmetics_others: None,
|
||||
cosmetics_hide_fx: false,
|
||||
cosmetics_solo: false,
|
||||
cosmetics_owner: false,
|
||||
horizon_marks: true,
|
||||
horizon_listed: true,
|
||||
three_d_skin_layers: false,
|
||||
pvp: Default::default(),
|
||||
}
|
||||
@@ -254,11 +229,6 @@ fn bool_text(v: bool) -> &'static str {
|
||||
if v { "true" } else { "false" }
|
||||
}
|
||||
impl Extras {
|
||||
/// How much of other players' cosmetics shows: the player's choice, else all of it for an
|
||||
/// account that owns a cosmetic and none for any other (a vanilla game).
|
||||
pub fn effective_others(&self) -> horizon_cosmetics::prefs::Others {
|
||||
horizon_cosmetics::prefs::Others::effective(self.cosmetics_others, self.cosmetics_owner)
|
||||
}
|
||||
pub fn load(dir: &Path) -> Self {
|
||||
fs::read_to_string(dir.join(FILE))
|
||||
.map(|t| Self::parse(&t))
|
||||
@@ -353,15 +323,6 @@ impl Extras {
|
||||
.collect()
|
||||
}
|
||||
"musicPage" => e.music_page = flag(e.music_page),
|
||||
"cosmeticsMine" => e.cosmetics_mine = flag(e.cosmetics_mine),
|
||||
"cosmeticsOthers" => {
|
||||
e.cosmetics_others =
|
||||
horizon_cosmetics::prefs::Others::parse(value).or(e.cosmetics_others)
|
||||
}
|
||||
"cosmeticsHideFx" => e.cosmetics_hide_fx = flag(e.cosmetics_hide_fx),
|
||||
"cosmeticsSolo" => e.cosmetics_solo = flag(e.cosmetics_solo),
|
||||
horizon_cosmetics::prefs::MARKS => e.horizon_marks = flag(e.horizon_marks),
|
||||
horizon_cosmetics::prefs::LISTED => e.horizon_listed = flag(e.horizon_listed),
|
||||
SKIN_LAYERS_KEY => e.three_d_skin_layers = flag(e.three_d_skin_layers),
|
||||
key => {
|
||||
e.pvp.parse_line(key, value);
|
||||
@@ -372,7 +333,7 @@ impl Extras {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
let mut text = format!(
|
||||
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsHideFx:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
|
||||
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\n{}:{}\n",
|
||||
bool_text(self.pack_browser),
|
||||
bool_text(self.pregen),
|
||||
bool_text(self.fullbright),
|
||||
@@ -421,16 +382,6 @@ impl Extras {
|
||||
bool_text(self.wheel_cycles_settings),
|
||||
self.music_off.join(","),
|
||||
bool_text(self.music_page),
|
||||
bool_text(self.cosmetics_mine),
|
||||
// Written once the player chose.
|
||||
self.cosmetics_others
|
||||
.map_or(String::new(), |o| format!("cosmeticsOthers:{}\n", o.name())),
|
||||
bool_text(self.cosmetics_hide_fx),
|
||||
bool_text(self.cosmetics_solo),
|
||||
horizon_cosmetics::prefs::MARKS,
|
||||
bool_text(self.horizon_marks),
|
||||
horizon_cosmetics::prefs::LISTED,
|
||||
bool_text(self.horizon_listed),
|
||||
SKIN_LAYERS_KEY,
|
||||
bool_text(self.three_d_skin_layers),
|
||||
);
|
||||
@@ -616,13 +567,6 @@ pub enum Item {
|
||||
/// The Select Resource Packs screen's Add button (Modrinth).
|
||||
PackBrowser,
|
||||
Pregen,
|
||||
/// Shown only while cosmetics are enabled (`horizon_cosmetics::enabled`).
|
||||
CosmeticsMine,
|
||||
CosmeticsOthers,
|
||||
CosmeticsHideFx,
|
||||
CosmeticsSolo,
|
||||
HorizonMarks,
|
||||
HorizonMe,
|
||||
/// On the PvP & HUD page (`pvp::rows`).
|
||||
Pvp(crate::pvp::Opt),
|
||||
}
|
||||
@@ -630,8 +574,10 @@ pub enum Item {
|
||||
pub fn rows() -> Vec<Result<Item, &'static str>> {
|
||||
use Item::*;
|
||||
// Fullbright, Dynamic lights and Hide block-break particles change the picture: they
|
||||
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here.
|
||||
let mut rows = vec![
|
||||
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here. The Cosmetics
|
||||
// group comes last, from the cosmetics' own section (`pages::Place::Extras`) while they are
|
||||
// offered.
|
||||
vec![
|
||||
Err("horizon.extras.header.third_person"),
|
||||
Ok(OwnNameTag),
|
||||
Err("horizon.extras.header.inventory"),
|
||||
@@ -652,19 +598,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
|
||||
Err("horizon.extras.header.menus"),
|
||||
Ok(WheelCyclesSettings),
|
||||
Ok(PackBrowser),
|
||||
];
|
||||
if horizon_cosmetics::enabled() {
|
||||
rows.extend([
|
||||
Err("horizon.extras.header.cosmetics"),
|
||||
Ok(CosmeticsMine),
|
||||
Ok(CosmeticsSolo),
|
||||
Ok(CosmeticsOthers),
|
||||
Ok(CosmeticsHideFx),
|
||||
Ok(HorizonMarks),
|
||||
Ok(HorizonMe),
|
||||
]);
|
||||
}
|
||||
rows
|
||||
]
|
||||
}
|
||||
impl Item {
|
||||
/// The language key of the caption (`horizon.*`, assets/horizon/lang).
|
||||
@@ -690,12 +624,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_settings",
|
||||
Item::PackBrowser => "horizon.extras.pack_browser",
|
||||
Item::Pregen => "horizon.extras.pregen",
|
||||
Item::CosmeticsMine => "horizon.extras.cosmetics_mine",
|
||||
Item::CosmeticsOthers => "horizon.extras.cosmetics_others",
|
||||
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx",
|
||||
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo",
|
||||
Item::HorizonMarks => "horizon.extras.horizon_marks",
|
||||
Item::HorizonMe => "horizon.extras.horizon_me",
|
||||
Item::Pvp(o) => o.caption_key(),
|
||||
}
|
||||
}
|
||||
@@ -722,12 +650,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_list_settings.tooltip",
|
||||
Item::PackBrowser => "horizon.extras.pack_browser.tooltip",
|
||||
Item::Pregen => "horizon.extras.pregen.tooltip",
|
||||
Item::CosmeticsMine => "horizon.extras.cosmetics_mine.tooltip",
|
||||
Item::CosmeticsOthers => "horizon.extras.cosmetics_others.tooltip",
|
||||
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx.tooltip",
|
||||
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo.tooltip",
|
||||
Item::HorizonMarks => "horizon.extras.horizon_marks.tooltip",
|
||||
Item::HorizonMe => "horizon.extras.horizon_me.tooltip",
|
||||
Item::Pvp(o) => o.description_key(),
|
||||
}
|
||||
}
|
||||
@@ -763,12 +685,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => "Scroll wheel changes settings",
|
||||
Item::PackBrowser => "Resource pack browser",
|
||||
Item::Pregen => "Chunk pregeneration",
|
||||
Item::CosmeticsMine => "My cosmetics",
|
||||
Item::CosmeticsOthers => "Other players' cosmetics",
|
||||
Item::CosmeticsHideFx => "Hide other players' trails and auras",
|
||||
Item::CosmeticsSolo => "In single player",
|
||||
Item::HorizonMarks => "Horizon logos",
|
||||
Item::HorizonMe => "Show me as Horizon",
|
||||
Item::Pvp(o) => o.caption(),
|
||||
}
|
||||
}
|
||||
@@ -783,8 +699,7 @@ impl Item {
|
||||
Pvp(o) => return o.parent().map(Pvp),
|
||||
Fullbright | DynamicLights | OwnNameTag | HideBlockParticles | MouseTweaks | Sort
|
||||
| AutoReconnect | NoAi | AgentControl | ReproKeys | Splitscreen | ChatAnimation
|
||||
| WheelCyclesSettings | MusicPage | PackBrowser | CosmeticsMine | CosmeticsOthers
|
||||
| CosmeticsHideFx | CosmeticsSolo | HorizonMarks | HorizonMe | Pregen => {
|
||||
| WheelCyclesSettings | MusicPage | PackBrowser | Pregen => {
|
||||
return None;
|
||||
}
|
||||
})
|
||||
@@ -842,24 +757,6 @@ impl Item {
|
||||
Item::ReproKeys => {
|
||||
"F3+R starts or stops a repro recording and F3+E writes its bundle. Off by default: a recording copies the world."
|
||||
}
|
||||
Item::CosmeticsMine => {
|
||||
"Hidden: every Horizon player sees you without your cosmetics, you included. The same switch as the launcher's Wardrobe."
|
||||
}
|
||||
Item::CosmeticsOthers => {
|
||||
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
|
||||
}
|
||||
Item::CosmeticsHideFx => {
|
||||
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
|
||||
}
|
||||
Item::CosmeticsSolo => {
|
||||
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
|
||||
}
|
||||
Item::HorizonMarks => {
|
||||
"Show the Horizon logo next to Horizon players: a small logo before the names of other players who use Horizon, above their heads and in the player list. Only Horizon players see it: servers receive nothing."
|
||||
}
|
||||
Item::HorizonMe => {
|
||||
"Show me as a Horizon player: other Horizon players see the logo by your name. On, the service answers anyone who asks about you that you played Horizon in the last 3 hours; off, it never reports you."
|
||||
}
|
||||
Item::Pvp(o) => o.description(),
|
||||
Item::WheelCyclesSettings => {
|
||||
"The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list."
|
||||
@@ -886,12 +783,6 @@ impl Item {
|
||||
| Item::Splitscreen
|
||||
| Item::WheelCyclesSettings
|
||||
| Item::PackBrowser => Some(Cost::None),
|
||||
Item::CosmeticsMine
|
||||
| Item::CosmeticsOthers
|
||||
| Item::CosmeticsHideFx
|
||||
| Item::CosmeticsSolo
|
||||
| Item::HorizonMarks
|
||||
| Item::HorizonMe => Some(Cost::Low),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -917,10 +808,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => e.wheel_cycles_settings,
|
||||
Item::PackBrowser => e.pack_browser,
|
||||
Item::Pregen => e.pregen,
|
||||
Item::CosmeticsHideFx => e.cosmetics_hide_fx,
|
||||
Item::CosmeticsSolo => e.cosmetics_solo,
|
||||
Item::HorizonMarks => e.horizon_marks,
|
||||
Item::HorizonMe => e.horizon_listed,
|
||||
Item::Pvp(o) => return o.switch(&e.pvp),
|
||||
_ => return None,
|
||||
})
|
||||
@@ -930,30 +817,14 @@ impl Item {
|
||||
match self {
|
||||
Item::SortKey => crate::keybinds::display_name(&e.sort_key).plain(),
|
||||
Item::Pvp(o) if o.switch(&e.pvp).is_none() => o.value(&e.pvp).unwrap_or_default(),
|
||||
Item::CosmeticsMine => match e.cosmetics_mine {
|
||||
true => crate::lang::get_or("horizon.extras.cosmetics.shown", "Shown"),
|
||||
false => crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden"),
|
||||
},
|
||||
Item::CosmeticsOthers => {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
match e.effective_others() {
|
||||
Others::Shown => crate::lang::get_or("horizon.extras.cosmetics.shown", "Shown"),
|
||||
Others::NameTags => {
|
||||
crate::lang::get_or("horizon.extras.cosmetics.name_tags", "Name tags only")
|
||||
}
|
||||
Others::Hidden => {
|
||||
crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
other => match other.switch(e) {
|
||||
Some(true) => crate::lang::get_or("On", "On"),
|
||||
_ => crate::lang::get_or("Off", "Off"),
|
||||
},
|
||||
}
|
||||
}
|
||||
/// A click (`back` for the right button: it turns the three-way cosmetics button the other
|
||||
/// way; every other item left is a switch, so both flip it). The sort key binds instead.
|
||||
/// A click (`back` for the right button: a PvP & HUD choice steps back; a switch flips
|
||||
/// either way). The sort key binds instead.
|
||||
pub fn cycle(self, e: &mut Extras, back: bool) {
|
||||
match self {
|
||||
Item::Fullbright => e.fullbright = !e.fullbright,
|
||||
@@ -975,16 +846,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => e.wheel_cycles_settings = !e.wheel_cycles_settings,
|
||||
Item::PackBrowser => e.pack_browser = !e.pack_browser,
|
||||
Item::Pregen => e.pregen = !e.pregen,
|
||||
Item::CosmeticsMine => e.cosmetics_mine = !e.cosmetics_mine,
|
||||
Item::CosmeticsOthers => {
|
||||
// From what the button shows; the result is the player's choice from now on.
|
||||
let o = e.effective_others();
|
||||
e.cosmetics_others = Some(if back { o.next().next() } else { o.next() });
|
||||
}
|
||||
Item::CosmeticsHideFx => e.cosmetics_hide_fx = !e.cosmetics_hide_fx,
|
||||
Item::CosmeticsSolo => e.cosmetics_solo = !e.cosmetics_solo,
|
||||
Item::HorizonMarks => e.horizon_marks = !e.horizon_marks,
|
||||
Item::HorizonMe => e.horizon_listed = !e.horizon_listed,
|
||||
Item::Pvp(o) => o.cycle(&mut e.pvp, back),
|
||||
Item::SortKey => {}
|
||||
}
|
||||
@@ -1003,73 +864,6 @@ impl Item {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn other_players_cosmetics_cycle_three_ways_and_read_the_old_switch() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut e = Extras {
|
||||
cosmetics_owner: true,
|
||||
..Extras::default()
|
||||
};
|
||||
assert_eq!(
|
||||
(e.effective_others(), e.cosmetics_hide_fx, e.cosmetics_mine),
|
||||
(Others::Shown, false, true)
|
||||
);
|
||||
Item::CosmeticsOthers.cycle(&mut e, false);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::NameTags));
|
||||
Item::CosmeticsOthers.cycle(&mut e, false);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::Hidden));
|
||||
// The right button goes back.
|
||||
Item::CosmeticsOthers.cycle(&mut e, true);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::NameTags));
|
||||
// Ownership is the service's, never written.
|
||||
assert_eq!(
|
||||
Extras::parse(&e.serialize()),
|
||||
Extras {
|
||||
cosmetics_owner: false,
|
||||
..e.clone()
|
||||
}
|
||||
);
|
||||
// The launcher's Wardrobe writes the same lines.
|
||||
assert_eq!(
|
||||
horizon_cosmetics::prefs::Prefs::parse(&e.serialize()).others,
|
||||
Some(Others::NameTags)
|
||||
);
|
||||
// Before this, the line was on or off.
|
||||
assert_eq!(
|
||||
Extras::parse("cosmeticsOthers:false").cosmetics_others,
|
||||
Some(Others::Hidden)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn others_cosmetics_are_off_for_a_player_who_owns_none_until_chosen() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut e = Extras::default();
|
||||
// Nothing chosen, nothing owned: the button shows Hidden and no line is written.
|
||||
assert_eq!(e.effective_others(), Others::Hidden);
|
||||
assert_eq!(
|
||||
Item::CosmeticsOthers.value(&e),
|
||||
crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden")
|
||||
);
|
||||
assert!(!e.serialize().contains("cosmeticsOthers"));
|
||||
// The service says the account owns one: shown, still not a choice.
|
||||
e.cosmetics_owner = true;
|
||||
assert_eq!(e.effective_others(), Others::Shown);
|
||||
assert!(!e.serialize().contains("cosmeticsOthers"));
|
||||
// A non-owner pressing the button goes on from Hidden, and that choice is kept.
|
||||
e.cosmetics_owner = false;
|
||||
Item::CosmeticsOthers.cycle(&mut e, false);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::Shown));
|
||||
let saved = Extras::parse(&e.serialize());
|
||||
assert_eq!(saved.effective_others(), Others::Shown);
|
||||
// An owner who chose Hidden keeps it.
|
||||
let hidden = Extras {
|
||||
cosmetics_owner: true,
|
||||
..Extras::parse("cosmeticsOthers:hidden\n")
|
||||
};
|
||||
assert_eq!(hidden.effective_others(), Others::Hidden);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_wheel_setting_from_before_the_default_changed_is_dropped() {
|
||||
assert!(!Extras::parse("wheelCyclesSettings:true\n").wheel_cycles_settings);
|
||||
|
||||
@@ -206,14 +206,13 @@ impl Screen for ExtrasScreen {
|
||||
fn note_lines(&self) -> usize {
|
||||
NOTE_LINES
|
||||
}
|
||||
/// The rows read the extras, the mods, whether cosmetics are offered and the sections' rows.
|
||||
/// The rows read the extras, the mods and the sections' rows.
|
||||
fn stamp(&self, s: &Settings) -> u64 {
|
||||
let mut added = Vec::new();
|
||||
for (_, section) in sections() {
|
||||
section.rows(&mut added);
|
||||
}
|
||||
let cosmetics = horizon_cosmetics::enabled();
|
||||
shell::stamp_of(&(&s.extras, &s.mods, cosmetics, added))
|
||||
shell::stamp_of(&(&s.extras, &s.mods, added))
|
||||
}
|
||||
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
|
||||
let mut groups = vec![Group {
|
||||
@@ -321,22 +320,6 @@ mod tests {
|
||||
assert_ne!(s.extras.mouse_tweaks_wheel, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_three_way_cosmetics_row_turns_both_ways() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut s = Settings::default();
|
||||
let mut screen = ExtrasScreen;
|
||||
let row = ExtraRow::Item(Item::CosmeticsOthers);
|
||||
// Not chosen, nothing owned: it shows Hidden, and forward wraps to Shown.
|
||||
assert_eq!(s.extras.effective_others(), Others::Hidden);
|
||||
screen.press(&mut s, row, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
|
||||
screen.press(&mut s, row, true);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
|
||||
screen.press(&mut s, row, true);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
|
||||
}
|
||||
|
||||
/// The sort key binds instead of cycling; the wheel leaves it be.
|
||||
#[test]
|
||||
fn the_key_row_waits_for_a_key() {
|
||||
|
||||
@@ -47,6 +47,10 @@ fn terrain_shader() -> String {
|
||||
include_str!("hdr_light.wgsl").to_owned() + include_str!("terrain.wgsl")
|
||||
}
|
||||
|
||||
fn border_shader() -> String {
|
||||
include_str!("hdr_light.wgsl").to_owned() + include_str!("border.wgsl")
|
||||
}
|
||||
|
||||
/// The static block-entity vertex entry point shares entity shading but lives
|
||||
/// in its own source file and pipeline.
|
||||
fn static_entity_shader() -> String {
|
||||
@@ -188,9 +192,11 @@ fn packed_wanted() -> bool {
|
||||
pub(crate) mod animate;
|
||||
mod arena;
|
||||
pub mod capture;
|
||||
mod census;
|
||||
pub mod compositor;
|
||||
pub(crate) mod core_lines;
|
||||
pub(crate) mod core_sky;
|
||||
pub(crate) mod core_text;
|
||||
mod cull;
|
||||
mod dx12_compiler;
|
||||
mod end_sky;
|
||||
@@ -212,7 +218,12 @@ pub use screenshot_save::Saved as SavedScreenshot;
|
||||
pub mod block_light;
|
||||
mod custom_sky;
|
||||
mod displace;
|
||||
mod frame_policy;
|
||||
pub mod lightning;
|
||||
mod linear_blend;
|
||||
#[cfg(test)]
|
||||
use frame_policy::frame_format;
|
||||
use frame_policy::{frame_configuration, horizon_tone_curve, overlay_format};
|
||||
mod lod;
|
||||
mod native_material;
|
||||
mod native_pack;
|
||||
@@ -1127,6 +1138,8 @@ pub struct Renderer {
|
||||
/// Sections upload their solid quads in the compact layout (`mesher::packed`): the solid
|
||||
/// passes read the arena themselves (`flat`) and no shader pack reads it as vertices.
|
||||
packed_quads: bool,
|
||||
/// The terrain's acceleration structures, with ray tracing on (`ray_scene`).
|
||||
ray_scene: Option<surface_setup::ray_query::ray_scene::RayScene>,
|
||||
/// The compact layout is allowed (`HORIZON_PACKED_QUADS`, `set_packed_layout`).
|
||||
packed_on: bool,
|
||||
/// The visibility buffer for flat frames' solid terrain (`visbuffer`,
|
||||
@@ -1166,6 +1179,7 @@ pub struct Renderer {
|
||||
lines_secondary: wgpu::RenderPipeline,
|
||||
/// A resource pack's `core/rendertype_lines`, which draws the block outline in their place.
|
||||
core_lines: Option<core_lines::CoreLines>,
|
||||
core_text: Option<core_text::CoreText>,
|
||||
/// The packs' `core/sky` and `core/stars`, drawn in place of the native sky and stars.
|
||||
core_sky: Option<core_sky::CoreSky>,
|
||||
/// Gizmo lines: opaque ones (`RenderTypes.lines`, which writes depth),
|
||||
@@ -2174,74 +2188,6 @@ fn draws_frame(format: wgpu::TextureFormatFeatures) -> bool {
|
||||
/// survive from the world to the tonemap and the HDR pass.
|
||||
const LINEAR_FRAME: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
|
||||
/// What a renderer's frame is drawn in: `LINEAR_FRAME` under the switch where the device draws
|
||||
/// it, for a 0.2 pack, or for the layered HDR frame where the surface offers HDR output
|
||||
/// (`linear`: `(a 0.2 pack picked, the layered HDR frame, the device draws it)`); else
|
||||
/// `WIDE_FRAME` when it is made with HDR on and Smooth Gradients (`wide_frame`,
|
||||
/// `output::resolve_frame`), the surface offering HDR output (`hdr_offered`) and the device
|
||||
/// drawing it (`draws_wide_frame`): an 8-bit frame's steps band in dark gradients, and HDR
|
||||
/// shows them. Else the swapchain's 8-bit format, as without HDR.
|
||||
fn frame_format(
|
||||
wide_frame: bool,
|
||||
hdr_offered: bool,
|
||||
device_draws_it: bool,
|
||||
swapchain: wgpu::TextureFormat,
|
||||
linear: (bool, bool, bool),
|
||||
) -> wgpu::TextureFormat {
|
||||
if linear.2 && (linear.0 || linear.1 && hdr_offered) {
|
||||
LINEAR_FRAME
|
||||
} else if wide_frame && hdr_offered && device_draws_it {
|
||||
WIDE_FRAME
|
||||
} else {
|
||||
swapchain.remove_srgb_suffix()
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame's format, and whether it is the layered HDR frame (HUD coverage): for HDR on
|
||||
/// under the switch (`hdr_float`), with no pack or a 0.2 one, where the device keeps the HUD
|
||||
/// on a layer of its own.
|
||||
fn frame_configuration(
|
||||
wide_frame: bool,
|
||||
device: (bool, bool, bool, bool),
|
||||
swapchain: wgpu::TextureFormat,
|
||||
linear_pack: bool,
|
||||
hdr_float: bool,
|
||||
) -> (wgpu::TextureFormat, bool) {
|
||||
let (hdr_offered, wide_ok, linear_ok, coverage_ok) = device;
|
||||
let layered = hdr_float && hdr_offered && linear_ok && coverage_ok;
|
||||
(
|
||||
frame_format(
|
||||
wide_frame,
|
||||
hdr_offered,
|
||||
wide_ok,
|
||||
swapchain,
|
||||
(linear_pack, layered, linear_ok),
|
||||
),
|
||||
layered,
|
||||
)
|
||||
}
|
||||
|
||||
/// What the hand, the camera overlays and the HUD draw on: the frame, except for a 0.2 pack's
|
||||
/// float frame without the HUD's layer, which is SDR: the pack's picture finishes on the
|
||||
/// swapchain's 8-bit format first, so they clip and blend per draw as on vanilla's frame.
|
||||
fn overlay_format(
|
||||
frame: wgpu::TextureFormat,
|
||||
layered: bool,
|
||||
swapchain: wgpu::TextureFormat,
|
||||
) -> wgpu::TextureFormat {
|
||||
if frame == LINEAR_FRAME && !layered {
|
||||
swapchain.remove_srgb_suffix()
|
||||
} else {
|
||||
frame
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Horizon's tone curve (`Hdr::tonemap`) runs on this frame: on the layered frame's
|
||||
/// world, unless a 0.2 pack's `[tonemap]` already mapped it. One tone mapping, never two.
|
||||
fn horizon_tone_curve(layered: bool, world: bool, pack_tonemap: bool) -> bool {
|
||||
layered && world && !pack_tonemap
|
||||
}
|
||||
|
||||
/// Opens a device on the first of `backend_order`'s backends that gives one.
|
||||
/// Offscreen rendering needs no surface, so it can use a GPU the window's
|
||||
/// display cannot present from (a headless compositor).
|
||||
@@ -2602,160 +2548,6 @@ enum FirstFrame {
|
||||
fn on_off(on: bool) -> &'static str {
|
||||
if on { "on" } else { "off" }
|
||||
}
|
||||
impl Renderer {
|
||||
/// Sections with GPU buffers, for soak tests watching residency.
|
||||
pub fn resident_sections(&self) -> usize {
|
||||
self.sections.len()
|
||||
}
|
||||
/// Quads of the far terrain tiles with GPU buffers.
|
||||
pub fn lod_quads(&self) -> u64 {
|
||||
self.lods
|
||||
.values()
|
||||
.map(|tile| u64::from(tile.mesh.solid + tile.mesh.translucent))
|
||||
.sum()
|
||||
}
|
||||
/// Tiles and quads of the far terrain tiles with GPU buffers, by level.
|
||||
pub fn lod_levels(&self) -> Vec<[u64; 2]> {
|
||||
let mut levels = Vec::new();
|
||||
for (key, tile) in &self.lods {
|
||||
let level = usize::from(key.level);
|
||||
if levels.len() <= level {
|
||||
levels.resize(level + 1, [0, 0]);
|
||||
}
|
||||
levels[level][0] += 1;
|
||||
levels[level][1] += u64::from(tile.mesh.solid + tile.mesh.translucent);
|
||||
}
|
||||
levels
|
||||
}
|
||||
/// Whether solid quads go to the arena in the compact layout
|
||||
/// (`packed_quads`), and whether far tiles' own records are on
|
||||
/// (`lod::packed::far_packing`).
|
||||
pub fn packed_layout(&self) -> [bool; 2] {
|
||||
[self.packed_quads, crate::lod::packed::far_packing()]
|
||||
}
|
||||
/// Bytes of the arena the resident chunk sections hold, and of the one far
|
||||
/// tiles hold: the same split `gpu_census` reports, for `status`, so the
|
||||
/// compact layouts can be seen working.
|
||||
pub fn vertex_bytes_split(&self) -> [u64; 2] {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
|
||||
[
|
||||
self.sections.values().map(used).sum(),
|
||||
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
|
||||
]
|
||||
}
|
||||
/// Bytes of the section vertex arenas' buffers, and of the vertices
|
||||
/// resident sections hold in them.
|
||||
pub fn section_vertex_bytes(&self) -> [u64; 2] {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let vertices: u64 = self
|
||||
.sections
|
||||
.values()
|
||||
.chain(self.lods.values().map(|tile| &*tile.mesh))
|
||||
.map(|s| s.solid_slots + u64::from(s.translucent) * 4)
|
||||
.sum();
|
||||
let translucent = self.translucent_arena.as_ref();
|
||||
let arenas = self.arena.bytes() + translucent.map_or(0, arena::Arena::bytes);
|
||||
[arenas, vertices * stride]
|
||||
}
|
||||
/// Bytes of the renderer's largest GPU resources, by name (`scene_replay --memory`):
|
||||
/// the terrain arenas (their pages, and what resident sections and far tiles use of
|
||||
/// them), the quad indices, the block sprites with their mips, the LabPBR maps, flat
|
||||
/// draws' buffers and the visibility buffer's.
|
||||
pub fn gpu_census(&self) -> Vec<(&'static str, u64)> {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
|
||||
let textures = |t: &[wgpu::Texture]| t.iter().map(texture_bytes).sum::<u64>();
|
||||
let translucent = self.translucent_arena.as_ref();
|
||||
vec![
|
||||
("section_arena_pages", self.arena.bytes()),
|
||||
(
|
||||
"translucent_arena_pages",
|
||||
translucent.map_or(0, arena::Arena::bytes),
|
||||
),
|
||||
("sorted_index_pages", self.sorted_indices.bytes()),
|
||||
(
|
||||
"near_section_vertices",
|
||||
self.sections.values().map(used).sum(),
|
||||
),
|
||||
(
|
||||
"held_section_vertices",
|
||||
self.hold
|
||||
.sections()
|
||||
.filter_map(|held| held.gpu.as_ref())
|
||||
.map(used)
|
||||
.sum(),
|
||||
),
|
||||
(
|
||||
"far_tile_vertices",
|
||||
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
|
||||
),
|
||||
("quad_indices", self.indices.size()),
|
||||
("block_sprites", textures(&self.sprite_textures)),
|
||||
(
|
||||
"pbr_maps",
|
||||
self.pbr_maps
|
||||
.as_ref()
|
||||
.map_or(0, |maps| maps.iter().flatten().map(texture_bytes).sum()),
|
||||
),
|
||||
(
|
||||
"flat_draws",
|
||||
self.flat.as_ref().map_or(0, flat::Flat::bytes),
|
||||
),
|
||||
(
|
||||
"visibility_buffer",
|
||||
self.visbuf.as_ref().map_or(0, visbuffer::Visbuffer::bytes),
|
||||
),
|
||||
]
|
||||
}
|
||||
/// Device memory the backend's allocator holds, by the label of what holds
|
||||
/// it, largest first, with its allocated and reserved totals (Vulkan and
|
||||
/// DirectX 12; none on Metal and OpenGL) (`horizon-ctl memory`).
|
||||
pub fn gpu_memory_by_label(&self) -> Option<(u64, u64, Vec<(String, u64)>)> {
|
||||
let report = self.device.generate_allocator_report()?;
|
||||
let mut by_label: HashMap<String, u64> = HashMap::new();
|
||||
for a in report.allocations {
|
||||
*by_label.entry(a.name).or_default() += a.size;
|
||||
}
|
||||
let mut labels: Vec<_> = by_label.into_iter().collect();
|
||||
labels.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
Some((
|
||||
report.total_allocated_bytes,
|
||||
report.total_reserved_bytes,
|
||||
labels,
|
||||
))
|
||||
}
|
||||
/// Heap bytes of the renderer's per-section bookkeeping (`horizon-ctl memory`):
|
||||
/// resident sections, far tiles, block entities and light sources by section.
|
||||
pub fn cpu_census(&self) -> [(&'static str, usize); 4] {
|
||||
use std::mem::size_of;
|
||||
let per_section = |map_len: usize, lists: usize| {
|
||||
map_len * (size_of::<SectionKey>() + size_of::<Vec<([i32; 3], u32)>>() + 1)
|
||||
+ lists * size_of::<([i32; 3], u32)>()
|
||||
};
|
||||
let lists = |map: &crate::fast_hash::FxHashMap<SectionKey, Vec<([i32; 3], u32)>>| {
|
||||
per_section(
|
||||
map.capacity(),
|
||||
map.values().map(Vec::capacity).sum::<usize>(),
|
||||
)
|
||||
};
|
||||
let animated: usize = self
|
||||
.sections
|
||||
.values()
|
||||
.map(|s| s.animated_layers.len() * size_of::<u16>())
|
||||
.sum();
|
||||
[
|
||||
("resident_sections", self.sections.heap_bytes() + animated),
|
||||
(
|
||||
"far_tiles",
|
||||
self.lods.capacity()
|
||||
* (size_of::<crate::lod::TileKey>() + size_of::<lod::Tile>() + 1),
|
||||
),
|
||||
("block_entities", lists(&self.block_entities)),
|
||||
("light_sources", lists(&self.light_blocks)),
|
||||
]
|
||||
}
|
||||
}
|
||||
impl Renderer {
|
||||
pub async fn new(
|
||||
window: Arc<Window>,
|
||||
@@ -2911,6 +2703,7 @@ impl Renderer {
|
||||
let frame = output::resolve_frame(settings, request, output_caps, display, config.format);
|
||||
let (frame_format, hud_coverage, overlay_format) =
|
||||
(frame.format, frame.hud_layer, frame.overlay_format);
|
||||
let linear_light = frame.linear_light;
|
||||
if previous.is_none() {
|
||||
startup_step(format_args!("creating the pipelines"));
|
||||
}
|
||||
@@ -3066,9 +2859,10 @@ impl Renderer {
|
||||
blend: Option<wgpu::BlendState>,
|
||||
hud: Option<bool>| {
|
||||
let mut constants = constants.to_vec();
|
||||
constants.push((
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
// The debug crosshair draws with the HUD, in gamma.
|
||||
constants.extend(linear_blend::frame_constants(
|
||||
frame_format,
|
||||
linear_light && hud.is_none(),
|
||||
));
|
||||
let desc = wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
@@ -3881,8 +3675,12 @@ impl Renderer {
|
||||
},
|
||||
)],
|
||||
});
|
||||
let weather =
|
||||
weather::Weather::new(&device, frame_format, &entity_bindings, &entity_shader);
|
||||
let weather = weather::Weather::new(
|
||||
&device,
|
||||
(frame_format, linear_light),
|
||||
&entity_bindings,
|
||||
&entity_shader,
|
||||
);
|
||||
let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Atlas"),
|
||||
..Default::default()
|
||||
@@ -4048,6 +3846,7 @@ impl Renderer {
|
||||
];
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
let entity_pipeline_in = |format,
|
||||
linear,
|
||||
label,
|
||||
cull_mode,
|
||||
depth_write,
|
||||
@@ -4086,10 +3885,7 @@ impl Renderer {
|
||||
module: &entity_shader,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
)],
|
||||
constants: &linear_blend::frame_constants(frame_format, linear),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
@@ -4107,6 +3903,7 @@ impl Renderer {
|
||||
|label, cull_mode, depth_write, compare, bias, blend, clouds, fragment, write_mask| {
|
||||
entity_pipeline_in(
|
||||
frame_format,
|
||||
linear_light,
|
||||
label,
|
||||
cull_mode,
|
||||
depth_write,
|
||||
@@ -4246,8 +4043,12 @@ impl Renderer {
|
||||
"fs_entity_unmarked",
|
||||
wgpu::ColorWrites::ALL,
|
||||
);
|
||||
let world_text_pipeline =
|
||||
world_text::pipeline(&device, Some(&entity_layout), &entity_shader, frame_format);
|
||||
let world_text_pipeline = world_text::pipeline(
|
||||
&device,
|
||||
Some(&entity_layout),
|
||||
&entity_shader,
|
||||
(frame_format, linear_light),
|
||||
);
|
||||
// RenderPipelines.ENTITY_SOLID: a surface of an entity that is coplanar with
|
||||
// terrain wins the tie in vanilla (the boat's floor over the trapdoor on the
|
||||
// ground it is stopped by, #5950). Terrain and entities are separate draw
|
||||
@@ -4270,10 +4071,12 @@ impl Renderer {
|
||||
alpha_blend,
|
||||
false,
|
||||
);
|
||||
// The hand as `entity_pipeline` draws it, in the overlays' format.
|
||||
let overlay_hand = (overlay_format != frame_format).then(|| {
|
||||
// The hand as `entity_pipeline` draws it, in the overlays' format and gamma: it draws
|
||||
// after a 0.2 pack's tonemap.
|
||||
let overlay_hand = (overlay_format != frame_format || linear_light).then(|| {
|
||||
entity_pipeline_in(
|
||||
overlay_format,
|
||||
false,
|
||||
"First-person hand",
|
||||
None,
|
||||
true,
|
||||
@@ -4307,7 +4110,10 @@ impl Renderer {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &static_entity_shader,
|
||||
entry_point: Some("fs_entity_unmarked"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &linear_blend::frame_constants(frame_format, linear_light),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: frame_format,
|
||||
// fs_entity's discard handles binary cutout alpha. Any
|
||||
@@ -4414,10 +4220,7 @@ impl Renderer {
|
||||
module: &entity_shader,
|
||||
entry_point: Some("fs_celestial"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
)],
|
||||
constants: &linear_blend::frame_constants(frame_format, linear_light),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
@@ -4512,10 +4315,7 @@ impl Renderer {
|
||||
module: &entity_shader,
|
||||
entry_point: Some("fs_sky_celestial"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
)],
|
||||
constants: &linear_blend::frame_constants(frame_format, linear_light),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
@@ -4654,7 +4454,7 @@ impl Renderer {
|
||||
});
|
||||
let border_shader = kept.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon world border"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("border.wgsl").into()),
|
||||
source: wgpu::ShaderSource::Wgsl(border_shader().into()),
|
||||
});
|
||||
let border_attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2];
|
||||
// RenderPipelines.WORLD_BORDER: BlendFunction.OVERLAY, no culling,
|
||||
@@ -4691,7 +4491,10 @@ impl Renderer {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &border_shader,
|
||||
entry_point: Some("fs_border"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &linear_blend::frame_constants(frame_format, linear_light),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: frame_format,
|
||||
blend: Some(wgpu::BlendState {
|
||||
@@ -4917,6 +4720,7 @@ impl Renderer {
|
||||
entity: entity_bindings.clone(),
|
||||
static_entity: static_entity_bindings.clone(),
|
||||
format: frame_format,
|
||||
linear: linear_light,
|
||||
sprite_constants,
|
||||
hdr_output: hdr_output.is_some(),
|
||||
});
|
||||
@@ -4927,7 +4731,13 @@ impl Renderer {
|
||||
log::info!(
|
||||
"Horizon: solid terrain through a visibility buffer (HORIZON_VISBUFFER)"
|
||||
);
|
||||
visbuffer::Visbuffer::new(&device, &bindings, f, frame_format, &sprite_constants)
|
||||
visbuffer::Visbuffer::new(
|
||||
&device,
|
||||
&bindings,
|
||||
f,
|
||||
(frame_format, linear_light),
|
||||
&sprite_constants,
|
||||
)
|
||||
});
|
||||
let multi_draw = device
|
||||
.features()
|
||||
@@ -5008,8 +4818,8 @@ impl Renderer {
|
||||
let world_post = world_post::Post::new(&device, frame_format);
|
||||
let uploads = upload::Uploads::new(&device);
|
||||
let section_uploads = upload::Uploads::with_chunk(&device, SECTION_UPLOAD_CHUNK);
|
||||
let stars = stars::Stars::new(&device, frame_format);
|
||||
let custom_sky = custom_sky::CustomSky::new(&device, frame_format);
|
||||
let stars = stars::Stars::new(&device, (frame_format, linear_light));
|
||||
let custom_sky = custom_sky::CustomSky::new(&device, (frame_format, linear_light));
|
||||
let entity_outline = entity_outline::EntityOutline::new(
|
||||
&device,
|
||||
(&entity_shader, &entity_layout, &entity_vertex),
|
||||
@@ -5037,7 +4847,8 @@ impl Renderer {
|
||||
on_off(gpu_timer.is_some()),
|
||||
));
|
||||
}
|
||||
let world_primitives = world_primitives::WorldPrimitives::new(&device, frame_format);
|
||||
let world_primitives =
|
||||
world_primitives::WorldPrimitives::new(&device, (frame_format, linear_light));
|
||||
let screenshot_saves = screenshot_save::Saves::new(&device);
|
||||
Ok(Self {
|
||||
display: None,
|
||||
@@ -5061,6 +4872,7 @@ impl Renderer {
|
||||
last_wait: std::time::Duration::ZERO,
|
||||
last_encode: std::time::Duration::ZERO,
|
||||
window,
|
||||
ray_scene: surface_setup::ray_query::ray_scene::RayScene::new(&device),
|
||||
device,
|
||||
reuse,
|
||||
queue: submit::Queue::new(queue, submit::wanted()),
|
||||
@@ -5095,6 +4907,7 @@ impl Renderer {
|
||||
lines,
|
||||
lines_secondary,
|
||||
core_lines: None,
|
||||
core_text: None,
|
||||
core_sky: None,
|
||||
debug_lines_opaque,
|
||||
debug_lines,
|
||||
@@ -5442,6 +5255,8 @@ impl Renderer {
|
||||
/// outline, or the built-in one.
|
||||
pub fn set_lines_shader(&mut self, program: Option<&crate::core_shader::CoreProgram>) {
|
||||
let format = self.output.frame.format;
|
||||
// A pack's GLSL writes vanilla's gamma: a frame of linear light keeps the built-in one.
|
||||
let program = program.filter(|_| !self.output.frame.linear_light);
|
||||
self.core_lines =
|
||||
program.and_then(|program| core_lines::CoreLines::new(&self.device, format, program));
|
||||
}
|
||||
@@ -5452,6 +5267,9 @@ impl Renderer {
|
||||
disc: Option<&crate::core_shader::CoreProgram>,
|
||||
stars: Option<&crate::core_shader::CoreProgram>,
|
||||
) {
|
||||
// As `set_lines_shader`: not on a frame of linear light.
|
||||
let keep = !self.output.frame.linear_light;
|
||||
let (disc, stars) = (disc.filter(|_| keep), stars.filter(|_| keep));
|
||||
self.core_sky = core_sky::CoreSky::new(&self.device, self.output.frame.format, disc, stars);
|
||||
}
|
||||
/// LevelRenderer.submitBlockOutline: the buffer holds `edges` per stroke, the black
|
||||
@@ -5477,17 +5295,7 @@ impl Renderer {
|
||||
pass.draw(0..4, first..first + edges);
|
||||
}
|
||||
fn quad_indices(device: &wgpu::Device, quads: u32) -> wgpu::Buffer {
|
||||
let indices: Vec<u32> = (0..quads)
|
||||
.flat_map(|q| {
|
||||
let b = q * 4;
|
||||
[b, b + 1, b + 2, b, b + 2, b + 3]
|
||||
})
|
||||
.collect();
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Quad indices"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
})
|
||||
surface_setup::ray_query::quad_indices(device, quads)
|
||||
}
|
||||
fn offscreen_target(
|
||||
device: &wgpu::Device,
|
||||
@@ -5966,6 +5774,7 @@ impl Renderer {
|
||||
section: Option<SectionGpu>,
|
||||
fading: Option<Instant>,
|
||||
) {
|
||||
self.ray_mark(key);
|
||||
let section = section.map(|section| SectionGpu { fading, ..section });
|
||||
self.set_detail(key, section.is_some() || mesh.empty);
|
||||
if let Some(mut section) = section {
|
||||
@@ -6654,6 +6463,7 @@ impl Renderer {
|
||||
self.newest_lod_fade = None;
|
||||
self.block_entity_revision = next_block_entity_revision();
|
||||
self.sections.clear();
|
||||
self.ray_clear();
|
||||
// (a hold on land that is gone ends; the arenas' clearing below takes what waited)
|
||||
self.hold.clear();
|
||||
self.lods.clear();
|
||||
@@ -7511,6 +7321,7 @@ impl Renderer {
|
||||
flat: self.flat.as_ref().map(|f| &f.draw_layout),
|
||||
sprites: self.terrain_sprites,
|
||||
format: self.output.frame.format,
|
||||
linear: self.output.frame.linear_light,
|
||||
sprite_tags: &sprite_tags,
|
||||
sprite_materials: &sprite_materials,
|
||||
entities: &self.entity_bindings,
|
||||
@@ -8882,6 +8693,7 @@ impl Renderer {
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
}
|
||||
self.ray_prepare(&mut encoder, camera, frame.title);
|
||||
self.origins.clear();
|
||||
let (sections, retiring) = (&self.sections, &self.lod_retiring);
|
||||
laps.lap("r_cameras");
|
||||
@@ -9428,6 +9240,8 @@ impl Renderer {
|
||||
&self.entity_atlas,
|
||||
)
|
||||
};
|
||||
let pack = self.pack_blocks(frame, fog, globals.fog_ends);
|
||||
self.write_core_text(&pack, views.primary(), pose, jitter, fog_color);
|
||||
let outline_edges = frame.outline.len().min(MAX_OUTLINE_EDGES as usize);
|
||||
if outline_edges > 0 {
|
||||
// The same per-line colour and pixel width layout as the gizmo lines. A high
|
||||
@@ -9461,20 +9275,14 @@ impl Renderer {
|
||||
self.uploads
|
||||
.buffer(&mut encoder, &self.outline, 0, bytemuck::cast_slice(lines));
|
||||
if let Some(core) = &self.core_lines {
|
||||
let size = [self.config.width, self.config.height];
|
||||
let block = crate::post_chain::Globals {
|
||||
glint_alpha: frame.glint_alpha,
|
||||
..frame.post_effects.globals
|
||||
};
|
||||
let [sky_end, clouds_end, ..] = globals.fog_ends;
|
||||
let data = core_lines::uniforms(
|
||||
block.bytes(size, frame.post_effects.menu_blur_radius as i32),
|
||||
pack.globals,
|
||||
views.primary(),
|
||||
pose,
|
||||
jitter,
|
||||
globals.outline_params[3],
|
||||
fog_color,
|
||||
[fog[0], fog[1], fog[2], fog[3], sky_end, clouds_end],
|
||||
pack.fog,
|
||||
);
|
||||
self.uploads.buffer(&mut encoder, core.uniform(), 0, &data);
|
||||
}
|
||||
@@ -9484,14 +9292,7 @@ impl Renderer {
|
||||
if let Some(core) = &self.core_sky
|
||||
&& !frame.title
|
||||
{
|
||||
let size = [self.config.width, self.config.height];
|
||||
let block = crate::post_chain::Globals {
|
||||
glint_alpha: frame.glint_alpha,
|
||||
..frame.post_effects.globals
|
||||
}
|
||||
.bytes(size, frame.post_effects.menu_blur_radius as i32);
|
||||
let [sky_end, clouds_end, ..] = globals.fog_ends;
|
||||
let fog = [fog[0], fog[1], fog[2], fog[3], sky_end, clouds_end];
|
||||
let (block, fog) = (pack.globals, pack.fog);
|
||||
let view = views.primary();
|
||||
// Far enough for the disc's rim, 512 out: the sky draws without a depth test.
|
||||
let fov = (2.0 * view.tan_half_fov.atan()).to_degrees();
|
||||
@@ -10211,6 +10012,7 @@ impl Renderer {
|
||||
a: 1.0,
|
||||
}
|
||||
};
|
||||
let clear = linear_blend::stored_clear(clear, self.output.frame.linear_light);
|
||||
let sky_visible = sky_open(frame);
|
||||
laps.lap("r_world_pass");
|
||||
// One of Horizon's own packs: this frame's inputs; in a timed frame its sky draws in a
|
||||
@@ -11049,7 +10851,7 @@ impl Renderer {
|
||||
}
|
||||
if text_quads > 0 && !oit_on {
|
||||
pass.set_pipeline(warped.map_or(&self.text_polygon_offset_pipeline, |w| &w.text));
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
self.bind_world_text(&mut pass, warped.is_none(), false);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(0..text_quads * 6, (entity_quads * 4) as i32, 0..1);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
@@ -11607,7 +11409,7 @@ impl Renderer {
|
||||
// LevelRenderer.executeSeeThrough, after the rest of the world.
|
||||
if see_through_quads > 0 {
|
||||
pass.set_pipeline(&self.see_through_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
self.bind_world_text(&mut pass, true, true);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
0..see_through_quads * 6,
|
||||
@@ -12961,143 +12763,11 @@ fn frustum(m: Mat4) -> [Vec4; 6] {
|
||||
planes.map(|p| p / p.truncate().length())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod frame_policy_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn float_frames_never_schedule_light_mark_passes() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, light_marks};
|
||||
use wgpu::TextureFormat::{Bgra8Unorm, Rgb10a2Unorm, Rgba8Unorm};
|
||||
for hdr in [false, true] {
|
||||
assert!(!light_marks(LINEAR_FRAME, hdr));
|
||||
for format in [WIDE_FRAME, Bgra8Unorm, Rgba8Unorm, Rgb10a2Unorm] {
|
||||
assert_eq!(light_marks(format, hdr), hdr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_policy_keeps_compatibility_frames_and_requires_layering_for_float_hdr() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, frame_configuration};
|
||||
use wgpu::TextureFormat::{Bgra8Unorm, Bgra8UnormSrgb};
|
||||
|
||||
for bits in 0..128 {
|
||||
let hdr = bits & 1 != 0;
|
||||
let offered = bits & 2 != 0;
|
||||
let wide_ok = bits & 4 != 0;
|
||||
let float_ok = bits & 8 != 0;
|
||||
let coverage_ok = bits & 16 != 0;
|
||||
let switch = bits & 32 != 0;
|
||||
let smooth = bits & 64 != 0;
|
||||
for pack in ["none", "0.1", "0.2", "shaders"] {
|
||||
let float_hdr = matches!(pack, "none" | "0.2");
|
||||
let expected = if switch && float_ok && float_hdr && hdr && offered && coverage_ok {
|
||||
(LINEAR_FRAME, true)
|
||||
} else if switch && float_ok && pack == "0.2" {
|
||||
(LINEAR_FRAME, false)
|
||||
} else if hdr && smooth && offered && wide_ok {
|
||||
(WIDE_FRAME, false)
|
||||
} else {
|
||||
(Bgra8Unorm, false)
|
||||
};
|
||||
assert_eq!(
|
||||
frame_configuration(
|
||||
hdr && smooth,
|
||||
(offered, wide_ok, float_ok, coverage_ok),
|
||||
Bgra8UnormSrgb,
|
||||
switch && pack == "0.2",
|
||||
switch && hdr && float_hdr,
|
||||
),
|
||||
expected,
|
||||
"bits {bits}, pack {pack}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_hdr_falls_back_for_each_missing_hud_layer_requirement() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, frame_configuration, hud_coverage};
|
||||
use wgpu::{DownlevelFlags as D, TextureFormat as F, TextureFormatFeatureFlags as Fl};
|
||||
use wgpu::{TextureFormatFeatures, TextureUsages as U};
|
||||
|
||||
let usages = U::RENDER_ATTACHMENT | U::TEXTURE_BINDING;
|
||||
for (attachments, flags, allowed_usages, format_flags) in [
|
||||
(1, D::INDEPENDENT_BLEND, usages, Fl::BLENDABLE),
|
||||
(2, D::empty(), usages, Fl::BLENDABLE),
|
||||
(2, D::INDEPENDENT_BLEND, U::TEXTURE_BINDING, Fl::BLENDABLE),
|
||||
(2, D::INDEPENDENT_BLEND, U::RENDER_ATTACHMENT, Fl::BLENDABLE),
|
||||
(2, D::INDEPENDENT_BLEND, usages, Fl::empty()),
|
||||
] {
|
||||
let coverage = hud_coverage::supported(
|
||||
attachments,
|
||||
flags,
|
||||
TextureFormatFeatures {
|
||||
allowed_usages,
|
||||
flags: format_flags,
|
||||
},
|
||||
);
|
||||
let device = (true, true, true, coverage);
|
||||
assert_eq!(
|
||||
frame_configuration(true, device, F::Bgra8Unorm, false, true),
|
||||
(WIDE_FRAME, false)
|
||||
);
|
||||
assert_eq!(
|
||||
frame_configuration(false, device, F::Bgra8Unorm, false, true),
|
||||
(F::Bgra8Unorm, false)
|
||||
);
|
||||
assert_eq!(
|
||||
frame_configuration(true, device, F::Bgra8Unorm, true, false),
|
||||
(LINEAR_FRAME, false)
|
||||
);
|
||||
// A 0.2 pack keeps its float frame, without the layer: SDR.
|
||||
assert_eq!(
|
||||
frame_configuration(true, device, F::Bgra8Unorm, true, true),
|
||||
(LINEAR_FRAME, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_0_2_pack_without_the_hud_layer_finishes_on_the_swapchains_8_bits() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, overlay_format};
|
||||
use wgpu::TextureFormat::{Bgra8Unorm, Bgra8UnormSrgb, Rgba8Unorm};
|
||||
assert_eq!(
|
||||
overlay_format(LINEAR_FRAME, false, Bgra8UnormSrgb),
|
||||
Bgra8Unorm
|
||||
);
|
||||
assert_eq!(overlay_format(LINEAR_FRAME, false, Rgba8Unorm), Rgba8Unorm);
|
||||
// Everything else draws its overlays on its frame: the layered frame's light, the
|
||||
// 16-bit frame, the 8-bit frame.
|
||||
for (frame, layered) in [
|
||||
(LINEAR_FRAME, true),
|
||||
(WIDE_FRAME, false),
|
||||
(Bgra8Unorm, false),
|
||||
] {
|
||||
assert_eq!(overlay_format(frame, layered, Bgra8UnormSrgb), frame);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_tone_curve_runs_on_the_layered_world() {
|
||||
use super::horizon_tone_curve;
|
||||
// No pack, or a 0.2 pack without `[tonemap]`: Horizon's curve.
|
||||
assert!(horizon_tone_curve(true, true, false));
|
||||
// A pack's `[tonemap]` mapped it already; never both.
|
||||
assert!(!horizon_tone_curve(true, true, true));
|
||||
// No world (the title screen), or no layered frame (SDR, the compatibility frames).
|
||||
assert!(!horizon_tone_curve(true, false, false));
|
||||
assert!(!horizon_tone_curve(false, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_no_pack_layered_world_applies_hdr_gain() {
|
||||
use super::no_pack_hdr_gain;
|
||||
assert!(no_pack_hdr_gain(true, false, true));
|
||||
assert!(!no_pack_hdr_gain(false, false, true));
|
||||
assert!(!no_pack_hdr_gain(true, true, true));
|
||||
assert!(!no_pack_hdr_gain(true, false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn taa_culling_covers_jittered_view_edges_but_still_rejects_behind_the_camera() {
|
||||
use super::*;
|
||||
@@ -13870,10 +13540,15 @@ mod tests {
|
||||
// The shared maths: it needs the file it is appended to. GPU culling's
|
||||
// occlusion test is appended to gpu_culling.wgsl, on Vulkan alone.
|
||||
continue;
|
||||
} else if name == "rt_unpack.wgsl" {
|
||||
// Appended to terrain.wgsl (`ray_scene`'s `Unpacker`), on ray tracing devices.
|
||||
continue;
|
||||
} else if name == "terrain.wgsl"
|
||||
|| name == "entity.wgsl"
|
||||
|| name == "stars.wgsl"
|
||||
|| name == "custom_sky.wgsl"
|
||||
|| name == "border.wgsl"
|
||||
|| name == "world_primitives.wgsl"
|
||||
{
|
||||
let base = include_str!("hdr_light.wgsl").to_owned() + &text;
|
||||
sources.push((path, crate::overlay::wgsl_constants() + &base));
|
||||
@@ -13891,7 +13566,7 @@ mod tests {
|
||||
// Appended to terrain.wgsl (gfx/visbuffer.rs).
|
||||
sources.push((path, super::terrain_shader() + &text));
|
||||
} else if name == "oit_border.wgsl" {
|
||||
let base = include_str!("border.wgsl").to_owned() + include_str!("oit.wgsl");
|
||||
let base = super::border_shader() + include_str!("oit.wgsl");
|
||||
sources.push((path, base + &text));
|
||||
} else if name == "oit_clouds.wgsl" {
|
||||
let base = super::entity_shader() + include_str!("oit.wgsl");
|
||||
|
||||
@@ -136,6 +136,8 @@ impl Arena {
|
||||
} else {
|
||||
wgpu::BufferUsages::VERTEX
|
||||
};
|
||||
// BLAS builds read sections' vertices here (`ray_scene`).
|
||||
let usage = usage | super::surface_setup::ray_query::blas_input(device);
|
||||
Self::with_usage(device, stride, capacity, usage, PAGE_BYTES)
|
||||
}
|
||||
/// Sorted translucent indices; `storage` when compute passes read them.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//! What the renderer holds, for status, soak tests and memory reports (`horizon-ctl memory`,
|
||||
//! `scene_replay --memory`): resident sections and far tiles, and the bytes of its largest
|
||||
//! GPU resources and of its per-section bookkeeping.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{
|
||||
Renderer, SectionGpu, SectionKey, TerrainVertex, arena, flat, lod, texture_bytes, visbuffer,
|
||||
};
|
||||
|
||||
impl Renderer {
|
||||
/// Sections with GPU buffers, for soak tests watching residency.
|
||||
pub fn resident_sections(&self) -> usize {
|
||||
self.sections.len()
|
||||
}
|
||||
/// Quads of the far terrain tiles with GPU buffers.
|
||||
pub fn lod_quads(&self) -> u64 {
|
||||
self.lods
|
||||
.values()
|
||||
.map(|tile| u64::from(tile.mesh.solid + tile.mesh.translucent))
|
||||
.sum()
|
||||
}
|
||||
/// Tiles and quads of the far terrain tiles with GPU buffers, by level.
|
||||
pub fn lod_levels(&self) -> Vec<[u64; 2]> {
|
||||
let mut levels = Vec::new();
|
||||
for (key, tile) in &self.lods {
|
||||
let level = usize::from(key.level);
|
||||
if levels.len() <= level {
|
||||
levels.resize(level + 1, [0, 0]);
|
||||
}
|
||||
levels[level][0] += 1;
|
||||
levels[level][1] += u64::from(tile.mesh.solid + tile.mesh.translucent);
|
||||
}
|
||||
levels
|
||||
}
|
||||
/// Whether solid quads go to the arena in the compact layout
|
||||
/// (`packed_quads`), and whether far tiles' own records are on
|
||||
/// (`lod::packed::far_packing`).
|
||||
pub fn packed_layout(&self) -> [bool; 2] {
|
||||
[self.packed_quads, crate::lod::packed::far_packing()]
|
||||
}
|
||||
/// Bytes of the arena the resident chunk sections hold, and of the one far
|
||||
/// tiles hold: the same split `gpu_census` reports, for `status`, so the
|
||||
/// compact layouts can be seen working.
|
||||
pub fn vertex_bytes_split(&self) -> [u64; 2] {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
|
||||
[
|
||||
self.sections.values().map(used).sum(),
|
||||
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
|
||||
]
|
||||
}
|
||||
/// Bytes of the section vertex arenas' buffers, and of the vertices
|
||||
/// resident sections hold in them.
|
||||
pub fn section_vertex_bytes(&self) -> [u64; 2] {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let vertices: u64 = self
|
||||
.sections
|
||||
.values()
|
||||
.chain(self.lods.values().map(|tile| &*tile.mesh))
|
||||
.map(|s| s.solid_slots + u64::from(s.translucent) * 4)
|
||||
.sum();
|
||||
let translucent = self.translucent_arena.as_ref();
|
||||
let arenas = self.arena.bytes() + translucent.map_or(0, arena::Arena::bytes);
|
||||
[arenas, vertices * stride]
|
||||
}
|
||||
/// Bytes of the renderer's largest GPU resources, by name (`scene_replay --memory`):
|
||||
/// the terrain arenas (their pages, and what resident sections and far tiles use of
|
||||
/// them), the quad indices, the block sprites with their mips, the LabPBR maps, flat
|
||||
/// draws' buffers and the visibility buffer's.
|
||||
pub fn gpu_census(&self) -> Vec<(&'static str, u64)> {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
|
||||
let textures = |t: &[wgpu::Texture]| t.iter().map(texture_bytes).sum::<u64>();
|
||||
let translucent = self.translucent_arena.as_ref();
|
||||
vec![
|
||||
("section_arena_pages", self.arena.bytes()),
|
||||
(
|
||||
"translucent_arena_pages",
|
||||
translucent.map_or(0, arena::Arena::bytes),
|
||||
),
|
||||
("sorted_index_pages", self.sorted_indices.bytes()),
|
||||
(
|
||||
"near_section_vertices",
|
||||
self.sections.values().map(used).sum(),
|
||||
),
|
||||
(
|
||||
"held_section_vertices",
|
||||
self.hold
|
||||
.sections()
|
||||
.filter_map(|held| held.gpu.as_ref())
|
||||
.map(used)
|
||||
.sum(),
|
||||
),
|
||||
(
|
||||
"far_tile_vertices",
|
||||
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
|
||||
),
|
||||
("quad_indices", self.indices.size()),
|
||||
("block_sprites", textures(&self.sprite_textures)),
|
||||
(
|
||||
"pbr_maps",
|
||||
self.pbr_maps
|
||||
.as_ref()
|
||||
.map_or(0, |maps| maps.iter().flatten().map(texture_bytes).sum()),
|
||||
),
|
||||
(
|
||||
"flat_draws",
|
||||
self.flat.as_ref().map_or(0, flat::Flat::bytes),
|
||||
),
|
||||
(
|
||||
"visibility_buffer",
|
||||
self.visbuf.as_ref().map_or(0, visbuffer::Visbuffer::bytes),
|
||||
),
|
||||
]
|
||||
}
|
||||
/// Device memory the backend's allocator holds, by the label of what holds
|
||||
/// it, largest first, with its allocated and reserved totals (Vulkan and
|
||||
/// DirectX 12; none on Metal and OpenGL) (`horizon-ctl memory`).
|
||||
pub fn gpu_memory_by_label(&self) -> Option<(u64, u64, Vec<(String, u64)>)> {
|
||||
let report = self.device.generate_allocator_report()?;
|
||||
let mut by_label: HashMap<String, u64> = HashMap::new();
|
||||
for a in report.allocations {
|
||||
*by_label.entry(a.name).or_default() += a.size;
|
||||
}
|
||||
let mut labels: Vec<_> = by_label.into_iter().collect();
|
||||
labels.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
Some((
|
||||
report.total_allocated_bytes,
|
||||
report.total_reserved_bytes,
|
||||
labels,
|
||||
))
|
||||
}
|
||||
/// Heap bytes of the renderer's per-section bookkeeping (`horizon-ctl memory`):
|
||||
/// resident sections, far tiles, block entities and light sources by section.
|
||||
pub fn cpu_census(&self) -> [(&'static str, usize); 4] {
|
||||
use std::mem::size_of;
|
||||
let per_section = |map_len: usize, lists: usize| {
|
||||
map_len * (size_of::<SectionKey>() + size_of::<Vec<([i32; 3], u32)>>() + 1)
|
||||
+ lists * size_of::<([i32; 3], u32)>()
|
||||
};
|
||||
let lists = |map: &crate::fast_hash::FxHashMap<SectionKey, Vec<([i32; 3], u32)>>| {
|
||||
per_section(
|
||||
map.capacity(),
|
||||
map.values().map(Vec::capacity).sum::<usize>(),
|
||||
)
|
||||
};
|
||||
let animated: usize = self
|
||||
.sections
|
||||
.values()
|
||||
.map(|s| s.animated_layers.len() * size_of::<u16>())
|
||||
.sum();
|
||||
[
|
||||
("resident_sections", self.sections.heap_bytes() + animated),
|
||||
(
|
||||
"far_tiles",
|
||||
self.lods.capacity()
|
||||
* (size_of::<crate::lod::TileKey>() + size_of::<lod::Tile>() + 1),
|
||||
),
|
||||
("block_entities", lists(&self.block_entities)),
|
||||
("light_sources", lists(&self.light_blocks)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//! A resource pack's `core/text` on world text, as vanilla 26.3's `TEXT_POLYGON_OFFSET` and
|
||||
//! `TEXT_SEE_THROUGH` draw text displays, name tags and their backgrounds.
|
||||
|
||||
use crate::core_shader::{CoreProgram, Glue};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The entity vertices' light bytes are sky then block; vanilla's `UV2` is block then sky.
|
||||
pub(crate) const WORLD_GLUE: Glue = Glue {
|
||||
inputs: &[
|
||||
("Position", "vec3"),
|
||||
("Color", "vec4"),
|
||||
("UV0", "vec2"),
|
||||
("UV2", "ivec2"),
|
||||
],
|
||||
prelude: "layout(location = 0) in vec3 hz_pos;\n\
|
||||
layout(location = 1) in vec2 hz_uv;\n\
|
||||
layout(location = 3) in vec4 hz_color;\n\
|
||||
layout(location = 4) in uvec4 hz_light;",
|
||||
setup: "Position = hz_pos; Color = hz_color; UV0 = hz_uv; UV2 = ivec2(hz_light.y, hz_light.x);",
|
||||
finish: "",
|
||||
vertex_id: "int(gl_VertexIndex)",
|
||||
};
|
||||
|
||||
pub(crate) const SEE_THROUGH_GLUE: Glue = Glue {
|
||||
inputs: &[("Position", "vec3"), ("Color", "vec4"), ("UV0", "vec2")],
|
||||
prelude: "layout(location = 0) in vec3 hz_pos;\n\
|
||||
layout(location = 1) in vec2 hz_uv;\n\
|
||||
layout(location = 3) in vec4 hz_color;",
|
||||
setup: "Position = hz_pos; Color = hz_color; UV0 = hz_uv;",
|
||||
finish: "",
|
||||
vertex_id: "int(gl_VertexIndex)",
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextShaders {
|
||||
pub gui: Option<Arc<CoreProgram>>,
|
||||
pub world: Option<Arc<CoreProgram>>,
|
||||
pub see_through: Option<Arc<CoreProgram>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct PackBlocks {
|
||||
pub(super) globals: [u8; 48],
|
||||
pub(super) fog: [f32; 6],
|
||||
}
|
||||
|
||||
impl super::Renderer {
|
||||
pub fn set_world_text_shaders(&mut self, shaders: &TextShaders) {
|
||||
self.core_text = CoreText::new(
|
||||
&self.device,
|
||||
self.output.frame.format,
|
||||
shaders.world.as_deref(),
|
||||
shaders.see_through.as_deref(),
|
||||
&self.entity_atlas,
|
||||
(&self.lightmap_view, &self.lightmap_sampler),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn pack_blocks(
|
||||
&self,
|
||||
frame: &super::Frame,
|
||||
fog: [f32; 4],
|
||||
ends: [f32; 4],
|
||||
) -> PackBlocks {
|
||||
let globals = crate::post_chain::Globals {
|
||||
glint_alpha: frame.glint_alpha,
|
||||
..frame.post_effects.globals
|
||||
};
|
||||
let size = [self.config.width, self.config.height];
|
||||
PackBlocks {
|
||||
globals: globals.bytes(size, frame.post_effects.menu_blur_radius as i32),
|
||||
fog: [fog[0], fog[1], fog[2], fog[3], ends[0], ends[1]],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn write_core_text(
|
||||
&self,
|
||||
pack: &PackBlocks,
|
||||
view: &super::View,
|
||||
pose: glam::Mat4,
|
||||
jitter: glam::Mat4,
|
||||
fog_color: [f32; 4],
|
||||
) {
|
||||
let Some(core) = &self.core_text else {
|
||||
return;
|
||||
};
|
||||
let data =
|
||||
super::core_lines::uniforms(pack.globals, view, pose, jitter, 1.0, fog_color, pack.fog);
|
||||
self.queue.write_buffer(&core.uniform, 0, &data);
|
||||
}
|
||||
|
||||
pub(super) fn bind_world_text(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
allowed: bool,
|
||||
see_through: bool,
|
||||
) {
|
||||
let pack = self
|
||||
.core_text
|
||||
.as_ref()
|
||||
.filter(|_| allowed)
|
||||
.and_then(|core| {
|
||||
let pipeline = if see_through {
|
||||
core.see_through.as_ref()
|
||||
} else {
|
||||
core.polygon_offset.as_ref()
|
||||
};
|
||||
Some((pipeline?, &core.group))
|
||||
});
|
||||
match pack {
|
||||
Some((pipeline, group)) => {
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, group, &[]);
|
||||
}
|
||||
None => pass.set_bind_group(0, &self.entity_group, &[]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCK_OFFSETS: [u64; 4] = [0, 256, 512, 768];
|
||||
const BLOCK_SIZES: [u64; 4] = [48, 64, 160, 48];
|
||||
|
||||
pub(super) struct CoreText {
|
||||
pub(super) polygon_offset: Option<wgpu::RenderPipeline>,
|
||||
pub(super) see_through: Option<wgpu::RenderPipeline>,
|
||||
pub(super) group: wgpu::BindGroup,
|
||||
uniform: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl CoreText {
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
world: Option<&CoreProgram>,
|
||||
see_through: Option<&CoreProgram>,
|
||||
atlas: &wgpu::TextureView,
|
||||
lightmap: (&wgpu::TextureView, &wgpu::Sampler),
|
||||
) -> Option<Self> {
|
||||
if world.is_none() && see_through.is_none() {
|
||||
return None;
|
||||
}
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
|
||||
let buffer = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: wgpu::BufferSize::new(BLOCK_SIZES[binding as usize]),
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let texture = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let sampler = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
};
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Resource pack world text"),
|
||||
entries: &[
|
||||
buffer(0),
|
||||
buffer(1),
|
||||
buffer(2),
|
||||
buffer(3),
|
||||
texture(4),
|
||||
sampler(5),
|
||||
texture(8),
|
||||
sampler(9),
|
||||
],
|
||||
});
|
||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Resource pack world text uniforms"),
|
||||
size: 1024,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let glyphs = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Resource pack world text glyphs"),
|
||||
..Default::default()
|
||||
});
|
||||
let mut resources: Vec<wgpu::BindGroupEntry> = (0..4)
|
||||
.map(|i| wgpu::BindGroupEntry {
|
||||
binding: i as u32,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &uniform,
|
||||
offset: BLOCK_OFFSETS[i],
|
||||
size: wgpu::BufferSize::new(BLOCK_SIZES[i]),
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
resources.extend([
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: wgpu::BindingResource::TextureView(atlas),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: wgpu::BindingResource::Sampler(&glyphs),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 8,
|
||||
resource: wgpu::BindingResource::TextureView(lightmap.0),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 9,
|
||||
resource: wgpu::BindingResource::Sampler(lightmap.1),
|
||||
},
|
||||
]);
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Resource pack world text"),
|
||||
layout: &layout,
|
||||
entries: &resources,
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Resource pack world text"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2];
|
||||
let buffers = [Some(wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<crate::mesher::Vertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &attributes,
|
||||
})];
|
||||
let pipeline = |label, program: &CoreProgram, depth: wgpu::DepthStencilState| {
|
||||
let module = |module: &naga::Module| {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module.clone())),
|
||||
})
|
||||
};
|
||||
let (vertex, fragment) = (module(&program.vertex), module(&program.fragment));
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vertex,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &buffers,
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(depth),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fragment,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let depth = |write, compare, bias| wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(write),
|
||||
depth_compare: Some(compare),
|
||||
stencil: Default::default(),
|
||||
bias,
|
||||
};
|
||||
let offset = wgpu::DepthBiasState {
|
||||
constant: 10,
|
||||
slope_scale: 1.0,
|
||||
clamp: 0.0,
|
||||
};
|
||||
let polygon_offset = world.map(|program| {
|
||||
pipeline(
|
||||
"Resource pack polygon offset text",
|
||||
program,
|
||||
depth(true, wgpu::CompareFunction::GreaterEqual, offset),
|
||||
)
|
||||
});
|
||||
let see_through = see_through.map(|program| {
|
||||
pipeline(
|
||||
"Resource pack see-through text",
|
||||
program,
|
||||
depth(false, wgpu::CompareFunction::Always, Default::default()),
|
||||
)
|
||||
});
|
||||
let internal = pollster::block_on(internal.pop());
|
||||
if let Some(e) = internal.or(pollster::block_on(validation.pop())) {
|
||||
log::warn!("the resource pack's world text shader is not used: {e}");
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
polygon_offset,
|
||||
see_through,
|
||||
group,
|
||||
uniform,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,201 @@
|
||||
use super::*;
|
||||
|
||||
fn device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
pollster::block_on(adapter.request_device(&Default::default())).ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pack_text_shader_reads_the_light_as_vanilla_does() {
|
||||
let Some((device, queue)) = device() else {
|
||||
eprintln!("no GPU: skipping");
|
||||
return;
|
||||
};
|
||||
let vsh = "#version 330\n#extension GL_ARB_separate_shader_objects : require\n\
|
||||
layout(location = 0) in vec3 Position;\nlayout(location = 1) in vec4 Color;\n\
|
||||
layout(location = 2) in vec2 UV0;\nlayout(location = 3) in ivec2 UV2;\n\
|
||||
layout(location = 0) flat out ivec2 light;\n\
|
||||
void main() { gl_Position = vec4(Position, 1.0); light = UV2; }\n";
|
||||
let fsh = "#version 330\n#extension GL_ARB_separate_shader_objects : require\n\
|
||||
layout(location = 0) flat in ivec2 light;\nlayout(location = 0) out vec4 fragColor;\n\
|
||||
void main() { fragColor = vec4(vec2(light) / 255.0, 0.0, 1.0); }\n";
|
||||
let program =
|
||||
crate::core_shader::translate_core(vsh, fsh, &[], &|_| None, &WORLD_GLUE).unwrap();
|
||||
const SIZE: u32 = 64;
|
||||
let texture = |format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let binding = wgpu::TextureUsages::TEXTURE_BINDING;
|
||||
let atlas = texture(wgpu::TextureFormat::Rgba8Unorm, binding).create_view(&Default::default());
|
||||
let sampler = device.create_sampler(&Default::default());
|
||||
let text = CoreText::new(
|
||||
&device,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
Some(&program),
|
||||
None,
|
||||
&atlas,
|
||||
(&atlas, &sampler),
|
||||
)
|
||||
.expect("the pack's pipeline");
|
||||
let target = wgpu::TextureUsages::RENDER_ATTACHMENT;
|
||||
let color = texture(
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
target | wgpu::TextureUsages::COPY_SRC,
|
||||
);
|
||||
let depth = texture(wgpu::TextureFormat::Depth32Float, target);
|
||||
let light = 0x30 | (0xC0 << 8);
|
||||
let corner = |x: f32, y: f32| crate::mesher::Vertex {
|
||||
pos: [x, y, 0.5],
|
||||
uv: [0.0; 2],
|
||||
layer: 0,
|
||||
color: u32::MAX,
|
||||
light,
|
||||
normal: [0; 4],
|
||||
glint_uv: [0.0; 2],
|
||||
};
|
||||
let quad = [
|
||||
corner(-1.0, -1.0),
|
||||
corner(1.0, -1.0),
|
||||
corner(1.0, 1.0),
|
||||
corner(-1.0, -1.0),
|
||||
corner(1.0, 1.0),
|
||||
corner(-1.0, 1.0),
|
||||
];
|
||||
let vertices = wgpu::util::DeviceExt::create_buffer_init(
|
||||
&device,
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&quad),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
},
|
||||
);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: u64::from(SIZE * SIZE * 4),
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let color_view = color.create_view(&Default::default());
|
||||
let depth_view = depth.create_view(&Default::default());
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &color_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(text.polygon_offset.as_ref().unwrap());
|
||||
pass.set_bind_group(0, &text.group, &[]);
|
||||
pass.set_vertex_buffer(0, vertices.slice(..));
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
encoder.copy_texture_to_buffer(
|
||||
color.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(SIZE * 4),
|
||||
rows_per_image: Some(SIZE),
|
||||
},
|
||||
},
|
||||
color.size(),
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let pixels = read.slice(..).get_mapped_range().unwrap().to_vec();
|
||||
let middle = ((SIZE / 2 * SIZE + SIZE / 2) * 4) as usize;
|
||||
assert_eq!(
|
||||
&pixels[middle..middle + 4],
|
||||
&[0xC0, 0x30, 0, 0xFF],
|
||||
"UV2 is (block, sky)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES or .local/horizon-client/26.3, and a GPU"]
|
||||
fn vanillas_text_shader_makes_both_world_text_pipelines() {
|
||||
let root = std::env::var_os("HORIZON_TEST_RESOURCES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION)
|
||||
});
|
||||
let read = |path: &str| std::fs::read_to_string(root.join(path)).ok();
|
||||
let vertex = read("assets/minecraft/shaders/core/text.vsh").expect("vanilla's text.vsh");
|
||||
let fragment = read("assets/minecraft/shaders/core/text.fsh").expect("vanilla's text.fsh");
|
||||
let include = |id: &str| {
|
||||
let (namespace, path) = id.split_once(':')?;
|
||||
read(&format!("assets/{namespace}/{path}"))
|
||||
};
|
||||
let translate = |defines: &[&str], glue: &Glue| {
|
||||
crate::core_shader::translate_core(&vertex, &fragment, defines, &include, glue)
|
||||
.expect("vanilla's text shader translates")
|
||||
};
|
||||
let world = translate(&[], &WORLD_GLUE);
|
||||
let see_through = translate(&["IS_SEE_THROUGH"], &SEE_THROUGH_GLUE);
|
||||
let (device, _) = device().expect("a GPU");
|
||||
let texture = || {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 4,
|
||||
height: 4,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&Default::default())
|
||||
};
|
||||
let (atlas, lightmap) = (texture(), texture());
|
||||
let sampler = device.create_sampler(&Default::default());
|
||||
let text = CoreText::new(
|
||||
&device,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
Some(&world),
|
||||
Some(&see_through),
|
||||
&atlas,
|
||||
(&lightmap, &sampler),
|
||||
)
|
||||
.expect("the pack's world text pipelines");
|
||||
assert!(text.polygon_offset.is_some() && text.see_through.is_some());
|
||||
}
|
||||
@@ -174,7 +174,12 @@ fn mesh() -> Vec<Vertex> {
|
||||
}
|
||||
|
||||
impl CustomSky {
|
||||
pub(super) fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
|
||||
/// The layers' pipelines for a frame of `format`, holding linear light where `linear`
|
||||
/// (`linear_blend`).
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
(format, linear): (wgpu::TextureFormat, bool),
|
||||
) -> Self {
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Custom sky bindings"),
|
||||
entries: &[
|
||||
@@ -261,10 +266,7 @@ impl CustomSky {
|
||||
"fs_custom_sky"
|
||||
}),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
constants: &super::linear_blend::frame_constants(format, linear),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
@@ -617,7 +619,7 @@ mod tests {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let sky = CustomSky::new(&device, wgpu::TextureFormat::Rgba16Float);
|
||||
let sky = CustomSky::new(&device, (wgpu::TextureFormat::Rgba16Float, false));
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[Some(&sky.layout)],
|
||||
@@ -661,19 +663,37 @@ mod tests {
|
||||
let view = target.create_view(&Default::default());
|
||||
// Independent scalar expectation: a sky mark of 206/255 gives this gamma gain.
|
||||
let hdr_gain = (1.0f32 + 3.0 * (1.0 - 206.0 / 255.0) / 0.24).powf(1.0 / 2.2);
|
||||
for (mode_index, mode, debug, written) in MODES
|
||||
// A frame of linear light (`FRAME_LINEAR`, `[frame] blend = "linear"`): the layer stored
|
||||
// decoded, products kept exact.
|
||||
let linear_modes = [
|
||||
Blend::Replace,
|
||||
Blend::Multiply,
|
||||
Blend::Overlay,
|
||||
Blend::Alpha,
|
||||
]
|
||||
.map(|m| {
|
||||
(
|
||||
MODES.iter().position(|&n| n == m).unwrap(),
|
||||
m,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
});
|
||||
for (mode_index, mode, debug, written, linear) in MODES
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, m)| (i, m, false, true))
|
||||
.chain([(5, Blend::Screen, true, true)])
|
||||
.map(|(i, m)| (i, m, false, true, false))
|
||||
.chain([(5, Blend::Screen, true, true, false)])
|
||||
.chain(
|
||||
MODES
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, m)| (i, m, false, false)),
|
||||
.map(|(i, m)| (i, m, false, false, false)),
|
||||
)
|
||||
.chain(linear_modes)
|
||||
{
|
||||
let gain = if written { hdr_gain } else { 1.0 };
|
||||
let uniform = Uniform {
|
||||
@@ -717,7 +737,10 @@ mod tests {
|
||||
module: &shader,
|
||||
entry_point: Some(entry),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[("HDR_WRITE_LIGHT", 1.0)],
|
||||
constants: &[
|
||||
("HDR_WRITE_LIGHT", 1.0),
|
||||
("FRAME_LINEAR", f64::from(u8::from(linear))),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
@@ -824,7 +847,17 @@ mod tests {
|
||||
(alpha - coverage).abs() < 0.001,
|
||||
"{mode:?}: alpha {alpha}, expected {coverage}"
|
||||
);
|
||||
let expected = if debug {
|
||||
let decode = |v: f32| v.powf(2.2);
|
||||
let expected = if linear {
|
||||
// The sky under it is linear light, 2.0; the layer is gamma.
|
||||
match mode {
|
||||
Blend::Replace => decode(t),
|
||||
Blend::Multiply => d * decode(1.0 + (t - 1.0) * k),
|
||||
Blend::Overlay => d * decode(2.0 * (0.5 + (t - 0.5) * k)),
|
||||
Blend::Alpha => decode(t) * k + d * (1.0 - k),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if debug {
|
||||
let grey = (0.2126f32 * 0.8f32.powf(2.2)
|
||||
+ 0.7152 * 0.4f32.powf(2.2)
|
||||
+ 0.0722 * 0.2f32.powf(2.2))
|
||||
@@ -845,11 +878,11 @@ mod tests {
|
||||
};
|
||||
assert!(
|
||||
(got - expected).abs() < 0.004,
|
||||
"{mode:?} debug {debug}: {got} expected {expected}"
|
||||
"{mode:?} debug {debug} linear {linear}: {got} expected {expected}"
|
||||
);
|
||||
}
|
||||
eprintln!(
|
||||
"HDR custom sky GPU ran all nine modes with and without gain, plus debug Screen, checking RGB and coverage"
|
||||
"HDR custom sky GPU ran all nine modes with and without gain, plus debug Screen and four on linear light, checking RGB and coverage"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,16 +56,25 @@ fn custom_sky_color(in: Out) -> vec4<f32> {
|
||||
(layer.mode == ADD || layer.mode == SUBTRACT || layer.mode == REPLACE || layer.mode == ALPHA) {
|
||||
color = hdr_written(color, 1.0 - 0.24 * layer.hdr_light.y, layer.hdr_settings, layer.hdr_shares);
|
||||
}
|
||||
// On a frame of linear light (`FRAME_LINEAR`) the layer's colour is decoded as it is stored.
|
||||
switch layer.mode {
|
||||
// dst * src: white leaves the sky alone.
|
||||
case MULTIPLY: { return vec4(mix(vec3(1.0), t.rgb, k), 1.0); }
|
||||
// 2 * dst * src: grey leaves it alone.
|
||||
case OVERLAY: { return vec4(mix(vec3(0.5), t.rgb, k), 1.0); }
|
||||
case REPLACE: { return vec4(color, 1.0); }
|
||||
// mix(dst, src, alpha).
|
||||
case ALPHA: { return vec4(color, k); }
|
||||
// add, subtract, dodge, burn, screen: the layer premultiplied, black leaves it alone.
|
||||
default: { return vec4(color * k, 1.0); }
|
||||
// dst * src: white leaves the sky alone. Decoded, the product stays exact:
|
||||
// (dst * src)^2.2 = dst^2.2 * src^2.2.
|
||||
case MULTIPLY: { return frame_stored(vec4(mix(vec3(1.0), t.rgb, k), 1.0)); }
|
||||
// 2 * dst * src: grey leaves it alone. Decoded as entity.wgsl's crumbling, (2 src)^2.2 / 2,
|
||||
// so it stays exact too.
|
||||
case OVERLAY: {
|
||||
let factor = mix(vec3(0.5), t.rgb, k);
|
||||
if FRAME_LINEAR { return vec4(pow(2.0 * factor, vec3(2.2)) * 0.5, 1.0); }
|
||||
return vec4(factor, 1.0);
|
||||
}
|
||||
case REPLACE: { return frame_stored(vec4(color, 1.0)); }
|
||||
// mix(dst, src, alpha): alpha is coverage, kept.
|
||||
case ALPHA: { return frame_stored(vec4(color, k)); }
|
||||
// add, subtract, dodge, burn, screen: the layer premultiplied, black leaves it alone. On
|
||||
// linear light add and subtract add and take away light, as `"linear"` means to; dodge,
|
||||
// burn and screen come out near their gamma results, not exactly.
|
||||
default: { return vec4(frame_stored(vec4(color, 1.0)).rgb * k, 1.0); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,34 +202,70 @@ pub fn is_adreno_dx12(info: &wgpu::AdapterInfo) -> bool {
|
||||
info.backend == wgpu::Backend::Dx12 && matches!(info.vendor, 0x4d4f_4351 | 0x5143)
|
||||
}
|
||||
|
||||
/// A released Mesa version from its driver info, allowing distribution suffixes.
|
||||
/// Development and release-candidate builds do not establish a stable release's safety.
|
||||
fn parse_mesa_version(driver_info: &str) -> Option<(u32, u32, u32)> {
|
||||
let version = driver_info
|
||||
.strip_prefix("Mesa ")?
|
||||
.split_whitespace()
|
||||
.next()?;
|
||||
let (version, suffix) = version.split_once('-').unwrap_or((version, ""));
|
||||
if suffix.starts_with("devel") || suffix.starts_with("rc") {
|
||||
return None;
|
||||
}
|
||||
let mut parts = version.split('.');
|
||||
let version = (
|
||||
parts.next()?.parse().ok()?,
|
||||
parts.next()?.parse().ok()?,
|
||||
parts.next()?.parse().ok()?,
|
||||
);
|
||||
parts.next().is_none().then_some(version)
|
||||
}
|
||||
|
||||
/// Intel Gen11 (Ice Lake) and Gen12 (Tiger Lake, Rocket Lake, Alder Lake,
|
||||
/// Raptor Lake) ANV Vulkan drivers have broken `first_instance` for
|
||||
/// Raptor Lake) ANV Vulkan drivers had broken `first_instance` for
|
||||
/// instance-step vertex attributes (#3529, #4599), causing terrain sections to
|
||||
/// be drawn with another section's origin and flicker.
|
||||
///
|
||||
/// PR #15007 tested Raptor Lake (RPL-U, 0xa7a1) on Mesa 26.0.8 without terrain
|
||||
/// flicker. Allow indirect multi-draw and GPU culling for Raptor Lake from that
|
||||
/// release; earlier or unidentified drivers keep the workaround.
|
||||
pub fn is_broken_intel_vulkan(info: &wgpu::AdapterInfo) -> bool {
|
||||
info.backend == wgpu::Backend::Vulkan
|
||||
&& info.vendor == 0x8086
|
||||
&& (matches!(
|
||||
info.device,
|
||||
0x8a50..=0x8a71 // Gen11: Ice Lake
|
||||
| 0x9a40..=0x9a7f // Gen12: Tiger Lake
|
||||
| 0x4c80..=0x4c9a // Gen12: Rocket Lake
|
||||
| 0x4600..=0x46ff // Gen12: Alder Lake (incl. 0x46a8 in #4599)
|
||||
| 0xa700..=0xa7ff // Gen12: Raptor Lake
|
||||
) || [
|
||||
"ICL",
|
||||
"Ice Lake",
|
||||
"TGL",
|
||||
"Tiger Lake",
|
||||
"RKL",
|
||||
"Rocket Lake",
|
||||
"ADL",
|
||||
"Alder Lake",
|
||||
"RPL",
|
||||
"Raptor Lake",
|
||||
]
|
||||
.iter()
|
||||
.any(|tag| info.name.contains(tag)))
|
||||
if info.backend != wgpu::Backend::Vulkan || info.vendor != 0x8086 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_raptor_lake = matches!(info.device, 0xa700..=0xa7ff)
|
||||
|| info.name.contains("RPL")
|
||||
|| info.name.contains("Raptor Lake");
|
||||
|
||||
if is_raptor_lake
|
||||
&& parse_mesa_version(&info.driver_info).is_some_and(|version| version >= (26, 0, 8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
matches!(
|
||||
info.device,
|
||||
0x8a50..=0x8a71 // Gen11: Ice Lake
|
||||
| 0x9a40..=0x9a7f // Gen12: Tiger Lake
|
||||
| 0x4c80..=0x4c9a // Gen12: Rocket Lake
|
||||
| 0x4600..=0x46ff // Gen12: Alder Lake (incl. 0x46a8 in #4599)
|
||||
| 0xa700..=0xa7ff // Gen12: Raptor Lake
|
||||
) || [
|
||||
"ICL",
|
||||
"Ice Lake",
|
||||
"TGL",
|
||||
"Tiger Lake",
|
||||
"RKL",
|
||||
"Rocket Lake",
|
||||
"ADL",
|
||||
"Alder Lake",
|
||||
"RPL",
|
||||
"Raptor Lake",
|
||||
]
|
||||
.iter()
|
||||
.any(|tag| info.name.contains(tag))
|
||||
}
|
||||
|
||||
fn supported(adapter: &wgpu::Adapter, device: &wgpu::Device) -> bool {
|
||||
@@ -1134,6 +1170,70 @@ mod tests {
|
||||
assert!(!is_adreno_dx12(&info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intel_vulkan_detection_preserves_workaround_on_older_mesa_and_allows_modern_raptor_lake() {
|
||||
let mut info = wgpu::AdapterInfo {
|
||||
name: "Intel(R) Iris(R) Xe Graphics (RPL-U)".into(),
|
||||
vendor: 0x8086,
|
||||
device: 0xa7a1,
|
||||
device_type: wgpu::DeviceType::IntegratedGpu,
|
||||
device_pci_bus_id: String::new(),
|
||||
driver: "Intel open-source Mesa driver".into(),
|
||||
driver_info: "Mesa 26.0.8-1ubuntu0.3".into(),
|
||||
backend: wgpu::Backend::Vulkan,
|
||||
subgroup_min_size: 0,
|
||||
subgroup_max_size: 0,
|
||||
transient_saves_memory: None,
|
||||
limit_bucket: None,
|
||||
};
|
||||
for (driver_info, broken) in [
|
||||
("Mesa 26.0.8-1ubuntu0.3", false),
|
||||
("Mesa 26.0.8", false),
|
||||
("Mesa 26.1.0", false),
|
||||
("Mesa 27.0.0", false),
|
||||
("Mesa 25.2.0", true),
|
||||
("Mesa 26.0.0", true),
|
||||
("Mesa 26.0.7", true),
|
||||
("Mesa 26.0.8-devel", true),
|
||||
("Mesa 27.0.0-rc1", true),
|
||||
("Mesa 27.0.0-devel (git-abcdef)", true),
|
||||
("Mesa 26", true),
|
||||
("Mesa 26.0.8.1", true),
|
||||
("Mesa unknown", true),
|
||||
("26.0.8", true),
|
||||
("", true),
|
||||
] {
|
||||
info.driver_info = driver_info.into();
|
||||
assert_eq!(is_broken_intel_vulkan(&info), broken, "{driver_info}");
|
||||
}
|
||||
|
||||
// PCI identification and the name fallback both allow the tested family.
|
||||
info.driver_info = "Mesa 26.0.8".into();
|
||||
info.device = 0;
|
||||
assert!(!is_broken_intel_vulkan(&info));
|
||||
info.name = "Intel Graphics".into();
|
||||
info.device = 0xa7a1;
|
||||
assert!(!is_broken_intel_vulkan(&info));
|
||||
|
||||
// Earlier families keep their workaround even with a modern Mesa release.
|
||||
for (device, name) in [
|
||||
(0x8a52, "Intel Ice Lake"),
|
||||
(0x9a49, "Intel Tiger Lake"),
|
||||
(0x4c8a, "Intel Rocket Lake"),
|
||||
(0x46a8, "Intel Alder Lake"),
|
||||
] {
|
||||
info.device = device;
|
||||
info.name = name.into();
|
||||
assert!(is_broken_intel_vulkan(&info), "{name}");
|
||||
}
|
||||
// A Mesa-looking version cannot turn on this workaround for other backends/vendors.
|
||||
info.backend = wgpu::Backend::Dx12;
|
||||
assert!(!is_broken_intel_vulkan(&info));
|
||||
info.backend = wgpu::Backend::Vulkan;
|
||||
info.vendor = 0x10de;
|
||||
assert!(!is_broken_intel_vulkan(&info));
|
||||
}
|
||||
|
||||
/// Draws as `render` writes them: solid (page, quads at a base vertex)
|
||||
/// first, then translucent (page, sorted page, count, first sorted index,
|
||||
/// base vertex).
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//! The frame policy: which frame a renderer draws, what the overlays draw on and whether
|
||||
//! Horizon's tone curve runs (tests in `frame_policy_tests.rs`).
|
||||
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME};
|
||||
|
||||
/// What a renderer's frame is drawn in: `LINEAR_FRAME` under the switch where the device draws
|
||||
/// it, for a 0.2 pack, or for the layered HDR frame where the surface offers HDR output
|
||||
/// (`linear`: `(a 0.2 pack picked, the layered HDR frame, the device draws it)`); else
|
||||
/// `WIDE_FRAME` when it is made with HDR on and Smooth Gradients (`wide_frame`,
|
||||
/// `output::resolve_frame`), the surface offering HDR output (`hdr_offered`) and the device
|
||||
/// drawing it (`draws_wide_frame`): an 8-bit frame's steps band in dark gradients, and HDR
|
||||
/// shows them. Else the swapchain's 8-bit format, as without HDR.
|
||||
pub(super) fn frame_format(
|
||||
wide_frame: bool,
|
||||
hdr_offered: bool,
|
||||
device_draws_it: bool,
|
||||
swapchain: wgpu::TextureFormat,
|
||||
linear: (bool, bool, bool),
|
||||
) -> wgpu::TextureFormat {
|
||||
if linear.2 && (linear.0 || linear.1 && hdr_offered) {
|
||||
LINEAR_FRAME
|
||||
} else if wide_frame && hdr_offered && device_draws_it {
|
||||
WIDE_FRAME
|
||||
} else {
|
||||
swapchain.remove_srgb_suffix()
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame's format, and whether it is the layered HDR frame (HUD coverage): for HDR on
|
||||
/// under the switch (`hdr_float`), with no pack or a 0.2 one, where the device keeps the HUD
|
||||
/// on a layer of its own.
|
||||
pub(super) fn frame_configuration(
|
||||
wide_frame: bool,
|
||||
device: (bool, bool, bool, bool),
|
||||
swapchain: wgpu::TextureFormat,
|
||||
linear_pack: bool,
|
||||
hdr_float: bool,
|
||||
) -> (wgpu::TextureFormat, bool) {
|
||||
let (hdr_offered, wide_ok, linear_ok, coverage_ok) = device;
|
||||
let layered = hdr_float && hdr_offered && linear_ok && coverage_ok;
|
||||
(
|
||||
frame_format(
|
||||
wide_frame,
|
||||
hdr_offered,
|
||||
wide_ok,
|
||||
swapchain,
|
||||
(linear_pack, layered, linear_ok),
|
||||
),
|
||||
layered,
|
||||
)
|
||||
}
|
||||
|
||||
/// What the hand, the camera overlays and the HUD draw on: the frame, except for a 0.2 pack's
|
||||
/// float frame without the HUD's layer, which is SDR: the pack's picture finishes on the
|
||||
/// swapchain's 8-bit format first, so they clip and blend per draw as on vanilla's frame.
|
||||
pub(super) fn overlay_format(
|
||||
frame: wgpu::TextureFormat,
|
||||
layered: bool,
|
||||
swapchain: wgpu::TextureFormat,
|
||||
) -> wgpu::TextureFormat {
|
||||
if frame == LINEAR_FRAME && !layered {
|
||||
swapchain.remove_srgb_suffix()
|
||||
} else {
|
||||
frame
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Horizon's tone curve (`Hdr::tonemap`) runs on this frame: on the layered frame's
|
||||
/// world, unless a 0.2 pack's `[tonemap]` already mapped it. One tone mapping, never two.
|
||||
pub(super) fn horizon_tone_curve(layered: bool, world: bool, pack_tonemap: bool) -> bool {
|
||||
layered && world && !pack_tonemap
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//! The frame policy: which frame a renderer draws (`frame_configuration`), and what follows
|
||||
//! from the frame chosen.
|
||||
|
||||
#[test]
|
||||
fn float_frames_never_schedule_light_mark_passes() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, light_marks};
|
||||
use wgpu::TextureFormat::{Bgra8Unorm, Rgb10a2Unorm, Rgba8Unorm};
|
||||
for hdr in [false, true] {
|
||||
assert!(!light_marks(LINEAR_FRAME, hdr));
|
||||
for format in [WIDE_FRAME, Bgra8Unorm, Rgba8Unorm, Rgb10a2Unorm] {
|
||||
assert_eq!(light_marks(format, hdr), hdr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_policy_keeps_compatibility_frames_and_requires_layering_for_float_hdr() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, frame_configuration};
|
||||
use wgpu::TextureFormat::{Bgra8Unorm, Bgra8UnormSrgb};
|
||||
|
||||
for bits in 0..128 {
|
||||
let hdr = bits & 1 != 0;
|
||||
let offered = bits & 2 != 0;
|
||||
let wide_ok = bits & 4 != 0;
|
||||
let float_ok = bits & 8 != 0;
|
||||
let coverage_ok = bits & 16 != 0;
|
||||
let switch = bits & 32 != 0;
|
||||
let smooth = bits & 64 != 0;
|
||||
for pack in ["none", "0.1", "0.2", "shaders"] {
|
||||
let float_hdr = matches!(pack, "none" | "0.2");
|
||||
let expected = if switch && float_ok && float_hdr && hdr && offered && coverage_ok {
|
||||
(LINEAR_FRAME, true)
|
||||
} else if switch && float_ok && pack == "0.2" {
|
||||
(LINEAR_FRAME, false)
|
||||
} else if hdr && smooth && offered && wide_ok {
|
||||
(WIDE_FRAME, false)
|
||||
} else {
|
||||
(Bgra8Unorm, false)
|
||||
};
|
||||
assert_eq!(
|
||||
frame_configuration(
|
||||
hdr && smooth,
|
||||
(offered, wide_ok, float_ok, coverage_ok),
|
||||
Bgra8UnormSrgb,
|
||||
switch && pack == "0.2",
|
||||
switch && hdr && float_hdr,
|
||||
),
|
||||
expected,
|
||||
"bits {bits}, pack {pack}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_hdr_falls_back_for_each_missing_hud_layer_requirement() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, frame_configuration, hud_coverage};
|
||||
use wgpu::{DownlevelFlags as D, TextureFormat as F, TextureFormatFeatureFlags as Fl};
|
||||
use wgpu::{TextureFormatFeatures, TextureUsages as U};
|
||||
|
||||
let usages = U::RENDER_ATTACHMENT | U::TEXTURE_BINDING;
|
||||
for (attachments, flags, allowed_usages, format_flags) in [
|
||||
(1, D::INDEPENDENT_BLEND, usages, Fl::BLENDABLE),
|
||||
(2, D::empty(), usages, Fl::BLENDABLE),
|
||||
(2, D::INDEPENDENT_BLEND, U::TEXTURE_BINDING, Fl::BLENDABLE),
|
||||
(2, D::INDEPENDENT_BLEND, U::RENDER_ATTACHMENT, Fl::BLENDABLE),
|
||||
(2, D::INDEPENDENT_BLEND, usages, Fl::empty()),
|
||||
] {
|
||||
let coverage = hud_coverage::supported(
|
||||
attachments,
|
||||
flags,
|
||||
TextureFormatFeatures {
|
||||
allowed_usages,
|
||||
flags: format_flags,
|
||||
},
|
||||
);
|
||||
let device = (true, true, true, coverage);
|
||||
assert_eq!(
|
||||
frame_configuration(true, device, F::Bgra8Unorm, false, true),
|
||||
(WIDE_FRAME, false)
|
||||
);
|
||||
assert_eq!(
|
||||
frame_configuration(false, device, F::Bgra8Unorm, false, true),
|
||||
(F::Bgra8Unorm, false)
|
||||
);
|
||||
assert_eq!(
|
||||
frame_configuration(true, device, F::Bgra8Unorm, true, false),
|
||||
(LINEAR_FRAME, false)
|
||||
);
|
||||
// A 0.2 pack keeps its float frame, without the layer: SDR.
|
||||
assert_eq!(
|
||||
frame_configuration(true, device, F::Bgra8Unorm, true, true),
|
||||
(LINEAR_FRAME, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_0_2_pack_without_the_hud_layer_finishes_on_the_swapchains_8_bits() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, overlay_format};
|
||||
use wgpu::TextureFormat::{Bgra8Unorm, Bgra8UnormSrgb, Rgba8Unorm};
|
||||
assert_eq!(
|
||||
overlay_format(LINEAR_FRAME, false, Bgra8UnormSrgb),
|
||||
Bgra8Unorm
|
||||
);
|
||||
assert_eq!(overlay_format(LINEAR_FRAME, false, Rgba8Unorm), Rgba8Unorm);
|
||||
// Everything else draws its overlays on its frame: the layered frame's light, the
|
||||
// 16-bit frame, the 8-bit frame.
|
||||
for (frame, layered) in [
|
||||
(LINEAR_FRAME, true),
|
||||
(WIDE_FRAME, false),
|
||||
(Bgra8Unorm, false),
|
||||
] {
|
||||
assert_eq!(overlay_format(frame, layered, Bgra8UnormSrgb), frame);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_tone_curve_runs_on_the_layered_world() {
|
||||
use super::horizon_tone_curve;
|
||||
// No pack, or a 0.2 pack without `[tonemap]`: Horizon's curve.
|
||||
assert!(horizon_tone_curve(true, true, false));
|
||||
// A pack's `[tonemap]` mapped it already; never both.
|
||||
assert!(!horizon_tone_curve(true, true, true));
|
||||
// No world (the title screen), or no layered frame (SDR, the compatibility frames).
|
||||
assert!(!horizon_tone_curve(true, false, false));
|
||||
assert!(!horizon_tone_curve(false, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_no_pack_layered_world_applies_hdr_gain() {
|
||||
use super::no_pack_hdr_gain;
|
||||
assert!(no_pack_hdr_gain(true, false, true));
|
||||
assert!(!no_pack_hdr_gain(false, false, true));
|
||||
assert!(!no_pack_hdr_gain(true, true, true));
|
||||
assert!(!no_pack_hdr_gain(true, false, false));
|
||||
}
|
||||
@@ -465,7 +465,7 @@ impl<'a> Draw<'a> {
|
||||
}
|
||||
|
||||
fn material(&mut self, api: horizon_shader_api::Api) {
|
||||
let source = super::super::native_material::coverage_test_shader(api);
|
||||
let source = super::super::native_material::coverage_test_shader((api, false));
|
||||
self.shader = self
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
@@ -478,7 +478,7 @@ impl<'a> Draw<'a> {
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
api,
|
||||
(api, false),
|
||||
);
|
||||
self.queue.write_buffer(
|
||||
&frame.buffer,
|
||||
|
||||
@@ -149,6 +149,12 @@ fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
|
||||
vec!["terrain.wgsl", "visbuffer.wgsl"],
|
||||
super::terrain_shader() + include_str!("../visbuffer.wgsl"),
|
||||
),
|
||||
// Ray tracing's unpacking of compact sections (gfx/ray_scene.rs's `Unpacker`).
|
||||
(
|
||||
"ray tracing unpack",
|
||||
vec!["terrain.wgsl", "rt_unpack.wgsl"],
|
||||
super::surface_setup::ray_query::ray_scene::unpack_shader(),
|
||||
),
|
||||
// A native mod's world warp (gfx/world_warp.rs): its copies with the plainest warp.
|
||||
(
|
||||
"world warp terrain",
|
||||
@@ -164,10 +170,15 @@ fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
|
||||
vec!["entity.wgsl", "static_entity.wgsl", "world_warp.wgsl"],
|
||||
super::world_warp::entity_shader(super::world_warp::IDENTITY).unwrap(),
|
||||
),
|
||||
(
|
||||
"world primitives",
|
||||
vec!["world_primitives.wgsl"],
|
||||
super::world_primitives::shader(),
|
||||
),
|
||||
(
|
||||
"improved transparency world border",
|
||||
vec!["border.wgsl", "oit.wgsl", "oit_border.wgsl"],
|
||||
include_str!("../border.wgsl").to_owned()
|
||||
super::border_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_border.wgsl"),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! A shader API 0.2 pack's `[frame] blend = "linear"` (docs/design/hdr-linear-light.md, How the
|
||||
//! frame stores the picture): its float frame holds linear light instead of vanilla's gamma, so
|
||||
//! glass, water and smoke blend as light does. The pack's material, sky and weather stages store
|
||||
//! their results as they are. Horizon's own draws into the frame decode theirs at the end of
|
||||
//! their shaders (hdr_light.wgsl's `frame_stored`), so a solid surface looks as under `"gamma"`.
|
||||
//! What the pack reads of the frame (`scene_color`, `opaque_color`, the water mirror) is copied
|
||||
//! as it is instead of decoded. The tonemap encodes the picture into gamma again, so the hand,
|
||||
//! the camera overlays, the HUD, the screenshots and HDR output see the same either way.
|
||||
|
||||
/// Whether a frame of `format` stores linear light for a 0.2 pack that asks for it (`linear`,
|
||||
/// `FrameRequest::linear_blend`): only the float frame. Where the device can't draw that, the
|
||||
/// 0.2 pack runs on the 8-bit frame, which keeps vanilla's gamma, as `"gamma"` does: eight bits
|
||||
/// of linear light would band in the darks.
|
||||
pub(super) fn stores_linear(linear: bool, format: wgpu::TextureFormat) -> bool {
|
||||
linear && format == super::LINEAR_FRAME
|
||||
}
|
||||
|
||||
/// The constants of a pipeline of Horizon's that draws into the world's frame of `format`
|
||||
/// (hdr_light.wgsl): light written, not marked, on the float frame; and decoded where the
|
||||
/// frame holds linear light (`linear`).
|
||||
pub(super) fn frame_constants(
|
||||
format: wgpu::TextureFormat,
|
||||
linear: bool,
|
||||
) -> [(&'static str, f64); 2] {
|
||||
[
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
),
|
||||
("FRAME_LINEAR", f64::from(u8::from(linear))),
|
||||
]
|
||||
}
|
||||
|
||||
/// The world's clear colour, given in vanilla's gamma, as a frame that holds linear light
|
||||
/// (`linear`) stores it.
|
||||
pub(super) fn stored_clear(color: wgpu::Color, linear: bool) -> wgpu::Color {
|
||||
if !linear {
|
||||
return color;
|
||||
}
|
||||
let decode = |v: f64| v.max(0.0).powf(2.2);
|
||||
wgpu::Color {
|
||||
r: decode(color.r),
|
||||
g: decode(color.g),
|
||||
b: decode(color.b),
|
||||
a: color.a,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "linear_blend_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,413 @@
|
||||
use super::super::{
|
||||
LINEAR_FRAME,
|
||||
native_post::{Decoder, INTERMEDIATE, line_helpers},
|
||||
};
|
||||
use super::{frame_constants, stored_clear};
|
||||
use horizon_shader_api::Api;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// One full-screen draw into the frame: `vec4(rgb, alpha)` from Horizon's own shaders, which
|
||||
/// give vanilla's gamma (`frame_stored`), or a 0.2 pack's, which gives linear light
|
||||
/// (`horizon_line_out`).
|
||||
#[derive(Clone, Copy)]
|
||||
enum Writer {
|
||||
Horizon,
|
||||
Pack,
|
||||
}
|
||||
|
||||
const SOURCE: &str = "
|
||||
// The colour comes in a buffer: naga 30 rejects this fragment with the colour made of
|
||||
// overrides (`ExpressionAlreadyInScope` once they are processed).
|
||||
@group(0) @binding(0) var<uniform> test_color: vec4<f32>;
|
||||
override TEST_PACK: bool = false;
|
||||
@vertex fn test_vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
|
||||
return vec4<f32>(vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
@fragment fn test_fs() -> @location(0) vec4<f32> {
|
||||
let c = test_color;
|
||||
if TEST_PACK {
|
||||
return vec4<f32>(horizon_line_out(c.rgb), c.a);
|
||||
}
|
||||
return frame_stored(c);
|
||||
}
|
||||
";
|
||||
|
||||
fn gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
pollster::block_on(adapter.request_device(&Default::default())).ok()
|
||||
}
|
||||
|
||||
/// What a 0.2 stage reads of a 1x1 frame (`scene_color`): the world cleared to `clear`, in
|
||||
/// vanilla's gamma, then `draws` alpha-blended over it, then the production decoding copy.
|
||||
fn stage_input(
|
||||
(device, queue): &(wgpu::Device, wgpu::Queue),
|
||||
linear: bool,
|
||||
clear: [f64; 3],
|
||||
draws: &[(Writer, [f32; 4])],
|
||||
) -> [f32; 4] {
|
||||
let texture = |format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let frame = texture(LINEAR_FRAME, wgpu::TextureUsages::TEXTURE_BINDING);
|
||||
let input = texture(INTERMEDIATE, wgpu::TextureUsages::COPY_SRC);
|
||||
let frame_view = frame.create_view(&Default::default());
|
||||
let source =
|
||||
include_str!("../hdr_light.wgsl").to_owned() + line_helpers(Api::V0_2, linear) + SOURCE;
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let [r, g, b] = clear;
|
||||
let clear = stored_clear(wgpu::Color { r, g, b, a: 1.0 }, linear);
|
||||
let mut load = wgpu::LoadOp::Clear(clear);
|
||||
for &(writer, color) in draws {
|
||||
let mut constants = frame_constants(LINEAR_FRAME, linear).to_vec();
|
||||
constants.push((
|
||||
"TEST_PACK",
|
||||
f64::from(u8::from(matches!(writer, Writer::Pack))),
|
||||
));
|
||||
let options = wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
};
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("test_vs"),
|
||||
compilation_options: options.clone(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("test_fs"),
|
||||
compilation_options: options,
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: LINEAR_FRAME,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
let color = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&color),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pipeline.get_bind_group_layout(0),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: color.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
pass.set_pipeline(&pipeline);
|
||||
pass.set_bind_group(0, &group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
load = wgpu::LoadOp::Load;
|
||||
}
|
||||
if let wgpu::LoadOp::Clear(_) = load {
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Decoder::new(device, linear).run(
|
||||
device,
|
||||
&mut encoder,
|
||||
&frame_view,
|
||||
&input.create_view(&Default::default()),
|
||||
);
|
||||
texel((device, queue), encoder, &input)
|
||||
}
|
||||
|
||||
/// The first texel of the half-float `texture` once `encoder` ran.
|
||||
fn texel(
|
||||
(device, queue): (&wgpu::Device, &wgpu::Queue),
|
||||
mut encoder: wgpu::CommandEncoder,
|
||||
texture: &wgpu::Texture,
|
||||
) -> [f32; 4] {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: 256,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
texture.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(256),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
buffer.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let bytes = buffer.get_mapped_range(..).unwrap();
|
||||
std::array::from_fn(|i| half::f16::from_le_bytes([bytes[2 * i], bytes[2 * i + 1]]).to_f32())
|
||||
}
|
||||
|
||||
fn assert_near(got: [f32; 4], want: [f32; 3], what: &str) {
|
||||
for i in 0..3 {
|
||||
// Half floats, and a gamma frame's half float decoded, hold about three digits.
|
||||
assert!(
|
||||
(got[i] - want[i]).abs() < 0.003 * want[i].max(1.0),
|
||||
"{what}: {got:?}, expected {want:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(got[3], 1.0, "{what}: {got:?}");
|
||||
}
|
||||
|
||||
// The background is gamma (0.8, 0.2, 0.5), linear (0.6121, 0.0290, 0.2176). Horizon's glass
|
||||
// gives gamma (0.1, 0.9, 0.4), the pack's linear (0.0, 0.5, 1.0); both cover a quarter. Under
|
||||
// "gamma" the frame mixes gamma, a stage reads `decode(0.25 * src + 0.75 * bg)`: Horizon's
|
||||
// (0.3556, 0.1156, 0.1944), the pack's, encoded first, (0.3250, 0.0887, 0.3556). Under
|
||||
// "linear" it mixes light, `0.25 * decode(src) + 0.75 * decode(bg)`: Horizon's
|
||||
// (0.4606, 0.2200, 0.1965), the pack's (0.4590, 0.1467, 0.4132).
|
||||
#[test]
|
||||
fn translucent_draws_blend_in_the_light_the_frame_holds() {
|
||||
let Some(gpu) = gpu() else {
|
||||
eprintln!("linear blend GPU unavailable");
|
||||
return;
|
||||
};
|
||||
let background = [0.8, 0.2, 0.5];
|
||||
for (linear, horizon, pack) in [
|
||||
(false, [0.3556, 0.1156, 0.1944], [0.3250, 0.0887, 0.3556]),
|
||||
(true, [0.4606, 0.2200, 0.1965], [0.4590, 0.1467, 0.4132]),
|
||||
] {
|
||||
let glass = [(Writer::Horizon, [0.1, 0.9, 0.4, 0.25])];
|
||||
let got = stage_input(&gpu, linear, background, &glass);
|
||||
assert_near(got, horizon, &format!("Horizon's glass, linear {linear}"));
|
||||
let glass = [(Writer::Pack, [0.0, 0.5, 1.0, 0.25])];
|
||||
let got = stage_input(&gpu, linear, background, &glass);
|
||||
assert_near(got, pack, &format!("the pack's glass, linear {linear}"));
|
||||
}
|
||||
}
|
||||
|
||||
// What a stage reads of solid surfaces is the same light under either blend: Horizon's gamma
|
||||
// (0.3, 0.7, 1.5) decodes to (0.0707, 0.4563, 2.4401), the pack's light (0.25, 0.5, 2.0) and
|
||||
// the clear colour, gamma (0.8, 0.2, 0.5), to (0.6121, 0.0290, 0.2176).
|
||||
#[test]
|
||||
fn stages_read_the_same_light_under_either_blend() {
|
||||
let Some(gpu) = gpu() else {
|
||||
eprintln!("linear blend GPU unavailable");
|
||||
return;
|
||||
};
|
||||
let clear = [0.8, 0.2, 0.5];
|
||||
let cases = [
|
||||
(
|
||||
vec![(Writer::Horizon, [0.3, 0.7, 1.5, 1.0])],
|
||||
[0.0707, 0.4563, 2.4401],
|
||||
),
|
||||
(
|
||||
vec![(Writer::Pack, [0.25, 0.5, 2.0, 1.0])],
|
||||
[0.25, 0.5, 2.0],
|
||||
),
|
||||
(vec![], [0.6121, 0.0290, 0.2176]),
|
||||
];
|
||||
for (draws, want) in cases {
|
||||
let [gamma, linear] = [false, true].map(|linear| stage_input(&gpu, linear, clear, &draws));
|
||||
assert_near(gamma, want, "gamma");
|
||||
assert_near(linear, want, "linear");
|
||||
for i in 0..3 {
|
||||
assert!((gamma[i] - linear[i]).abs() < 0.003 * want[i].max(1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A section fading in mixes the pack's material result toward the fog colour in vanilla's
|
||||
// gamma, as the fog does. Fog gamma 0.8, the pack's light 0.2 (gamma 0.2^(1/2.2) = 0.4812), half
|
||||
// visible: under "linear" the frame stores (0.5 * 0.8 + 0.5 * 0.4812)^2.2 = 0.3757, not the
|
||||
// 0.5 a mix in light gives; under "gamma" the result is already gamma, 0.5 * 0.8 + 0.5 * 0.2.
|
||||
#[test]
|
||||
fn a_fading_section_mixes_toward_the_fog_in_gamma() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
eprintln!("linear blend GPU unavailable");
|
||||
return;
|
||||
};
|
||||
let globals = super::super::Globals {
|
||||
fog_color: [0.8; 4],
|
||||
// The pack's material result, as `horizon_material_color` gives it.
|
||||
sky_color: [0.2, 0.2, 0.2, 1.0],
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
let globals = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::bytes_of(&globals),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
for (linear, want) in [(false, 0.5), (true, 0.3757)] {
|
||||
let source = super::super::native_material::coverage_test_shader((Api::V0_2, linear))
|
||||
+ "
|
||||
@fragment fn test_faded() -> @location(0) vec4<f32> {
|
||||
var o: HorizonMaterialOut;
|
||||
// Half visible (`horizon_material_vertex`), past 32 blocks.
|
||||
o.hdr_code = 32768u << 16u;
|
||||
o.spherical = 40.0;
|
||||
return horizon_material_faded(o, horizon_terrain_globals.sky_color);
|
||||
}
|
||||
";
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let constants = frame_constants(LINEAR_FRAME, linear)
|
||||
.map(|(name, value)| (format!("horizon_terrain_{name}"), value));
|
||||
let constants: Vec<(&str, f64)> = constants.iter().map(|(n, v)| (n.as_str(), *v)).collect();
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("test_vertex"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("test_faded"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(LINEAR_FRAME.into())],
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pipeline.get_bind_group_layout(0),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: globals.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let frame = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: LINEAR_FRAME,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame.create_view(&Default::default()),
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&pipeline);
|
||||
pass.set_bind_group(0, &group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
let got = texel((&device, &queue), encoder, &frame);
|
||||
assert!(
|
||||
(got[0] - want).abs() < 0.002,
|
||||
"linear {linear}: {got:?}, expected {want}"
|
||||
);
|
||||
assert_eq!(got[3], 1.0, "alpha kept");
|
||||
}
|
||||
}
|
||||
|
||||
// The frame O1 resolves (`output::resolve_frame`) holds linear light only for a 0.2 pack that
|
||||
// asks for it, on the float frame. On a device without the float frame the pack draws anyway,
|
||||
// in gamma. A blend changed is another frame, so the game makes the renderer again
|
||||
// (`Renderer::frame_follows`).
|
||||
#[test]
|
||||
fn only_a_float_frame_holds_linear_light() {
|
||||
use super::super::output::{FrameRequest, resolve_frame, tests::window};
|
||||
let settings = crate::graphics::Graphics::default();
|
||||
for float_frame in [false, true] {
|
||||
for (linear_pack, linear_blend) in [(false, false), (true, false), (true, true)] {
|
||||
let (mut caps, reading) = window(None, None);
|
||||
caps.float_frame = float_frame;
|
||||
let request = FrameRequest {
|
||||
linear_pack,
|
||||
linear_blend,
|
||||
hdr_float: false,
|
||||
};
|
||||
let frame = resolve_frame(
|
||||
&settings,
|
||||
request,
|
||||
caps,
|
||||
reading,
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
);
|
||||
assert_eq!(frame.format == LINEAR_FRAME, float_frame && linear_pack);
|
||||
assert_eq!(
|
||||
frame.linear_light,
|
||||
float_frame && linear_blend,
|
||||
"float {float_frame}, linear {linear_blend}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,9 @@ const ENTRIES: &str = "
|
||||
override horizon_material_fog: bool = true;
|
||||
fn horizon_material_fogged(color: vec4<f32>, spherical: f32, cylindrical: f32) -> vec4<f32> {
|
||||
if horizon_material_fog {
|
||||
return horizon_terrain_apply_fog(color, spherical, cylindrical);
|
||||
// In vanilla's gamma, as vanilla mixes it: on a frame of linear light, encoded for it.
|
||||
let gamma = horizon_terrain_frame_gamma(color);
|
||||
return horizon_terrain_frame_stored(horizon_terrain_apply_fog(gamma, spherical, cylindrical));
|
||||
}
|
||||
return color;
|
||||
}
|
||||
@@ -61,11 +63,14 @@ fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color
|
||||
}
|
||||
// terrain.wgsl's fade-in (terrain.vsh's chunkVisibility): whole within 16 blocks, the section's
|
||||
// own visibility from 32 out, toward the fog colour, alpha kept. The distance blend runs here on
|
||||
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle.
|
||||
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle. In
|
||||
// vanilla's gamma, as the fog: on a frame of linear light, encoded for it.
|
||||
fn horizon_material_faded(in: HorizonMaterialOut, color: vec4<f32>) -> vec4<f32> {
|
||||
let instance = 1.0 - f32(in.hdr_code >> 16u) / 65535.0;
|
||||
let visibility = mix(1.0, instance, clamp((in.spherical - 16.0) / 16.0, 0.0, 1.0));
|
||||
return vec4(mix(horizon_terrain_globals.fog_color.rgb, color.rgb, visibility), color.a);
|
||||
let gamma = horizon_terrain_frame_gamma(color);
|
||||
let faded = mix(horizon_terrain_globals.fog_color.rgb, gamma.rgb, visibility);
|
||||
return horizon_terrain_frame_stored(vec4(faded, color.a));
|
||||
}
|
||||
// `light`: sky, block, 16 per level.
|
||||
fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
|
||||
@@ -277,8 +282,9 @@ pub struct TerrainShape<'a> {
|
||||
pub flat: Option<&'a wgpu::BindGroupLayout>,
|
||||
/// terrain.wgsl's `SPRITE_ARRAYS` and `SPRITE_SPLIT`.
|
||||
pub sprites: [f64; 2],
|
||||
/// The world target's colour format.
|
||||
/// The world target's colour format, and whether it holds linear light (`linear_blend`).
|
||||
pub format: wgpu::TextureFormat,
|
||||
pub linear: bool,
|
||||
/// The pack's tags of each tagged sprite layer, as `(layer, bits)`
|
||||
/// (`Manifest::sprite_tags`).
|
||||
pub sprite_tags: &'a [(u32, u32)],
|
||||
@@ -369,12 +375,12 @@ fn shader_source(
|
||||
tags: &[(u32, u32)],
|
||||
materials: &[(u32, u32)],
|
||||
(vertex, displaced): (bool, bool),
|
||||
api: horizon_shader_api::Api,
|
||||
(api, linear): (horizon_shader_api::Api, bool),
|
||||
) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{}{ENTRIES}{}{}",
|
||||
vanilla_terrain(),
|
||||
super::native_post::line_helpers(api),
|
||||
super::native_post::line_helpers(api, linear),
|
||||
if vertex {
|
||||
DISPLACE_PACK
|
||||
} else {
|
||||
@@ -398,9 +404,9 @@ fn module(
|
||||
materials: &[(u32, u32)],
|
||||
vertex: bool,
|
||||
displaced: bool,
|
||||
api: horizon_shader_api::Api,
|
||||
line: (horizon_shader_api::Api, bool),
|
||||
) -> Result<naga::Module, String> {
|
||||
let text = shader_source(&stage.source, tags, materials, (vertex, displaced), api);
|
||||
let text = shader_source(&stage.source, tags, materials, (vertex, displaced), line);
|
||||
naga::front::wgsl::parse_str(&text)
|
||||
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))
|
||||
}
|
||||
@@ -424,7 +430,7 @@ impl NativeMaterial {
|
||||
shape.sprite_materials,
|
||||
vertex,
|
||||
displaced.is_some(),
|
||||
frame.api,
|
||||
(frame.api, frame.linear),
|
||||
)?;
|
||||
let mut constants = present_constants(&module, constants);
|
||||
let sprite_names = [
|
||||
@@ -433,10 +439,10 @@ impl NativeMaterial {
|
||||
];
|
||||
constants.extend(sprite_names.into_iter().zip(shape.sprites));
|
||||
constants.push(("horizon_material_fog", f64::from(u8::from(fog))));
|
||||
constants.push((
|
||||
"horizon_terrain_HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(shape.format == super::LINEAR_FRAME)),
|
||||
));
|
||||
let [(_, write_light), (_, linear)] =
|
||||
super::linear_blend::frame_constants(shape.format, frame.linear);
|
||||
constants.push(("horizon_terrain_HDR_WRITE_LIGHT", write_light));
|
||||
constants.push(("horizon_terrain_FRAME_LINEAR", linear));
|
||||
constants.push((
|
||||
"horizon_displace_distance",
|
||||
displaced.map_or(0.0, |d| f64::from(d.distance)),
|
||||
@@ -586,7 +592,9 @@ impl NativeMaterial {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
|
||||
/// A material pack's terrain shader on shader API line `api`, its frame holding linear light
|
||||
/// where `linear`, with a full-screen `test_vertex`.
|
||||
pub(super) fn coverage_test_shader((api, linear): (horizon_shader_api::Api, bool)) -> String {
|
||||
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
|
||||
let manifest = format!(
|
||||
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
|
||||
@@ -601,7 +609,7 @@ pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
|
||||
.iter()
|
||||
.find(|s| s.kind == StageKind::Material)
|
||||
.unwrap();
|
||||
shader_source(&stage.source, &[], &[], (false, true), api)
|
||||
shader_source(&stage.source, &[], &[], (false, true), (api, linear))
|
||||
+ r#"
|
||||
override TEST_TIER: u32 = 0u;
|
||||
override TEST_OVERLAY: u32 = 0u;
|
||||
@@ -687,7 +695,7 @@ mod tests {
|
||||
let tags = [(7, 1), (3, 1), (9, 2)];
|
||||
let materials = [(5, 1000)];
|
||||
assert!(layer_table("t", &tags).contains("case 3u, 7u: { return 1u; }"));
|
||||
let module = module(&stage, &tags, &materials, false, false, Api::V0_1).unwrap();
|
||||
let module = module(&stage, &tags, &materials, false, false, (Api::V0_1, false)).unwrap();
|
||||
let info = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -701,7 +709,7 @@ mod tests {
|
||||
&tags,
|
||||
&materials,
|
||||
(false, false),
|
||||
Api::V0_1
|
||||
(Api::V0_1, false)
|
||||
))
|
||||
)
|
||||
});
|
||||
@@ -743,7 +751,7 @@ mod tests {
|
||||
.into();
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let stage = report.stages.iter().find(|s| s.kind == StageKind::Material);
|
||||
let module = module(stage.unwrap(), &[], &[], true, false, Api::V0_2).unwrap();
|
||||
let module = module(stage.unwrap(), &[], &[], true, false, (Api::V0_2, false)).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -766,14 +774,20 @@ mod tests {
|
||||
let mut report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let stage = report.stages.remove(0);
|
||||
let materials = [(5, 1000)];
|
||||
let module = module(&stage, &[], &materials, false, true, Api::V0_1).unwrap();
|
||||
let module = module(&stage, &[], &materials, false, true, (Api::V0_1, false)).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap_or_else(|e| {
|
||||
let text = shader_source(&stage.source, &[], &materials, (false, true), Api::V0_1);
|
||||
let text = shader_source(
|
||||
&stage.source,
|
||||
&[],
|
||||
&materials,
|
||||
(false, true),
|
||||
(Api::V0_1, false),
|
||||
);
|
||||
panic!("{}", e.emit_to_string(&text))
|
||||
});
|
||||
for entry in [
|
||||
@@ -830,7 +844,7 @@ mod tests {
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let source = report.stages[0].source.clone()
|
||||
+ horizon_shader_api::stages::PBR_STUB
|
||||
+ super::super::native_post::line_helpers(api)
|
||||
+ super::super::native_post::line_helpers(api, false)
|
||||
+ entry;
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
|
||||
@@ -129,7 +129,7 @@ impl NativePack {
|
||||
textures,
|
||||
lights,
|
||||
terrain.pbr,
|
||||
api,
|
||||
(api, terrain.linear && api == horizon_shader_api::Api::V0_2),
|
||||
);
|
||||
// `[displacement]`, unless its option is off; nothing to displace without heights.
|
||||
let displacement = report
|
||||
@@ -361,6 +361,7 @@ mod tests {
|
||||
flat: None,
|
||||
sprites: [1.0, 2048.0],
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
linear: false,
|
||||
sprite_tags: &[],
|
||||
sprite_materials: &[],
|
||||
entities: &empty,
|
||||
|
||||
@@ -16,43 +16,57 @@ use super::Frame;
|
||||
pub(super) const INTERMEDIATE: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
|
||||
/// What the renderer's own entries call where a stage's colours cross into and out of it: as
|
||||
/// they are under 0.1; under 0.2 vanilla's 2.2 power, as the prelude's `horizon_decode` and
|
||||
/// `horizon_encode` (the shadow pass has no prelude, so these stand alone).
|
||||
pub fn line_helpers(api: Api) -> &'static str {
|
||||
match api {
|
||||
Api::V0_1 => {
|
||||
/// they are under 0.1; under 0.2 vanilla's 2.2 power in, as the prelude's `horizon_decode`, and
|
||||
/// out as the frame stores them: encoded, as `horizon_encode`, or where the frame holds linear
|
||||
/// light (`linear`, `[frame] blend = "linear"`) as they are. The shadow pass has no prelude, so
|
||||
/// these stand alone.
|
||||
pub fn line_helpers(api: Api, linear: bool) -> &'static str {
|
||||
match (api, linear) {
|
||||
(Api::V0_1, _) => {
|
||||
"fn horizon_line_in(c: vec3<f32>) -> vec3<f32> { return c; }
|
||||
fn horizon_line_out(c: vec3<f32>) -> vec3<f32> { return c; }
|
||||
"
|
||||
}
|
||||
Api::V0_2 => {
|
||||
(Api::V0_2, false) => {
|
||||
"fn horizon_line_in(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(select(vec3<f32>(0.0), c, c > vec3<f32>(0.0)), vec3<f32>(2.2));
|
||||
}
|
||||
fn horizon_line_out(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(select(vec3<f32>(0.0), c, c > vec3<f32>(0.0)), vec3<f32>(1.0 / 2.2));
|
||||
}
|
||||
"
|
||||
}
|
||||
(Api::V0_2, true) => {
|
||||
"fn horizon_line_in(c: vec3<f32>) -> vec3<f32> {
|
||||
return pow(select(vec3<f32>(0.0), c, c > vec3<f32>(0.0)), vec3<f32>(2.2));
|
||||
}
|
||||
fn horizon_line_out(c: vec3<f32>) -> vec3<f32> { return c; }
|
||||
"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 0.2's decoding copy: a picture in the frame's gamma into linear light in `INTERMEDIATE`, of
|
||||
/// the same size, alpha 1 (`scene_color`, `opaque_color`, the mirror).
|
||||
/// 0.2's decoding copy: a picture of the frame into linear light in `INTERMEDIATE`, of the same
|
||||
/// size, alpha 1 (`scene_color`, `opaque_color`, the mirror). A frame of vanilla's gamma is
|
||||
/// decoded; one that holds linear light (`linear`) is copied as it is.
|
||||
#[derive(Clone)]
|
||||
pub struct Decoder(wgpu::RenderPipeline);
|
||||
|
||||
impl Decoder {
|
||||
fn new(device: &wgpu::Device) -> Self {
|
||||
let source = line_helpers(Api::V0_2).to_owned()
|
||||
+ "@group(0) @binding(0) var horizon_from: texture_2d<f32>;
|
||||
@vertex fn horizon_vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
|
||||
pub(super) fn new(device: &wgpu::Device, linear: bool) -> Self {
|
||||
let read = if linear { "c" } else { "horizon_line_in(c)" };
|
||||
let source = line_helpers(Api::V0_2, false).to_owned()
|
||||
+ &format!(
|
||||
"@group(0) @binding(0) var horizon_from: texture_2d<f32>;
|
||||
@vertex fn horizon_vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {{
|
||||
return vec4<f32>(vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
@fragment fn horizon_fs(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(horizon_line_in(textureLoad(horizon_from, vec2<i32>(p.xy), 0).rgb), 1.0);
|
||||
}
|
||||
";
|
||||
}}
|
||||
@fragment fn horizon_fs(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {{
|
||||
let c = textureLoad(horizon_from, vec2<i32>(p.xy), 0).rgb;
|
||||
return vec4<f32>({read}, 1.0);
|
||||
}}
|
||||
"
|
||||
);
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Shader API 0.2 decode"),
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
@@ -353,9 +367,11 @@ pub struct NativeFrame {
|
||||
frames: u32,
|
||||
/// `frame.wetness`, and when it was last written.
|
||||
wetness: (f32, Option<std::time::Instant>),
|
||||
/// The shader API line of the pack, and under 0.2 the decoding copy and the mirror it
|
||||
/// decoded (`linear_mirror`).
|
||||
/// The shader API line of the pack, whether the world's frame holds linear light for it
|
||||
/// (`[frame] blend = "linear"`), and under 0.2 the decoding copy and the mirror it decoded
|
||||
/// (`linear_mirror`).
|
||||
pub api: Api,
|
||||
pub linear: bool,
|
||||
pub decoder: Option<Decoder>,
|
||||
mirror: Option<wgpu::TextureView>,
|
||||
}
|
||||
@@ -433,15 +449,16 @@ fn eased(value: f32, target: f32, half_life: f32, dt: f32) -> f32 {
|
||||
}
|
||||
|
||||
impl NativeFrame {
|
||||
/// The frame of a pack with the `[shadow]` settings `shadow`, if any, and the
|
||||
/// block light colours `lights`.
|
||||
/// The frame of a pack of the shader API line `api` with the `[shadow]` settings `shadow`, if
|
||||
/// any, and the block light colours `lights`, onto a world's frame that holds linear light
|
||||
/// when `linear` (a 0.2 pack's).
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
shadow: Option<&horizon_shader_api::manifest::Shadow>,
|
||||
textures: Vec<(wgpu::TextureView, wgpu::Sampler)>,
|
||||
lights: super::block_light::LightTable,
|
||||
pbr: Option<&[wgpu::Texture; 2]>,
|
||||
api: Api,
|
||||
(api, linear): (Api, bool),
|
||||
) -> Self {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Native shader pack frame"),
|
||||
@@ -565,7 +582,8 @@ impl NativeFrame {
|
||||
shadow_camera: shadow.map(super::native_shadow::camera),
|
||||
reflects: false,
|
||||
api,
|
||||
decoder: (api == Api::V0_2).then(|| Decoder::new(device)),
|
||||
linear,
|
||||
decoder: (api == Api::V0_2).then(|| Decoder::new(device, linear)),
|
||||
mirror: None,
|
||||
last: None,
|
||||
eye: [f32::NAN; 4],
|
||||
@@ -1759,7 +1777,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_1,
|
||||
(Api::V0_1, false),
|
||||
);
|
||||
frame.pack.still_ticks = Some(47_811);
|
||||
frame.pack.eye_light = [0, 15];
|
||||
@@ -1809,7 +1827,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_1,
|
||||
(Api::V0_1, false),
|
||||
)
|
||||
};
|
||||
let (mut used, mut fresh) = (new(), new());
|
||||
@@ -1875,7 +1893,7 @@ file = \"c.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_1,
|
||||
(Api::V0_1, false),
|
||||
);
|
||||
let mut post = NativePost::new(
|
||||
&device,
|
||||
@@ -1988,7 +2006,7 @@ file = \"c.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_1,
|
||||
(Api::V0_1, false),
|
||||
);
|
||||
queue.write_buffer(
|
||||
&frame.buffer,
|
||||
@@ -2116,7 +2134,7 @@ file = \"b.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_1,
|
||||
(Api::V0_1, false),
|
||||
);
|
||||
let mut post = NativePost::new(
|
||||
&device,
|
||||
@@ -2236,7 +2254,7 @@ file = \"b.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_1,
|
||||
(Api::V0_1, false),
|
||||
);
|
||||
let mut post = NativePost::new(
|
||||
&device,
|
||||
@@ -2571,7 +2589,7 @@ file = \"b.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_2,
|
||||
(Api::V0_2, false),
|
||||
);
|
||||
let inputs = FrameInputs {
|
||||
headroom: 1.0,
|
||||
@@ -2655,7 +2673,7 @@ file = \"b.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_2,
|
||||
(Api::V0_2, false),
|
||||
);
|
||||
let fields = |i: &FrameInputs| (i.hdr, i.headroom, i.paper_white, i.peak, i.black_level);
|
||||
assert_eq!(
|
||||
@@ -2725,7 +2743,7 @@ file = \"b.wgsl\"
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
Api::V0_2,
|
||||
(Api::V0_2, false),
|
||||
);
|
||||
let inputs = FrameInputs {
|
||||
hdr: u32::from(hdr),
|
||||
|
||||
@@ -516,7 +516,7 @@ fn shader_source(
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
PREFIX
|
||||
),
|
||||
super::native_post::line_helpers(api)
|
||||
super::native_post::line_helpers(api, false)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,12 +39,13 @@ fn vanilla_sky() -> String {
|
||||
prefixed(&text, PREFIX)
|
||||
}
|
||||
|
||||
/// The sky shader for the pack's sky stage `source`.
|
||||
fn shader_source(source: &str, api: horizon_shader_api::Api) -> String {
|
||||
/// The sky shader for the pack's sky stage `source`, of the line `api`, onto a frame that holds
|
||||
/// linear light when `linear`.
|
||||
fn shader_source(source: &str, (api, linear): (horizon_shader_api::Api, bool)) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{}{PACK_ENTRY}",
|
||||
vanilla_sky(),
|
||||
super::native_post::line_helpers(api)
|
||||
super::native_post::line_helpers(api, linear)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ fn build(
|
||||
frame: &NativeFrame,
|
||||
stage: &CheckedStage,
|
||||
) -> Result<(naga::Module, wgpu::PipelineLayout), String> {
|
||||
let text = shader_source(&stage.source, frame.api);
|
||||
let text = shader_source(&stage.source, (frame.api, frame.linear));
|
||||
let module = naga::front::wgsl::parse_str(&text)
|
||||
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
|
||||
let mut groups: [Option<&wgpu::BindGroupLayout>; GROUP as usize + 1] = Default::default();
|
||||
@@ -198,7 +199,7 @@ mod tests {
|
||||
// A pack may use the renderer's names for its own things.
|
||||
let own = "override gain: f32;\nconst PI: f32 = 3.0;\nfn view_ray() -> f32 { return PI; }\n\
|
||||
fn sky(input: SkyInput) -> vec4<f32> { return input.color * gain * view_ray(); }\n";
|
||||
let text = shader_source(&stage(own).source, horizon_shader_api::Api::V0_1);
|
||||
let text = shader_source(&stage(own).source, (horizon_shader_api::Api::V0_1, false));
|
||||
let module = naga::front::wgsl::parse_str(&text).unwrap_or_else(|e| {
|
||||
panic!("{}", e.emit_to_string(&text));
|
||||
});
|
||||
@@ -261,7 +262,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
None,
|
||||
horizon_shader_api::Api::V0_1,
|
||||
(horizon_shader_api::Api::V0_1, false),
|
||||
);
|
||||
queue.write_buffer(
|
||||
&frame.buffer,
|
||||
|
||||
@@ -972,7 +972,7 @@ mod tests {
|
||||
);
|
||||
let border = stages("border");
|
||||
validate(
|
||||
&(include_str!("../border.wgsl").to_owned()
|
||||
&(super::super::border_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_border.wgsl")),
|
||||
&border.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Features {
|
||||
let border_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon improved transparency world border"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../border.wgsl").to_owned()
|
||||
(super::border_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_border.wgsl"))
|
||||
.into(),
|
||||
|
||||
@@ -77,6 +77,9 @@ impl DisplayReading {
|
||||
pub struct FrameRequest {
|
||||
/// A shader API 0.2 pack is picked under `HORIZON_HDR_FRAME=float`: its frame is float.
|
||||
pub linear_pack: bool,
|
||||
/// That pack's `[frame] blend` is `"linear"`: its frame holds linear light where it is
|
||||
/// float (`linear_blend`).
|
||||
pub linear_blend: bool,
|
||||
/// Under `HORIZON_HDR_FRAME=float`, with no pack available or a 0.2 one: the frame is
|
||||
/// the layered float one wherever HDR output goes.
|
||||
pub hdr_float: bool,
|
||||
@@ -89,6 +92,8 @@ pub struct FrameConfiguration {
|
||||
pub(crate) overlay_format: wgpu::TextureFormat,
|
||||
/// The layered float HDR frame: an 8-bit HUD target and an R8 coverage target beside it.
|
||||
pub(crate) hud_layer: bool,
|
||||
/// The frame holds linear light, not vanilla's gamma (`linear_blend::stores_linear`).
|
||||
pub(crate) linear_light: bool,
|
||||
}
|
||||
|
||||
/// The display a 0.2 pack's `Frame` describes (docs/api/shaders-reference-0.2.md).
|
||||
@@ -166,6 +171,7 @@ impl EffectiveOutput {
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
overlay_format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
hud_layer: false,
|
||||
linear_light: false,
|
||||
},
|
||||
nits: [203.0, 1000.0, 0.0],
|
||||
ui_brightness: 1.0,
|
||||
@@ -259,6 +265,7 @@ pub(crate) fn resolve_frame(
|
||||
format,
|
||||
overlay_format: super::overlay_format(format, hud_layer, swapchain),
|
||||
hud_layer,
|
||||
linear_light: super::linear_blend::stores_linear(request.linear_blend, format),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,10 +390,12 @@ pub(crate) mod tests {
|
||||
fn the_resolver_picks_one_output_and_frame_for_each_combination() {
|
||||
let no_pack = FrameRequest {
|
||||
linear_pack: false,
|
||||
linear_blend: false,
|
||||
hdr_float: true,
|
||||
};
|
||||
let pack_0_2 = FrameRequest {
|
||||
linear_pack: true,
|
||||
linear_blend: false,
|
||||
hdr_float: true,
|
||||
};
|
||||
// A 0.1 pack, or none without the float switch; a refused, missing or disabled
|
||||
@@ -499,6 +508,7 @@ pub(crate) mod tests {
|
||||
};
|
||||
let request = FrameRequest {
|
||||
linear_pack: false,
|
||||
linear_blend: false,
|
||||
hdr_float: true,
|
||||
};
|
||||
let on = |driver: Option<&str>| {
|
||||
@@ -741,6 +751,7 @@ pub(crate) mod tests {
|
||||
};
|
||||
let no_pack = FrameRequest {
|
||||
linear_pack: false,
|
||||
linear_blend: false,
|
||||
hdr_float: true,
|
||||
};
|
||||
let (caps, reading) = window(Some(Output::Pq), Some(true));
|
||||
|
||||
@@ -460,6 +460,7 @@ mod tests {
|
||||
|
||||
const NO_PACK: FrameRequest = FrameRequest {
|
||||
linear_pack: false,
|
||||
linear_blend: false,
|
||||
hdr_float: true,
|
||||
};
|
||||
fn on() -> Graphics {
|
||||
|
||||
@@ -3,9 +3,45 @@
|
||||
//! draws with it yet. Without the variable, and in a build without `horizon-extras` (a Horizon
|
||||
//! extra, docs/layers.md), the device is opened exactly as before.
|
||||
|
||||
/// The terrain's acceleration structures (step 2).
|
||||
#[path = "ray_scene.rs"]
|
||||
pub(in crate::gfx) mod ray_scene;
|
||||
|
||||
/// The ray query feature.
|
||||
const RAY_QUERY: wgpu::Features = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
|
||||
|
||||
/// Whether `device` was opened with ray queries.
|
||||
pub(in crate::gfx) fn on(device: &wgpu::Device) -> bool {
|
||||
device.features().contains(RAY_QUERY)
|
||||
}
|
||||
|
||||
/// What buffers that BLAS builds read need on top of their own usage: nothing without ray
|
||||
/// queries, so the off path's buffers are main's.
|
||||
pub(in crate::gfx) fn blas_input(device: &wgpu::Device) -> wgpu::BufferUsages {
|
||||
if on(device) {
|
||||
wgpu::BufferUsages::BLAS_INPUT
|
||||
} else {
|
||||
wgpu::BufferUsages::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// The index buffer of `quads` quads (triangles 0 1 2 and 0 2 3), which the terrain draws
|
||||
/// and BLAS builds both read.
|
||||
pub(in crate::gfx) fn quad_indices(device: &wgpu::Device, quads: u32) -> wgpu::Buffer {
|
||||
use wgpu::util::DeviceExt;
|
||||
let indices: Vec<u32> = (0..quads)
|
||||
.flat_map(|q| {
|
||||
let b = q * 4;
|
||||
[b, b + 1, b + 2, b, b + 2, b + 3]
|
||||
})
|
||||
.collect();
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Quad indices"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX | blas_input(device),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the player asked for ray tracing (the device needs the feature from its creation).
|
||||
fn requested() -> bool {
|
||||
asked(std::env::var("HORIZON_RAY_TRACING").ok().as_deref())
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
//! The terrain as acceleration structures, with ray tracing on (#13780 step 2, after the #1180
|
||||
//! spike; `ray_query`): one BLAS per section's solid quads within `reach` chunks of the camera,
|
||||
//! compacted once built, and one TLAS of them, built again only when a BLAS changes or the camera
|
||||
//! enters another chunk column (its instances sit relative to that column). Nothing traces them
|
||||
//! yet. Without ray queries on the device there is no scene and nothing here runs.
|
||||
//!
|
||||
//! The section arena keeps its compact layout: a section about to get a BLAS has its solid quads
|
||||
//! written out as vertices first (`Unpacker`, `rt_unpack.wgsl`), as the solid draws expand them.
|
||||
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::fast_hash::FxHashMap;
|
||||
use crate::gfx::{Renderer, SectionGpu, SectionKey, arena::Arena, resident::Resident};
|
||||
use crate::mesher::TerrainVertex;
|
||||
|
||||
/// BLAS builds per frame at most: a world that just loaded builds over a few frames.
|
||||
const BUILDS_PER_FRAME: usize = 512;
|
||||
/// Chunks around the camera the TLAS spans, unless `HORIZON_RT_CHUNKS` says otherwise. The
|
||||
/// #1180 spike's TLAS over every section cost 2-7 ms of CPU a frame at render distance 32.
|
||||
const REACH: i32 = 8;
|
||||
|
||||
/// The renderer's side: the calls gfx.rs makes, each one line there.
|
||||
impl Renderer {
|
||||
/// Section `key` got a new mesh, or lost it.
|
||||
pub(in crate::gfx) fn ray_mark(&mut self, key: SectionKey) {
|
||||
if let Some(rt) = &mut self.ray_scene {
|
||||
rt.mark(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every section is gone (a new world, a new dimension).
|
||||
pub(in crate::gfx) fn ray_clear(&mut self) {
|
||||
if let Some(rt) = &mut self.ray_scene {
|
||||
rt.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame's BLAS builds and TLAS, recorded in `encoder` before the world pass.
|
||||
pub(in crate::gfx) fn ray_prepare(
|
||||
&mut self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
camera: DVec3,
|
||||
title: bool,
|
||||
) {
|
||||
if let Some(rt) = &mut self.ray_scene
|
||||
&& !title
|
||||
{
|
||||
crate::spans::time("ray scene", || {
|
||||
rt.prepare(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
encoder,
|
||||
&self.sections,
|
||||
&self.arena,
|
||||
&self.indices,
|
||||
camera,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The terrain's acceleration structures.
|
||||
pub(in crate::gfx) struct RayScene {
|
||||
/// Each section's BLAS, while it has solid quads within reach.
|
||||
blas: FxHashMap<SectionKey, wgpu::Blas>,
|
||||
/// Sections meshed again or emptied since their BLAS was built.
|
||||
dirty: Vec<SectionKey>,
|
||||
tlas: wgpu::Tlas,
|
||||
/// The camera's chunk column when the TLAS was built: its instances are placed relative to
|
||||
/// that column's corner, so moving inside it needs no new TLAS.
|
||||
anchor: Option<(i32, i32)>,
|
||||
/// Chunks around the anchor the TLAS spans.
|
||||
reach: i32,
|
||||
/// BLAS built this frame, to measure for compaction once submitted.
|
||||
to_compact: Vec<(SectionKey, wgpu::Blas)>,
|
||||
/// BLAS whose compacted size the GPU is measuring.
|
||||
compacting: Vec<(SectionKey, wgpu::Blas)>,
|
||||
/// Sections in the arena's compact layout, written out as vertices for their BLAS.
|
||||
unpacker: Unpacker,
|
||||
}
|
||||
|
||||
impl RayScene {
|
||||
/// The scene, when the device has ray queries (`ray_query`).
|
||||
pub(in crate::gfx) fn new(device: &wgpu::Device) -> Option<Self> {
|
||||
if !super::on(device) {
|
||||
return None;
|
||||
}
|
||||
let reach = std::env::var("HORIZON_RT_CHUNKS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(REACH);
|
||||
Some(Self {
|
||||
blas: FxHashMap::default(),
|
||||
dirty: Vec::new(),
|
||||
tlas: create_tlas(device, 1024),
|
||||
anchor: None,
|
||||
reach,
|
||||
to_compact: Vec::new(),
|
||||
compacting: Vec::new(),
|
||||
unpacker: Unpacker::new(device, 1 << 20),
|
||||
})
|
||||
}
|
||||
|
||||
fn mark(&mut self, key: SectionKey) {
|
||||
self.dirty.push(key);
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.blas.clear();
|
||||
self.dirty.clear();
|
||||
self.to_compact.clear();
|
||||
self.compacting.clear();
|
||||
self.anchor = None;
|
||||
}
|
||||
|
||||
/// Compacts BLAS built on earlier frames: built with room to spare, a compacted copy (made
|
||||
/// once the GPU has measured it) takes about a quarter. Whether a BLAS was replaced.
|
||||
fn compact(&mut self, queue: &wgpu::Queue) -> bool {
|
||||
let mut changed = false;
|
||||
let blas = &mut self.blas;
|
||||
self.compacting.retain(|(key, b)| {
|
||||
if blas.get(key) != Some(b) {
|
||||
return false;
|
||||
}
|
||||
if !b.ready_for_compaction() {
|
||||
return true;
|
||||
}
|
||||
blas.insert(*key, queue.compact_blas(b));
|
||||
changed = true;
|
||||
false
|
||||
});
|
||||
// Built and submitted last frame: the GPU can now measure them.
|
||||
for (key, b) in std::mem::take(&mut self.to_compact) {
|
||||
if self.blas.get(&key) == Some(&b) {
|
||||
b.prepare_compaction_async(|_| {});
|
||||
self.compacting.push((key, b));
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Builds the BLAS of sections marked since the last frame (at most `BUILDS_PER_FRAME`, and
|
||||
/// as many as the unpacker has room for), and the TLAS again when a BLAS changed or the
|
||||
/// camera entered another chunk column.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
sections: &Resident<SectionGpu>,
|
||||
arena: &Arena,
|
||||
indices: &wgpu::Buffer,
|
||||
camera: DVec3,
|
||||
) {
|
||||
let column = crate::gfx::resident::camera_column(camera);
|
||||
// Only sections within `reach` get a BLAS (at render distance 32 all of them took 1.3 GB
|
||||
// of video memory); those past `reach + 2` lose theirs, so walking along a chunk border
|
||||
// doesn't build the same ones over and over.
|
||||
let keep = self.reach + 2;
|
||||
let mut changed = self.compact(queue);
|
||||
if self.anchor != Some(column) {
|
||||
let before = self.blas.len();
|
||||
self.blas.retain(|k, _| within(k, column, keep));
|
||||
changed |= self.blas.len() != before;
|
||||
for (key, s, _) in sections.iter() {
|
||||
if within(&key, column, self.reach) && s.solid > 0 && !self.blas.contains_key(&key)
|
||||
{
|
||||
self.dirty.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
let take = self.dirty.len().min(BUILDS_PER_FRAME);
|
||||
let mut builds: Vec<Build<'_>> = Vec::new();
|
||||
let mut deferred = Vec::new();
|
||||
// Vertices the unpacker writes this frame.
|
||||
let mut unpacked = 0;
|
||||
for key in self.dirty.drain(..take) {
|
||||
if builds.iter().any(|b| b.key == key) {
|
||||
continue;
|
||||
}
|
||||
match sections
|
||||
.find(key)
|
||||
.filter(|s| s.solid > 0 && within(&key, column, self.reach))
|
||||
{
|
||||
Some(s) => {
|
||||
let at = if s.packed {
|
||||
let vertices = u64::from(s.solid) * 4;
|
||||
if unpacked + vertices > self.unpacker.capacity {
|
||||
if unpacked > 0 {
|
||||
// The rest waits for the next frame's room.
|
||||
deferred.push(key);
|
||||
continue;
|
||||
}
|
||||
self.unpacker.grow(device, vertices);
|
||||
}
|
||||
unpacked += vertices;
|
||||
Some(unpacked - vertices)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (size, blas) = new_blas(device, s.solid);
|
||||
builds.push(Build {
|
||||
key,
|
||||
s,
|
||||
size,
|
||||
blas,
|
||||
unpacked: at,
|
||||
});
|
||||
}
|
||||
None => changed |= self.blas.remove(&key).is_some(),
|
||||
}
|
||||
}
|
||||
self.dirty.extend(deferred);
|
||||
if builds.is_empty() && !changed && self.anchor == Some(column) {
|
||||
return;
|
||||
}
|
||||
self.unpacker.run(device, encoder, arena, &builds);
|
||||
for b in &builds {
|
||||
self.blas.insert(b.key, b.blas.clone());
|
||||
self.to_compact.push((b.key, b.blas.clone()));
|
||||
}
|
||||
let near = |k: &SectionKey| within(k, column, self.reach);
|
||||
let count = self.blas.keys().filter(|k| near(k)).count();
|
||||
if count > self.tlas.get().len() {
|
||||
self.tlas = create_tlas(device, (count as u32).next_power_of_two());
|
||||
}
|
||||
let min_y = sections.min_y();
|
||||
let mut n = 0;
|
||||
for (key, blas) in self.blas.iter().filter(|(k, _)| near(k)) {
|
||||
let o = instance_origin(*key, min_y, column);
|
||||
self.tlas[n] = Some(wgpu::TlasInstance::new(
|
||||
blas,
|
||||
[
|
||||
1.0, 0.0, 0.0, o[0], 0.0, 1.0, 0.0, o[1], 0.0, 0.0, 1.0, o[2],
|
||||
],
|
||||
0,
|
||||
0xff,
|
||||
));
|
||||
n += 1;
|
||||
}
|
||||
for i in n..self.tlas.get().len() {
|
||||
if self.tlas[i].is_none() {
|
||||
break;
|
||||
}
|
||||
self.tlas[i] = None;
|
||||
}
|
||||
let entries = build_entries(&builds, arena, &self.unpacker, indices);
|
||||
encoder.build_acceleration_structures(entries.iter(), std::iter::once(&self.tlas));
|
||||
self.anchor = Some(column);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether section `key` is within `reach` chunks of column `column`, along x and z.
|
||||
fn within(key: &SectionKey, column: (i32, i32), reach: i32) -> bool {
|
||||
(key.0 - column.0).abs() <= reach && (key.2 - column.1).abs() <= reach
|
||||
}
|
||||
|
||||
/// A BLAS to build this frame.
|
||||
struct Build<'a> {
|
||||
key: SectionKey,
|
||||
s: &'a SectionGpu,
|
||||
size: wgpu::BlasTriangleGeometrySizeDescriptor,
|
||||
blas: wgpu::Blas,
|
||||
/// Its first vertex in the unpacker's buffer, for a section in the compact layout.
|
||||
unpacked: Option<u64>,
|
||||
}
|
||||
|
||||
impl Build<'_> {
|
||||
/// The buffer holding its solid quads as vertices, and its first vertex there.
|
||||
fn vertices<'b>(&self, arena: &'b Arena, unpacker: &'b Unpacker) -> (&'b wgpu::Buffer, u64) {
|
||||
match self.unpacked {
|
||||
Some(at) => (&unpacker.out, at),
|
||||
None => (arena.buffer(self.s.page), self.s.start),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `rt_unpack.wgsl`'s `RtJob`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct RtJob {
|
||||
base: u32,
|
||||
quads: u32,
|
||||
out: u32,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
/// Writes the solid quads of sections in the arena's compact layout out as vertices, for their
|
||||
/// BLAS (BLAS builds read vertices; the records are only for the draws to expand). The buffer
|
||||
/// is reused every frame: a frame takes only the sections it has room for.
|
||||
struct Unpacker {
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
out: wgpu::Buffer,
|
||||
/// Vertices `out` holds.
|
||||
capacity: u64,
|
||||
}
|
||||
|
||||
impl Unpacker {
|
||||
fn new(device: &wgpu::Device, capacity: u64) -> Self {
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Ray tracing: unpack sections"),
|
||||
source: wgpu::ShaderSource::Wgsl(unpack_shader().into()),
|
||||
});
|
||||
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("Ray tracing: unpack sections"),
|
||||
layout: None,
|
||||
module: &module,
|
||||
entry_point: Some("cs_rt_unpack"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
Self {
|
||||
pipeline,
|
||||
out: unpacked_buffer(device, capacity),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Room for at least `vertices` (one section larger than the buffer).
|
||||
fn grow(&mut self, device: &wgpu::Device, vertices: u64) {
|
||||
self.capacity = vertices.next_power_of_two();
|
||||
self.out = unpacked_buffer(device, self.capacity);
|
||||
}
|
||||
|
||||
/// Writes out the builds' sections that are in the compact layout, one dispatch per arena
|
||||
/// page.
|
||||
fn run(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
arena: &Arena,
|
||||
builds: &[Build<'_>],
|
||||
) {
|
||||
let mut pages: Vec<u32> = builds
|
||||
.iter()
|
||||
.filter(|b| b.unpacked.is_some())
|
||||
.map(|b| b.s.page)
|
||||
.collect();
|
||||
pages.sort_unstable();
|
||||
pages.dedup();
|
||||
if pages.is_empty() {
|
||||
return;
|
||||
}
|
||||
let empty = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &self.pipeline.get_bind_group_layout(0),
|
||||
entries: &[],
|
||||
});
|
||||
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some("Ray tracing: unpack sections"),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &empty, &[]);
|
||||
for page in pages {
|
||||
let jobs: Vec<RtJob> = builds
|
||||
.iter()
|
||||
.filter_map(|b| {
|
||||
let at = b.unpacked.filter(|_| b.s.page == page)?;
|
||||
Some(RtJob {
|
||||
base: b.s.solid_base(),
|
||||
quads: b.s.solid,
|
||||
out: at as u32,
|
||||
pad: 0,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let jobs_buffer = wgpu::util::DeviceExt::create_buffer_init(
|
||||
device,
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Ray tracing: unpack jobs"),
|
||||
contents: bytemuck::cast_slice(&jobs),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
},
|
||||
);
|
||||
let arena_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &self.pipeline.get_bind_group_layout(1),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 19,
|
||||
resource: arena.buffer(page).as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let out_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &self.pipeline.get_bind_group_layout(2),
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: jobs_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: self.out.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
let most = jobs.iter().map(|j| j.quads * 4).max().unwrap_or(0);
|
||||
pass.set_bind_group(1, &arena_group, &[]);
|
||||
pass.set_bind_group(2, &out_group, &[]);
|
||||
pass.dispatch_workgroups(most.div_ceil(64), jobs.len() as u32, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// terrain.wgsl with the unpacking entry point after it, which reads sections as its solid
|
||||
/// draws do (`section_corner`).
|
||||
pub(in crate::gfx) fn unpack_shader() -> String {
|
||||
crate::gfx::terrain_shader() + include_str!("../rt_unpack.wgsl")
|
||||
}
|
||||
|
||||
fn unpacked_buffer(device: &wgpu::Device, vertices: u64) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Ray tracing: unpacked sections"),
|
||||
size: vertices * std::mem::size_of::<TerrainVertex>() as u64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::BLAS_INPUT,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_tlas(device: &wgpu::Device, capacity: u32) -> wgpu::Tlas {
|
||||
device.create_tlas(&wgpu::CreateTlasDescriptor {
|
||||
label: Some("Terrain sections"),
|
||||
max_instances: capacity,
|
||||
flags: wgpu::AccelerationStructureFlags::PREFER_FAST_TRACE,
|
||||
update_mode: wgpu::AccelerationStructureUpdateMode::Build,
|
||||
})
|
||||
}
|
||||
|
||||
/// A new BLAS for `solid` quads, and its size.
|
||||
fn new_blas(
|
||||
device: &wgpu::Device,
|
||||
solid: u32,
|
||||
) -> (wgpu::BlasTriangleGeometrySizeDescriptor, wgpu::Blas) {
|
||||
let size = wgpu::BlasTriangleGeometrySizeDescriptor {
|
||||
vertex_format: wgpu::VertexFormat::Float32x3,
|
||||
vertex_count: solid * 4,
|
||||
index_format: Some(wgpu::IndexFormat::Uint32),
|
||||
index_count: Some(solid * 6),
|
||||
flags: wgpu::AccelerationStructureGeometryFlags::OPAQUE,
|
||||
};
|
||||
let blas = device.create_blas(
|
||||
&wgpu::CreateBlasDescriptor {
|
||||
label: None,
|
||||
flags: wgpu::AccelerationStructureFlags::PREFER_FAST_TRACE
|
||||
| wgpu::AccelerationStructureFlags::ALLOW_COMPACTION,
|
||||
update_mode: wgpu::AccelerationStructureUpdateMode::Build,
|
||||
},
|
||||
wgpu::BlasGeometrySizeDescriptors::Triangles {
|
||||
descriptors: vec![size.clone()],
|
||||
},
|
||||
);
|
||||
(size, blas)
|
||||
}
|
||||
|
||||
/// Build entries reading each section's solid vertices (`pos` is `TerrainVertex`'s first field)
|
||||
/// with the shared quad indices.
|
||||
fn build_entries<'a>(
|
||||
builds: &'a [Build<'a>],
|
||||
arena: &'a Arena,
|
||||
unpacker: &'a Unpacker,
|
||||
indices: &'a wgpu::Buffer,
|
||||
) -> Vec<wgpu::BlasBuildEntry<'a>> {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
builds
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let (source, first) = b.vertices(arena, unpacker);
|
||||
wgpu::BlasBuildEntry {
|
||||
blas: &b.blas,
|
||||
geometry: wgpu::BlasGeometries::TriangleGeometries(vec![
|
||||
wgpu::BlasTriangleGeometry {
|
||||
size: &b.size,
|
||||
vertex_buffer: source,
|
||||
first_vertex: first as u32,
|
||||
vertex_stride: stride,
|
||||
index_buffer: Some(indices),
|
||||
first_index: Some(0),
|
||||
transform_buffer: None,
|
||||
transform_buffer_offset: None,
|
||||
},
|
||||
]),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where section `key` sits relative to the corner of chunk column `anchor`.
|
||||
fn instance_origin(key: SectionKey, min_y: i32, anchor: (i32, i32)) -> [f32; 3] {
|
||||
[
|
||||
((key.0 - anchor.0) * 16) as f32,
|
||||
(min_y + key.1 * 16) as f32,
|
||||
((key.2 - anchor.1) * 16) as f32,
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn instances_sit_relative_to_the_anchor_column() {
|
||||
// Section 5 of column (3, -2), the world starting at y = -64, seen from column (2, -2).
|
||||
assert_eq!(instance_origin((3, 5, -2), -64, (2, -2)), [16.0, 16.0, 0.0]);
|
||||
assert_eq!(instance_origin((2, 0, -2), -64, (2, -2)), [0.0, -64.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_unpack_shader_validates_with_terrain() {
|
||||
let source = unpack_shader();
|
||||
let module = naga::front::wgsl::parse_str(&source).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The off path is main's: a device opened without ray queries (every device without
|
||||
/// `HORIZON_RAY_TRACING`, and every plain build) gets no scene, and its buffers no usage.
|
||||
#[test]
|
||||
fn without_ray_queries_there_is_no_scene() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let Ok((device, _)) = pollster::block_on(adapter.request_device(&Default::default()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
assert!(!super::super::on(&device));
|
||||
assert!(RayScene::new(&device).is_none());
|
||||
assert!(super::super::blas_input(&device).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,6 @@ impl<T> Resident<T> {
|
||||
self.items.iter_mut().map(|(_, v)| v)
|
||||
}
|
||||
/// Every section's key, value and bounds.
|
||||
#[cfg(any(test, debug_assertions))]
|
||||
pub fn iter(&self) -> impl Iterator<Item = (SectionKey, &T, Option<[Vec3; 2]>)> {
|
||||
let items = self.items.iter().zip(&self.bounds);
|
||||
items.map(|((key, value), bounds)| (*key, value, *bounds))
|
||||
|
||||
@@ -60,7 +60,11 @@ pub(super) struct Stars {
|
||||
}
|
||||
|
||||
impl Stars {
|
||||
pub(super) fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
|
||||
/// The stars onto a frame of `format`, holding linear light when `linear`.
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
(format, linear): (wgpu::TextureFormat, bool),
|
||||
) -> Self {
|
||||
let triangles = star_triangles();
|
||||
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Stars vertex buffer"),
|
||||
@@ -135,10 +139,7 @@ impl Stars {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_stars"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
constants: &super::linear_blend::frame_constants(format, linear),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
|
||||
@@ -22,6 +22,6 @@ struct Stars {
|
||||
if HDR_WRITE_LIGHT && stars.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(stars.color.rgb * b, 0.175, stars.hdr_settings, stars.hdr_shares), b);
|
||||
}
|
||||
if HDR_WRITE_LIGHT { return vec4(stars.color.rgb * b, b); }
|
||||
if HDR_WRITE_LIGHT { return vec4(frame_stored(stars.color).rgb * b, b); }
|
||||
return vec4(stars.color.rgb * b, 0.175);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use super::{
|
||||
};
|
||||
// Ray tracing on request, which the device needs from its creation (gfx.rs may not grow).
|
||||
#[path = "ray_query.rs"]
|
||||
mod ray_query;
|
||||
pub(super) mod ray_query;
|
||||
use std::sync::Arc;
|
||||
use winit::window::Window;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Visbuffer {
|
||||
device: &wgpu::Device,
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
flat: &Flat,
|
||||
color: wgpu::TextureFormat,
|
||||
(color, linear): (wgpu::TextureFormat, bool),
|
||||
constants: &[(&str, f64)],
|
||||
) -> Self {
|
||||
let flat_layout = &flat.draw_layout;
|
||||
@@ -65,13 +65,8 @@ impl Visbuffer {
|
||||
let resolve_constants: Vec<(&str, f64)> = constants
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([
|
||||
("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(color == super::LINEAR_FRAME)),
|
||||
),
|
||||
])
|
||||
.chain([("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64)])
|
||||
.chain(super::linear_blend::frame_constants(color, linear))
|
||||
.collect();
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon visibility buffer"),
|
||||
|
||||
@@ -14,9 +14,10 @@ pub(super) struct Weather {
|
||||
count: u32,
|
||||
}
|
||||
impl Weather {
|
||||
/// The weather's pipeline onto a frame of `format`, holding linear light when `linear`.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
(format, linear): (wgpu::TextureFormat, bool),
|
||||
entities: &wgpu::BindGroupLayout,
|
||||
shader: &wgpu::ShaderModule,
|
||||
) -> Self {
|
||||
@@ -62,7 +63,10 @@ impl Weather {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_weather"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &super::linear_blend::frame_constants(format, linear),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
@@ -225,11 +229,11 @@ struct HorizonWeatherOut {
|
||||
";
|
||||
|
||||
/// The shader of a native pack's weather stage `source`.
|
||||
fn native_source(source: &str, api: horizon_shader_api::Api) -> String {
|
||||
fn native_source(source: &str, (api, linear): (horizon_shader_api::Api, bool)) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's weather (entity.wgsl)\n{}{}{NATIVE_ENTRIES}",
|
||||
super::wgsl_prefix::prefixed(&super::entity_shader(), "horizon_entity_"),
|
||||
super::native_post::line_helpers(api)
|
||||
super::native_post::line_helpers(api, linear)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,7 +250,7 @@ pub(super) fn native_pipeline(
|
||||
constants: &[(String, f64)],
|
||||
) -> Result<wgpu::RenderPipeline, String> {
|
||||
use horizon_shader_api::stages::GROUP;
|
||||
let text = native_source(&stage.source, frame.api);
|
||||
let text = native_source(&stage.source, (frame.api, frame.linear));
|
||||
let module = naga::front::wgsl::parse_str(&text)
|
||||
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
|
||||
let present = super::native_post::present_constants(&module, constants);
|
||||
@@ -327,7 +331,7 @@ mod tests {
|
||||
let source = StageKind::Weather.prelude(api)
|
||||
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
|
||||
return input.color * vec4(input.light, input.position.x, f32(input.snow));\n}\n";
|
||||
let text = native_source(&source, api);
|
||||
let text = native_source(&source, (api, false));
|
||||
let module = naga::front::wgsl::parse_str(&text)
|
||||
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
|
||||
naga::valid::Validator::new(
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
use super::Vertex;
|
||||
use glam::Mat4;
|
||||
|
||||
/// world_primitives.wgsl after what it shares with the frame's other shaders (`frame_stored`).
|
||||
pub(super) fn shader() -> String {
|
||||
include_str!("../hdr_light.wgsl").to_owned() + include_str!("world_primitives.wgsl")
|
||||
}
|
||||
|
||||
/// A frame's primitives, camera-relative, as the game tessellated them: quads of 4 vertices
|
||||
/// from the shared atlas (plain colours on its white texel, text on its glyphs, beams on the
|
||||
/// beacon beam sprite) and lines as `gizmos::camera_relative` gives them. `*_through` ones are
|
||||
@@ -57,11 +62,12 @@ pub struct WorldPrimitives {
|
||||
pub type Upload<'u> = &'u mut dyn FnMut(&wgpu::Buffer, &[u8]);
|
||||
|
||||
impl WorldPrimitives {
|
||||
/// Pipelines drawing into `color_format` over a reversed Depth32Float depth.
|
||||
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
|
||||
/// Pipelines drawing into `color_format` over a reversed Depth32Float depth, decoding their
|
||||
/// colours where the frame holds linear light (`linear`).
|
||||
pub fn new(device: &wgpu::Device, (color_format, linear): (wgpu::TextureFormat, bool)) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("World primitives"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("world_primitives.wgsl").into()),
|
||||
source: wgpu::ShaderSource::Wgsl(shader().into()),
|
||||
});
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("World primitives"),
|
||||
@@ -150,7 +156,10 @@ impl WorldPrimitives {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some(entry[1]),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &super::linear_blend::frame_constants(color_format, linear),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
@@ -468,7 +477,7 @@ mod tests {
|
||||
lines_through: &lines_through,
|
||||
};
|
||||
|
||||
let mut primitives = WorldPrimitives::new(&device, format);
|
||||
let mut primitives = WorldPrimitives::new(&device, (format, false));
|
||||
let atlas_view = atlas.create_view(&Default::default());
|
||||
assert!(primitives.prepare(
|
||||
&device,
|
||||
|
||||
@@ -41,7 +41,7 @@ struct QuadOut {
|
||||
if in.wrap.y > 0.0 { uv.y = in.wrap.x + fract(in.uv.y) * in.wrap.y; }
|
||||
let color = textureSample(atlas, atlas_sampler, uv) * in.color;
|
||||
if color.a <= 0.0 { discard; }
|
||||
return color;
|
||||
return frame_stored(color);
|
||||
}
|
||||
|
||||
struct LineOut {
|
||||
@@ -73,5 +73,5 @@ struct LineOut {
|
||||
}
|
||||
|
||||
@fragment fn fs_line(in: LineOut) -> @location(0) vec4<f32> {
|
||||
return in.color;
|
||||
return frame_stored(in.color);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ pub(super) fn pipeline(
|
||||
device: &wgpu::Device,
|
||||
layout: Option<&wgpu::PipelineLayout>,
|
||||
shader: &wgpu::ShaderModule,
|
||||
format: wgpu::TextureFormat,
|
||||
(format, linear): (wgpu::TextureFormat, bool),
|
||||
) -> wgpu::RenderPipeline {
|
||||
let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
@@ -34,7 +34,10 @@ pub(super) fn pipeline(
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_entity_unmarked"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &super::linear_blend::frame_constants(format, linear),
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
@@ -63,6 +66,8 @@ mod tests {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
r#"
|
||||
override HDR_WRITE_LIGHT: bool = false;
|
||||
override FRAME_LINEAR: bool = false;
|
||||
struct Out { @builtin(position) pos:vec4<f32>, @location(0) color:vec4<f32> }
|
||||
@vertex fn vs_entity(@location(0) pos:vec3<f32>, @location(3) color:vec4<f32>)->Out {
|
||||
return Out(vec4(pos,1.0),color);
|
||||
@@ -72,7 +77,12 @@ struct Out { @builtin(position) pos:vec4<f32>, @location(0) color:vec4<f32> }
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let pipeline = pipeline(&device, None, &shader, wgpu::TextureFormat::Rgba8Unorm);
|
||||
let pipeline = pipeline(
|
||||
&device,
|
||||
None,
|
||||
&shader,
|
||||
(wgpu::TextureFormat::Rgba8Unorm, false),
|
||||
);
|
||||
let texture = |format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
|
||||
@@ -148,6 +148,8 @@ pub(super) struct Kit {
|
||||
pub entity: wgpu::BindGroupLayout,
|
||||
pub static_entity: wgpu::BindGroupLayout,
|
||||
pub format: wgpu::TextureFormat,
|
||||
/// The frame holds linear light (`linear_blend`).
|
||||
pub linear: bool,
|
||||
pub sprite_constants: [(&'static str, f64); 2],
|
||||
/// Whether the display offers HDR output: the solid terrain is built lit by it too.
|
||||
pub hdr_output: bool,
|
||||
@@ -319,20 +321,19 @@ impl WorldWarps {
|
||||
let buffered = [Some(vertices), Some(instances.clone())];
|
||||
let pulled = [None, Some(instances)];
|
||||
// As gfx.rs's: a float frame takes light where others take marks.
|
||||
let write_light = (
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(kit.format == super::LINEAR_FRAME)),
|
||||
);
|
||||
let [write_light, linear] = super::linear_blend::frame_constants(kit.format, kit.linear);
|
||||
let lit = [
|
||||
kit.sprite_constants[0],
|
||||
kit.sprite_constants[1],
|
||||
write_light,
|
||||
linear,
|
||||
];
|
||||
let plain = [
|
||||
kit.sprite_constants[0],
|
||||
kit.sprite_constants[1],
|
||||
("HDR_LIGHTING", 0.0),
|
||||
write_light,
|
||||
linear,
|
||||
];
|
||||
let depth = |write| wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
@@ -454,7 +455,7 @@ impl WorldWarps {
|
||||
module: &entity,
|
||||
entry_point: Some(look.fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[write_light],
|
||||
constants: &[write_light, linear],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
|
||||
@@ -66,5 +66,5 @@ fn warp_fragment(in: TerrainOut, front: bool) -> Fragment {
|
||||
let mark = hdr_scene_alpha(mix(hdr_scene(in.scene.x, in.scene.y, hdr_facing()), globals.hdr_light.y, fog));
|
||||
return vec4(hdr_written(finished.rgb, mark, globals.hdr_settings, globals.hdr_shares), finished.a);
|
||||
}
|
||||
return finished;
|
||||
return frame_stored(finished);
|
||||
}
|
||||
@@ -824,6 +824,11 @@ pub struct Graphics {
|
||||
pub overlay: Overlay,
|
||||
/// World resolution as a percentage of the window's (50..=200).
|
||||
pub resolution_scale: u32,
|
||||
/// Dynamic Resolution: below 100%, `resolution_scale` is the floor, and the scale rises
|
||||
/// toward 100% while the GPU keeps `target_fps` (`crate::dynamic_resolution`). Off by default.
|
||||
pub dynamic_resolution: bool,
|
||||
/// The frame rate Dynamic Resolution keeps (30..=240).
|
||||
pub target_fps: u32,
|
||||
/// FXAA over the world.
|
||||
pub fxaa: bool,
|
||||
/// TAA over the world (FXAA is off while it is on).
|
||||
@@ -920,6 +925,8 @@ impl Default for Graphics {
|
||||
workers: 0,
|
||||
overlay: Overlay::Off,
|
||||
resolution_scale: 100,
|
||||
dynamic_resolution: false,
|
||||
target_fps: 60,
|
||||
fxaa: false,
|
||||
taa: false,
|
||||
upscaling: Upscaling::Native,
|
||||
@@ -1081,6 +1088,8 @@ impl Graphics {
|
||||
}
|
||||
"workers" => g.workers = number(g.workers).min(64),
|
||||
"resolutionScale" => g.resolution_scale = number(g.resolution_scale).clamp(50, 200),
|
||||
"dynamicResolution" => g.dynamic_resolution = flag(g.dynamic_resolution),
|
||||
"targetFps" => g.target_fps = number(g.target_fps).clamp(30, 240),
|
||||
"fxaa" => g.fxaa = flag(g.fxaa),
|
||||
"taa" => g.taa = flag(g.taa),
|
||||
"upscaling" => g.upscaling = Upscaling::parse(value),
|
||||
@@ -1135,7 +1144,7 @@ impl Graphics {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
let mut text = format!(
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nentityCullingPrecheck:{}\nparticleCulling:{}\nleafCulling:{}\nopaqueIce:{}\nblockEntityCacheShulkerBoxes:{}\nblockEntityCacheSigns:{}\nblockEntityCacheDecoratedPots:{}\nblockEntityCacheBells:{}\nblockEntityCacheChests:{}\nblockEntityCacheBanners:{}\nblockEntityCacheCopperGolemStatues:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\ntextureBudgetMiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\nditherCutoutMips:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\ntaa:{}\nupscaling:{}\nupscalingSharpness:{}\nscreenshotQuality:{}\nframeGeneration:{}\nhdr:{}\nhdrPaperWhite:{}\nhdrPeak:{}\nhdrBlackLevel:{}\nhdrUiNits:{}\nhdrLighting:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\nhdrSmoothGradients:{}\ngpuCulling:{}\n",
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nentityCullingPrecheck:{}\nparticleCulling:{}\nleafCulling:{}\nopaqueIce:{}\nblockEntityCacheShulkerBoxes:{}\nblockEntityCacheSigns:{}\nblockEntityCacheDecoratedPots:{}\nblockEntityCacheBells:{}\nblockEntityCacheChests:{}\nblockEntityCacheBanners:{}\nblockEntityCacheCopperGolemStatues:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\ntextureBudgetMiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\nditherCutoutMips:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\ndynamicResolution:{}\ntargetFps:{}\nfxaa:{}\ntaa:{}\nupscaling:{}\nupscalingSharpness:{}\nscreenshotQuality:{}\nframeGeneration:{}\nhdr:{}\nhdrPaperWhite:{}\nhdrPeak:{}\nhdrBlackLevel:{}\nhdrUiNits:{}\nhdrLighting:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\nhdrSmoothGradients:{}\ngpuCulling:{}\n",
|
||||
self.preset.name(),
|
||||
self.lod_distance,
|
||||
self.far_detail.name(),
|
||||
@@ -1172,6 +1181,8 @@ impl Graphics {
|
||||
Overlay::Graph => "graph",
|
||||
},
|
||||
self.resolution_scale,
|
||||
bool_text(self.dynamic_resolution),
|
||||
self.target_fps,
|
||||
bool_text(self.fxaa),
|
||||
bool_text(self.taa),
|
||||
self.upscaling.name(),
|
||||
@@ -1537,6 +1548,8 @@ mod tests {
|
||||
workers: 6,
|
||||
overlay: Overlay::Graph,
|
||||
resolution_scale: 75,
|
||||
dynamic_resolution: false,
|
||||
target_fps: 60,
|
||||
fxaa: true,
|
||||
taa: true,
|
||||
upscaling: Upscaling::Balanced,
|
||||
|
||||
@@ -100,6 +100,8 @@ pub enum H {
|
||||
Workers,
|
||||
Overlay,
|
||||
ResolutionScale,
|
||||
DynamicResolution,
|
||||
TargetFps,
|
||||
Antialiasing,
|
||||
Upscaling,
|
||||
UpscalingSharpness,
|
||||
@@ -366,6 +368,16 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
|
||||
High,
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.",
|
||||
),
|
||||
h(
|
||||
H::DynamicResolution,
|
||||
Medium,
|
||||
"Raises Resolution Scale toward 100% while the frame rate allows it.",
|
||||
),
|
||||
h(
|
||||
H::TargetFps,
|
||||
Low,
|
||||
"The frame rate Dynamic Resolution keeps.",
|
||||
),
|
||||
h(
|
||||
H::Antialiasing,
|
||||
Medium,
|
||||
@@ -795,6 +807,7 @@ impl H {
|
||||
}
|
||||
H::Workers => Some(g.workers as f32 / max_workers() as f32),
|
||||
H::ResolutionScale => Some((g.resolution_scale as f32 - 50.0) / 150.0),
|
||||
H::TargetFps => Some((g.target_fps as f32 - 30.0) / 210.0),
|
||||
H::UpscalingSharpness => Some(g.upscaling_sharpness as f32 / 100.0),
|
||||
// Auto, then 10..480 by 10.
|
||||
H::HdrPaperWhite => Some(crate::graphics::nits_position(g.hdr_paper_white)),
|
||||
@@ -815,6 +828,7 @@ impl H {
|
||||
H::UploadBudget => Some((UPLOAD_STEPS.len() - 1) as u32),
|
||||
H::Workers => Some(max_workers()),
|
||||
H::ResolutionScale => Some(30),
|
||||
H::TargetFps => Some(42),
|
||||
H::UpscalingSharpness => Some(20),
|
||||
H::HdrPaperWhite | H::HdrUiBrightness => Some(crate::graphics::NITS_STEPS),
|
||||
H::HdrPeak => Some(PEAK_POSITIONS),
|
||||
@@ -914,6 +928,8 @@ impl H {
|
||||
n => labelled("Worker Threads", n),
|
||||
},
|
||||
H::ResolutionScale => labelled_percent("Resolution Scale", g.resolution_scale),
|
||||
H::DynamicResolution => labelled("Dynamic Resolution", on_off(g.dynamic_resolution)),
|
||||
H::TargetFps => labelled("Target FPS", g.target_fps),
|
||||
H::Antialiasing => labelled(
|
||||
"Anti-aliasing",
|
||||
match (g.fxaa, g.taa) {
|
||||
@@ -978,6 +994,7 @@ impl H {
|
||||
}
|
||||
H::Workers => g.workers = (v * max_workers() as f32).round() as u32,
|
||||
H::ResolutionScale => g.resolution_scale = (50.0 + (v * 30.0).round() * 5.0) as u32,
|
||||
H::TargetFps => g.target_fps = (30.0 + (v * 42.0).round() * 5.0) as u32,
|
||||
H::UpscalingSharpness => g.upscaling_sharpness = (v * 20.0).round() as u32 * 5,
|
||||
H::HdrPaperWhite => g.hdr_paper_white = crate::graphics::nits_at(v),
|
||||
H::HdrUiBrightness => g.hdr_ui_nits = crate::graphics::nits_at(v),
|
||||
@@ -1037,6 +1054,7 @@ impl H {
|
||||
H::Fog => g.fog = !g.fog,
|
||||
H::Weather => g.weather = !g.weather,
|
||||
H::AnimatedTextures => g.animated_textures = !g.animated_textures,
|
||||
H::DynamicResolution => g.dynamic_resolution = !g.dynamic_resolution,
|
||||
H::DitherCutoutMips => g.dither_cutout_mips = !g.dither_cutout_mips,
|
||||
H::PresentMode => g.present_mode = step(&PresentMode::ALL, g.present_mode, back),
|
||||
// Off, FXAA, TAA.
|
||||
@@ -1187,6 +1205,7 @@ impl Item {
|
||||
Item::Horizon(H::Fog) => g.fog,
|
||||
Item::Horizon(H::Weather) => g.weather,
|
||||
Item::Horizon(H::AnimatedTextures) => g.animated_textures,
|
||||
Item::Horizon(H::DynamicResolution) => g.dynamic_resolution,
|
||||
Item::Horizon(H::DitherCutoutMips) => g.dither_cutout_mips,
|
||||
Item::Horizon(H::HdrLighting) => g.hdr_lighting,
|
||||
Item::Horizon(H::BatteryMode) => g.battery,
|
||||
@@ -2704,6 +2723,7 @@ mod tests {
|
||||
H::UploadBudget,
|
||||
H::Workers,
|
||||
H::ResolutionScale,
|
||||
H::TargetFps,
|
||||
H::UpscalingSharpness,
|
||||
H::HdrPaperWhite,
|
||||
H::HdrPeak,
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
override HDR_WRITE_LIGHT: bool = false;
|
||||
// The frame holds linear light: a shader API 0.2 pack's `[frame] blend = "linear"`.
|
||||
override FRAME_LINEAR: bool = false;
|
||||
|
||||
// What the frame stores of `c`, a colour in vanilla's gamma: `c`, or its light (vanilla's 2.2
|
||||
// power) where the frame holds linear light, so what blends over it mixes as light does.
|
||||
fn frame_stored(c: vec4<f32>) -> vec4<f32> {
|
||||
if FRAME_LINEAR {
|
||||
return vec4(pow(max(c.rgb, vec3(0.0)), vec3(2.2)), c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
// `frame_stored` undone: the stored colour `c` in vanilla's gamma.
|
||||
fn frame_gamma(c: vec4<f32>) -> vec4<f32> {
|
||||
if FRAME_LINEAR {
|
||||
return vec4(pow(max(c.rgb, vec3(0.0)), vec3(1.0 / 2.2)), c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// The light of colour `f` under the world's mark `a` (the alpha the world left, hdr_mark) where
|
||||
// the world `shows`, else the colour clipped at white; and whether a light source lifted it.
|
||||
|
||||
@@ -102,9 +102,7 @@ pub struct HudAtlas {
|
||||
pub glint: (Vec<u8>, u32, u32),
|
||||
/// `misc/enchanted_glint_armor`, the armour layers' `GlintSampler`.
|
||||
pub glint_armor: (Vec<u8>, u32, u32),
|
||||
/// A resource pack's own `core/text` shader for the GUI's text, translated;
|
||||
/// none draws the text natively.
|
||||
pub text_shader: Option<std::sync::Arc<crate::core_shader::CoreProgram>>,
|
||||
pub text_shaders: crate::gfx::core_text::TextShaders,
|
||||
/// A resource pack's own `core/rendertype_lines` shader for the block outline, translated;
|
||||
/// none draws the outline natively.
|
||||
pub lines_shader: Option<std::sync::Arc<crate::core_shader::CoreProgram>>,
|
||||
@@ -981,7 +979,7 @@ impl HudAtlas {
|
||||
font: crate::font::Font::metrics(root),
|
||||
glint: (Vec::new(), 1, 1),
|
||||
glint_armor: (Vec::new(), 1, 1),
|
||||
text_shader: None,
|
||||
text_shaders: Default::default(),
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
@@ -1050,7 +1048,7 @@ impl HudAtlas {
|
||||
glint: (vec![0; 4], 1, 1),
|
||||
glint_armor: (vec![0; 4], 1, 1),
|
||||
white,
|
||||
text_shader: None,
|
||||
text_shaders: Default::default(),
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
@@ -1581,7 +1579,7 @@ impl HudAtlas {
|
||||
sprite_animations,
|
||||
glint,
|
||||
glint_armor,
|
||||
text_shader: None,
|
||||
text_shaders: Default::default(),
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
|
||||
@@ -396,6 +396,7 @@ fn coverage_uses_pack_texts_final_alpha_and_discard() {
|
||||
let program = crate::core_shader::CoreProgram {
|
||||
vertex: naga::front::wgsl::parse_str("@vertex fn main(@location(0) p: vec2<f32>) -> @builtin(position) vec4<f32> { return vec4(p.x / 2.0 - 1.0, 1.0 - p.y, 0.0, 1.0); }").unwrap(),
|
||||
fragment: naga::front::wgsl::parse_str("struct Result { @location(0) color: vec4<f32> } @fragment fn main(@builtin(position) p: vec4<f32>) -> Result { if p.x > 2.0 { discard; } return Result(vec4(0.4, 0.2, 0.1, 0.5)); }").unwrap(),
|
||||
other: Vec::new(),
|
||||
};
|
||||
gpu.hud.set_text_shader(&gpu.device, Some(&program));
|
||||
assert!(gpu.hud.pack_text.is_some());
|
||||
@@ -417,6 +418,7 @@ fn a_pack_text_shader_without_a_colour_output_is_dropped_not_wrapped() {
|
||||
let program = crate::core_shader::CoreProgram {
|
||||
vertex: naga::front::wgsl::parse_str("@vertex fn main(@location(0) p: vec2<f32>) -> @builtin(position) vec4<f32> { return vec4(p, 0.0, 1.0); }").unwrap(),
|
||||
fragment: naga::front::wgsl::parse_str("@fragment fn main() -> @location(1) vec4<f32> { return vec4(1.0); }").unwrap(),
|
||||
other: Vec::new(),
|
||||
};
|
||||
gpu.hud.set_text_shader(&gpu.device, Some(&program));
|
||||
assert!(gpu.hud.pack_text.is_none());
|
||||
|
||||
@@ -76,9 +76,8 @@ pub(super) fn load_core(
|
||||
}
|
||||
}
|
||||
|
||||
/// The packs' core shaders the atlas carries (text, block outline, sky, stars).
|
||||
pub(super) fn load_shaders(atlas: &mut super::HudAtlas, root: &Path, defaults: &Path) {
|
||||
atlas.text_shader = load(root, defaults);
|
||||
atlas.text_shaders.gui = load(root, defaults);
|
||||
atlas.lines_shader = load_core(
|
||||
root,
|
||||
defaults,
|
||||
@@ -89,6 +88,12 @@ pub(super) fn load_shaders(atlas: &mut super::HudAtlas, root: &Path, defaults: &
|
||||
let sky = |name| load_core(root, defaults, name, &[], &crate::gfx::core_sky::GLUE);
|
||||
atlas.sky_shader = sky("sky");
|
||||
atlas.stars_shader = sky("stars");
|
||||
let world = |defines: &[&str], glue| load_core(root, defaults, "text", defines, glue);
|
||||
atlas.text_shaders.world = world(&[], &crate::gfx::core_text::WORLD_GLUE);
|
||||
atlas.text_shaders.see_through = world(
|
||||
&["IS_SEE_THROUGH"],
|
||||
&crate::gfx::core_text::SEE_THROUGH_GLUE,
|
||||
);
|
||||
}
|
||||
|
||||
/// Each uniform block's offset in the one buffer (`core_shader::BLOCKS` order).
|
||||
|
||||
@@ -43,6 +43,7 @@ pub mod crash;
|
||||
pub mod custom_sky;
|
||||
pub mod datapack;
|
||||
pub mod dynamic_light;
|
||||
pub mod dynamic_resolution;
|
||||
pub mod effects;
|
||||
pub mod environment;
|
||||
pub mod extras;
|
||||
|
||||
@@ -21,9 +21,7 @@ use crate::{
|
||||
models::catalog::BakedFace,
|
||||
};
|
||||
use horizon_assets::model::Direction as ModelDirection;
|
||||
use horizon_core::{
|
||||
biome::BiomeZoom, geometry::Direction, position::BlockPos, random::position_seed,
|
||||
};
|
||||
use horizon_core::{biome::BiomeZoom, geometry::Direction, position::BlockPos};
|
||||
use horizon_optifine::ctm::connect::{Axes as CtmAxes, Face as CtmFace};
|
||||
use horizon_world::{
|
||||
block_render::FaceVisibility,
|
||||
@@ -927,19 +925,7 @@ impl MeshTables {
|
||||
|| (matches!(name, "sculk_sensor" | "calibrated_sculk_sensor")
|
||||
&& props.get("sculk_sensor_phase").map(String::as_str) == Some("active"));
|
||||
// OffsetType.XZ/XYZ with getMaxHorizontalOffset/getMaxVerticalOffset.
|
||||
let offset = s.random_offset.then(|| {
|
||||
let horizontal = if s.has_class("SpeleothemBlock") {
|
||||
0.125
|
||||
} else {
|
||||
0.25
|
||||
};
|
||||
let vertical = match name {
|
||||
"small_dripleaf" => 0.1,
|
||||
"short_grass" | "fern" | "short_dry_grass" | "tall_dry_grass" => 0.2,
|
||||
_ => 0.0,
|
||||
};
|
||||
(horizontal, vertical)
|
||||
});
|
||||
let offset = s.max_offset();
|
||||
let mut face = [0u8; 6];
|
||||
let mut cover = [0f32; 6];
|
||||
// A server's visibility knows no state (`FaceVisibility::empty`):
|
||||
@@ -1223,15 +1209,7 @@ impl MeshTables {
|
||||
}
|
||||
/// `OffsetType.XZ`/`XYZ`: a per-column jitter from `Mth.getSeed(x, 0, z)`.
|
||||
fn offset(info: &Info, x: i32, z: i32) -> Option<[f64; 3]> {
|
||||
let (horizontal, vertical) = info.offset?;
|
||||
let seed = position_seed(BlockPos::new(x, 0, z));
|
||||
let nibble = |shift: u32| ((seed >> shift) & 15) as f32 / 15.0;
|
||||
let h = f64::from(horizontal);
|
||||
Some([
|
||||
((f64::from(nibble(0)) - 0.5) * 0.5).clamp(-h, h),
|
||||
(f64::from(nibble(4)) - 1.0) * f64::from(vertical),
|
||||
((f64::from(nibble(8)) - 0.5) * 0.5).clamp(-h, h),
|
||||
])
|
||||
Some(horizon_items::blocks::column_offset(info.offset?, x, z))
|
||||
}
|
||||
/// Border width: smooth lighting samples up to two blocks out along a face normal.
|
||||
const B: i32 = 2;
|
||||
@@ -2281,22 +2259,14 @@ impl Mesher<'_> {
|
||||
translucent: face.translucent,
|
||||
opaque: face.opaque,
|
||||
};
|
||||
// A tile keeps the quad's forced translucency; `<skip>` and `<default>` are its own.
|
||||
let tile = |t: TileLayer| match t {
|
||||
TileLayer::Layer {
|
||||
layer,
|
||||
translucent,
|
||||
opaque,
|
||||
} => TileLayer::Layer {
|
||||
layer,
|
||||
translucent: translucent || q.render_type.is_none() && q.material.force_translucent,
|
||||
opaque,
|
||||
},
|
||||
TileLayer::Skip | TileLayer::Default => own,
|
||||
};
|
||||
let own_translucent = self.translucent(own);
|
||||
let tile = |t: TileLayer| in_face_pass(t, own, own_translucent);
|
||||
match pick {
|
||||
Pick::Keep => self.quad(out, force_solid, own, corners.map(|c| c.0), tint),
|
||||
Pick::Tile(t) => self.quad(out, force_solid, tile(t), corners.map(|c| c.0), tint),
|
||||
Pick::Tile(t) => {
|
||||
let v = corners.map(|c| c.0);
|
||||
self.quad_as(out, force_solid, tile(t), v, false, tint);
|
||||
}
|
||||
Pick::Quarters(tiles) => {
|
||||
let cuts = cuts(&corners);
|
||||
for s in cuts.windows(2) {
|
||||
@@ -2305,7 +2275,7 @@ impl Mesher<'_> {
|
||||
.map(|(s, t)| bilinear(&corners, s, t));
|
||||
let quarter = quarter_of(&piece);
|
||||
let piece = piece.map(|c| c.0);
|
||||
self.quad(out, force_solid, tile(tiles[quarter]), piece, tint);
|
||||
self.quad_as(out, force_solid, tile(tiles[quarter]), piece, false, tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2318,12 +2288,7 @@ impl Mesher<'_> {
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let face_translucent = match pick {
|
||||
Pick::Keep => self.translucent(own),
|
||||
Pick::Tile(t) => self.translucent(tile(t)),
|
||||
Pick::Quarters(tiles) => tiles.into_iter().any(|t| self.translucent(tile(t))),
|
||||
Pick::Nothing => false,
|
||||
};
|
||||
let face_translucent = !matches!(pick, Pick::Nothing) && own_translucent;
|
||||
// Overlay tiles face the standard way too (Continuity: "overlay textures always have the
|
||||
// standard orientation"), whatever turns the model's own UVs (dirt's and sand's random
|
||||
// rotations).
|
||||
@@ -2472,6 +2437,21 @@ fn cuts(c: &[(TerrainVertex, [f32; 2]); 4]) -> Cuts {
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Tile `t` as drawn over a face whose own sprite is `own`, in the face's pass
|
||||
/// (`own_translucent`): CTM picks a texture, not a render layer (OptiFine draws a tile in the
|
||||
/// block's). The tile's texels only drop the alpha test when all are opaque. A Conquest grass
|
||||
/// blade tile with 7 partly clear texels of 1024 blended, and wrote the depth of its clear
|
||||
/// rectangle, which Bliss fogs. `<skip>` and `<default>` are the face's own sprite.
|
||||
fn in_face_pass(t: TileLayer, own: TileLayer, own_translucent: bool) -> TileLayer {
|
||||
match t {
|
||||
TileLayer::Layer { layer, opaque, .. } => TileLayer::Layer {
|
||||
layer,
|
||||
translucent: own_translucent,
|
||||
opaque,
|
||||
},
|
||||
TileLayer::Skip | TileLayer::Default => own,
|
||||
}
|
||||
}
|
||||
/// The UV a vanilla cube's face gives the block-local point `p` (`BlockElement`'s default UVs),
|
||||
/// in sprite units: up is north on the top face, south on the bottom one, and up on the sides.
|
||||
fn standard_uv(facing: horizon_assets::model::Direction, [x, y, z]: [f32; 3]) -> [f32; 2] {
|
||||
@@ -2596,6 +2576,38 @@ mod quarter_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_draws_in_its_faces_pass() {
|
||||
// Conquest's grass blade tile: a few partly clear texels, over an opaque face.
|
||||
let blade = TileLayer::Layer {
|
||||
layer: 5,
|
||||
translucent: true,
|
||||
opaque: false,
|
||||
};
|
||||
let face = |translucent| TileLayer::Layer {
|
||||
layer: 1,
|
||||
translucent,
|
||||
opaque: !translucent,
|
||||
};
|
||||
let cut = TileLayer::Layer {
|
||||
layer: 5,
|
||||
translucent: false,
|
||||
opaque: false,
|
||||
};
|
||||
assert_eq!(in_face_pass(blade, face(false), false), cut);
|
||||
// Over stained glass the tile blends, as the face does.
|
||||
let blend = TileLayer::Layer {
|
||||
layer: 5,
|
||||
translucent: true,
|
||||
opaque: false,
|
||||
};
|
||||
assert_eq!(in_face_pass(blade, face(true), true), blend);
|
||||
assert_eq!(
|
||||
in_face_pass(TileLayer::Default, face(false), false),
|
||||
face(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_take_the_cubes_own_uvs() {
|
||||
use horizon_assets::model::Direction as D;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! Controls, Key Binds, Mouse Settings, Chat Settings, Accessibility,
|
||||
//! Language and Resource Packs, laid out like vanilla's HeaderAndFooter /
|
||||
//! OptionsList screens and drawn with the shared widgets.
|
||||
mod pack_drop;
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
pub use crate::settings::Opt;
|
||||
@@ -1415,6 +1417,8 @@ fn matches_focus(id: &FocusId, i: usize, wd: &Widget) -> bool {
|
||||
/// What the game must do after an interaction.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Event {
|
||||
/// Dropped resource packs accepted on the confirmation screen.
|
||||
ImportPacks(Vec<std::path::PathBuf>),
|
||||
/// The Pack Options page closed with changes: the player's choices of its entries, the
|
||||
/// ones that differ from their defaults (the others went back to theirs).
|
||||
PackOptions {
|
||||
@@ -1967,23 +1971,6 @@ impl OptionsUi {
|
||||
Page::Video | Page::Controls | Page::KeyBinds | Page::ModBrowser
|
||||
)
|
||||
}
|
||||
/// Whether the resource pack screen is open (not Create World's data packs on the same
|
||||
/// page).
|
||||
pub fn resource_packs_open(&self) -> bool {
|
||||
self.page() == Page::Packs && self.data_pack_dir.is_none()
|
||||
}
|
||||
/// The packs in `resourcepacks/` changed while the pack screen is open
|
||||
/// (`PackSelectionScreen`'s directory watcher): the screen is made again over
|
||||
/// `infos`, keeping the selection made on it so far.
|
||||
pub fn update_pack_infos(&mut self, infos: Vec<crate::pack_screen::PackInfo>) {
|
||||
if let Some(p) = self.pack_screen.take()
|
||||
&& p.changed
|
||||
{
|
||||
self.pack_selection = p.commit();
|
||||
self.pack_reselected = true;
|
||||
}
|
||||
self.pack_infos = infos;
|
||||
}
|
||||
pub fn page(&self) -> Page {
|
||||
*self.stack.last().unwrap_or(&Page::Main)
|
||||
}
|
||||
@@ -3833,6 +3820,7 @@ impl OptionsUi {
|
||||
self.pack_focus = Instant::now();
|
||||
}
|
||||
match action {
|
||||
crate::pack_screen::Action::Import(paths) => events.push(Event::ImportPacks(paths)),
|
||||
crate::pack_screen::Action::OpenFolder => events.push(match &self.data_pack_dir {
|
||||
Some(dir) => Event::OpenDataPackFolder(dir.clone()),
|
||||
None => Event::OpenPackFolder,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Resource-pack drop confirmation and selection-preserving list refresh.
|
||||
use super::*;
|
||||
|
||||
impl OptionsUi {
|
||||
/// Whether the resource pack screen is open (not Create World's data packs on the same
|
||||
/// page).
|
||||
pub fn resource_packs_open(&self) -> bool {
|
||||
self.page() == Page::Packs && self.data_pack_dir.is_none()
|
||||
}
|
||||
/// Returns whether the Resource Packs screen owns this drop; data packs keep their route.
|
||||
pub fn drop_resource_pack(
|
||||
&mut self,
|
||||
path: std::path::PathBuf,
|
||||
atlas: &HudAtlas,
|
||||
size: (f32, f32),
|
||||
) -> bool {
|
||||
if !self.resource_packs_open() {
|
||||
return false;
|
||||
}
|
||||
if self.resource_error.is_none() {
|
||||
self.packs(atlas, size).drop_file(path);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// The packs in `resourcepacks/` changed while the pack screen is open
|
||||
/// (`PackSelectionScreen`'s directory watcher): the screen is made again over
|
||||
/// `infos`, keeping the selection made on it so far.
|
||||
pub fn update_pack_infos(&mut self, infos: Vec<crate::pack_screen::PackInfo>) {
|
||||
if let Some(p) = self.pack_screen.take()
|
||||
&& p.changed
|
||||
{
|
||||
self.pack_selection = p.commit();
|
||||
self.pack_reselected = true;
|
||||
}
|
||||
self.pack_infos = infos;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_resource_packs_receive_drops_and_yes_emits_the_import_request() {
|
||||
let atlas =
|
||||
HudAtlas::build(std::path::Path::new("/nonexistent/horizon-test-assets")).unwrap();
|
||||
let size = (480.0, 270.0);
|
||||
let mut ui = OptionsUi::default();
|
||||
let mut settings = Settings::default();
|
||||
assert!(!ui.drop_resource_pack("Ignored.zip".into(), &atlas, size));
|
||||
ui.open_at(false, Page::Packs);
|
||||
assert!(ui.drop_resource_pack("Pack.zip".into(), &atlas, size));
|
||||
// The ConfirmScreen's Yes button, centred at y = height/2 + 15.
|
||||
let events = ui.click(
|
||||
&mut settings,
|
||||
&atlas,
|
||||
90.0,
|
||||
156.0,
|
||||
size,
|
||||
[480, 270],
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert!(events.iter().any(|event| matches!(event, Event::ImportPacks(paths) if paths == &[std::path::PathBuf::from("Pack.zip")])));
|
||||
ui.data_pack_dir = Some("datapacks".into());
|
||||
assert!(!ui.drop_resource_pack("Data.zip".into(), &atlas, size));
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ impl OptionsUi {
|
||||
added::Row::Text { text, .. } => {
|
||||
out.extend(wrap(&text, 64).into_iter().map(Row::Note))
|
||||
}
|
||||
added::Row::Space(_) => continue,
|
||||
added::Row::Space(_) | added::Row::Field { .. } => continue,
|
||||
added::Row::Button {
|
||||
label,
|
||||
active,
|
||||
|
||||
@@ -304,10 +304,16 @@ enum Focus {
|
||||
Button(usize),
|
||||
}
|
||||
|
||||
/// The open `ConfirmScreen` for an incompatible pack.
|
||||
#[derive(Clone, Debug)]
|
||||
enum ConfirmAction {
|
||||
Select(String),
|
||||
Import(Vec<std::path::PathBuf>),
|
||||
}
|
||||
|
||||
/// The open `ConfirmScreen` for an incompatible pack or dropped files.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Confirm {
|
||||
id: String,
|
||||
action: ConfirmAction,
|
||||
title: Text,
|
||||
message: Text,
|
||||
}
|
||||
@@ -316,6 +322,8 @@ struct Confirm {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
None,
|
||||
/// The player accepted `pack.dropConfirm`.
|
||||
Import(Vec<std::path::PathBuf>),
|
||||
/// The Open Pack Folder button.
|
||||
OpenFolder,
|
||||
/// The Done button (`onClose`: commit).
|
||||
@@ -795,11 +803,45 @@ impl PackScreen {
|
||||
l.focused = false;
|
||||
}
|
||||
self.confirm = Some(Confirm {
|
||||
id: id.to_owned(),
|
||||
action: ConfirmAction::Select(id.to_owned()),
|
||||
title: tr("pack.incompatible.confirm.title"),
|
||||
message: tr(&format!("pack.incompatible.confirm.{}", compat.key())),
|
||||
});
|
||||
}
|
||||
/// Winit delivers one event per path; collect a multi-file drop in one confirmation.
|
||||
pub fn drop_file(&mut self, path: std::path::PathBuf) {
|
||||
if matches!(
|
||||
self.confirm.as_ref().map(|c| &c.action),
|
||||
Some(ConfirmAction::Select(_))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut paths = match self.confirm.take() {
|
||||
Some(Confirm {
|
||||
action: ConfirmAction::Import(paths),
|
||||
..
|
||||
}) => paths,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
let names = paths
|
||||
.iter()
|
||||
.filter_map(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
self.set_focus(Focus::None);
|
||||
for list in &mut self.lists {
|
||||
list.focused = false;
|
||||
}
|
||||
self.confirm = Some(Confirm {
|
||||
action: ConfirmAction::Import(paths),
|
||||
title: tr("pack.dropConfirm"),
|
||||
message: Text::literal(names),
|
||||
});
|
||||
}
|
||||
/// The ConfirmScreen's layout: title, message lines and the two buttons.
|
||||
fn confirm_layout(
|
||||
&self,
|
||||
@@ -834,12 +876,17 @@ impl PackScreen {
|
||||
// The callback: back to the pack screen (repositioned), then select.
|
||||
self.confirm = None;
|
||||
self.reposition(m);
|
||||
if i == 0
|
||||
&& let Some(index) = self.model_index(0, &c.id)
|
||||
{
|
||||
self.model.activate(false, index, true);
|
||||
self.changed = true;
|
||||
self.populate(Some(&c.id));
|
||||
if i == 0 {
|
||||
match c.action {
|
||||
ConfirmAction::Import(paths) => return Action::Import(paths),
|
||||
ConfirmAction::Select(id) => {
|
||||
if let Some(index) = self.model_index(0, &id) {
|
||||
self.model.activate(false, index, true);
|
||||
self.changed = true;
|
||||
self.populate(Some(&id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Action::None;
|
||||
}
|
||||
@@ -1750,6 +1797,58 @@ impl PackScreen {
|
||||
mod icon_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dropped_files_require_yes_and_keep_the_pending_selection() {
|
||||
let atlas =
|
||||
crate::hud::HudAtlas::build(std::path::Path::new("/nonexistent/horizon-test-assets"))
|
||||
.unwrap();
|
||||
let m = Measure(&atlas.font);
|
||||
let mut screen = PackScreen::new(
|
||||
&m,
|
||||
tr("resourcePack.title"),
|
||||
480,
|
||||
270,
|
||||
false,
|
||||
vec![pack("file/Keep", None)],
|
||||
&[],
|
||||
);
|
||||
screen.model.activate(false, 0, true);
|
||||
screen.changed = true;
|
||||
let before = screen.commit();
|
||||
assert_eq!(before, ["file/Keep"]);
|
||||
screen.drop_file("One.zip".into());
|
||||
screen.drop_file("Two".into());
|
||||
screen.drop_file("One.zip".into());
|
||||
let no = screen
|
||||
.confirm_layout(&m, screen.confirm.as_ref().unwrap())
|
||||
.2[1];
|
||||
assert_eq!(
|
||||
screen.click(&m, f64::from(no.0 + 1), f64::from(no.1 + 1)),
|
||||
Action::None
|
||||
);
|
||||
assert!(!screen.confirming());
|
||||
assert_eq!(screen.commit(), before);
|
||||
screen.drop_file("One.zip".into());
|
||||
screen.drop_file("Two".into());
|
||||
screen.drop_file("One.zip".into());
|
||||
let yes = screen
|
||||
.confirm_layout(&m, screen.confirm.as_ref().unwrap())
|
||||
.2[0];
|
||||
assert_eq!(
|
||||
screen.click(&m, f64::from(yes.0 + 1), f64::from(yes.1 + 1)),
|
||||
Action::Import(vec!["One.zip".into(), "Two".into()])
|
||||
);
|
||||
assert_eq!(screen.commit(), before);
|
||||
screen.drop_file("Cancelled.zip".into());
|
||||
assert!(screen.cancel_confirm(&m));
|
||||
assert!(!screen.confirming());
|
||||
let mut options = crate::options_ui::OptionsUi::default();
|
||||
options.pack_screen = Some(screen);
|
||||
options.update_pack_infos(vec![pack("file/Keep", None), pack("file/Imported", None)]);
|
||||
assert_eq!(options.pack_selection, ["file/Keep"]);
|
||||
assert!(options.pack_screen.is_none());
|
||||
}
|
||||
|
||||
fn png(w: u32, h: u32, rgba: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let mut encoder = png::Encoder::new(&mut out, w, h);
|
||||
@@ -2001,7 +2100,7 @@ mod click_sound_tests {
|
||||
);
|
||||
}
|
||||
screen.confirm = Some(Confirm {
|
||||
id: "incompatible".into(),
|
||||
action: ConfirmAction::Select("incompatible".into()),
|
||||
title: Text::literal("Confirm"),
|
||||
message: Text::literal("Use this pack?"),
|
||||
});
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
//! and an overlay resource root that layers the enabled packs over the
|
||||
//! vanilla resources (later packs win), so every loader sees one tree.
|
||||
mod cache;
|
||||
mod import;
|
||||
use crate::text::Style;
|
||||
use crate::{text::Text, zip::Zip};
|
||||
pub use horizon_client_core::pattern;
|
||||
pub use import::copy_pack;
|
||||
use pattern::Pattern;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -660,7 +662,10 @@ pub fn folder(dir: &Path, format: (i64, i64)) -> Vec<Pack> {
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let source = if path.is_dir() {
|
||||
Source::Dir(path)
|
||||
} else if name.ends_with(".zip") {
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("zip"))
|
||||
{
|
||||
Source::Zip(path)
|
||||
} else {
|
||||
return None;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//! `PackSelectionScreen.copyPacks`: copy a dropped pack without replacing an existing one.
|
||||
use std::{fs, io, path::Path};
|
||||
|
||||
fn invalid() -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "not a regular resource pack")
|
||||
}
|
||||
|
||||
fn copy_tree(from: &Path, to: &Path) -> io::Result<()> {
|
||||
let mut directories = vec![(from.to_path_buf(), to.to_path_buf())];
|
||||
while let Some((from, to)) = directories.pop() {
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let kind = entry.file_type()?;
|
||||
let target = to.join(entry.file_name());
|
||||
if kind.is_symlink() {
|
||||
return Err(invalid());
|
||||
} else if kind.is_dir() {
|
||||
fs::create_dir(&target)?;
|
||||
directories.push((entry.path(), target));
|
||||
} else if kind.is_file() {
|
||||
fs::copy(entry.path(), target)?;
|
||||
} else {
|
||||
return Err(invalid());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies a zip or a folder with root `pack.mcmeta` into `dir`. Existing entries are
|
||||
/// never overwritten; a failed copy removes only the destination this call created.
|
||||
pub fn copy_pack(from: &Path, dir: &Path) -> io::Result<()> {
|
||||
let kind = fs::symlink_metadata(from)?.file_type();
|
||||
if kind.is_symlink() {
|
||||
return Err(invalid());
|
||||
}
|
||||
if kind.is_dir() {
|
||||
if !from.join("pack.mcmeta").is_file() {
|
||||
return Err(invalid());
|
||||
}
|
||||
} else if !kind.is_file()
|
||||
|| !from
|
||||
.extension()
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("zip"))
|
||||
|| !crate::zip::Zip::open(from)?.contains("pack.mcmeta")
|
||||
{
|
||||
return Err(invalid());
|
||||
}
|
||||
let name = from.file_name().ok_or_else(invalid)?;
|
||||
fs::create_dir_all(dir)?;
|
||||
// A folder dropped onto its own descendant must not recursively copy itself.
|
||||
if kind.is_dir() && dir.canonicalize()?.starts_with(from.canonicalize()?) {
|
||||
return Err(invalid());
|
||||
}
|
||||
let target = dir.join(name);
|
||||
let result = if kind.is_dir() {
|
||||
fs::create_dir(&target)?;
|
||||
copy_tree(from, &target)
|
||||
} else {
|
||||
let mut source = fs::File::open(from)?;
|
||||
let mut output = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&target)?;
|
||||
io::copy(&mut source, &mut output).map(|_| ())
|
||||
};
|
||||
if result.is_err() {
|
||||
let cleanup = if kind.is_dir() {
|
||||
fs::remove_dir_all(&target)
|
||||
} else {
|
||||
fs::remove_file(&target)
|
||||
};
|
||||
if let Err(error) = cleanup {
|
||||
log::warn!(
|
||||
"could not remove incomplete resource pack {}: {error}",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
fn scratch(name: &str) -> std::path::PathBuf {
|
||||
let root = std::env::temp_dir().join(format!("horizon-drop-{name}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
root
|
||||
}
|
||||
|
||||
fn folder(path: &Path) {
|
||||
fs::create_dir_all(path.join("assets/example")).unwrap();
|
||||
fs::write(
|
||||
path.join("pack.mcmeta"),
|
||||
br#"{"pack":{"pack_format":1,"description":"Test"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(path.join("assets/example/test.txt"), b"texture").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_folders_and_zips_that_discovery_can_list_without_enabling_them() {
|
||||
let root = scratch("valid");
|
||||
let pack = root.join("My Pack");
|
||||
folder(&pack);
|
||||
let zip_path = root.join("Other.ZIP");
|
||||
let mut zip = zip::ZipWriter::new(fs::File::create(&zip_path).unwrap());
|
||||
zip.start_file("pack.mcmeta", zip::write::SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
zip.write_all(&fs::read(pack.join("pack.mcmeta")).unwrap())
|
||||
.unwrap();
|
||||
zip.finish().unwrap();
|
||||
let dir = root.join("resourcepacks");
|
||||
copy_pack(&pack, &dir).unwrap();
|
||||
copy_pack(&zip_path, &dir).unwrap();
|
||||
assert_eq!(
|
||||
copy_pack(&zip_path, &dir).unwrap_err().kind(),
|
||||
io::ErrorKind::AlreadyExists
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(dir.join("My Pack/assets/example/test.txt")).unwrap(),
|
||||
b"texture"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(dir.join("Other.ZIP")).unwrap(),
|
||||
fs::read(&zip_path).unwrap()
|
||||
);
|
||||
let packs = super::super::folder(&dir, super::super::RESOURCE_FORMAT);
|
||||
assert_eq!(
|
||||
packs.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
|
||||
["file/My Pack", "file/Other.ZIP"]
|
||||
);
|
||||
assert!(super::super::enabled(&packs, &[]).is_empty());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_inputs_and_collisions_leave_existing_files_untouched() {
|
||||
let root = scratch("refused");
|
||||
let dir = root.join("resourcepacks");
|
||||
let pack = root.join("Pack");
|
||||
folder(&pack);
|
||||
copy_pack(&pack, &dir).unwrap();
|
||||
fs::write(pack.join("assets/example/test.txt"), b"replacement").unwrap();
|
||||
assert_eq!(
|
||||
copy_pack(&pack, &dir).unwrap_err().kind(),
|
||||
io::ErrorKind::AlreadyExists
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(dir.join("Pack/assets/example/test.txt")).unwrap(),
|
||||
b"texture"
|
||||
);
|
||||
fs::write(root.join("bad.zip"), b"not a zip").unwrap();
|
||||
fs::create_dir(root.join("empty")).unwrap();
|
||||
assert!(copy_pack(&root.join("bad.zip"), &dir).is_err());
|
||||
assert!(copy_pack(&root.join("empty"), &dir).is_err());
|
||||
assert_eq!(fs::read_dir(&dir).unwrap().count(), 1);
|
||||
assert!(copy_pack(&pack, &pack.join("nested")).is_err());
|
||||
assert!(!pack.join("nested/Pack").exists());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn refuses_symlinks_and_removes_its_partial_copy() {
|
||||
let root = scratch("symlink");
|
||||
let pack = root.join("Pack");
|
||||
folder(&pack);
|
||||
std::os::unix::fs::symlink(&pack, pack.join("loop")).unwrap();
|
||||
let dir = root.join("resourcepacks");
|
||||
assert!(copy_pack(&pack, &dir).is_err());
|
||||
assert!(!dir.join("Pack").exists());
|
||||
assert!(pack.join("pack.mcmeta").exists());
|
||||
std::os::unix::fs::symlink(&pack, root.join("linked")).unwrap();
|
||||
assert!(copy_pack(&root.join("linked"), &dir).is_err());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -4693,20 +4693,19 @@ impl ParticleEngine {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Camera-facing quads (`QuadParticleRenderState.renderRotatedQuad`).
|
||||
/// Camera-facing quads (`QuadParticleRenderState.renderRotatedQuad`), turned by
|
||||
/// `Camera.rotation()`: `rotationYXZ(PI - yRot, -xRot, 0)`.
|
||||
pub fn render(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
base: Quat,
|
||||
partial: f32,
|
||||
light_at: &dyn Fn([i32; 3]) -> u8,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
self.render_inner::<false>(
|
||||
camera,
|
||||
yaw,
|
||||
pitch,
|
||||
base,
|
||||
partial,
|
||||
0.0,
|
||||
None,
|
||||
@@ -4721,8 +4720,7 @@ impl ParticleEngine {
|
||||
pub fn render_culled(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
base: Quat,
|
||||
partial: f32,
|
||||
pixel_focal_length: f32,
|
||||
culling_enabled: bool,
|
||||
@@ -4732,13 +4730,12 @@ impl ParticleEngine {
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
if !culling_enabled {
|
||||
self.render(camera, yaw, pitch, partial, light_at, out);
|
||||
self.render(camera, base, partial, light_at, out);
|
||||
return;
|
||||
}
|
||||
self.render_inner::<true>(
|
||||
camera,
|
||||
yaw,
|
||||
pitch,
|
||||
base,
|
||||
partial,
|
||||
pixel_focal_length,
|
||||
frustum,
|
||||
@@ -4751,8 +4748,7 @@ impl ParticleEngine {
|
||||
fn render_inner<const CULL: bool>(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
base: Quat,
|
||||
partial: f32,
|
||||
pixel_focal_length: f32,
|
||||
frustum: Option<&crate::gfx::Frustum>,
|
||||
@@ -4760,9 +4756,6 @@ impl ParticleEngine {
|
||||
light_at: &dyn Fn([i32; 3]) -> u8,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
// Camera.setRotation: rotationYXZ(PI - yRot, -xRot, 0).
|
||||
let base = Quat::from_rotation_y(std::f32::consts::PI - yaw.to_radians())
|
||||
* Quat::from_rotation_x(-pitch.to_radians());
|
||||
let mut light_cache = ParticleLightCache::default();
|
||||
let mut additive = self.additive.lock().unwrap_or_else(|e| e.into_inner());
|
||||
additive.clear();
|
||||
@@ -5173,12 +5166,11 @@ mod tests {
|
||||
|
||||
let light_at = |_: [i32; 3]| 0xf0;
|
||||
let mut regular = Vec::new();
|
||||
engine.render(DVec3::ZERO, 0.0, 0.0, 0.5, &light_at, &mut regular);
|
||||
engine.render(DVec3::ZERO, Quat::IDENTITY, 0.5, &light_at, &mut regular);
|
||||
let mut disabled = Vec::new();
|
||||
engine.render_culled(
|
||||
DVec3::ZERO,
|
||||
0.0,
|
||||
0.0,
|
||||
Quat::IDENTITY,
|
||||
0.5,
|
||||
900.0,
|
||||
false,
|
||||
@@ -5207,6 +5199,36 @@ mod tests {
|
||||
assert_eq!(attributes(&disabled), attributes(®ular));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quads_face_the_rotation_they_are_given() {
|
||||
let mut engine = ParticleEngine::new(Path::new(""), &HashMap::new(), &HashSet::new());
|
||||
let sprite = "test:smoke".to_owned();
|
||||
let rect = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
};
|
||||
engine.atlas.insert(sprite.clone(), (rect, false));
|
||||
let mut random = LegacyRandom::new(7);
|
||||
let mut particle =
|
||||
Particle::moving(&mut random, ParticleType::Smoke, [0.0, 0.0, -3.0], [0.0; 3]);
|
||||
particle.sprite = Sprite::Named(sprite);
|
||||
engine.particles.push(particle);
|
||||
let draw = |rotation: Quat| {
|
||||
let mut out = Vec::new();
|
||||
engine.render(DVec3::ZERO, rotation, 0.5, &|_| 0xf0, &mut out);
|
||||
out.iter().map(|v| v.pos).collect::<Vec<_>>()
|
||||
};
|
||||
// A bed's view turns the quads as the player's own yaw would not.
|
||||
let ahead = draw(Quat::IDENTITY);
|
||||
let turned = draw(Quat::from_rotation_y(std::f32::consts::FRAC_PI_2));
|
||||
assert_eq!(ahead.len(), 4);
|
||||
assert_ne!(ahead, turned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mod_particles_json_becomes_its_sprite_set() {
|
||||
let dir =
|
||||
@@ -5600,7 +5622,7 @@ mod tests {
|
||||
particle.sprite = Sprite::Index(0);
|
||||
engine.particles.push(particle);
|
||||
let mut vertices = Vec::new();
|
||||
engine.render(DVec3::ZERO, 0.0, 0.0, 0.0, &|_| 0, &mut vertices);
|
||||
engine.render(DVec3::ZERO, Quat::IDENTITY, 0.0, &|_| 0, &mut vertices);
|
||||
assert_eq!(vertices.len(), 4);
|
||||
assert!(vertices.iter().all(
|
||||
|v| v.color >> 24 == 127 && v.layer & crate::gfx::TRANSLUCENT_PARTICLE_OVERLAY != 0
|
||||
|
||||
@@ -143,21 +143,39 @@ mod tests {
|
||||
p.size = 0.1;
|
||||
e.particles.push(p.clone());
|
||||
let mut vertices = Vec::new();
|
||||
e.render(DVec3::ZERO, 0.0, 0.0, 0.5, &|_| 0, &mut vertices);
|
||||
e.render(
|
||||
DVec3::ZERO,
|
||||
glam::Quat::IDENTITY,
|
||||
0.5,
|
||||
&|_| 0,
|
||||
&mut vertices,
|
||||
);
|
||||
assert_eq!(vertices.len(), 4);
|
||||
assert!(vertices.iter().all(
|
||||
|v| v.color >> 24 == 127 && v.layer & crate::gfx::TRANSLUCENT_PARTICLE_OVERLAY != 0
|
||||
));
|
||||
e.particles[0].set_visible(false);
|
||||
vertices.clear();
|
||||
e.render(DVec3::ZERO, 0.0, 0.0, 0.5, &|_| 0, &mut vertices);
|
||||
e.render(
|
||||
DVec3::ZERO,
|
||||
glam::Quat::IDENTITY,
|
||||
0.5,
|
||||
&|_| 0,
|
||||
&mut vertices,
|
||||
);
|
||||
assert!(vertices.is_empty());
|
||||
assert_eq!(e.particles[0].age, 2);
|
||||
e.pending.push(copy);
|
||||
e.reload(Path::new(""), &HashMap::new(), &HashSet::new());
|
||||
assert!(e.pending.is_empty());
|
||||
vertices.clear();
|
||||
e.render(DVec3::ZERO, 0.0, 0.0, 0.5, &|_| 0, &mut vertices);
|
||||
e.render(
|
||||
DVec3::ZERO,
|
||||
glam::Quat::IDENTITY,
|
||||
0.5,
|
||||
&|_| 0,
|
||||
&mut vertices,
|
||||
);
|
||||
assert!(vertices.is_empty());
|
||||
e.tick_particle(&mut p, &level);
|
||||
assert!(p.removed);
|
||||
|
||||
@@ -1971,7 +1971,7 @@ mod tests {
|
||||
// The camera 5 blocks south, at the particle's height.
|
||||
let camera = DVec3::new(p.position[0], p.position[1], p.position[2] + 5.0);
|
||||
let mut out = Vec::new();
|
||||
engine.render(camera, 0.0, 0.0, 0.0, &|_| 0xF0, &mut out);
|
||||
engine.render(camera, glam::Quat::IDENTITY, 0.0, &|_| 0xF0, &mut out);
|
||||
assert_eq!(out.len(), 4);
|
||||
for v in &out {
|
||||
// The texture's top row (v 0) under the centre, its bottom row over it.
|
||||
@@ -2023,8 +2023,7 @@ mod tests {
|
||||
let mut out = Vec::new();
|
||||
engine.render(
|
||||
DVec3::new(1.0, 64.0, 6.0),
|
||||
0.0,
|
||||
0.0,
|
||||
glam::Quat::IDENTITY,
|
||||
0.0,
|
||||
&|_| 0xF0,
|
||||
&mut out,
|
||||
@@ -2138,8 +2137,7 @@ mod tests {
|
||||
let mut out = Vec::new();
|
||||
engine.render(
|
||||
DVec3::new(1.0, 70.0, 1.0),
|
||||
0.0,
|
||||
0.0,
|
||||
glam::Quat::IDENTITY,
|
||||
0.0,
|
||||
&|_| 0xF0,
|
||||
&mut out,
|
||||
|
||||
@@ -10,10 +10,8 @@ pub fn create() -> LegacyRandom {
|
||||
LegacyRandom::new(unique_seed())
|
||||
}
|
||||
|
||||
/// An entity's own random (`Entity.random`). Vanilla's is
|
||||
/// `RandomSource.createThreadLocalInstance`, seeded from Java's
|
||||
/// `ThreadLocalRandom`; Horizon seeds it as `create` does. Draws from an
|
||||
/// entity's own random never touch the level's.
|
||||
/// An entity's own random (`Entity.random`), `RandomSource.create()` as
|
||||
/// vanilla's is. Draws from an entity's own random never touch the level's.
|
||||
pub fn entity() -> LegacyRandom {
|
||||
create()
|
||||
}
|
||||
@@ -22,9 +20,103 @@ pub fn entity() -> LegacyRandom {
|
||||
const UNIQUIFIER_START: i64 = 8_682_522_807_148_012;
|
||||
const UNIQUIFIER_STEP: i64 = 1_181_783_497_276_652_981;
|
||||
|
||||
struct SeedOrder {
|
||||
draws: std::sync::Arc<horizon_core::random::OrderedDraws>,
|
||||
lane: usize,
|
||||
item: usize,
|
||||
lanes: usize,
|
||||
}
|
||||
|
||||
/// Seeds recorded by a diagnostic serial run and replayed by its grouped copy. Client threads
|
||||
/// continue to allocate normal seeds; verification never rewinds the process-wide counter.
|
||||
pub struct SeedReplay {
|
||||
seeds: Vec<i64>,
|
||||
next: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
impl SeedReplay {
|
||||
pub fn complete(&self) -> bool {
|
||||
self.next.load(std::sync::atomic::Ordering::Relaxed) == self.seeds.len()
|
||||
}
|
||||
}
|
||||
|
||||
enum SeedScope {
|
||||
Record(Vec<i64>),
|
||||
Replay(std::sync::Arc<SeedReplay>),
|
||||
}
|
||||
|
||||
struct RestoreSeeds(Option<SeedScope>);
|
||||
impl Drop for RestoreSeeds {
|
||||
fn drop(&mut self) {
|
||||
SEEDS.with(|s| *s.borrow_mut() = self.0.take());
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// This thread's uniquifier under `HORIZON_TEST_SEEDS`.
|
||||
static THREAD_UNIQUIFIER: std::cell::Cell<i64> = const { std::cell::Cell::new(UNIQUIFIER_START) };
|
||||
/// New randoms created by a grouped mob turn must follow the same list order as level draws.
|
||||
static SEED_ORDER: std::cell::RefCell<Option<SeedOrder>> =
|
||||
const { std::cell::RefCell::new(None) };
|
||||
static SEEDS: std::cell::RefCell<Option<SeedScope>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Run the serial diagnostic copy, retaining the actual seeds it allocated (including clocks).
|
||||
pub fn record_seeds<R>(f: impl FnOnce() -> R) -> (R, std::sync::Arc<SeedReplay>) {
|
||||
let _restore = RestoreSeeds(SEEDS.with(|s| s.replace(Some(SeedScope::Record(Vec::new())))));
|
||||
let result = f();
|
||||
let seeds = SEEDS.with(|s| match s.take() {
|
||||
Some(SeedScope::Record(seeds)) => seeds,
|
||||
_ => unreachable!("recording scope restored"),
|
||||
});
|
||||
(
|
||||
result,
|
||||
std::sync::Arc::new(SeedReplay {
|
||||
seeds,
|
||||
next: std::sync::atomic::AtomicUsize::new(0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Replay the serial copy's seeds on this group's thread. The turn's seed ordering scope orders
|
||||
/// accesses across groups; an extra or missing allocation makes `complete` false.
|
||||
pub fn replay_seeds<R>(seeds: std::sync::Arc<SeedReplay>, f: impl FnOnce() -> R) -> R {
|
||||
let _restore = RestoreSeeds(SEEDS.with(|s| s.replace(Some(SeedScope::Replay(seeds)))));
|
||||
f()
|
||||
}
|
||||
|
||||
/// The current diagnostic replay, to inherit on scoped worker threads.
|
||||
pub fn seed_replay() -> Option<std::sync::Arc<SeedReplay>> {
|
||||
SEEDS.with(|s| match &*s.borrow() {
|
||||
Some(SeedScope::Replay(seeds)) => Some(std::sync::Arc::clone(seeds)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Scope new random creation to one item of the parallel mob loop. Nothing waits until a seed
|
||||
/// is actually requested. Earlier items finish before it, just as for the level random, and a
|
||||
/// panic restores the previous scope. This does not rewind or reseed the shared counter.
|
||||
pub fn in_seed_order<R>(
|
||||
draws: std::sync::Arc<horizon_core::random::OrderedDraws>,
|
||||
lane: usize,
|
||||
item: usize,
|
||||
lanes: usize,
|
||||
f: impl FnOnce() -> R,
|
||||
) -> R {
|
||||
struct Restore(Option<SeedOrder>);
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
SEED_ORDER.with(|s| *s.borrow_mut() = self.0.take());
|
||||
}
|
||||
}
|
||||
let _restore = Restore(SEED_ORDER.with(|s| {
|
||||
s.replace(Some(SeedOrder {
|
||||
draws,
|
||||
lane,
|
||||
item,
|
||||
lanes,
|
||||
}))
|
||||
}));
|
||||
f()
|
||||
}
|
||||
|
||||
/// `RandomSupport.generateUniqueSeed`. Under `HORIZON_STILL` (screenshots that
|
||||
@@ -33,9 +125,41 @@ thread_local! {
|
||||
/// left out too and each thread steps a uniquifier of its own, as the parity
|
||||
/// oracles' agent makes vanilla do (docs/parity-workflow.md, "Randoms that
|
||||
/// repeat"): the client's draws then don't shift the server thread's.
|
||||
static UNIQUIFIER: std::sync::atomic::AtomicI64 =
|
||||
std::sync::atomic::AtomicI64::new(UNIQUIFIER_START);
|
||||
|
||||
pub fn unique_seed() -> i64 {
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
static UNIQUIFIER: AtomicI64 = AtomicI64::new(UNIQUIFIER_START);
|
||||
use std::sync::atomic::Ordering;
|
||||
SEED_ORDER.with(|scope| {
|
||||
if let Some(order) = &*scope.borrow() {
|
||||
for lane in 0..order.lanes {
|
||||
if lane != order.lane {
|
||||
order.draws.wait_past(lane, order.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let replay = SEEDS.with(|s| match &*s.borrow() {
|
||||
Some(SeedScope::Replay(replay)) => {
|
||||
let at = replay.next.fetch_add(1, Ordering::Relaxed);
|
||||
replay.seeds.get(at).copied()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(seed) = replay {
|
||||
return seed;
|
||||
}
|
||||
let seed = allocate_seed();
|
||||
SEEDS.with(|s| {
|
||||
if let Some(SeedScope::Record(seeds)) = &mut *s.borrow_mut() {
|
||||
seeds.push(seed);
|
||||
}
|
||||
});
|
||||
seed
|
||||
}
|
||||
|
||||
fn allocate_seed() -> i64 {
|
||||
use std::sync::atomic::Ordering;
|
||||
if test_seeds() {
|
||||
return THREAD_UNIQUIFIER.with(|u| {
|
||||
u.set(u.get().wrapping_mul(UNIQUIFIER_STEP));
|
||||
@@ -79,6 +203,12 @@ fn test_seeds() -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `HORIZON_TEST_SEEDS` is on: unseeded randoms repeat, in the order each thread takes
|
||||
/// them (the parallel mob loop then stays on one thread, so the order holds).
|
||||
pub fn repeating_per_thread() -> bool {
|
||||
test_seeds()
|
||||
}
|
||||
|
||||
/// A harness's sync step: the calling thread's uniquifier from now on. Only
|
||||
/// under `HORIZON_TEST_SEEDS`, where the clock is left out.
|
||||
pub fn set_thread_uniquifier(value: i64) -> Result<(), &'static str> {
|
||||
@@ -102,4 +232,96 @@ mod tests {
|
||||
assert_eq!(seeds.len(), 1000);
|
||||
assert_ne!(create().next_long(), create().next_long());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_is_scoped_and_detects_missing_or_extra_seeds() {
|
||||
let (expected, tape) = record_seeds(|| [unique_seed(), unique_seed()]);
|
||||
replay_seeds(std::sync::Arc::clone(&tape), || {
|
||||
assert_eq!(unique_seed(), expected[0]);
|
||||
assert!(!tape.complete(), "one allocation is missing");
|
||||
assert_eq!(unique_seed(), expected[1]);
|
||||
assert!(tape.complete());
|
||||
unique_seed();
|
||||
assert!(!tape.complete(), "an extra allocation is detected");
|
||||
});
|
||||
assert!(seed_replay().is_none());
|
||||
let caught = std::panic::catch_unwind(|| {
|
||||
record_seeds(|| panic!("recording panics"));
|
||||
});
|
||||
assert!(caught.is_err());
|
||||
let (fresh, fresh_tape) = record_seeds(unique_seed);
|
||||
replay_seeds(fresh_tape, || assert_eq!(unique_seed(), fresh));
|
||||
}
|
||||
|
||||
/// New RNGs and level draws share the same dependency direction, even when the later lane
|
||||
/// gets to its allocation first. A panic releases both its lane and its TLS scopes.
|
||||
#[test]
|
||||
fn grouped_new_randoms_follow_serial_order_and_release_on_unwind() {
|
||||
use horizon_core::random::OrderedDraws;
|
||||
use std::sync::{Arc, mpsc};
|
||||
let (sent, finished) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let (expected, tape) =
|
||||
record_seeds(|| (0..60).map(|_| unique_seed()).collect::<Vec<_>>());
|
||||
let draws = OrderedDraws::new(&LegacyRandom::new(7), 3);
|
||||
let mut actual = std::thread::scope(|s| {
|
||||
let handles: Vec<_> = (0..3)
|
||||
.rev()
|
||||
.map(|lane| {
|
||||
let (draws, tape) = (Arc::clone(&draws), Arc::clone(&tape));
|
||||
s.spawn(move || {
|
||||
replay_seeds(tape, || {
|
||||
let mut random = draws.lane(lane);
|
||||
let _done = random.lane_done();
|
||||
let mut out = Vec::new();
|
||||
for item in (lane..30).step_by(3) {
|
||||
random.begin_item(item);
|
||||
let values =
|
||||
in_seed_order(Arc::clone(&draws), lane, item, 3, || {
|
||||
[unique_seed(), unique_seed()]
|
||||
});
|
||||
out.extend([(item * 2, values[0]), (item * 2 + 1, values[1])]);
|
||||
random.end_item();
|
||||
}
|
||||
out
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
handles
|
||||
.into_iter()
|
||||
.flat_map(|h| h.join().unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
actual.sort_by_key(|v| v.0);
|
||||
assert_eq!(
|
||||
actual.into_iter().map(|v| v.1).collect::<Vec<_>>(),
|
||||
expected
|
||||
);
|
||||
assert!(tape.complete());
|
||||
|
||||
let draws = OrderedDraws::new(&LegacyRandom::new(7), 2);
|
||||
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
std::thread::scope(|s| {
|
||||
let earlier = Arc::clone(&draws);
|
||||
s.spawn(move || {
|
||||
let mut random = earlier.lane(0);
|
||||
let _done = random.lane_done();
|
||||
random.begin_item(0);
|
||||
in_seed_order(earlier, 0, 0, 2, || panic!("earlier turn"));
|
||||
});
|
||||
let mut random = draws.lane(1);
|
||||
let _done = random.lane_done();
|
||||
random.begin_item(1);
|
||||
in_seed_order(Arc::clone(&draws), 1, 1, 2, unique_seed);
|
||||
});
|
||||
}));
|
||||
assert!(caught.is_err());
|
||||
assert!(SEED_ORDER.with(|s| s.borrow().is_none()));
|
||||
let _ = sent.send(());
|
||||
});
|
||||
finished
|
||||
.recv_timeout(std::time::Duration::from_secs(20))
|
||||
.expect("seed wait released");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Appended to terrain.wgsl (gfx/ray_query.rs's `Unpacker`): the solid quads of sections
|
||||
// about to get a BLAS, written out as plain vertices (`TerrainVertex`, six words each), so
|
||||
// ray tracing keeps the section arena in its compact layout. `section_corner` reads a corner
|
||||
// exactly as the solid draws do, from a record or a vertex, out of the page bound at
|
||||
// `arena_page`.
|
||||
struct RtJob {
|
||||
// The draw vertex index of the section's first solid quad's first corner (`solid_base`).
|
||||
base: u32,
|
||||
// Its solid quads.
|
||||
quads: u32,
|
||||
// Where its first vertex goes in `rt_out`.
|
||||
out: u32,
|
||||
pad: u32,
|
||||
}
|
||||
@group(2) @binding(0) var<storage, read> rt_jobs: array<RtJob>;
|
||||
@group(2) @binding(1) var<storage, read_write> rt_out: array<u32>;
|
||||
|
||||
// One invocation per vertex: x over a section's vertices, y over the page's sections.
|
||||
@compute @workgroup_size(64) fn cs_rt_unpack(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let job = rt_jobs[id.y];
|
||||
if id.x >= job.quads * 4u {
|
||||
return;
|
||||
}
|
||||
let c = section_corner(job.base + id.x).c;
|
||||
let o = (job.out + id.x) * 6u;
|
||||
rt_out[o] = c.p.x;
|
||||
rt_out[o + 1u] = c.p.y;
|
||||
rt_out[o + 2u] = c.p.z;
|
||||
rt_out[o + 3u] = c.r.x;
|
||||
rt_out[o + 4u] = c.r.y;
|
||||
rt_out[o + 5u] = c.r.z;
|
||||
}
|
||||
@@ -580,6 +580,15 @@ pub fn button(
|
||||
"widget/button"
|
||||
};
|
||||
sprite(out, sprite_id, x, y, w, h);
|
||||
// `AbstractWidget.WithInactiveMessage.defaultInactiveMessage`: an inactive
|
||||
// widget's message is -6250336 (0xFFA0A0A0), not the active white.
|
||||
let inactive;
|
||||
let label = if active {
|
||||
label
|
||||
} else {
|
||||
inactive = label.clone().with_color(0xFFA0_A0A0);
|
||||
&inactive
|
||||
};
|
||||
scrolling_string_over_contents(out, m, label, x, y, w, h, 2, now_ms);
|
||||
}
|
||||
/// `Tooltip.splitTooltip` of a widget's tooltip, shown under the mouse.
|
||||
@@ -1150,6 +1159,33 @@ mod button_label_tests {
|
||||
button(&mut out, m, text, 10, 20, w, 20, true, false, now_ms);
|
||||
out
|
||||
}
|
||||
/// `WithInactiveMessage`: a disabled button's message is grey, not white.
|
||||
#[test]
|
||||
fn an_inactive_buttons_label_is_grey() {
|
||||
let atlas = atlas();
|
||||
let m = Measure(&atlas.font);
|
||||
let mut out = Vec::new();
|
||||
button(
|
||||
&mut out,
|
||||
&m,
|
||||
&Text::literal("Go"),
|
||||
10,
|
||||
20,
|
||||
100,
|
||||
20,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
let color = out
|
||||
.iter()
|
||||
.find_map(|d| match d {
|
||||
Draw::Text { text, .. } => text.spans.iter().find_map(|s| s.style.color),
|
||||
_ => None,
|
||||
})
|
||||
.expect("the label is drawn with its inactive style");
|
||||
assert_eq!(color & 0xFF_FFFF, 0xA0_A0A0);
|
||||
}
|
||||
fn scissor_of(draws: &[Draw]) -> Option<[i32; 4]> {
|
||||
draws.iter().find_map(|d| match d {
|
||||
Draw::Scissor { x0, y0, x1, y1 } => Some([*x0, *y0, *x1, *y1]),
|
||||
|
||||
@@ -83,6 +83,14 @@ impl SignText {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.lines.iter().all(String::is_empty)
|
||||
}
|
||||
/// `SignText.hasEditableText`: every line's contents is plain text, so the
|
||||
/// editor can rewrite it without losing anything. A style does not count
|
||||
/// (a red literal is still plain), a `translatable`, `score` or other
|
||||
/// contents does. (Horizon keeps no filtered text, so
|
||||
/// `Player.isTextFilteringEnabled` never selects that copy.)
|
||||
pub fn has_editable_text(&self) -> bool {
|
||||
(0..self.lines.len()).all(|i| plain_contents(&self.message(i)))
|
||||
}
|
||||
/// `DyeColor.getTextColor` of this side.
|
||||
pub fn text_color(&self) -> u32 {
|
||||
crate::pack_palette::text(usize::from(self.color.min(15)))
|
||||
@@ -245,6 +253,17 @@ fn is_styled(tag: &Tag) -> bool {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// Whether a message's contents is `PlainTextContents` (`Component.getContents`):
|
||||
/// a string, a `{text: ...}` object, or a list of them (`createFromList` keeps
|
||||
/// the first element's contents and appends the rest as siblings).
|
||||
fn plain_contents(tag: &Tag) -> bool {
|
||||
match tag {
|
||||
Tag::String(_) => true,
|
||||
Tag::List(list) => list.first().is_some_and(plain_contents),
|
||||
Tag::Compound(_) => tag.get("text").is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// The fields of a component's own style (`Style.CODEC`), the ones a literal
|
||||
/// made from it keeps. A list's style is its first element's.
|
||||
const STYLE_KEYS: [&str; 11] = [
|
||||
|
||||
@@ -93,15 +93,18 @@ pub fn unpack(value: &str, signature: Signature, valid_uris: &[impl AsRef<str>])
|
||||
signed: signature == Signature::Signed,
|
||||
..empty.clone()
|
||||
};
|
||||
// Each texture is loaded independently: an invalid or disallowed URL for one
|
||||
// texture (cape, elytra) must not drop another (the skin). Vanilla's
|
||||
// SkinManager works the same way.
|
||||
for (kind, texture) in textures {
|
||||
let Some(url) = texture.get("url").and_then(|u| u.as_str()) else {
|
||||
return empty;
|
||||
continue;
|
||||
};
|
||||
let allowed = valid_uris
|
||||
.iter()
|
||||
.any(|u| url.starts_with(&u.as_ref().replace("{textureId}", "")));
|
||||
if !allowed {
|
||||
return empty;
|
||||
continue;
|
||||
}
|
||||
if kind == "SKIN" {
|
||||
out.slim = texture
|
||||
@@ -1385,7 +1388,10 @@ mod tests {
|
||||
&super::TEXTURE_URIS,
|
||||
);
|
||||
assert_eq!(textures.slim, slim && !url.contains("evil"));
|
||||
assert_eq!(textures.cape.is_some(), !url.contains("evil"));
|
||||
// Textures are judged per URL (vanilla's SkinManager): the
|
||||
// cape's URL is always allowed here, so it is kept even when
|
||||
// the skin's URL is not.
|
||||
assert!(textures.cape.is_some());
|
||||
assert_eq!(parsed.skin.is_some(), !url.contains("evil"));
|
||||
assert!(std::sync::Arc::ptr_eq(&parsed, &cache.get(&profile)));
|
||||
}
|
||||
@@ -1538,6 +1544,19 @@ mod tests {
|
||||
assert!(t.slim);
|
||||
assert!(!unpack(wide, Signature::Signed, &TEXTURE_URIS).slim);
|
||||
}
|
||||
/// `unpack` keeps each texture independently: a cape from another origin
|
||||
/// must not drop the skin.
|
||||
#[test]
|
||||
fn unpack_keeps_the_skin_when_another_texture_is_rejected() {
|
||||
use super::{Signature, TEXTURE_URIS, unpack};
|
||||
let mixed = "eyJ0ZXh0dXJlcyI6IHsiU0tJTiI6IHsidXJsIjogImh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL3NraW4xIn0sICJDQVBFIjogeyJ1cmwiOiAiaHR0cHM6Ly9ldmlsLmV4YW1wbGUvdGV4dHVyZS9jYXBlMSJ9fX0=";
|
||||
let t = unpack(mixed, Signature::Unsigned, &TEXTURE_URIS);
|
||||
assert_eq!(
|
||||
t.skin.as_deref(),
|
||||
Some("https://textures.minecraft.net/texture/skin1")
|
||||
);
|
||||
assert_eq!(t.cape, None);
|
||||
}
|
||||
#[test]
|
||||
fn profiles_parse_like_resolvable_profiles() {
|
||||
use super::{NameOrId, Signature, parse_profile};
|
||||
|
||||
@@ -506,8 +506,8 @@ pub struct Streaming {
|
||||
decoration_order: DecorationOrder,
|
||||
order_area: Option<((i32, i32), i32)>,
|
||||
/// What the queued jobs' priorities were computed from (`requeue`): the
|
||||
/// centre, the band, and the extra areas.
|
||||
queue_basis: (((i32, i32), Option<Band>), Vec<((i32, i32), i32)>),
|
||||
/// centre, the band, the extra areas, and the forced chunks.
|
||||
queue_basis: forced_ring::Basis,
|
||||
finishing: FxHashSet<(i32, i32)>,
|
||||
decorates: bool,
|
||||
/// `ChunkGenerator::decorates_beside_final`.
|
||||
@@ -1071,6 +1071,7 @@ pub struct WorkerContext {
|
||||
}
|
||||
mod bounded;
|
||||
mod forced_cover;
|
||||
mod forced_ring;
|
||||
mod relight_now;
|
||||
use bounded::nearest_first;
|
||||
use forced_cover::ForcedCover;
|
||||
@@ -1354,11 +1355,9 @@ impl Streaming {
|
||||
/// render distance 32 nothing was decorated until every proto chunk had
|
||||
/// been generated.
|
||||
fn requeue(&mut self) {
|
||||
let basis = (self.center, self.band);
|
||||
if self.queue_basis.0 == basis && self.queue_basis.1 == self.extra {
|
||||
if !forced_ring::basis_changed(self) {
|
||||
return;
|
||||
}
|
||||
self.queue_basis = (basis, self.extra.clone());
|
||||
let mut queue = self.shared.queue.lock().unwrap();
|
||||
rank_again(&mut queue, |x, z| self.distance(x, z));
|
||||
}
|
||||
@@ -2862,7 +2861,7 @@ impl Ranges<'_> {
|
||||
_ => own,
|
||||
}
|
||||
}
|
||||
fn distance(&self, x: i32, z: i32) -> i64 {
|
||||
fn player_distance(&self, x: i32, z: i32) -> i64 {
|
||||
let d = |c: (i32, i32)| {
|
||||
let (dx, dz) = (i64::from(x - c.0), i64::from(z - c.1));
|
||||
dx * dx + dz * dz
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
//! How a forced chunk's ring is ranked (#13175).
|
||||
use super::{Band, FORCED_TERRAIN_RING, Ranges, Streaming};
|
||||
use std::collections::HashSet;
|
||||
|
||||
type Chunks = HashSet<(i32, i32)>;
|
||||
|
||||
/// What queued jobs were ranked from (`Streaming::requeue`): the centre, the
|
||||
/// band, the extra areas, and the forced chunks.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Basis {
|
||||
player: ((i32, i32), Option<Band>),
|
||||
extra: Vec<((i32, i32), i32)>,
|
||||
forced: (Chunks, Chunks),
|
||||
}
|
||||
|
||||
/// Whether the queued jobs' ranking is out of date; it is brought up to date.
|
||||
pub(super) fn basis_changed(s: &mut Streaming) -> bool {
|
||||
let b = &mut s.queue_basis;
|
||||
let same = b.player == (s.center, s.band)
|
||||
&& b.extra == s.extra
|
||||
&& b.forced.0 == s.forced
|
||||
&& b.forced.1 == s.mod_forced;
|
||||
if !same {
|
||||
b.player = (s.center, s.band);
|
||||
b.extra.clone_from(&s.extra);
|
||||
b.forced.0.clone_from(&s.forced);
|
||||
b.forced.1.clone_from(&s.mod_forced);
|
||||
}
|
||||
!same
|
||||
}
|
||||
|
||||
impl Ranges<'_> {
|
||||
/// `player_distance`, or the nearer one to a forced chunk whose ring reaches
|
||||
/// (x, z): see `forced_distance`.
|
||||
pub(super) fn distance(&self, x: i32, z: i32) -> i64 {
|
||||
self.player_distance(x, z).min(self.forced_distance(x, z))
|
||||
}
|
||||
|
||||
/// Distance (squared) to the nearest forced chunk whose ring reaches
|
||||
/// (x, z), or `i64::MAX`. A forced chunk's ring is loaded with its ticket
|
||||
/// (levels 32 to 35 around the FORCED ticket's 31), so the 3x3 it needs to
|
||||
/// tick is ranked around it, not by how far it lies from the player: vanilla
|
||||
/// ticks the chunk on the first tick (`ChunkMap.prepareTickingChunk`).
|
||||
pub(super) fn forced_distance(&self, x: i32, z: i32) -> i64 {
|
||||
const SIDE: usize = (2 * FORCED_TERRAIN_RING + 1) as usize;
|
||||
let held = self.forced.len() + self.mod_forced.len();
|
||||
if held == 0 {
|
||||
return i64::MAX;
|
||||
}
|
||||
let d = |f: (i32, i32)| {
|
||||
let (dx, dz) = (i64::from(x - f.0), i64::from(z - f.1));
|
||||
dx * dx + dz * dz
|
||||
};
|
||||
if held <= SIDE * SIDE {
|
||||
return self
|
||||
.forced_chunks()
|
||||
.filter(|f| (x - f.0).abs().max((z - f.1).abs()) <= FORCED_TERRAIN_RING)
|
||||
.map(|&f| d(f))
|
||||
.min()
|
||||
.unwrap_or(i64::MAX);
|
||||
}
|
||||
// Many forced chunks: look at the columns around (x, z) instead.
|
||||
(-FORCED_TERRAIN_RING..=FORCED_TERRAIN_RING)
|
||||
.flat_map(|dz| (-FORCED_TERRAIN_RING..=FORCED_TERRAIN_RING).map(move |dx| (dx, dz)))
|
||||
.map(|(dx, dz)| (x + dx, z + dz))
|
||||
.filter(|f| self.is_forced(f.0, f.1))
|
||||
.map(d)
|
||||
.min()
|
||||
.unwrap_or(i64::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::{GenerationInputs, GenerationScan, NO_CHUNKS};
|
||||
use super::*;
|
||||
use crate::fast_hash::{FxHashMap, FxHashSet};
|
||||
use crate::world::World;
|
||||
|
||||
/// A forced chunk's ring is loaded with its ticket, so it ranks around
|
||||
/// the forced chunk and not by how far it lies from the player. Vanilla
|
||||
/// then ticks the chunk on the first tick after a reload
|
||||
/// (`ChunkMap.prepareTickingChunk`: its 3x3 is full at once); ranked from
|
||||
/// the player at the origin, a ring 4096 blocks out arrived west/north
|
||||
/// first, over tens of ticks.
|
||||
#[test]
|
||||
fn a_forced_chunks_ring_is_ranked_around_it() {
|
||||
let ranges = |forced| Ranges {
|
||||
center: (0, 0),
|
||||
radius: 2,
|
||||
extra: &[],
|
||||
forced,
|
||||
mod_forced: &NO_CHUNKS,
|
||||
cover: None,
|
||||
forced_cover: None,
|
||||
band: None,
|
||||
};
|
||||
let forced: HashSet<(i32, i32)> = [(256, 256)].into();
|
||||
let r = ranges(&forced);
|
||||
// Squared distance to the forced chunk, by hand: (255, 254) is
|
||||
// one west and two north of it.
|
||||
assert_eq!(r.priority(256, 256), 0);
|
||||
assert_eq!(r.priority(255, 254), 1 + 4);
|
||||
assert_eq!(r.priority(260, 252), 16 + 16);
|
||||
// Past the fourth ring (level 36 and up) the player's distance rules,
|
||||
// as does the whole map without a forced chunk.
|
||||
assert_eq!(r.priority(261, 256), 261 * 261 + 256 * 256);
|
||||
let none = HashSet::new();
|
||||
assert_eq!(ranges(&none).priority(255, 254), 255 * 255 + 254 * 254);
|
||||
// Many forced chunks (the neighbour lookups) agree with few.
|
||||
let many: HashSet<(i32, i32)> = (0..100).map(|i| (256 + 20 * i, 256)).collect();
|
||||
assert_eq!(ranges(&many).priority(255, 254), 5);
|
||||
assert_eq!(ranges(&many).priority(276, 258), 4);
|
||||
// The generation plan fetches the 3x3 the chunk ticks with first.
|
||||
let world = World::new(horizon_world::registry::BlockStateId(0), 0);
|
||||
let (generating, finishing) = (FxHashSet::default(), FxHashSet::default());
|
||||
let protos = FxHashMap::default();
|
||||
let input = GenerationInputs {
|
||||
ranges: ranges(&forced),
|
||||
world: &world,
|
||||
generating: &generating,
|
||||
protos: &protos,
|
||||
finishing: &finishing,
|
||||
decorates: true,
|
||||
remote: false,
|
||||
};
|
||||
let candidates = GenerationScan::default()
|
||||
.candidates(input, usize::MAX)
|
||||
.expect("a scan");
|
||||
let ring: Vec<(i32, i32)> = candidates
|
||||
.iter()
|
||||
.map(|&(_, x, z)| (x, z))
|
||||
.filter(|&(x, z)| (x - 256).abs().max((z - 256).abs()) <= FORCED_TERRAIN_RING)
|
||||
.collect();
|
||||
assert_eq!(ring.len(), 81);
|
||||
assert_eq!(ring[0], (256, 256));
|
||||
let d = |(x, z): (i32, i32)| (x - 256).pow(2) + (z - 256).pow(2);
|
||||
for pair in ring.windows(2) {
|
||||
assert!(
|
||||
d(pair[0]) <= d(pair[1]),
|
||||
"{:?} before {:?}",
|
||||
pair[0],
|
||||
pair[1]
|
||||
);
|
||||
}
|
||||
let first_nine: HashSet<(i32, i32)> = ring[..9].iter().copied().collect();
|
||||
let expected: HashSet<(i32, i32)> = (-1..=1)
|
||||
.flat_map(|dx| (-1..=1).map(move |dz| (256 + dx, 256 + dz)))
|
||||
.collect();
|
||||
assert_eq!(first_nine, expected);
|
||||
}
|
||||
|
||||
/// Jobs queued before a chunk was forced follow it once it is
|
||||
/// (`Streaming::requeue` ranks them again).
|
||||
#[test]
|
||||
fn queued_jobs_follow_a_forced_chunk() {
|
||||
use super::super::{Job, Queued, rank_again};
|
||||
use std::collections::BinaryHeap;
|
||||
let job = |x, z, order| Queued {
|
||||
priority: 0,
|
||||
at: Some((x, z)),
|
||||
bias: 0,
|
||||
order,
|
||||
job: Job::Generate(x, z),
|
||||
};
|
||||
let first_after = |forced: &HashSet<(i32, i32)>| {
|
||||
let ranges = Ranges {
|
||||
center: (0, 0),
|
||||
radius: 2,
|
||||
extra: &[],
|
||||
forced,
|
||||
mod_forced: &NO_CHUNKS,
|
||||
cover: None,
|
||||
forced_cover: None,
|
||||
band: None,
|
||||
};
|
||||
// (255, 254) lies nearer the origin than (258, 256), the
|
||||
// forced chunk's neighbour two chunks east.
|
||||
let mut queue = BinaryHeap::from(vec![job(255, 254, 1), job(258, 256, 2)]);
|
||||
rank_again(&mut queue, |x, z| ranges.priority(x, z));
|
||||
queue.pop().and_then(|q| q.at)
|
||||
};
|
||||
assert_eq!(first_after(&HashSet::new()), Some((255, 254)));
|
||||
assert_eq!(first_after(&[(256, 256)].into()), Some((258, 256)));
|
||||
}
|
||||
}
|
||||
@@ -790,7 +790,7 @@ fn solid_light(finished: vec4<f32>) -> vec4<f32> {
|
||||
if globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(finished.rgb, finished.a, globals.hdr_settings, globals.hdr_shares), 1.0);
|
||||
}
|
||||
return vec4(finished.rgb, 1.0);
|
||||
return frame_stored(vec4(finished.rgb, 1.0));
|
||||
}
|
||||
return finished;
|
||||
}
|
||||
@@ -872,7 +872,7 @@ fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<
|
||||
let mark = hdr_scene_alpha(mix(hdr_scene(in.scene.x, in.scene.y, hdr_facing()), globals.hdr_light.y, fogged));
|
||||
return vec4(hdr_written(finished.rgb, mark, globals.hdr_settings, globals.hdr_shares), finished.a);
|
||||
}
|
||||
return finished;
|
||||
return frame_stored(finished);
|
||||
}
|
||||
|
||||
// Native still-fluid boxes use block sprites and lightmap without terrain AO/emission
|
||||
@@ -1078,7 +1078,7 @@ fn sky_finished(color: vec3<f32>) -> vec4<f32> {
|
||||
if globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(color, mark, globals.hdr_settings, globals.hdr_shares), 1.0);
|
||||
}
|
||||
return vec4(color, 1.0);
|
||||
return frame_stored(vec4(color, 1.0));
|
||||
}
|
||||
return vec4(color, mark);
|
||||
}
|
||||
@@ -1111,7 +1111,7 @@ struct SunriseOut {
|
||||
let d = view_ray(in.ndc);
|
||||
let dist = disc_distance(d, -4.0);
|
||||
if dist.x < 0.0 { discard; }
|
||||
return vec4(sky_fog(vec3(0.0), dist), 1.0);
|
||||
return frame_stored(vec4(sky_fog(vec3(0.0), dist), 1.0));
|
||||
}
|
||||
|
||||
// rendertype_lines.vsh: screen-space stroke and camera-space depth separation.
|
||||
@@ -1149,7 +1149,7 @@ struct LineOut {
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_line(in: LineOut) -> @location(0) vec4<f32> {
|
||||
return apply_fog(in.color, in.spherical, in.cylindrical);
|
||||
return frame_stored(apply_fog(in.color, in.spherical, in.cylindrical));
|
||||
}
|
||||
// Gizmo lines (rendertype_lines with a per-vertex colour and LineWidth in
|
||||
// pixels; no layering transform).
|
||||
@@ -1182,5 +1182,5 @@ struct DebugLineOut {
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_debug_line(in: DebugLineOut) -> @location(0) vec4<f32> {
|
||||
return apply_fog(in.color, in.spherical, in.cylindrical);
|
||||
return frame_stored(apply_fog(in.color, in.spherical, in.cylindrical));
|
||||
}
|
||||
@@ -1104,6 +1104,34 @@ pub fn tooltip_menu(
|
||||
);
|
||||
tooltip_box(batch, atlas, lines, x as f32, y as f32, w, h);
|
||||
}
|
||||
/// `BelowOrAboveWidgetTooltipPositioner.positionTooltip`: a keyboard-focused widget's tooltip.
|
||||
pub fn tooltip_focused(batch: &mut HudBatch, atlas: &HudAtlas, lines: &[Text], rect: [f32; 4]) {
|
||||
if lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (w, h) = tooltip_size(atlas, lines);
|
||||
let (x, y) = focused_tooltip_position(
|
||||
(batch.width as i32, batch.height as i32),
|
||||
rect.map(|v| v as i32),
|
||||
(w.ceil() as i32, h.ceil() as i32),
|
||||
);
|
||||
tooltip_box(batch, atlas, lines, x as f32, y as f32, w, h);
|
||||
}
|
||||
|
||||
/// `BelowOrAboveWidgetTooltipPositioner.positionTooltip`: below, or above when it does not fit.
|
||||
fn focused_tooltip_position(screen: (i32, i32), rect: [i32; 4], size: (i32, i32)) -> (i32, i32) {
|
||||
let [left, top, width, height] = rect;
|
||||
let mut x = left + 3;
|
||||
let mut y = top + height + 4;
|
||||
if y + size.1 + 3 > screen.1 {
|
||||
y = top - size.1 - 4;
|
||||
}
|
||||
if x + size.0 > screen.0 {
|
||||
x = (left + width - size.0 - 3).max(4);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// A widget's tooltip while the controller's focus is on it (Horizon's: vanilla has no gamepad
|
||||
/// input, and the hidden pointer mid-widget would put it over the widget).
|
||||
pub fn tooltip_under(batch: &mut HudBatch, atlas: &HudAtlas, lines: &[Text], rect: [f32; 4]) {
|
||||
@@ -2614,6 +2642,33 @@ mod tests {
|
||||
assert!(!delete_words(&mut v, 1));
|
||||
assert_eq!(v, "stone");
|
||||
}
|
||||
#[test]
|
||||
fn a_keyboard_focused_tooltip_uses_vanillas_widget_edges() {
|
||||
let screen = (320, 240);
|
||||
assert_eq!(
|
||||
focused_tooltip_position(screen, [7, 160, 98, 20], (170, 40)),
|
||||
(10, 184)
|
||||
);
|
||||
// Exact fit below; one pixel taller goes above.
|
||||
assert_eq!(
|
||||
focused_tooltip_position(screen, [7, 160, 98, 20], (170, 53)),
|
||||
(10, 184)
|
||||
);
|
||||
assert_eq!(
|
||||
focused_tooltip_position(screen, [7, 160, 98, 20], (170, 54)),
|
||||
(10, 102)
|
||||
);
|
||||
// Right overflow aligns against the widget's right edge, not the screen's.
|
||||
assert_eq!(
|
||||
focused_tooltip_position(screen, [250, 100, 60, 20], (170, 40)),
|
||||
(137, 124)
|
||||
);
|
||||
assert_eq!(
|
||||
focused_tooltip_position((120, 240), [7, 160, 98, 20], (170, 40)),
|
||||
(4, 184)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pad_focused_tooltip_hangs_from_its_widgets_bottom_left() {
|
||||
let screen = (427.0, 240.0);
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
fast_hash::{FxHashMap, FxHashSet},
|
||||
section_mask::SectionMask,
|
||||
};
|
||||
use horizon_core::random::LegacyRandom;
|
||||
use horizon_core::random::{LegacyRandom, LevelRandom};
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -69,7 +69,7 @@ pub struct World {
|
||||
pub shape: LevelShape,
|
||||
pub fast_lava: bool,
|
||||
pub chunks: crate::chunk_map::ChunkMap,
|
||||
pub random: LegacyRandom,
|
||||
pub random: LevelRandom,
|
||||
pub revision: u64,
|
||||
pub health_rules: HealthRules,
|
||||
pub air: BlockStateId,
|
||||
@@ -219,7 +219,7 @@ impl World {
|
||||
Self {
|
||||
shape: LevelShape::OVERWORLD,
|
||||
chunks: Default::default(),
|
||||
random: LegacyRandom::new(seed),
|
||||
random: LevelRandom::for_level(LegacyRandom::new(seed)),
|
||||
revision: 0,
|
||||
health_rules: HealthRules::default(),
|
||||
air,
|
||||
@@ -319,6 +319,12 @@ impl World {
|
||||
pub fn bottom_y(&self) -> i32 {
|
||||
self.layout().min_y()
|
||||
}
|
||||
/// `Entity.checkBelowWorld`'s floor, `Level.getMinY() - 64`: the level's own
|
||||
/// lowest Y (0 in the Nether and the End), not the chunks' widened column.
|
||||
/// An entity below it is out of the world.
|
||||
pub fn below_world_y(&self) -> f64 {
|
||||
f64::from(self.shape.min_y - 64)
|
||||
}
|
||||
/// One above the highest block Y the level's chunks hold.
|
||||
pub fn top_y(&self) -> i32 {
|
||||
self.layout().max_y()
|
||||
|
||||
@@ -33,7 +33,8 @@ horizon-protocol = { path = "../horizon-protocol" }
|
||||
horizon-server-files = { path = "../horizon-server-files" }
|
||||
# The dedicated server's anticheat, off unless server.toml's [anticheat] turns it on (docs/anticheat.md).
|
||||
horizon-anticheat = { path = "../horizon-anticheat" }
|
||||
# The dedicated server's chat filter: [chat_filter], the slur list, mutes (docs/chat-filter.md).
|
||||
# The dedicated server's chat filter: [chat_filter], the slur list, mutes (docs/chat-filter.md);
|
||||
# `entry` installs it into the engine's hook (`horizon_server_files::chat_guard`).
|
||||
horizon-chat-filter = { path = "../horizon-chat-filter" }
|
||||
# Shader packs: reading them and turning their GLSL into naga modules.
|
||||
horizon-shaders = { path = "../horizon-shaders" }
|
||||
|
||||
@@ -331,6 +331,8 @@
|
||||
"Frame Pacing": "Frame Pacing",
|
||||
"Worker Threads": "Worker Threads",
|
||||
"Resolution Scale": "Resolution Scale",
|
||||
"Dynamic Resolution": "Dynamic Resolution",
|
||||
"Target FPS": "Target FPS",
|
||||
"Anti-aliasing": "Anti-aliasing",
|
||||
"Upscaling": "Upscaling",
|
||||
"Upscaling Sharpness": "Upscaling Sharpness",
|
||||
@@ -435,6 +437,8 @@
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Which chunk rebuilds wait for the frame: none, nearby or yours.",
|
||||
"Animates water, lava, fire and other moving textures.": "Animates water, lava, fire and other moving textures.",
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.": "World pixels per screen pixel. Below 100% is faster on the GPU.",
|
||||
"Raises Resolution Scale toward 100% while the frame rate allows it.": "Raises Resolution Scale toward 100% while the frame rate allows it.",
|
||||
"The frame rate Dynamic Resolution keeps.": "The frame rate Dynamic Resolution keeps.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Renders the world smaller and rebuilds it with TAA. Faster, softer.",
|
||||
"Sharpens the upscaled picture. Higher: crisper edges, more shimmer.": "Sharpens the upscaled picture. Higher: crisper edges, more shimmer.",
|
||||
@@ -586,6 +590,8 @@
|
||||
"horizon.osk.type": "Type",
|
||||
"horizon.osk.symbols": "Symbols",
|
||||
"horizon.osk.close": "Close",
|
||||
"horizon.osk.new_line": "New Line",
|
||||
"horizon.osk.complete": "Complete",
|
||||
"horizon.extras.chat_animation.tooltip": "A new chat line slides in from the left while it fades in, instead of appearing whole.",
|
||||
"horizon.extras.chat_indicator.tooltip": "The coloured bar at the left of a chat line.",
|
||||
"horizon.extras.pack_browser.tooltip": "Select Resource Packs gets an Add button that searches Modrinth and installs a pack into your packs folder. Off, the game never contacts Modrinth.",
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
"horizon.options.gameplay_guide.basic": "พื้นฐาน",
|
||||
"horizon.options.gameplay_guide.full": "เต็ม",
|
||||
"horizon.options.gameplay_guide.off": "ปิด",
|
||||
"horizon.options.radial_mode": "เมนูวงล้อ",
|
||||
"horizon.options.radial_mode.about": "กดปุ่มเปิดค้างไว้เพื่อใช้เมนูวงล้อ หรือกดหนึ่งครั้งเพื่อเปิดและกดอีกครั้งเพื่อปิด",
|
||||
"horizon.options.radial_mode.hold.about": "กดปุ่มเปิดค้างไว้แล้วปล่อยเพื่อเรียกใช้คำสั่งที่อนาล็อกชี้อยู่",
|
||||
"horizon.options.radial_mode.toggle.about": "กดปุ่มเปิดเพื่อเปิดเมนู จากนั้นกดปุ่ม A หรือปุ่มเปิดอีกครั้งเพื่อเรียกใช้คำสั่งที่อนาล็อกชี้อยู่ หรือกดปุ่ม B เพื่อปิด",
|
||||
"horizon.options.radial_stick": "เลือกคำสั่งในเมนูวงล้อด้วย",
|
||||
"horizon.options.radial_stick.about": "ก้านอนาล็อกที่ใช้ชี้เลือกช่องในเมนูวงล้อ",
|
||||
"horizon.options.radial_stick.right": "อนาล็อกขวา",
|
||||
"horizon.options.radial_stick.left": "อนาล็อกซ้าย",
|
||||
"horizon.options.radial_stick.either": "ข้างใดก็ได้",
|
||||
"horizon.options.radial_stick.right.about": "ใช้อนาล็อกขวาชี้เลือกช่อง ส่วนอนาล็อกซ้ายยังคงใช้เดินตามปกติ",
|
||||
"horizon.options.radial_stick.left.about": "ใช้อนาล็อกซ้ายชี้เลือกช่องและคุณจะหยุดนิ่ง ปุ่ม D-pad ขวาจะอยู่ใต้นิ้วโป้งนั้น: เลือกปุ่มเปิดในหน้าเมนูวงล้อ",
|
||||
"horizon.options.radial_stick.either.about": "อนาล็อกข้างล่าสุดที่ดันจะใช้ชี้เลือกช่อง โดยจะไม่เดินและไม่หมุนมุมมอง มีเพียงปุ่ม Bumper, Trigger หรือปุ่ม View เท่านั้นที่สามารถใช้เปิดเมนูได้",
|
||||
"horizon.controller.guide.use": "ใช้",
|
||||
"horizon.controller.guide.open": "เปิด",
|
||||
"horizon.controller.guide.ride": "ขี่",
|
||||
@@ -129,6 +141,82 @@
|
||||
"horizon.pages.shader_options": "ตัวเลือกเชเดอร์",
|
||||
"horizon.pages.controller": "การตั้งค่าคอนโทรลเลอร์",
|
||||
"horizon.pages.controller_binds": "ตัวควบคุมผูก",
|
||||
"horizon.pages.controller_radial": "เมนูวงล้อ",
|
||||
"horizon.radial.page.general": "ทั่วไป",
|
||||
"horizon.radial.page.camera": "กล้อง",
|
||||
"horizon.radial.release_stick": "ปล่อยก้านอนาล็อก",
|
||||
"horizon.radial.action.key_inventory": "ช่องเก็บของ",
|
||||
"horizon.radial.action.key_chat": "แชต",
|
||||
"horizon.radial.action.key_command": "คำสั่ง",
|
||||
"horizon.radial.action.key_advancements": "ความก้าวหน้า",
|
||||
"horizon.radial.action.key_togglePerspective": "มุมมอง",
|
||||
"horizon.radial.action.key_swapOffhand": "สลับมือ",
|
||||
"horizon.radial.action.key_drop": "ทิ้งไอเทม",
|
||||
"horizon.radial.action.key_pickItem": "เลือกบล็อก",
|
||||
"horizon.radial.action.key_screenshot": "จับภาพหน้าจอ",
|
||||
"horizon.radial.action.key_toggleGui": "ซ่อน HUD",
|
||||
"horizon.radial.action.key_smoothCamera": "กล้องภาพยนตร์",
|
||||
"horizon.radial.action.key_socialInteractions": "การโต้ตอบทางสังคม",
|
||||
"horizon.radial.action.key_emote": "ท่าทาง (Emote)",
|
||||
"horizon.radial.action.pad_hotbarPrevious": "ช่องก่อนหน้า",
|
||||
"horizon.radial.action.pad_hotbarNext": "ช่องถัดไป",
|
||||
"horizon.radial.action.key_fullscreen": "เต็มจอ",
|
||||
"horizon.radial.action.key_quickActions": "คำสั่งด่วน",
|
||||
"horizon.radial.action.key_debug_overlay": "หน้าจอดีบัก",
|
||||
"horizon.radial.missing": "ไม่มีคำสั่ง",
|
||||
"horizon.radial.why.missing": "ไม่รู้จักคำสั่งนี้",
|
||||
"horizon.radial.why.cosmetics": "จำเป็นต้องเปิดใช้ของตกแต่ง",
|
||||
"horizon.radial.why.quick_actions": "เซิร์ฟเวอร์นี้ไม่มีคำสั่งด่วน",
|
||||
"horizon.radial.why.social": "ใช้ได้บนเซิร์ฟเวอร์เท่านั้น",
|
||||
"horizon.radial.next": "→ %s",
|
||||
"horizon.radial.state.first_person": "บุคคลที่หนึ่ง",
|
||||
"horizon.radial.state.third_person_back": "บุคคลที่สาม",
|
||||
"horizon.radial.state.third_person_front": "มุมมองด้านหน้า",
|
||||
"horizon.radial.state.shown": "แสดง",
|
||||
"horizon.radial.state.hidden": "ซ่อน",
|
||||
"horizon.radial.state.on": "เปิด",
|
||||
"horizon.radial.state.off": "ปิด",
|
||||
"horizon.radial.pick_title": "เลือกคำสั่ง",
|
||||
"horizon.radial.group.actions": "คำสั่ง",
|
||||
"horizon.radial.pick.slot": "ช่อง %s ของ %s",
|
||||
"horizon.radial.pick.about": "ใส่คำสั่งนี้ลงในช่องแล้วย้อนกลับ คำสั่งเดิมของช่องจะถูกทำเครื่องหมายไว้",
|
||||
"horizon.radial.pick.current": "ปัจจุบัน",
|
||||
"horizon.radial.missing.about": "เกมไม่รู้จักคำสั่ง %s: ช่องนี้จะยังคงเก็บคำสั่งเดิมไว้แต่จะไม่ทำงาน กดหรือกดขวาเพื่อเลือกคำสั่งอื่นแทน",
|
||||
"horizon.radial.page.new": "หน้าใหม่",
|
||||
"horizon.pages.open.controller_radial": "เมนูวงล้อ...",
|
||||
"horizon.radial.group.slots": "ช่อง",
|
||||
"horizon.radial.group.page": "หน้า",
|
||||
"horizon.radial.group.all": "ทุกหน้า",
|
||||
"horizon.radial.empty": "ว่าง",
|
||||
"horizon.radial.page_of": "หน้า %s จาก %s ช่องแต่ละช่องคือทิศทางของก้านอนาล็อกที่เลือกขณะกดปุ่มเปิดค้างไว้",
|
||||
"horizon.radial.slot.0": "บน",
|
||||
"horizon.radial.slot.1": "บน-ขวา",
|
||||
"horizon.radial.slot.2": "ขวา",
|
||||
"horizon.radial.slot.3": "ล่าง-ขวา",
|
||||
"horizon.radial.slot.4": "ล่าง",
|
||||
"horizon.radial.slot.5": "ล่าง-ซ้าย",
|
||||
"horizon.radial.slot.6": "ซ้าย",
|
||||
"horizon.radial.slot.7": "บน-ซ้าย",
|
||||
"horizon.radial.slot.about": "คำสั่งที่ทิศทางนี้จะเรียกใช้ กดเพื่อดูรายการคำสั่ง กดขวาเพื่อไปยังคำสั่งถัดไป กดซ้ายหรือ Shift-คลิกเพื่อไปยังคำสั่งก่อนหน้า",
|
||||
"horizon.radial.rename": "เปลี่ยนชื่อหน้า",
|
||||
"horizon.radial.rename.about": "ตั้งชื่อหน้านี้ตามต้องการ",
|
||||
"horizon.radial.name_title": "ตั้งชื่อหน้านี้",
|
||||
"horizon.radial.new_page.about": "เพิ่มหน้าว่างต่อท้ายหน้าสุดท้าย และเปิดแสดงหน้านั้น",
|
||||
"horizon.radial.new_page.full": "เมนูสามารถมีได้สูงสุด %s หน้า",
|
||||
"horizon.radial.delete_page": "ลบหน้า",
|
||||
"horizon.radial.delete_page.about": "ลบหน้านี้และช่องทั้งหมดทันที สามารถกดเลิกทำ (Undo) เพื่อนำกลับมาได้",
|
||||
"horizon.radial.delete_page.last": "เมนูต้องมีอย่างน้อยหนึ่งหน้า",
|
||||
"horizon.radial.reset": "รีเซ็ตทุกหน้า",
|
||||
"horizon.radial.reset.about": "กู้คืนหน้าทั่วไปและหน้ากล้อง และลบหน้าที่เหลือทันที สามารถกดเลิกทำ (Undo) เพื่อนำกลับมาได้",
|
||||
"horizon.radial.reset.default": "หน้าต่างๆ เป็นค่าเริ่มต้นอยู่แล้ว",
|
||||
"horizon.radial.opener": "ปุ่มเปิดเมนู",
|
||||
"horizon.radial.opener.about": "ปุ่มที่ใช้กดค้างเพื่อเปิดเมนู กดปุ่มนี้แล้วกดปุ่มบนคอนโทรลเลอร์ที่ต้องการใช้: ปุ่ม Bumper หรือ D-pad (ไม่ใช่ A, B, X, Y หรืออนาล็อกขวา ซึ่งใช้ควบคุมและยกเลิก)",
|
||||
"horizon.radial.opener.about.left": "ปุ่มที่ใช้กดค้างเพื่อเปิดเมนู กดปุ่มนี้แล้วกดปุ่มบนคอนโทรลเลอร์ที่ต้องการใช้: ปุ่ม Bumper, A, X หรือ Y (ไม่ใช่ D-pad หรืออนาล็อกซ้าย ซึ่งใช้ควบคุมทิศทาง และไม่ใช่ B ซึ่งใช้ยกเลิก)",
|
||||
"horizon.radial.opener.about.either": "ปุ่มที่ใช้กดค้างเพื่อเปิดเมนู กดปุ่มนี้แล้วกดปุ่มบนคอนโทรลเลอร์ที่ต้องการใช้: ปุ่ม Bumper, Trigger หรือ View (ทั้งสองนิ้วโป้งใช้บังคับทิศทาง และปุ่ม B ใช้ยกเลิก)",
|
||||
"horizon.radial.unbound": "ยังไม่ได้กำหนดปุ่มเปิดเมนู (ปุ่มที่อยู่ใต้นิ้วโป้งที่ใช้เลือกถูกปล่อยออก): กดปุ่มนี้ จากนั้นกดปุ่มบนคอนโทรลเลอร์",
|
||||
"horizon.radial.conflict": "ใช้ปุ่มซ้ำกับคำสั่งอื่น: ขณะเล่นเกมเมนูจะแย่งใช้ปุ่มนี้ กดเพื่อเลือกปุ่มอื่น",
|
||||
"Radial Menu": "เมนูวงล้อ",
|
||||
"Named pages of eight actions, held open with a button.": "หน้าคำสั่ง 8 ช่องพร้อมชื่อกำกับ เปิดใช้งานโดยการกดปุ่มค้างไว้",
|
||||
"horizon.pages.extras": "ส่วนเสริม Horizon",
|
||||
"horizon.pages.settings": "การตั้งค่า Horizon",
|
||||
"horizon.pages.studio": "สตูดิโอรีเพลย์",
|
||||
@@ -287,6 +375,7 @@
|
||||
"This feature is not yet available in Horizon.": "ฟีเจอร์นี้ยังไม่มีให้บริการใน Horizon",
|
||||
"Your display or system doesn't offer HDR output.": "จอแสดงผลหรือระบบของคุณไม่มีเอาต์พุต HDR",
|
||||
"Turn HDR on to use it.": "เปิด HDR เพื่อใช้งาน",
|
||||
"The shader pack lights the world itself.": "แพ็กเชเดอร์เป็นตัวจัดการแสงสว่างในโลกของเกมเอง",
|
||||
"Your GPU or graphics API doesn't support it.": "GPU หรือกราฟิก API ของคุณไม่รองรับ",
|
||||
"Not with software rendering, which runs it on the CPU.": "ใช้ไม่ได้กับการเรนเดอร์ด้วยซอฟต์แวร์ ซึ่งรันบน CPU",
|
||||
"Out of video memory: off until the next start.": "หน่วยความจำวิดีโอหมด: ปิดจนกว่าจะเริ่มครั้งถัดไป",
|
||||
@@ -310,6 +399,7 @@
|
||||
"Lifts shadows onto your display's darkest black. 0: true black, no lift.": "ยกเงาไปยังสีดำที่มืดที่สุดของจอคุณ 0: ดำสนิท ไม่ยก",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "ไฮไลท์จะสว่างขนาดไหน อัตโนมัติ: จุดสูงสุดที่ระบบของคุณรายงาน",
|
||||
"How bright white looks in the world. Auto: your desktop's white.": "สีขาวสว่างแค่ไหนในโลก อัตโนมัติ: สีขาวของเดสก์ท็อปคุณ",
|
||||
"How bright the HUD and menus are. Auto: at paper white.": "ความสว่างของ HUD และเมนู อัตโนมัติ: สว่างเท่ากระดาษขาว",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "ฝูงชน พืชผล และเครื่องจักรยังคงทำงานต่อไปได้ไกลแค่ไหน เวลาซีพียู",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "จะมีการดึง mobs และไอเท็มออกไปไกลแค่ไหนซึ่งเป็นเรื่องปกติ",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "ข้ามภูมิประเทศที่ซ่อนอยู่หลังส่วนที่ปิด ออกค่าใช้จ่ายเยอะมาก",
|
||||
@@ -443,6 +533,7 @@
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "ตั้งค่าการแสดงผลของคุณเรียบร้อยแล้ว ปรับเทียบเฉพาะในกรณีที่ HDR ดูผิดปกติ",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "ปรับความสว่างสูงสุดและสีขาวกระดาษกลับไปที่การตั้งค่าจอแสดงผลของคุณ",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "ตั้งค่าสำหรับจอแสดงผลของคุณ ปล่อยให้เป็นอัตโนมัติเว้นแต่จะดูผิด",
|
||||
"HDR output stopped the game the last time it started with this graphics driver. Choose a mode to try again.": "การแสดงผล HDR ทำให้เกมหยุดทำงานในครั้งล่าสุดที่เปิดด้วยไดรเวอร์กราฟิกนี้ โปรดเลือกโหมดอื่นเพื่อลองใหม่อีกครั้ง",
|
||||
"Lower it until the logo just disappears into the black.": "ลดระดับลงจนกระทั่งโลโก้หายไปจนกลายเป็นสีดำ",
|
||||
"Raise it until the logo just disappears into the bright square.": "ยกขึ้นจนกระทั่งโลโก้หายไปในสี่เหลี่ยมจัตุรัสสว่างๆ",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "ทำให้การ์ดเป็นสีขาวที่มองได้สบายตาเป็นเวลานานในแสงที่คุณเล่น",
|
||||
@@ -586,6 +677,23 @@
|
||||
"Shaders": "เชเดอร์",
|
||||
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "แพ็กเชเดอร์ของ Horizon และจากโฟลเดอร์ shaders/ พร้อมตัวเลือกของมัน",
|
||||
"Shader packs": "แพ็กเชเดอร์",
|
||||
"Installed": "ติดตั้งแล้ว",
|
||||
"Apply": "นำไปใช้",
|
||||
"Reset all options": "รีเซ็ตตัวเลือกทั้งหมด",
|
||||
"A Horizon shader pack.": "แพ็กเชเดอร์ของ Horizon",
|
||||
"An OptiFine or Iris shader pack.": "แพ็กเชเดอร์ของ OptiFine หรือ Iris",
|
||||
"In use: press to turn shaders off.": "กำลังใช้งาน: กดเพื่อปิดเชเดอร์",
|
||||
"Press to use it.": "กดเพื่อใช้งาน",
|
||||
"Put shader pack .zip files or folders in it.": "ใส่ไฟล์ .zip หรือโฟลเดอร์ของแพ็กเชเดอร์ลงไป",
|
||||
"Puts every option of this pack back to its default.": "คืนค่าทุกตัวเลือกของแพ็กนี้เป็นค่าเริ่มต้น",
|
||||
"Sets many options at once, as the pack groups them.": "ตั้งหลายตัวเลือกพร้อมกัน ตามที่แพ็กจัดกลุ่มไว้",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "ยังไม่มีแพ็กเชเดอร์: เปิดโฟลเดอร์แพ็กแล้วใส่เข้าไปสักหน่อย",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "เลือกแพ็กเชเดอร์ การเลือกแพ็กที่กำลังใช้อยู่จะปิดเชเดอร์",
|
||||
"Changed options apply with Apply, or when you leave.": "ตัวเลือกที่เปลี่ยนจะมีผลเมื่อกดนำไปใช้ หรือเมื่อออกจากหน้านี้",
|
||||
"Pack settings": "การตั้งค่าของแพ็ก",
|
||||
"Built-in settings": "การตั้งค่าในตัว",
|
||||
"%s settings": "การตั้งค่าของ %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "การตั้งค่าที่ผู้สร้างแพ็กทำไว้: เปลี่ยนเฉพาะแพ็กนี้เท่านั้น",
|
||||
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "พรีเซ็ตของวานิลลาเอง: ตั้งเฉพาะตัวเลือกของวานิลลา อย่างที่ Minecraft ทำ",
|
||||
"Fullscreen takes the display over, at the mode below.": "เต็มหน้าจอจะยึดจอแสดงผล ตามโหมดด้านล่าง",
|
||||
"The display mode exclusive fullscreen uses. Applies when you leave.": "โหมดจอแสดงผลที่เต็มหน้าจอแบบเอกซ์คลูซีฟใช้ มีผลเมื่อคุณออก",
|
||||
@@ -693,23 +801,6 @@
|
||||
"horizon.extras.cosmetics.shown": "แสดงแล้ว",
|
||||
"horizon.extras.cosmetics.hidden": "ซ่อนเร้น",
|
||||
"horizon.extras.cosmetics.name_tags": "ป้ายชื่อเท่านั้น",
|
||||
"Installed": "ติดตั้งแล้ว",
|
||||
"Apply": "นำไปใช้",
|
||||
"Reset all options": "รีเซ็ตตัวเลือกทั้งหมด",
|
||||
"A Horizon shader pack.": "แพ็กเชเดอร์ของ Horizon",
|
||||
"An OptiFine or Iris shader pack.": "แพ็กเชเดอร์ของ OptiFine หรือ Iris",
|
||||
"In use: press to turn shaders off.": "กำลังใช้งาน: กดเพื่อปิดเชเดอร์",
|
||||
"Press to use it.": "กดเพื่อใช้งาน",
|
||||
"Put shader pack .zip files or folders in it.": "ใส่ไฟล์ .zip หรือโฟลเดอร์ของแพ็กเชเดอร์ลงไป",
|
||||
"Puts every option of this pack back to its default.": "คืนค่าทุกตัวเลือกของแพ็กนี้เป็นค่าเริ่มต้น",
|
||||
"Sets many options at once, as the pack groups them.": "ตั้งหลายตัวเลือกพร้อมกัน ตามที่แพ็กจัดกลุ่มไว้",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "ยังไม่มีแพ็กเชเดอร์: เปิดโฟลเดอร์แพ็กแล้วใส่เข้าไปสักหน่อย",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "เลือกแพ็กเชเดอร์ การเลือกแพ็กที่กำลังใช้อยู่จะปิดเชเดอร์",
|
||||
"Changed options apply with Apply, or when you leave.": "ตัวเลือกที่เปลี่ยนจะมีผลเมื่อกดนำไปใช้ หรือเมื่อออกจากหน้านี้",
|
||||
"Pack settings": "การตั้งค่าของแพ็ก",
|
||||
"Built-in settings": "การตั้งค่าในตัว",
|
||||
"%s settings": "การตั้งค่าของ %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "การตั้งค่าที่ผู้สร้างแพ็กทำไว้: เปลี่ยนเฉพาะแพ็กนี้เท่านั้น",
|
||||
"horizon.moderation.duration.minute": "%s นาที",
|
||||
"horizon.moderation.duration.minutes": "%s นาที",
|
||||
"horizon.moderation.duration.hour": "%s ชั่วโมง",
|
||||
@@ -1016,6 +1107,7 @@
|
||||
"Press the button for this. %s cancels.": "กดปุ่มสำหรับสิ่งนี้ %s ยกเลิก",
|
||||
"This button also does: %s": "ปุ่มนี้ยังทำ: %s",
|
||||
"Not bound: press this row, then the button that should do it.": "ยังไม่ผูก: กดแถวนี้ แล้วกดปุ่มที่ควรทำสิ่งนั้น",
|
||||
"horizon.pad.action.radial": "เมนูวงล้อ",
|
||||
"horizon.pad.notice.connected.press": "กดเพื่อใช้งาน",
|
||||
"horizon.recipe_transfer.missing": "ไอเทมที่ขาด",
|
||||
"horizon.recipe_transfer.no_room": "คลังเต็ม",
|
||||
@@ -1053,5 +1145,8 @@
|
||||
"horizon.studio.topbar.add_marker": "เพิ่มเครื่องหมาย",
|
||||
"horizon.studio.topbar.clear_keyframes": "🗑 ล้างคีย์เฟรม",
|
||||
"horizon.studio.topbar.timeline_hud": "⏱ ไทม์ไลน์",
|
||||
"horizon.studio.topbar.split_scene": "แยกฉาก"
|
||||
"horizon.studio.topbar.split_scene": "แยกฉาก",
|
||||
"horizon.recipes.fuel.options": "กด SHIFT ค้างไว้เพื่อตรึงคำแนะนำนี้ จากนั้นคลิกที่วัตถุดิบเพื่อดูสูตร",
|
||||
"horizon.recipes.fuel.any": "เชื้อเพลิงใดก็ได้",
|
||||
"horizon.recipes.fuel.minecraft": "Minecraft"
|
||||
}
|
||||
@@ -227,7 +227,7 @@ fn entity_state(mob: &mut Mob, species: Species, case: &Value) {
|
||||
mob.anim.flap_speed_o = num("flapSpeed");
|
||||
}
|
||||
Species::Wolf => {
|
||||
mob.extra.sitting = flag("isSitting");
|
||||
mob.extra.sitting_pose = flag("isSitting");
|
||||
mob.extra.anger = if flag("isAngry") { 100 } else { 0 };
|
||||
mob.anim.v[tame::SHAKE] = num("shakeAnim");
|
||||
mob.anim.vo[tame::SHAKE] = num("shakeAnim");
|
||||
|
||||
@@ -13,7 +13,10 @@ use horizon_client::{
|
||||
},
|
||||
random_sequences::RandomSequences,
|
||||
};
|
||||
use horizon_core::{geometry::Vec3, random::LegacyRandom};
|
||||
use horizon_core::{
|
||||
geometry::Vec3,
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
use horizon_world::loot::{LootDamage, LootData, LootEntity, LootStack, PackOverlay};
|
||||
use std::{collections::BTreeMap, fmt::Write as _, fs, path::Path, sync::Arc};
|
||||
|
||||
@@ -203,7 +206,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
}
|
||||
}
|
||||
m.extra.drop_chances = Some(Box::new(chances));
|
||||
m.random = LegacyRandom::new(rand + k);
|
||||
m.random.set_seed(rand + k);
|
||||
// The killer, the last player to hurt the victim and the damage source.
|
||||
let killer_id = get("killer", "none");
|
||||
let weapon = c.get("weapon").map(|w| stack(w));
|
||||
|
||||
@@ -108,7 +108,7 @@ impl FinalizeLevel for Level<'_> {
|
||||
let index = self.created;
|
||||
self.created += 1;
|
||||
let mut m = Mob::new(index as u32 + 1, t, Vec3::ZERO, 0.0);
|
||||
m.random = LegacyRandom::new(self.eseed.wrapping_add(index as i64));
|
||||
m.random.set_seed(self.eseed.wrapping_add(index as i64));
|
||||
Some(m)
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 496 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user