mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 22:43:47 -10:00
Compare commits
496
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aedfd10c93 | ||
|
|
f64f181d1d | ||
|
|
fe020bb113 | ||
|
|
0ae694bb66 | ||
|
|
25b7c04314 | ||
|
|
1a8589a0dd | ||
|
|
7176e0df23 | ||
|
|
fad4b0f5dd | ||
|
|
8ca3dc19dd | ||
|
|
26ee23d376 | ||
|
|
fe524fefb2 | ||
|
|
bb26451389 | ||
|
|
2e2635f83d | ||
|
|
aee2c83c2f | ||
|
|
6ef71901bf | ||
|
|
da954fba54 | ||
|
|
4f54fcf47b | ||
|
|
22e6e5c8a4 | ||
|
|
d77d31fff1 | ||
|
|
2c04e1ba85 | ||
|
|
edddce4485 | ||
|
|
6ebe4a33c3 | ||
|
|
4018f7925e | ||
|
|
cbb2b99022 | ||
|
|
7fdce634b4 | ||
|
|
7d084fc8aa | ||
|
|
edb0905d0f | ||
|
|
f6dda117a8 | ||
|
|
5c0ac57986 | ||
|
|
a12e378c46 | ||
|
|
c1e81319cb | ||
|
|
7232e9f6bd | ||
|
|
fe63866acc | ||
|
|
4c66382f4e | ||
|
|
604e9cb82f | ||
|
|
205a2387c3 | ||
|
|
1d56d438df | ||
|
|
7a87965ec8 | ||
|
|
1334feb5fa | ||
|
|
fb75635ec3 | ||
|
|
af4a313bc7 | ||
|
|
9b0c6f48ea | ||
|
|
f030d9c807 | ||
|
|
bf3191ed83 | ||
|
|
631eef433b | ||
|
|
7ba6d345b0 | ||
|
|
2d29c7f8e9 | ||
|
|
76bfbf1ad8 | ||
|
|
87b5ae27a6 | ||
|
|
28acf8303c | ||
|
|
2a66276bcc | ||
|
|
38f9ce31e7 | ||
|
|
67f84002d3 | ||
|
|
97f4aa5e7f | ||
|
|
3b89ded9f9 | ||
|
|
5c9bf7ad11 | ||
|
|
de12dbd275 | ||
|
|
957475f644 | ||
|
|
33e0bf2aec | ||
|
|
dca25b7662 | ||
|
|
964e31a3c3 | ||
|
|
da4e8d8d49 | ||
|
|
efe5e70188 | ||
|
|
901e640591 | ||
|
|
e7c443cdf6 | ||
|
|
baf9a32a0e | ||
|
|
854a5dc246 | ||
|
|
8c672e7f9e | ||
|
|
4a1f7dfad4 | ||
|
|
e6d44af84e | ||
|
|
23ea8d66a6 | ||
|
|
8abbc6b9e4 | ||
|
|
f2fe9c1dfe | ||
|
|
6a11db7ef7 | ||
|
|
7a5d306949 | ||
|
|
66d06e23f7 | ||
|
|
417677fdda | ||
|
|
8721db0e28 | ||
|
|
190c17fc7a | ||
|
|
9e1abc64d1 | ||
|
|
d1782f8954 | ||
|
|
00c184e250 | ||
|
|
c4b55f1113 | ||
|
|
7054def60b | ||
|
|
776a3b08a0 | ||
|
|
f79643821e | ||
|
|
3b52978005 | ||
|
|
f4c7a94ad6 | ||
|
|
252f02e02a | ||
|
|
1d267df0b3 | ||
|
|
9651982e21 | ||
|
|
3e3a0ffa10 | ||
|
|
11a7d2cd89 | ||
|
|
2415fb9e91 | ||
|
|
6f395191c2 | ||
|
|
01eefdcc63 | ||
|
|
cafee2f14d | ||
|
|
bd8e0da359 | ||
|
|
bff8ecf203 | ||
|
|
affb2b6924 | ||
|
|
d450bc3b5d | ||
|
|
3ac68d69d3 | ||
|
|
22c6c667a2 | ||
|
|
d91706378e | ||
|
|
92e96819f5 | ||
|
|
f9ed804bb7 | ||
|
|
2394ee861d | ||
|
|
f54f5c41b6 | ||
|
|
048eb18683 | ||
|
|
ace3883402 | ||
|
|
86e57cb74c | ||
|
|
33c96ac1f2 | ||
|
|
2bc3726b4e | ||
|
|
3a05ee18c6 | ||
|
|
e1f9a8c096 | ||
|
|
0b2270dd45 | ||
|
|
4dbb00502f | ||
|
|
6c75463502 | ||
|
|
c471036d29 | ||
|
|
41de65db6e | ||
|
|
1c15d2b68c | ||
|
|
0eb8b8ec26 | ||
|
|
1d0621ac38 | ||
|
|
1ebc813852 | ||
|
|
59994ad2a2 | ||
|
|
9a62e99376 | ||
|
|
0b10ddd665 | ||
|
|
406ac72d0a | ||
|
|
e2116fb8c2 | ||
|
|
acb17d5694 | ||
|
|
65a2bf4613 | ||
|
|
eb69b3986b | ||
|
|
389224abec | ||
|
|
3bea613053 | ||
|
|
a9d293cb18 | ||
|
|
cd8ebdff23 | ||
|
|
6edc5de20f | ||
|
|
314edb02fc | ||
|
|
5e2b6c675f | ||
|
|
01c53f23d2 | ||
|
|
40aed431b6 | ||
|
|
4c3cb9a51b | ||
|
|
2052b918ec | ||
|
|
c23236bceb | ||
|
|
4ca78266a5 | ||
|
|
cf9569747b | ||
|
|
423abdd863 | ||
|
|
8f0a7f4494 | ||
|
|
4b38881802 | ||
|
|
7daecbbce2 | ||
|
|
1f5cd1e43b | ||
|
|
d6be40bbf7 | ||
|
|
e020e1d440 | ||
|
|
01edf84d9f | ||
|
|
8540619240 | ||
|
|
97020c6abe | ||
|
|
0a9ce82a0b | ||
|
|
9dda4ce381 | ||
|
|
4d91ddc86c | ||
|
|
e1ae4fb750 | ||
|
|
d0dc8f1ad6 | ||
|
|
8ac6fa92a1 | ||
|
|
b1ebe3d6e0 | ||
|
|
8485f4c0ef | ||
|
|
f38098f921 | ||
|
|
87bb072d77 | ||
|
|
c7389334f1 | ||
|
|
733d34582d | ||
|
|
aac6bc2c2a | ||
|
|
af6fc73a51 | ||
|
|
8bf4597a57 | ||
|
|
cc3d62a7cd | ||
|
|
3042d4068b | ||
|
|
e1f01aff0b | ||
|
|
9c24961df0 | ||
|
|
bbabaf720b | ||
|
|
1e1c77126d | ||
|
|
933ab57aa1 | ||
|
|
c5067a988b | ||
|
|
3936cafd12 | ||
|
|
3b6d2fc635 | ||
|
|
bd6025952f | ||
|
|
98abaa9217 | ||
|
|
c6eef4b0b7 | ||
|
|
1fb469a969 | ||
|
|
7adf3992a5 | ||
|
|
8629067aa4 | ||
|
|
9f6d28e257 | ||
|
|
ec2c0c8d1f | ||
|
|
358c4e0c01 | ||
|
|
90b4e7d794 | ||
|
|
989afe1c00 | ||
|
|
0dc90c1d29 | ||
|
|
3e4f2a7305 | ||
|
|
1c9380fa56 | ||
|
|
6e5cd517d7 | ||
|
|
5a0906f5f4 | ||
|
|
b9404f32c3 | ||
|
|
951fe53102 | ||
|
|
7abea17aa3 | ||
|
|
0f56e3165a | ||
|
|
3943b0e8a7 | ||
|
|
f7a3c0f2e4 | ||
|
|
92b1ae1132 | ||
|
|
316752b706 | ||
|
|
aa3919d3ae | ||
|
|
a1afb474a5 | ||
|
|
fdb6ad8b8f | ||
|
|
7f505e07dd | ||
|
|
98b4d049fa | ||
|
|
32faed9621 | ||
|
|
af61f6751e | ||
|
|
a4df995446 | ||
|
|
6c2498236d | ||
|
|
7ddbb41f86 | ||
|
|
4b38e08b7f | ||
|
|
5c00a74d4e | ||
|
|
858f248202 | ||
|
|
290560bc52 | ||
|
|
30c20a7a54 | ||
|
|
f96d9e89b5 | ||
|
|
a45b450e56 | ||
|
|
e6281842d8 | ||
|
|
842c094de7 | ||
|
|
fc55599794 | ||
|
|
12af27a781 | ||
|
|
bca5833f8d | ||
|
|
ca0da4d9bb | ||
|
|
4a8a101391 | ||
|
|
07aab11e71 | ||
|
|
fdd3d1e59c | ||
|
|
bc21da03bc | ||
|
|
93fc4b2ba9 | ||
|
|
f3cc9257df | ||
|
|
8c1f6115d5 | ||
|
|
069cc39f05 | ||
|
|
0a407714e0 | ||
|
|
3bb4e7827c | ||
|
|
546eab04b7 | ||
|
|
a9fba955bd | ||
|
|
e74f789659 | ||
|
|
e02c201c97 | ||
|
|
139e83fff8 | ||
|
|
1d6e5d01b5 | ||
|
|
2adedf5570 | ||
|
|
4e6920a319 | ||
|
|
c214f59711 | ||
|
|
5e7e654901 | ||
|
|
7e4e421224 | ||
|
|
719fa5e2bd | ||
|
|
485323aaaf | ||
|
|
4a374440f4 | ||
|
|
b99e2087d3 | ||
|
|
b84c97088f | ||
|
|
57da7aecbc | ||
|
|
e93afc5ade | ||
|
|
a7296b75e0 | ||
|
|
9d25e30101 | ||
|
|
4543e2f454 | ||
|
|
36584650ea | ||
|
|
aee2c63c3b | ||
|
|
ef23545cef | ||
|
|
92c708b3fe | ||
|
|
c6f935414d | ||
|
|
73231e735d | ||
|
|
a71408736f | ||
|
|
fb4805072f | ||
|
|
111c22dd5e | ||
|
|
ce08c38337 | ||
|
|
8d2900e6ce | ||
|
|
cd1318bf02 | ||
|
|
2f661c08ed | ||
|
|
1bf8766c1d | ||
|
|
9f92bb23b4 | ||
|
|
64f4bab6f3 | ||
|
|
52d1a0b475 | ||
|
|
c0fe9adca6 | ||
|
|
992ea05825 | ||
|
|
2e11de8943 | ||
|
|
7351ad6d91 | ||
|
|
e949fd4cc1 | ||
|
|
c1cdeb19d4 | ||
|
|
b03127ab97 | ||
|
|
1f9493c4fc | ||
|
|
9524516752 | ||
|
|
a465c3efae | ||
|
|
904b3036ab | ||
|
|
ebf06768f1 | ||
|
|
fd03679d85 | ||
|
|
27ed9c66f7 | ||
|
|
738861aae2 | ||
|
|
c7ff85fdf4 | ||
|
|
175c2d453a | ||
|
|
673d5cff49 | ||
|
|
ca896819d7 | ||
|
|
0a8df72a16 | ||
|
|
074aa1124f | ||
|
|
bf321a8425 | ||
|
|
2fdc1509ae | ||
|
|
703c0ea18b | ||
|
|
d15f455027 | ||
|
|
23f0fd6675 | ||
|
|
8b583a15e1 | ||
|
|
48782a8231 | ||
|
|
d2d9dd7ae1 | ||
|
|
c0a3af67b0 | ||
|
|
acf0cabce9 | ||
|
|
72e6ef91db | ||
|
|
ab069b576c | ||
|
|
f521237d6c | ||
|
|
18bbc46240 | ||
|
|
a75a57bd18 | ||
|
|
aed804a26d | ||
|
|
8ce0848a31 | ||
|
|
48c080e387 | ||
|
|
02e546660a | ||
|
|
46e77b05a8 | ||
|
|
3c73be2517 | ||
|
|
917ce67f0d | ||
|
|
b11fe1da84 | ||
|
|
fdb23b8ac6 | ||
|
|
ac85e523c7 | ||
|
|
386ab14035 | ||
|
|
ca82b37eb0 | ||
|
|
a505caa32a | ||
|
|
9aa5c9fe4a | ||
|
|
c7a2daa593 | ||
|
|
3584268d11 | ||
|
|
bf1070413f | ||
|
|
2573d1c76a | ||
|
|
3a967d8838 | ||
|
|
000e13fbe6 | ||
|
|
bd967852e5 | ||
|
|
ecdecc8fd2 | ||
|
|
c4c084be94 | ||
|
|
6c5d89ffac | ||
|
|
6e72e2de7d | ||
|
|
8b0028bbeb | ||
|
|
e69f6a5050 | ||
|
|
7c34cfa6b2 | ||
|
|
c6f52257e2 | ||
|
|
07d0cfc344 | ||
|
|
949eef6772 | ||
|
|
ecce33154e | ||
|
|
d6f88d2483 | ||
|
|
0cdc4a49da | ||
|
|
68459f0442 | ||
|
|
2877c7fbfa | ||
|
|
5be2aba2ae | ||
|
|
6b371cdc48 | ||
|
|
1b63e0abc2 | ||
|
|
9ab82c59cd | ||
|
|
e0cbbdb50a | ||
|
|
56ec50a875 | ||
|
|
066b6e9cef | ||
|
|
172d7c51e6 | ||
|
|
5cf4a1dec8 | ||
|
|
4bf9b604f7 | ||
|
|
a9612bdabc | ||
|
|
a20d3296b4 | ||
|
|
eadbb41095 | ||
|
|
bc59734a29 | ||
|
|
626c60740d | ||
|
|
0bd5a4f201 | ||
|
|
60fff3987f | ||
|
|
da4d26b00d | ||
|
|
342315073f | ||
|
|
de0132b638 | ||
|
|
30f1fc145d | ||
|
|
f509e284bf | ||
|
|
3d5216e717 | ||
|
|
14ba691e53 | ||
|
|
877cb37978 | ||
|
|
12273a4e8e | ||
|
|
34194ab7ec | ||
|
|
7803f1351e | ||
|
|
c9b07b1ffb | ||
|
|
ec77214342 | ||
|
|
bec6ac6448 | ||
|
|
a2b3f669ef | ||
|
|
7bfecd3fd1 | ||
|
|
43ef30f98c | ||
|
|
ce4f0d7074 | ||
|
|
4ba236928d | ||
|
|
34c5bb84e6 | ||
|
|
e809bed768 | ||
|
|
031037cee5 | ||
|
|
70342ed68b | ||
|
|
98d7553124 | ||
|
|
ef6fc399f5 | ||
|
|
94aeeac20d | ||
|
|
7f51885a85 | ||
|
|
5ecd155b76 | ||
|
|
5fe374444f | ||
|
|
c3f7e94775 | ||
|
|
ff50de2553 | ||
|
|
d4ea299fdd | ||
|
|
5383881562 | ||
|
|
1c402c47a7 | ||
|
|
6e5f7e88f7 | ||
|
|
75dd0dde7d | ||
|
|
8c4e3bb507 | ||
|
|
dabae908cd | ||
|
|
211b53f598 | ||
|
|
2d94afaa61 | ||
|
|
14a5193b41 | ||
|
|
00d050bb8f | ||
|
|
7225a84b10 | ||
|
|
40afd73cd8 | ||
|
|
c54c8c0f42 | ||
|
|
939d492535 | ||
|
|
41934f3d93 | ||
|
|
85a6a8b9dc | ||
|
|
4ca3d55e78 | ||
|
|
b97f0f61d7 | ||
|
|
0c40da1a82 | ||
|
|
e01adc67e3 | ||
|
|
32137ae44f | ||
|
|
9503a3cc7d | ||
|
|
7c4a6d1ea6 | ||
|
|
3c4d936707 | ||
|
|
44f5a37e64 | ||
|
|
5ffd5c6515 | ||
|
|
8cd7616054 | ||
|
|
ea99e6572c | ||
|
|
c5dbb620c2 | ||
|
|
642ac0d489 | ||
|
|
92bedb26d0 | ||
|
|
721d4cfa44 | ||
|
|
78b21e4afb | ||
|
|
26496db7c3 | ||
|
|
d5f931ca64 | ||
|
|
a08827d6d5 | ||
|
|
c0806b9ee3 | ||
|
|
70bc17720c | ||
|
|
25ea320338 | ||
|
|
6fa7f8896a | ||
|
|
94ed2f4cc8 | ||
|
|
517f54d076 | ||
|
|
9626c7446e | ||
|
|
da2b437f49 | ||
|
|
6e0f482e83 | ||
|
|
38a1babc5a | ||
|
|
3e5e8c4841 | ||
|
|
ed1ce7495e | ||
|
|
d1f8cccb7c | ||
|
|
60a2c3f164 | ||
|
|
4fa237a12e | ||
|
|
aaa674f54b | ||
|
|
e2367a4b6a | ||
|
|
408e2606d8 | ||
|
|
fb3f1bce90 | ||
|
|
833bb5cab4 | ||
|
|
5d9d9fb3ab | ||
|
|
65e2abae80 | ||
|
|
dddf351a05 | ||
|
|
570d53a780 | ||
|
|
8db7c3ddc0 | ||
|
|
4cc2a04c25 | ||
|
|
ce25d100ef | ||
|
|
8b577fc24c | ||
|
|
974305151d | ||
|
|
e6941baa5a | ||
|
|
d72b2a555b | ||
|
|
7cd9dbc7a2 | ||
|
|
6d1746d729 | ||
|
|
2dd6879bc4 | ||
|
|
8cd009e149 | ||
|
|
ebfcad98f6 | ||
|
|
e16378accd | ||
|
|
282a7005c8 | ||
|
|
381c5a1e62 | ||
|
|
6a634e49e2 | ||
|
|
2f7a46dfd3 | ||
|
|
dfe4c2cb51 | ||
|
|
3ed00f4071 | ||
|
|
1066efbafb | ||
|
|
2688eb16d6 | ||
|
|
a0c5f26cc2 | ||
|
|
ac4859eab4 | ||
|
|
087c763432 | ||
|
|
d3e7858cda | ||
|
|
6d5c7c51a6 | ||
|
|
dd4528f4e4 | ||
|
|
ab6900b4c9 | ||
|
|
58e32ea813 | ||
|
|
4304cc44b5 | ||
|
|
96c7d68d0d | ||
|
|
5988321440 | ||
|
|
47dc19fa20 | ||
|
|
9141f1d538 | ||
|
|
a33e5b574a | ||
|
|
78cf6e36a0 | ||
|
|
bd991e7917 | ||
|
|
18f1a16d12 | ||
|
|
c7fc74cb26 |
No files matched your search
@@ -168,7 +168,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
|
||||
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server`, `horizon-anticheat` (L: `lan/anticheat.rs`) |
|
||||
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities), `world_session.rs` (opening and closing a world); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
|
||||
| Input | game.rs `fn window_event`; G: `play_keys.rs` (`fn key_pressed`), `play_input.rs` (`fn play_mouse`, toggled keys, the hotbar wheel); crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
|
||||
| Input | game.rs `fn window_event`; G: `play_keys.rs` (`fn key_pressed`), `play_input.rs` (`fn play_mouse`, toggled keys, the hotbar wheel); crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs`, `pad_craft.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
|
||||
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
|
||||
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `mobs/cem.rs` + `mobs/cem_anim.rs` (OptiFine/EMF entity models and their animations); `horizon-client-core/src/lang.rs`; crates `horizon-assets`, `horizon-optifine`; G: `packs.rs` |
|
||||
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
|
||||
|
||||
@@ -309,3 +309,11 @@ Permission to use, copy, modify, and distribute this
|
||||
software is freely granted, provided that this notice
|
||||
is preserved.
|
||||
```
|
||||
|
||||
## Starling
|
||||
|
||||
`crates/horizon-client-render/src/touch/layout.rs` is adapted from the touch layout of
|
||||
[Starling](https://github.com/starling-client/starling), an Android port of Horizon, at commit
|
||||
[150a414b09a1](https://github.com/starling-client/starling/tree/150a414b09a18c900489d9462a7fc035dae744b7)
|
||||
(`android` branch), written mostly by Vasysik. Starling is licensed under the same MIT license as
|
||||
Horizon (Copyright (c) 2026 Ludovic Marie and the Horizon contributors).
|
||||
@@ -412,10 +412,11 @@ pub fn to_text_with(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let translated = Text::translate(key, &args);
|
||||
// TranslatableContents.decompose: Language.getOrDefault(key, fallback), so the
|
||||
// fallback is the template itself, never looked up as another key.
|
||||
match o.get("fallback").and_then(Value::as_str) {
|
||||
Some(fallback) if translated.plain() == key => Text::translate(fallback, &args),
|
||||
_ => translated,
|
||||
Some(fallback) => Text::translate_with_fallback(key, fallback, &args),
|
||||
None => Text::translate(key, &args),
|
||||
}
|
||||
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
|
||||
resolve(sel)
|
||||
@@ -513,6 +514,43 @@ pub fn name_text(tag: &Tag) -> Text {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lang::{self, Language, ResourceSource};
|
||||
|
||||
struct Mem(std::collections::HashMap<String, Vec<u8>>);
|
||||
impl ResourceSource for Mem {
|
||||
fn read(&self, path: &str) -> Option<Vec<u8>> {
|
||||
self.0.get(path).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// TranslatableContents.decompose: Language.getOrDefault(key, fallback). A missing key
|
||||
/// shows the fallback as written, even when the fallback is another key the language has.
|
||||
#[test]
|
||||
fn a_translatable_fallback_stays_literal() {
|
||||
let pack = Mem([(
|
||||
"assets/minecraft/lang/en_us.json".to_owned(),
|
||||
br#"{"real.key": "Translated", "greet": "Hello %s"}"#.to_vec(),
|
||||
)]
|
||||
.into());
|
||||
let ((), _) = lang::scoped(Language::load(&[&pack], "en_us"), || {
|
||||
let parse = |v: Value| to_text(&v, &mut |_| Text::empty()).plain();
|
||||
let missing = serde_json::json!({"translate": "missing.key", "fallback": "real.key"});
|
||||
assert_eq!(parse(missing), "real.key");
|
||||
// The fallback is a template: its arguments are filled in.
|
||||
let args = serde_json::json!({"translate": "missing.key", "fallback": "Hi %s", "with": ["Steve"]});
|
||||
assert_eq!(parse(args), "Hi Steve");
|
||||
// A key the language has wins over its fallback.
|
||||
let found =
|
||||
serde_json::json!({"translate": "greet", "fallback": "real.key", "with": ["Alex"]});
|
||||
assert_eq!(parse(found), "Hello Alex");
|
||||
// A component built with the constructor keeps its meaning through its JSON.
|
||||
let built = Text::translate_with_fallback("missing.key", "real.key", &[]);
|
||||
let again = to_text(&built.component(), &mut |_| Text::empty());
|
||||
assert_eq!(again.plain(), built.plain());
|
||||
assert_eq!(again.component(), built.component());
|
||||
});
|
||||
}
|
||||
|
||||
/// SnbtGrammar: a compound or list may end with one comma; two in a row
|
||||
/// stay an error.
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! NBT paths (`NbtPathArgument`): `foo.bar[0]`, `[]`, `list[{id:1}]`,
|
||||
//! `{root:match}`, with get, set, insert and remove on a tag tree, and the
|
||||
//! partial NBT comparison (`NbtUtils.compareNbt`) selectors and conditions use.
|
||||
pub use horizon_world::nbt::compare_nbt;
|
||||
use horizon_world::nbt::{Compound, JavaString, Tag, parse_snbt};
|
||||
|
||||
/// An error carrying the translation key and its arguments.
|
||||
@@ -47,26 +48,6 @@ fn get_child<'a>(c: &'a Compound, name: &str) -> Option<&'a Tag> {
|
||||
c.get(&key(name))
|
||||
}
|
||||
|
||||
/// `NbtUtils.compareNbt(expected, actual, true)`.
|
||||
pub fn compare_nbt(expected: &Tag, actual: Option<&Tag>) -> bool {
|
||||
let Some(actual) = actual else { return false };
|
||||
if std::mem::discriminant(expected) != std::mem::discriminant(actual) {
|
||||
return false;
|
||||
}
|
||||
match (expected, actual) {
|
||||
(Tag::Compound(e), Tag::Compound(a)) => {
|
||||
a.len() >= e.len() && e.iter().all(|(k, v)| compare_nbt(v, a.get(k)))
|
||||
}
|
||||
(Tag::List(e), Tag::List(a)) => {
|
||||
if e.is_empty() {
|
||||
return a.is_empty();
|
||||
}
|
||||
a.len() >= e.len() && e.iter().all(|x| a.iter().any(|y| compare_nbt(x, Some(y))))
|
||||
}
|
||||
_ => expected == actual,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- list-like tags ----------------------------------------------------------
|
||||
|
||||
/// The elements of a list or array tag.
|
||||
|
||||
@@ -89,6 +89,31 @@ pub struct Registries {
|
||||
pub trigger_objectives: Vec<String>,
|
||||
/// Objectives with the `trigger` criterion -> holder -> whether its score is locked.
|
||||
pub trigger_scores: BTreeMap<String, BTreeMap<String, bool>>,
|
||||
/// The `ConfiguredFeature` codec for an inline `minecraft:worldgen/feature`
|
||||
/// value (`ResourceOrIdArgument`), where the source has the server's live
|
||||
/// features; None leaves the decode to when the command runs.
|
||||
pub inline_feature: Option<InlineFeatureCheck>,
|
||||
}
|
||||
|
||||
/// A codec check of an inline value: its error message when it does not decode.
|
||||
#[derive(Clone)]
|
||||
pub struct InlineFeatureCheck(
|
||||
std::sync::Arc<dyn Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync>,
|
||||
);
|
||||
impl InlineFeatureCheck {
|
||||
pub fn new(
|
||||
check: impl Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self(std::sync::Arc::new(check))
|
||||
}
|
||||
pub fn check(&self, value: &horizon_world::nbt::Tag) -> Result<(), String> {
|
||||
(self.0)(value)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for InlineFeatureCheck {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("InlineFeatureCheck(..)")
|
||||
}
|
||||
}
|
||||
/// The ids and tag ids of one registry.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -583,6 +583,10 @@ pub struct PadSettings {
|
||||
/// The sneak button flips sneaking (Bedrock's default) instead of
|
||||
/// holding it, unless vanilla's toggle crouch is on already.
|
||||
pub toggle_sneak: bool,
|
||||
/// LT + A on a recipe crafts on every server, not only in single player and on LAN worlds
|
||||
/// found in the server list. Off by default: a server's rules may count a repeated craft as a
|
||||
/// macro.
|
||||
pub craft_on_servers: bool,
|
||||
pub glyphs: GlyphStyle,
|
||||
/// A Nintendo pad's face buttons go by their letters (A confirms and jumps, as on the
|
||||
/// Switch's own menus and keyboard) instead of their places (B, at the bottom, does): on
|
||||
@@ -614,6 +618,7 @@ impl Default for PadSettings {
|
||||
look_deadzone: 0.12,
|
||||
trigger_threshold: 0.4,
|
||||
toggle_sneak: true,
|
||||
craft_on_servers: false,
|
||||
glyphs: GlyphStyle::Auto,
|
||||
nintendo_letters: cfg!(target_os = "horizon"),
|
||||
keyboard: TextKeyboard::Auto,
|
||||
@@ -684,6 +689,7 @@ impl PadSettings {
|
||||
s.trigger_threshold = unit(s.trigger_threshold).clamp(0.1, 0.9)
|
||||
}
|
||||
"toggleSneak" => s.toggle_sneak = flag(s.toggle_sneak),
|
||||
"craftOnServers" => s.craft_on_servers = flag(s.craft_on_servers),
|
||||
"glyphs" => s.glyphs = GlyphStyle::parse(value),
|
||||
"nintendoLetters" => s.nintendo_letters = flag(s.nintendo_letters),
|
||||
"keyboard" => s.keyboard = TextKeyboard::parse(value),
|
||||
@@ -745,7 +751,8 @@ impl PadSettings {
|
||||
self.keyboard.name(),
|
||||
);
|
||||
text.push_str(&format!(
|
||||
"gameplayGuide:{}\npointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
|
||||
"craftOnServers:{}\ngameplayGuide:{}\npointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
|
||||
bool_text(self.craft_on_servers),
|
||||
self.guide_detail.name(),
|
||||
self.pointer_speed, self.scroll_speed, self.repeat_delay, self.repeat_interval,
|
||||
));
|
||||
|
||||
@@ -32,6 +32,11 @@ fn pack(properties: &str, shadow: Option<&str>, reads_shadow: bool) -> horizon_s
|
||||
write("final.fsh", if reads_shadow { SHADOW_READ } else { COLOUR });
|
||||
write("gbuffers_entities.vsh", VERTEX);
|
||||
write("gbuffers_entities.fsh", COLOUR);
|
||||
// Terrain programs keep the load free of "nothing draws it" notes; no terrain is drawn.
|
||||
for terrain in ["gbuffers_terrain", "gbuffers_water"] {
|
||||
write(&format!("{terrain}.vsh"), VERTEX);
|
||||
write(&format!("{terrain}.fsh"), COLOUR);
|
||||
}
|
||||
if let Some(shadow) = shadow {
|
||||
write("shadow.vsh", VERTEX);
|
||||
write("shadow.fsh", shadow);
|
||||
|
||||
@@ -661,6 +661,7 @@ fn remesh(
|
||||
blend_radius,
|
||||
smooth_lighting,
|
||||
cutout_leaves,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling,
|
||||
};
|
||||
|
||||
@@ -50,10 +50,14 @@ fn pack(renderer: &mut Renderer, folder: &Path, valid: bool) {
|
||||
std::fs::create_dir_all(&shaders).unwrap();
|
||||
let vertex = "#version 120\nvarying vec2 uv; void main(){gl_Position=ftransform();uv=gl_MultiTexCoord0.xy;}\n";
|
||||
let fragment = "#version 120\nuniform int entityId; uniform int blockEntityId; uniform vec4 entityColor; void main(){gl_FragData[0]=vec4(float(entityId)/255.0,entityColor.a,float(blockEntityId)/255.0,1.0);}\n";
|
||||
// The terrain programs keep the load free of "nothing draws it" notes; this
|
||||
// fixture draws no terrain.
|
||||
for program in [
|
||||
"gbuffers_entities",
|
||||
"gbuffers_block",
|
||||
"gbuffers_entities_translucent",
|
||||
"gbuffers_terrain",
|
||||
"gbuffers_water",
|
||||
] {
|
||||
std::fs::write(shaders.join(format!("{program}.vsh")), vertex).unwrap();
|
||||
std::fs::write(
|
||||
|
||||
@@ -1604,6 +1604,14 @@ impl Audio {
|
||||
let id = self.engine.add(instance, false);
|
||||
self.engine.play_delayed(id, ticks);
|
||||
}
|
||||
/// `SoundManager.playDelayed` of an instance the caller made: it starts
|
||||
/// `ticks` sound-engine ticks from now.
|
||||
pub fn play_instance_after(&mut self, instance: SoundInstance, ticks: i32) {
|
||||
*self.heard.entry(instance.identifier.clone()).or_default() += 1;
|
||||
self.sync_volumes();
|
||||
let id = self.engine.add(instance, false);
|
||||
self.engine.play_delayed(id, ticks);
|
||||
}
|
||||
/// `play` for this client only: a sound a packet's handler plays
|
||||
/// (`playLocalSound`), which the guests play from that packet themselves.
|
||||
pub fn play_unshared(&mut self, event: &str, at: Option<[f64; 3]>, volume: f32, pitch: f32) {
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//! GPU Culling's Auto mode: the setting switched by what limits the frame, measured here.
|
||||
//!
|
||||
//! GPU Culling moves the walk over the near sections and the listing of their draws from the
|
||||
//! render thread to the GPU. That pays where the CPU is the slower of the two and costs where
|
||||
//! the GPU is, and which a machine is can't be told from its parts: a fast CPU with a modest
|
||||
//! GPU, a slow one with a strong GPU and every mix between depend on the scene, the render
|
||||
//! distance and the resolution. So Auto watches the frames' own CPU and GPU times, and only
|
||||
//! where the CPU is clearly the slower one it tries GPU Culling, measures the frame again, and
|
||||
//! keeps it if the frame got faster. A trial that didn't help is undone and not repeated for
|
||||
//! minutes of play, then for longer, then not until the next start, so a machine or a scene it
|
||||
//! doesn't suit loses a few frames' time a few times, not every half minute.
|
||||
//!
|
||||
//! A verdict is the medians of a window of frames (`WINDOW`), which a few slow frames (a chunk
|
||||
//! load, say) don't move. Nothing here reads the GPU or draws anything: the caller feeds it the
|
||||
//! frames and applies what it answers.
|
||||
|
||||
/// Frames in one verdict.
|
||||
pub const WINDOW: usize = 240;
|
||||
/// The CPU's time must be this many times the GPU's for a window to count as CPU-bound.
|
||||
const CPU_BOUND: f32 = 1.3;
|
||||
/// Below this CPU time, in milliseconds, a frame is fast enough that neither matters.
|
||||
const WORTH_TRYING_MS: f32 = 1.5;
|
||||
/// A trial is kept when the frame's busier side dropped under this share of what it was.
|
||||
const KEEP_BELOW: f32 = 0.97;
|
||||
/// Windows of CPU-bound frames in a row before a trial.
|
||||
const STREAK: u8 = 2;
|
||||
/// Windows left out after a switch, while its effects settle.
|
||||
const SETTLE: u8 = 1;
|
||||
/// How long, in milliseconds of frames watched, after the first and the second trial that didn't
|
||||
/// help before another; after a third there is none.
|
||||
const RETRY_AFTER_MS: [f64; 2] = [5.0 * 60_000.0, 20.0 * 60_000.0];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum State {
|
||||
/// Off, looking for a CPU-bound frame.
|
||||
Watching,
|
||||
/// On for a trial: left out `SETTLE` windows, then measured against the frame before.
|
||||
Trial,
|
||||
/// The trial made the frame faster: on from here.
|
||||
Kept,
|
||||
}
|
||||
|
||||
/// What Auto has decided and why, a step at a time.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GpuCullingAuto {
|
||||
cpu: Vec<f32>,
|
||||
gpu: Vec<f32>,
|
||||
on: bool,
|
||||
state: State,
|
||||
/// CPU-bound windows in a row.
|
||||
streak: u8,
|
||||
/// The frame's busier side before the trial, in milliseconds.
|
||||
before: f32,
|
||||
/// Windows still left out after a switch.
|
||||
settling: u8,
|
||||
/// The frames watched so far, in milliseconds of their own time.
|
||||
clock_ms: f64,
|
||||
/// When the next trial may start, on that clock.
|
||||
retry_at_ms: f64,
|
||||
/// Trials that didn't help.
|
||||
rejected: usize,
|
||||
}
|
||||
|
||||
impl Default for GpuCullingAuto {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuCullingAuto {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cpu: Vec::with_capacity(WINDOW),
|
||||
gpu: Vec::with_capacity(WINDOW),
|
||||
on: false,
|
||||
state: State::Watching,
|
||||
streak: 0,
|
||||
before: 0.0,
|
||||
settling: 0,
|
||||
clock_ms: 0.0,
|
||||
retry_at_ms: 0.0,
|
||||
rejected: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Auto has GPU Culling on now.
|
||||
pub fn on(&self) -> bool {
|
||||
self.on
|
||||
}
|
||||
|
||||
/// Where Auto is, for the status: `watching`, `trial` or `kept`.
|
||||
pub fn state(&self) -> &'static str {
|
||||
match self.state {
|
||||
State::Watching => "watching",
|
||||
State::Trial => "trial",
|
||||
State::Kept => "kept",
|
||||
}
|
||||
}
|
||||
|
||||
/// Back to the start: off, with nothing measured (the mode changed, or the world did).
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::new();
|
||||
}
|
||||
|
||||
/// One frame's CPU time, a GPU time where its timestamps have arrived, and its whole time,
|
||||
/// in milliseconds. Answers the new state when this frame completed a window that changed it.
|
||||
pub fn observe(&mut self, cpu_ms: f32, gpu_ms: Option<f32>, frame_ms: f32) -> Option<bool> {
|
||||
self.clock_ms += f64::from(frame_ms);
|
||||
self.cpu.push(cpu_ms);
|
||||
if let Some(gpu) = gpu_ms {
|
||||
self.gpu.push(gpu);
|
||||
}
|
||||
if self.cpu.len() < WINDOW {
|
||||
return None;
|
||||
}
|
||||
let cpu = median(&mut self.cpu);
|
||||
// A GPU that can't be timed leaves nothing to compare with: Auto stays as it is.
|
||||
let gpu = (self.gpu.len() >= WINDOW / 4).then(|| median(&mut self.gpu));
|
||||
self.cpu.clear();
|
||||
self.gpu.clear();
|
||||
self.verdict(cpu, gpu?)
|
||||
}
|
||||
|
||||
fn verdict(&mut self, cpu: f32, gpu: f32) -> Option<bool> {
|
||||
if self.settling > 0 {
|
||||
self.settling -= 1;
|
||||
return None;
|
||||
}
|
||||
match self.state {
|
||||
State::Kept => None,
|
||||
State::Watching => {
|
||||
if self.clock_ms < self.retry_at_ms {
|
||||
return None;
|
||||
}
|
||||
let bound = cpu >= WORTH_TRYING_MS && cpu > gpu * CPU_BOUND;
|
||||
self.streak = if bound { self.streak + 1 } else { 0 };
|
||||
if self.streak < STREAK {
|
||||
return None;
|
||||
}
|
||||
self.streak = 0;
|
||||
self.before = cpu.max(gpu);
|
||||
self.state = State::Trial;
|
||||
self.settling = SETTLE;
|
||||
self.on = true;
|
||||
Some(true)
|
||||
}
|
||||
State::Trial => {
|
||||
if cpu.max(gpu) < self.before * KEEP_BELOW {
|
||||
self.state = State::Kept;
|
||||
return None;
|
||||
}
|
||||
self.state = State::Watching;
|
||||
self.retry_at_ms = RETRY_AFTER_MS
|
||||
.get(self.rejected)
|
||||
.map_or(f64::INFINITY, |wait| self.clock_ms + wait);
|
||||
self.rejected += 1;
|
||||
self.on = false;
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn median(values: &mut [f32]) -> f32 {
|
||||
values.sort_by(f32::total_cmp);
|
||||
values[values.len() / 2]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A window of frames at constant times, each as long as its busier side; the last answer.
|
||||
fn window(auto: &mut GpuCullingAuto, cpu: f32, gpu: f32) -> Option<bool> {
|
||||
(0..WINDOW)
|
||||
.filter_map(|_| auto.observe(cpu, Some(gpu), cpu.max(gpu)))
|
||||
.last()
|
||||
}
|
||||
|
||||
/// A machine whose CPU limits the frame is tried, and keeps GPU Culling when the frame
|
||||
/// gets faster.
|
||||
#[test]
|
||||
fn a_cpu_bound_frame_tries_it_and_keeps_it_when_it_helps() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
assert!(!auto.on());
|
||||
// One CPU-bound window is not yet a verdict.
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), Some(true));
|
||||
assert!(auto.on());
|
||||
assert_eq!(auto.state(), "trial");
|
||||
// The window after the switch is left out, whatever it shows.
|
||||
assert_eq!(window(&mut auto, 9.0, 9.0), None);
|
||||
// The frame's busier side went from 5.0 to 3.0 ms.
|
||||
assert_eq!(window(&mut auto, 3.0, 2.6), None);
|
||||
assert_eq!(auto.state(), "kept");
|
||||
assert!(auto.on());
|
||||
// Kept stays kept, even if a later scene is GPU-bound.
|
||||
assert_eq!(window(&mut auto, 1.0, 8.0), None);
|
||||
assert!(auto.on());
|
||||
}
|
||||
|
||||
/// Where it didn't help, it is undone, and not tried again for 5 minutes, then 20, then
|
||||
/// not at all: a scene it doesn't suit is not tried every half minute.
|
||||
#[test]
|
||||
fn a_trial_that_does_not_help_is_undone_and_waits_longer_each_time() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
// One window is 240 frames of 5 ms: 1.2 s. A trial, and its rejection:
|
||||
let reject = |auto: &mut GpuCullingAuto| {
|
||||
window(auto, 5.0, 2.0);
|
||||
assert_eq!(window(auto, 5.0, 2.0), Some(true));
|
||||
window(auto, 5.0, 2.0);
|
||||
// The GPU took over the frame: 5.0 ms before, 5.4 after.
|
||||
assert_eq!(window(auto, 2.5, 5.4), Some(false));
|
||||
assert!(!auto.on());
|
||||
assert_eq!(auto.state(), "watching");
|
||||
};
|
||||
reject(&mut auto);
|
||||
// CPU-bound as ever, and left alone for 5 minutes of frames: 250 windows.
|
||||
let mut waited = 0;
|
||||
while window(&mut auto, 5.0, 2.0).is_none() {
|
||||
waited += 1;
|
||||
}
|
||||
assert!((248..=252).contains(&waited), "{waited}");
|
||||
// That window started the second trial; it is rejected too.
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
|
||||
// Now 20 minutes: 1000 windows.
|
||||
let mut waited = 0;
|
||||
while window(&mut auto, 5.0, 2.0).is_none() {
|
||||
waited += 1;
|
||||
}
|
||||
assert!((998..=1002).contains(&waited), "{waited}");
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
|
||||
// A third rejection is for good.
|
||||
for _ in 0..5000 {
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// A GPU-bound machine is never switched: its frame is the GPU's.
|
||||
#[test]
|
||||
fn a_gpu_bound_frame_is_left_alone() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..20 {
|
||||
assert_eq!(window(&mut auto, 0.5, 3.6), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// Within the margin neither side limits the frame, and a single CPU-bound window among
|
||||
/// others breaks no streak into a trial.
|
||||
#[test]
|
||||
fn neither_a_balanced_nor_an_occasional_cpu_bound_frame_is_tried() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..10 {
|
||||
assert_eq!(window(&mut auto, 3.0, 2.6), None);
|
||||
}
|
||||
for _ in 0..10 {
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
assert_eq!(window(&mut auto, 2.0, 2.0), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// Fast frames aren't worth a trial, bound or not.
|
||||
#[test]
|
||||
fn a_frame_under_the_floor_is_not_tried() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..10 {
|
||||
assert_eq!(window(&mut auto, 1.2, 0.2), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// Without GPU times (a GPU that can't be timed) there is nothing to compare.
|
||||
#[test]
|
||||
fn frames_without_gpu_times_decide_nothing() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..WINDOW * 10 {
|
||||
assert_eq!(auto.observe(8.0, None, 8.0), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// A spike in a window doesn't move its verdict: the median ignores it.
|
||||
#[test]
|
||||
fn a_few_slow_frames_do_not_make_a_window_cpu_bound() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..4 {
|
||||
let mut answer = None;
|
||||
for i in 0..WINDOW {
|
||||
// One frame in ten is a 30 ms chunk load, the rest are GPU-bound.
|
||||
let cpu = if i % 10 == 0 { 30.0 } else { 0.8 };
|
||||
answer = answer.or(auto.observe(cpu, Some(3.0), 3.0));
|
||||
}
|
||||
assert_eq!(answer, None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_starts_over() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
assert!(auto.on());
|
||||
auto.reset();
|
||||
assert!(!auto.on());
|
||||
assert_eq!(auto.state(), "watching");
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Biome colours from the pinned biome definitions and colormaps
|
||||
//! (`Biome.getGrassColor`/`getFoliageColor`/..., `BlockTintSources`).
|
||||
use crate::Result;
|
||||
use horizon_client_core::particle_type::ParticleType;
|
||||
use horizon_core::{noise::SimplexNoise, position::BlockPos, random::LegacyRandom};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -106,11 +107,28 @@ pub struct BiomeInfo {
|
||||
/// The biome's own fog colour attribute; otherwise the dimension's applies.
|
||||
pub fog: Option<u32>,
|
||||
pub water_fog: u32,
|
||||
/// `minecraft:visual/ambient_particles` entries and their probabilities.
|
||||
pub ambient_particles: Vec<(ParticleType, f32)>,
|
||||
/// `BiomeGenerationSettings.features`: the placed feature ids of each
|
||||
/// `GenerationStep.Decoration` step, in order.
|
||||
pub features: Vec<Vec<String>>,
|
||||
}
|
||||
impl BiomeInfo {
|
||||
fn ambient_particles(v: &Value) -> Vec<(ParticleType, f32)> {
|
||||
let list = &v["attributes"]["minecraft:visual/ambient_particles"];
|
||||
let entries = list["argument"].as_array().or_else(|| list.as_array());
|
||||
entries
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
Some((
|
||||
ParticleType::from_name(entry["particle"]["type"].as_str()?)?,
|
||||
entry["probability"].as_f64().unwrap_or(0.0) as f32,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
|
||||
/// with height above `sea_level + 17`.
|
||||
pub fn temperature_at(&self, pos: BlockPos, sea_level: i32) -> f32 {
|
||||
@@ -245,6 +263,7 @@ impl BiomeInfo {
|
||||
sky: color(&attributes["minecraft:visual/sky_color"]),
|
||||
fog: color(&attributes["minecraft:visual/fog_color"]),
|
||||
water_fog: color(&attributes["minecraft:visual/water_fog_color"]).unwrap_or(0x050533),
|
||||
ambient_particles: Self::ambient_particles(v),
|
||||
features: v["features"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
@@ -563,7 +582,9 @@ mod pack_tests {
|
||||
#[test]
|
||||
fn packs_replace_in_place_and_add_after_the_builtin_biomes() {
|
||||
let base = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
|
||||
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"}});
|
||||
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"},
|
||||
"attributes": {"minecraft:visual/ambient_particles": {
|
||||
"argument": [{"particle": {"type": "minecraft:ash"}, "probability": 0.25}]}}});
|
||||
let hot = json!({"effects": {"grass_color": 0x123456}});
|
||||
let files = [
|
||||
("pack:zeta", &hot),
|
||||
@@ -576,6 +597,9 @@ mod pack_tests {
|
||||
assert_eq!(world.id("minecraft:river"), 2);
|
||||
assert_eq!(world.get(2).water, 0xFF0000);
|
||||
assert_eq!(world.get(2).temperature, 2.0);
|
||||
assert_eq!(world.get(2).ambient_particles.len(), 1);
|
||||
assert_eq!(world.get(2).ambient_particles[0].0.name(), "ash");
|
||||
assert_eq!(world.get(2).ambient_particles[0].1, 0.25);
|
||||
// New biomes follow, in registry order.
|
||||
assert_eq!(world.id("minecraft:molten_peaks"), 3);
|
||||
assert_eq!(world.id("pack:zeta"), 4);
|
||||
|
||||
@@ -576,7 +576,7 @@ pub struct Chunk {
|
||||
/// Block entities read from a save, moved into the world on arrival.
|
||||
pub block_entities: Vec<((i32, i32, i32), block_entity::BlockEntity)>,
|
||||
/// `CustomName` of the container block entities (shown as the screen title).
|
||||
pub block_names: Vec<((i32, i32, i32), String)>,
|
||||
pub block_names: Vec<((i32, i32, i32), Tag)>,
|
||||
/// Generation postprocessing marks (ProtoChunk.postProcessing) as world
|
||||
/// positions, applied once by LevelChunk.postProcessGeneration.
|
||||
pub postprocess: Vec<[i32; 3]>,
|
||||
|
||||
@@ -83,6 +83,8 @@ pub enum BlockEntity {
|
||||
/// `DaylightDetectorBlockEntity`: no data, only the ticker
|
||||
/// (`DaylightDetectorBlock.tickEntity`).
|
||||
DaylightDetector,
|
||||
/// `BellBlockEntity`: saved identity only; ringing state is kept separately.
|
||||
Bell,
|
||||
/// Suspicious sand and gravel: saved loot and the transient brushed face.
|
||||
Brushable(Box<Brushable>),
|
||||
/// A shelf holds three displayed item stacks.
|
||||
@@ -452,6 +454,9 @@ pub struct Jukebox {
|
||||
pub items: [Option<ItemStack>; 1],
|
||||
/// `ticks_since_song_started`, while a song plays.
|
||||
pub ticks: i64,
|
||||
/// `JukeboxBlockEntity.saveAdditional` writes the clock only when its song
|
||||
/// reference exists. Keep this separate from legacy playback adoption.
|
||||
pub song_clock_present: bool,
|
||||
pub playing: bool,
|
||||
}
|
||||
impl Jukebox {
|
||||
|
||||
@@ -19,26 +19,75 @@ pub struct ModMenuType {
|
||||
pub slots: usize,
|
||||
pub data_slots: usize,
|
||||
pub results: Vec<usize>,
|
||||
/// its crafting grid, if it has one (`crafting`).
|
||||
pub crafting: Option<ModCrafting>,
|
||||
/// its linked slots, if it has any (`links`).
|
||||
pub links: Option<ModLinks>,
|
||||
}
|
||||
|
||||
/// a mod menu type's linked slots: `count` container slots from `from` on,
|
||||
/// which show the slots of the containers a native mod links them to
|
||||
/// (`native_menu_links`) instead of the block entity's.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ModLinks {
|
||||
pub from: usize,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl ModLinks {
|
||||
/// The linked container slots.
|
||||
pub fn slots(&self) -> std::ops::Range<usize> {
|
||||
self.from..self.from + self.count
|
||||
}
|
||||
}
|
||||
|
||||
/// a mod menu type's crafting grid: `size`x`size` container slots from
|
||||
/// `grid` on, row by row, and the slot showing what they craft. The block
|
||||
/// entity holds the grid; the result is the menu's.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ModCrafting {
|
||||
pub grid: usize,
|
||||
pub size: usize,
|
||||
pub result: usize,
|
||||
}
|
||||
|
||||
impl ModCrafting {
|
||||
/// The grid's container slots.
|
||||
pub fn grid_slots(&self) -> std::ops::Range<usize> {
|
||||
self.grid..self.grid + self.size * self.size
|
||||
}
|
||||
}
|
||||
|
||||
static MOD_MENU_TYPES: std::sync::OnceLock<Vec<ModMenuType>> = std::sync::OnceLock::new();
|
||||
|
||||
/// registers the content mods' menu types, `(id, slots, data slots,
|
||||
/// results)`, a list set once per process (false when it already was).
|
||||
pub fn register_mod_menu_types(
|
||||
menus: impl IntoIterator<Item = (String, usize, usize, Vec<usize>)>,
|
||||
) -> bool {
|
||||
/// One menu type as [`register_mod_menu_types`] takes it: `(id, slots, data
|
||||
/// slots, results, crafting, links)`.
|
||||
pub type ModMenuTypeEntry = (
|
||||
String,
|
||||
usize,
|
||||
usize,
|
||||
Vec<usize>,
|
||||
Option<ModCrafting>,
|
||||
Option<ModLinks>,
|
||||
);
|
||||
|
||||
/// registers the content mods' menu types, a list set once per process
|
||||
/// (false when it already was).
|
||||
pub fn register_mod_menu_types(menus: impl IntoIterator<Item = ModMenuTypeEntry>) -> bool {
|
||||
if MOD_MENU_TYPES.get().is_some() {
|
||||
return false;
|
||||
}
|
||||
let menus = menus
|
||||
.into_iter()
|
||||
.map(|(id, slots, data_slots, results)| ModMenuType {
|
||||
id: Box::leak(id.into_boxed_str()),
|
||||
slots,
|
||||
data_slots,
|
||||
results,
|
||||
});
|
||||
let menus =
|
||||
menus.into_iter().map(
|
||||
|(id, slots, data_slots, results, crafting, links)| ModMenuType {
|
||||
id: Box::leak(id.into_boxed_str()),
|
||||
slots,
|
||||
data_slots,
|
||||
results,
|
||||
crafting,
|
||||
links,
|
||||
},
|
||||
);
|
||||
MOD_MENU_TYPES.set(menus.collect()).is_ok()
|
||||
}
|
||||
|
||||
@@ -392,6 +441,9 @@ impl BlockEntity {
|
||||
if name == "minecraft:end_gateway" {
|
||||
return Some(Self::EndGateway(EndGateway::default()));
|
||||
}
|
||||
if name == "minecraft:bell" {
|
||||
return Some(Self::Bell);
|
||||
}
|
||||
if name == "minecraft:daylight_detector" {
|
||||
return Some(Self::DaylightDetector);
|
||||
}
|
||||
@@ -487,6 +539,7 @@ impl BlockEntity {
|
||||
Self::Crafter(_) => name == "minecraft:crafter",
|
||||
Self::EndGateway(_) => name == "minecraft:end_gateway",
|
||||
Self::DaylightDetector => name == "minecraft:daylight_detector",
|
||||
Self::Bell => name == "minecraft:bell",
|
||||
Self::StructureBlock(_) => name == "minecraft:structure_block",
|
||||
Self::TestBlock(_) => name == "minecraft:test_block",
|
||||
Self::TestInstanceBlock(_) => name == "minecraft:test_instance_block",
|
||||
@@ -525,6 +578,7 @@ impl BlockEntity {
|
||||
Self::Skull(_) => "minecraft:skull",
|
||||
Self::EndGateway(_) => "minecraft:end_gateway",
|
||||
Self::DaylightDetector => "minecraft:daylight_detector",
|
||||
Self::Bell => "minecraft:bell",
|
||||
Self::Brushable(_) => "minecraft:brushable_block",
|
||||
Self::Campfire(_) => "minecraft:campfire",
|
||||
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
|
||||
@@ -559,6 +613,7 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Brushable(_) => Vec::new(),
|
||||
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
|
||||
Self::Lectern(l) => l.book.iter().cloned().collect(),
|
||||
@@ -589,6 +644,7 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Brushable(_) => &[],
|
||||
Self::Jukebox(j) => &j.items,
|
||||
Self::Crafter(c) => &c.items,
|
||||
@@ -624,6 +680,7 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Brushable(_) => &mut [],
|
||||
Self::Jukebox(j) => &mut j.items,
|
||||
Self::Crafter(c) => &mut c.items,
|
||||
|
||||
@@ -191,6 +191,10 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
|
||||
Opt::PadGameplayGuide,
|
||||
"Show the buttons for what you can do, while you play with a controller.",
|
||||
),
|
||||
option(
|
||||
Opt::PadCraftOnServers,
|
||||
"Lets LT + A craft on servers other than your own worlds. Some servers count it as a macro.",
|
||||
),
|
||||
option(
|
||||
Opt::PadTriggerThreshold,
|
||||
"How far a trigger is pulled before it counts as pressed.",
|
||||
@@ -344,6 +348,7 @@ impl Item {
|
||||
Item::Option(Opt::PadAdaptiveTriggers) => Some(c.adaptive_triggers),
|
||||
Item::Option(Opt::PadInvertX) => Some(c.invert_x),
|
||||
Item::Option(Opt::PadInvertY) => Some(c.invert_y),
|
||||
Item::Option(Opt::PadCraftOnServers) => Some(c.craft_on_servers),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1149,7 +1149,7 @@ mod tests {
|
||||
for pack in ["void", "islands"] {
|
||||
let dir = game.join(NEW_WORLD_PACKS).join(pack);
|
||||
std::fs::create_dir_all(dir.join("data/v/function")).unwrap();
|
||||
std::fs::write(dir.join("pack.mcmeta"), "{}").unwrap();
|
||||
std::fs::write(dir.join("pack.mcmeta"), r#"{"pack":{"description":""}}"#).unwrap();
|
||||
std::fs::write(dir.join("data/v/function/a.mcfunction"), "say a").unwrap();
|
||||
}
|
||||
let (temp, ids) = seed_create_world(&game).unwrap();
|
||||
|
||||
@@ -33,7 +33,10 @@ struct Globals {
|
||||
hdr_windows: array<vec4<f32>, 2>,
|
||||
// HDR lighting: x how strongly the sky lights the world now (`Sky::sky_light_factor`),
|
||||
// y what the sky and fog get: the same, but none under water.
|
||||
// w: this entire frame writes light, so alpha is coverage rather than a mark.
|
||||
hdr_light: vec4<f32>,
|
||||
hdr_settings: vec4<f32>,
|
||||
hdr_shares: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> globals: Globals;
|
||||
@group(0) @binding(1) var atlas: texture_2d<f32>;
|
||||
@@ -276,6 +279,18 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
|
||||
}
|
||||
// CRUMBLING_UV_END
|
||||
@fragment fn fs_entity(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
return entity_written_color(in, front);
|
||||
}
|
||||
fn entity_written_color(in: Out, front: bool) -> vec4<f32> {
|
||||
let color = entity_color(in, front);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 && in.overlay != 11u && in.overlay != 12u {
|
||||
var mark = hdr_entity_mark(in);
|
||||
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;
|
||||
}
|
||||
@fragment fn fs_entity_unmarked(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
return entity_color(in, front);
|
||||
}
|
||||
fn entity_color(in: Out, front: bool) -> vec4<f32> {
|
||||
@@ -358,15 +373,23 @@ fn shadow_color(in: Out) -> vec4<f32> {
|
||||
if in.overlay == 6u { uv.y = in.wrap.x + fract(in.uv.y) * in.wrap.y; }
|
||||
let texel = textureSample(atlas, atlas_sampler, uv);
|
||||
if texel.a < 0.1 { discard; }
|
||||
return vec4(0.0, 0.0, 0.0, hdr_entity_mark(in));
|
||||
}
|
||||
fn hdr_entity_mark(in: Out) -> f32 {
|
||||
var uv = in.uv;
|
||||
if in.overlay == 6u { uv.y = in.wrap.x + fract(in.uv.y) * in.wrap.y; }
|
||||
let texel = textureSample(atlas, atlas_sampler, uv);
|
||||
// Additive and procedural colour draws can survive where the legacy mark pass skips.
|
||||
if texel.a < 0.1 { return 1.0; }
|
||||
// Fog hides a light source and the light on a surface as it hides everything else.
|
||||
let fogged = max(linear_fog(in.fog_distance.x, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.fog_distance.y, globals.fog.z, globals.fog.w)) * globals.fog_color.a;
|
||||
if in.tier == 0u {
|
||||
return vec4(0.0, 0.0, 0.0, hdr_scene_alpha(mix(in.scene, globals.hdr_light.y, fogged)));
|
||||
return hdr_scene_alpha(mix(in.scene, globals.hdr_light.y, fogged));
|
||||
}
|
||||
// A layer that fades (the warden's) shines by what's left of it.
|
||||
let y = brightness(texel.rgb * in.color.rgb) * in.color.a * (1.0 - fogged);
|
||||
return vec4(0.0, 0.0, 0.0, hdr_mark(in.tier, y));
|
||||
return hdr_mark(in.tier, y);
|
||||
}
|
||||
fn brightness(c: vec3<f32>) -> f32 {
|
||||
let l = pow(max(c, vec3(0.0)), vec3(2.2));
|
||||
@@ -407,7 +430,11 @@ fn celestial_color(in: Out) -> vec4<f32> {
|
||||
return textureSample(atlas, atlas_sampler, in.uv) * in.color * in.light_color;
|
||||
}
|
||||
@fragment fn fs_celestial(in: Out) -> @location(0) vec4<f32> {
|
||||
return celestial_color(in);
|
||||
let color = celestial_color(in);
|
||||
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;
|
||||
}
|
||||
|
||||
// The sky's celestial pass (sun, moon, the End sky): fs_celestial's light,
|
||||
@@ -424,7 +451,11 @@ fn celestial_color(in: Out) -> vec4<f32> {
|
||||
let y = brightness(added);
|
||||
// Only what's past the tier's window takes the mark: a soft edge keeps the sky's light,
|
||||
// or it would come out darker than the sky around it.
|
||||
if y < globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u] { return vec4(added, 0.0); }
|
||||
let full = y >= globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u];
|
||||
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 !full { return vec4(added, 0.0); }
|
||||
return vec4(added, 1.0 - hdr_mark(tier, y));
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,35 @@ mod tests {
|
||||
assert_eq!(screen.wheel_step(&mut s, ExtraRow::Zoom(0), true), None);
|
||||
}
|
||||
|
||||
/// Extras has no Undo: a change leaves none to press or reach.
|
||||
#[test]
|
||||
fn a_change_on_extras_leaves_no_undo() {
|
||||
use crate::settings_shell::{Columns, Target};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
let mut s = Settings::default();
|
||||
let mut shell = Shell::<ExtrasScreen>::default();
|
||||
let at = shell.point_at(&s, SIZE, ExtraRow::Item(Item::Sort));
|
||||
assert!(
|
||||
!shell
|
||||
.click(&mut s, at, SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(s.extras.sort && !shell.changed());
|
||||
let cols = Columns::of(SIZE.0, SIZE.1).with_note_lines(NOTE_LINES);
|
||||
let [x, y, ..] = cols.button(1);
|
||||
assert!(
|
||||
shell
|
||||
.click(&mut s, (x + 2.0, y + 2.0), SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(s.extras.sort);
|
||||
let reached = (0..100).any(|_| {
|
||||
shell.key(&mut s, K::Tab, None, false, SIZE);
|
||||
shell.focused() == Some(Target::Undo)
|
||||
});
|
||||
assert!(!reached);
|
||||
}
|
||||
|
||||
/// A page with no page list has Done under the search field: Down goes to the first row
|
||||
/// instead, from the keys and, as the frame declares it, from a controller's focus.
|
||||
#[test]
|
||||
|
||||
@@ -32,7 +32,15 @@ const TERRAIN_ATTRIBUTES: [wgpu::VertexAttribute; 5] =
|
||||
|
||||
/// entity.wgsl after the OverlayTexture constants it reads.
|
||||
fn entity_shader() -> String {
|
||||
crate::overlay::wgsl_constants() + include_str!("entity.wgsl")
|
||||
crate::overlay::wgsl_constants() + include_str!("hdr_light.wgsl") + include_str!("entity.wgsl")
|
||||
}
|
||||
|
||||
fn written_lane(layered: bool, format: wgpu::TextureFormat, pack: bool, world: bool) -> bool {
|
||||
layered && format == LINEAR_FRAME && !pack && world
|
||||
}
|
||||
|
||||
fn terrain_shader() -> String {
|
||||
include_str!("hdr_light.wgsl").to_owned() + include_str!("terrain.wgsl")
|
||||
}
|
||||
|
||||
/// The static block-entity vertex entry point shares entity shading but lives
|
||||
@@ -162,6 +170,7 @@ mod arena;
|
||||
pub mod capture;
|
||||
pub mod compositor;
|
||||
pub(crate) mod core_lines;
|
||||
pub(crate) mod core_sky;
|
||||
mod cull;
|
||||
mod end_sky;
|
||||
pub mod entity_skin;
|
||||
@@ -431,6 +440,9 @@ struct Globals {
|
||||
/// full (`hdr::Hdr::windows`); zero without HDR.
|
||||
hdr_windows: [[f32; 4]; 2],
|
||||
hdr_light: [f32; 4],
|
||||
/// Headroom, effective scene gain, world/look and debug, from the resolve pass's snapshot.
|
||||
hdr_settings: [f32; 4],
|
||||
hdr_shares: [f32; 4],
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
@@ -515,6 +527,9 @@ struct SectionGpu {
|
||||
/// (`packed`, `mesher::packed`) a record each and the vertices of those without one.
|
||||
solid_slots: u64,
|
||||
packed: bool,
|
||||
/// Its records are a far tile's (`lod::packed`), not a section's: the two
|
||||
/// layouts are told apart by the base vertex its draws use.
|
||||
packed_far: bool,
|
||||
/// Where the solid quads' buckets by facing side end (`mesher::Sides`).
|
||||
sides: Sides,
|
||||
translucent: u32,
|
||||
@@ -588,14 +603,19 @@ impl SectionGpu {
|
||||
self.solid > 0 && (0..sections as i32).contains(&key.1)
|
||||
}
|
||||
/// The base vertex of its solid draws: quad `q`'s corners are `base + 4 * q` on (the
|
||||
/// quad indices). In the compact layout they name its record's slot past
|
||||
/// `PACKED_BASE` (terrain.wgsl's `section_corner`).
|
||||
/// quad indices). In a compact layout they name its record's slot past
|
||||
/// `PACKED_BASE`, or past `FAR_BASE` for a far tile's records, which is how
|
||||
/// terrain.wgsl's `section_corner` tells the two apart.
|
||||
fn solid_base(&self) -> u32 {
|
||||
if self.packed {
|
||||
packed::PACKED_BASE + 4 * self.start as u32
|
||||
} else {
|
||||
self.start as u32
|
||||
if !self.packed {
|
||||
return self.start as u32;
|
||||
}
|
||||
let base = if self.packed_far {
|
||||
crate::lod::packed::FAR_BASE
|
||||
} else {
|
||||
packed::PACKED_BASE
|
||||
};
|
||||
base + 4 * self.start as u32
|
||||
}
|
||||
/// GPU culling's record of it at `key`, its quads within `bounds`
|
||||
/// (`SectionMesh::bounds`) and in their buckets by facing side (`sides`),
|
||||
@@ -1214,6 +1234,8 @@ 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>,
|
||||
/// 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),
|
||||
/// translucent ones (`RenderTypes.linesTranslucentNoDepthWrite`) and the
|
||||
/// always-on-top ones.
|
||||
@@ -1481,6 +1503,9 @@ pub struct Renderer {
|
||||
frame_format: wgpu::TextureFormat,
|
||||
/// The no-pack float HDR lane, with independent blending and an R8 coverage target.
|
||||
hud_coverage: bool,
|
||||
/// What `frame_format` was picked from besides the settings and the pack: whether the
|
||||
/// surface offers HDR output, the device draws the 16-bit frame, and it draws the float one.
|
||||
frame_device: (bool, bool, bool),
|
||||
/// Whether a pack picked or dropped since the renderer was made has been told it runs on
|
||||
/// the frame that exists (`set_native_shader_pack`).
|
||||
frame_kept_logged: bool,
|
||||
@@ -1489,6 +1514,7 @@ pub struct Renderer {
|
||||
hdr_marker: Option<std::path::PathBuf>,
|
||||
/// The HDR Lighting setting, kept for the HDR output made when HDR turns on.
|
||||
hdr_lighting_setting: bool,
|
||||
hdr_written: bool,
|
||||
/// The shader pack's post-processing, and the dimension (legacy id) it was loaded for.
|
||||
pack_post: Option<(i32, Option<pack_post::PackPost>)>,
|
||||
/// A `shaders/` pack whose pipelines build on a thread of their own (`load_shader_pack`).
|
||||
@@ -1510,6 +1536,9 @@ pub struct Renderer {
|
||||
/// The block sprites as one atlas, while a shader pack is loaded.
|
||||
pack_atlas: Option<pack_atlas::PackAtlas>,
|
||||
pack_material_atlases: Option<[pack_atlas::PackAtlas; 2]>,
|
||||
/// The shared atlas's LabPBR normal and specular paged maps (`set_shared_material_maps`),
|
||||
/// which shader packs read for entities, items and the hand.
|
||||
shared_material_maps: [Option<wgpu::Texture>; 2],
|
||||
/// The pack's terrain program reads flat draws' vertices (`pack_terrain_inputs`).
|
||||
pack_terrain_flat: bool,
|
||||
/// OptiFine/Iris separate ambient occlusion: pass vertex alpha to hz_Color.a.
|
||||
@@ -2473,6 +2502,23 @@ impl Renderer {
|
||||
}
|
||||
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] {
|
||||
@@ -2778,7 +2824,7 @@ impl Renderer {
|
||||
let gpu_timer = timing::GpuTimer::new(&device, &queue);
|
||||
let shader = kept.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon terrain"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("terrain.wgsl").into()),
|
||||
source: wgpu::ShaderSource::Wgsl(terrain_shader().into()),
|
||||
});
|
||||
let entry = |binding, visibility, ty| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
@@ -2917,6 +2963,14 @@ impl Renderer {
|
||||
depth_test: bool,
|
||||
blend: Option<wgpu::BlendState>,
|
||||
coverage: bool| {
|
||||
let mut constants = constants.to_vec();
|
||||
let plain = constants
|
||||
.iter()
|
||||
.any(|&(k, v)| k == "HDR_LIGHTING" && v == 0.0);
|
||||
constants.push((
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME && !plain)),
|
||||
));
|
||||
let desc = wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
@@ -2947,7 +3001,7 @@ impl Renderer {
|
||||
module: &shader,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants,
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
@@ -2963,7 +3017,7 @@ impl Renderer {
|
||||
hud_coverage::pipeline(
|
||||
&device,
|
||||
&desc,
|
||||
wgpu::ShaderSource::Wgsl(include_str!("terrain.wgsl").into()),
|
||||
wgpu::ShaderSource::Wgsl(terrain_shader().into()),
|
||||
)
|
||||
.expect("the debug crosshair's coverage")
|
||||
} else {
|
||||
@@ -3922,7 +3976,13 @@ impl Renderer {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &entity_shader,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: frame_format,
|
||||
blend: Some(blend),
|
||||
@@ -4035,7 +4095,7 @@ impl Renderer {
|
||||
// writes, culled like every world text pipeline (TEXT_SNIPPET keeps
|
||||
// RenderPipeline.Builder's default): a name tag or text display hides
|
||||
// its back.
|
||||
let see_through_pipeline = entity_pipeline(
|
||||
let see_through_pipeline = entity_pipeline_with(
|
||||
"See-through text",
|
||||
Some(wgpu::Face::Back),
|
||||
false,
|
||||
@@ -4043,11 +4103,13 @@ impl Renderer {
|
||||
Default::default(),
|
||||
alpha_blend,
|
||||
false,
|
||||
"fs_entity_unmarked",
|
||||
wgpu::ColorWrites::ALL,
|
||||
);
|
||||
// RenderPipelines.TEXT_POLYGON_OFFSET: a text display's text and
|
||||
// background win against the surface they lie on (bias 1, 10), from
|
||||
// the front only.
|
||||
let text_polygon_offset_pipeline = entity_pipeline(
|
||||
let text_polygon_offset_pipeline = entity_pipeline_with(
|
||||
"Polygon offset text",
|
||||
Some(wgpu::Face::Back),
|
||||
true,
|
||||
@@ -4059,6 +4121,8 @@ impl Renderer {
|
||||
},
|
||||
alpha_blend,
|
||||
false,
|
||||
"fs_entity_unmarked",
|
||||
wgpu::ColorWrites::ALL,
|
||||
);
|
||||
let world_text_pipeline =
|
||||
world_text::pipeline(&device, Some(&entity_layout), &entity_shader, frame_format);
|
||||
@@ -4105,7 +4169,7 @@ impl Renderer {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &static_entity_shader,
|
||||
entry_point: Some("fs_entity"),
|
||||
entry_point: Some("fs_entity_unmarked"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: frame_format,
|
||||
@@ -4212,7 +4276,13 @@ impl Renderer {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &entity_shader,
|
||||
entry_point: Some("fs_celestial"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: frame_format,
|
||||
blend: Some(wgpu::BlendState {
|
||||
@@ -4304,7 +4374,13 @@ impl Renderer {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &entity_shader,
|
||||
entry_point: Some("fs_sky_celestial"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(frame_format == LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: frame_format,
|
||||
// RenderPipelines.CELESTIAL: additive (src alpha, one), with
|
||||
@@ -4605,6 +4681,10 @@ impl Renderer {
|
||||
});
|
||||
let static_entity_index_quads = 1 << 14;
|
||||
let static_entity_indices = Self::quad_indices(&device, static_entity_index_quads);
|
||||
let indirect_execution = adapter
|
||||
.get_downlevel_capabilities()
|
||||
.flags
|
||||
.contains(wgpu::DownlevelFlags::INDIRECT_EXECUTION);
|
||||
let flat_wanted = flat::wanted(&adapter, &device);
|
||||
// Metal draws solid terrain in two passes (`split_solid`): meshes put all
|
||||
// their opaque quads first, so each pass draws its part in a run or two.
|
||||
@@ -4643,6 +4723,7 @@ impl Renderer {
|
||||
&bindings,
|
||||
arena.page_vertices(),
|
||||
min_sections,
|
||||
indirect_execution,
|
||||
|layout, label, vertex, fragment, blend, cull, pulled| {
|
||||
let buffers: &[Option<wgpu::VertexBufferLayout<'_>>] =
|
||||
if pulled { &[] } else { &buffers };
|
||||
@@ -4703,10 +4784,7 @@ impl Renderer {
|
||||
let multi_draw = device
|
||||
.features()
|
||||
.contains(wgpu::Features::INDIRECT_FIRST_INSTANCE)
|
||||
&& adapter
|
||||
.get_downlevel_capabilities()
|
||||
.flags
|
||||
.contains(wgpu::DownlevelFlags::INDIRECT_EXECUTION);
|
||||
&& indirect_execution;
|
||||
let batchable = translucent_batchable(&adapter.get_info(), multi_draw);
|
||||
crate::graphics::offer_translucent_batching(batchable);
|
||||
let translucent_batching = batchable && translucent_batching;
|
||||
@@ -4865,6 +4943,7 @@ impl Renderer {
|
||||
lines,
|
||||
lines_secondary,
|
||||
core_lines: None,
|
||||
core_sky: None,
|
||||
debug_lines_opaque,
|
||||
debug_lines,
|
||||
debug_lines_on_top,
|
||||
@@ -5027,10 +5106,12 @@ impl Renderer {
|
||||
entity_atlas: atlas_view.clone(),
|
||||
hdr: None,
|
||||
hdr_lighting_setting: true,
|
||||
hdr_written: false,
|
||||
emission_stats,
|
||||
hdr_output,
|
||||
frame_format,
|
||||
hud_coverage,
|
||||
frame_device: (hdr_output.is_some(), wide_ok, linear.2),
|
||||
frame_kept_logged: false,
|
||||
hdr_marker: None,
|
||||
pack_post: None,
|
||||
@@ -5044,6 +5125,7 @@ impl Renderer {
|
||||
pack_weather: None,
|
||||
pack_atlas: None,
|
||||
pack_material_atlases: None,
|
||||
shared_material_maps: [None, None],
|
||||
pack_terrain_flat: false,
|
||||
separate_ao: false,
|
||||
pack_lava_layers: Vec::new(),
|
||||
@@ -5087,7 +5169,7 @@ impl Renderer {
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(0..quads * 6, (first * 4) as i32, 0..1);
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
// Drawn into the world: the light sources it holds are marked as the world's are.
|
||||
pass.set_pipeline(&self.hdr_entity_mark_pipeline);
|
||||
pass.draw_indexed(0..quads * 6, (first * 4) as i32, 0..1);
|
||||
@@ -5095,7 +5177,7 @@ impl Renderer {
|
||||
}
|
||||
/// HDR lighting on the clouds just drawn: the sky's value (`fs_hdr_cloud_mark`).
|
||||
fn mark_clouds(&self, pass: &mut wgpu::RenderPass<'_>, quads: u32) {
|
||||
if self.hdr.as_ref().is_none_or(|h| !h.lighting) {
|
||||
if self.hdr_written || self.hdr.as_ref().is_none_or(|h| !h.lighting) {
|
||||
return;
|
||||
}
|
||||
pass.set_pipeline(&self.hdr_cloud_mark_pipeline);
|
||||
@@ -5114,7 +5196,7 @@ impl Renderer {
|
||||
solid_draws: u32,
|
||||
translucent_draws: u32,
|
||||
) {
|
||||
if self.hdr.as_ref().is_none_or(|h| !h.lighting) {
|
||||
if self.hdr_written || self.hdr.as_ref().is_none_or(|h| !h.lighting) {
|
||||
return;
|
||||
}
|
||||
match flat {
|
||||
@@ -5135,6 +5217,47 @@ impl Renderer {
|
||||
}
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
}
|
||||
/// The shared atlas's LabPBR maps (`HudAtlas::material_maps`), uploaded for the shader packs
|
||||
/// loaded from now on; a map too large for this GPU is left out.
|
||||
pub fn set_shared_material_maps(&mut self, maps: &[Option<crate::paged_maps::PagedMap>; 2]) {
|
||||
let limit = self.device.limits().max_texture_dimension_2d;
|
||||
let sizes: Vec<String> = (maps.iter().zip(["normals", "specular"]))
|
||||
.filter_map(|(m, name)| {
|
||||
m.as_ref()
|
||||
.map(|m| format!("{name} {}x{}", m.width, m.height))
|
||||
})
|
||||
.collect();
|
||||
if !sizes.is_empty() {
|
||||
let bytes: usize = maps.iter().flatten().map(|m| m.pixels.len()).sum();
|
||||
log::info!(
|
||||
"shared atlas LabPBR maps: {} ({:.1} MiB)",
|
||||
sizes.join(", "),
|
||||
bytes as f64 / f64::from(1 << 20)
|
||||
);
|
||||
}
|
||||
self.shared_material_maps = maps.each_ref().map(|map| {
|
||||
let map = map.as_ref().filter(|m| m.width.max(m.height) <= limit)?;
|
||||
Some(self.device.create_texture_with_data(
|
||||
&self.queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("Shared atlas material map"),
|
||||
size: wgpu::Extent3d {
|
||||
width: map.width,
|
||||
height: map.height,
|
||||
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: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&map.pixels,
|
||||
))
|
||||
});
|
||||
}
|
||||
/// The pack's OptiFine custom sky layer textures (`crate::custom_sky::CustomSky::load`).
|
||||
pub fn set_custom_sky(&mut self, pack: &crate::custom_sky::CustomSky) {
|
||||
self.custom_sky.set_pack(&self.device, &self.queue, pack);
|
||||
@@ -5151,6 +5274,15 @@ impl Renderer {
|
||||
self.core_lines =
|
||||
program.and_then(|program| core_lines::CoreLines::new(&self.device, format, program));
|
||||
}
|
||||
/// The resource packs' `core/sky` and `core/stars` (`HudAtlas::sky_shader`,
|
||||
/// `stars_shader`), or the built-in sky and stars.
|
||||
pub fn set_sky_shaders(
|
||||
&mut self,
|
||||
disc: Option<&horizon_shaders::CoreProgram>,
|
||||
stars: Option<&horizon_shaders::CoreProgram>,
|
||||
) {
|
||||
self.core_sky = core_sky::CoreSky::new(&self.device, self.frame_format, disc, stars);
|
||||
}
|
||||
/// LevelRenderer.submitBlockOutline: the buffer holds `edges` per stroke, the black
|
||||
/// secondary stroke first when high contrast is on.
|
||||
fn draw_outline(&self, pass: &mut wgpu::RenderPass<'_>, edges: u32, high_contrast: bool) {
|
||||
@@ -5901,6 +6033,7 @@ impl Renderer {
|
||||
solid,
|
||||
solid_slots,
|
||||
packed,
|
||||
packed_far: packed && mesh.packed_far,
|
||||
sides: if mesh.sides.quads() == solid {
|
||||
mesh.sides
|
||||
} else {
|
||||
@@ -6402,7 +6535,9 @@ impl Renderer {
|
||||
}
|
||||
self.packed_quads = want;
|
||||
packed::pack_quads(want);
|
||||
if !want && self.sections.values().any(|s| s.packed) {
|
||||
let laid_out = self.sections.values().any(|s| s.packed)
|
||||
|| self.lods.values().any(|t| t.drawn().packed);
|
||||
if !want && laid_out {
|
||||
self.sections_dropped = true;
|
||||
self.clear_sections(self.layout);
|
||||
}
|
||||
@@ -6722,6 +6857,21 @@ impl Renderer {
|
||||
hdr.look = on;
|
||||
}
|
||||
}
|
||||
/// Whether this renderer's frame is the one a renderer made now would draw in, with HDR and
|
||||
/// Smooth Gradients (`wide_frame`) and the pack picked (`linear_pack`, `hdr_float`, as
|
||||
/// `Renderer::new` takes them). Under `HORIZON_HDR_FRAME=float` the game makes the renderer
|
||||
/// again when it isn't (docs/design/hdr-linear-light.md, "When it changes").
|
||||
pub fn frame_follows(&self, wide_frame: bool, linear_pack: bool, hdr_float: bool) -> bool {
|
||||
let (hdr_offered, wide_ok, linear_ok) = self.frame_device;
|
||||
self.frame_format
|
||||
== frame_format(
|
||||
wide_frame,
|
||||
hdr_offered,
|
||||
wide_ok,
|
||||
self.config.format,
|
||||
(linear_pack, hdr_float, linear_ok),
|
||||
)
|
||||
}
|
||||
/// HDR output (Horizon setting): HDR10 or scRGB at `paper_white` nits, the world's
|
||||
/// highlights expanded up to `peak` nits. Off where the surface can't present it.
|
||||
/// Offscreen (`hdr::offscreen_output`) the frame is drawn the same and its HDR screenshot
|
||||
@@ -6959,7 +7109,7 @@ impl Renderer {
|
||||
dimension: i32,
|
||||
post: std::result::Result<Option<pack_post::PackPost>, pack_post::Refused>,
|
||||
) -> std::result::Result<Option<(Vec<String>, Vec<String>)>, pack_post::Refused> {
|
||||
let post = match post {
|
||||
let mut post = match post {
|
||||
Ok(post) => post,
|
||||
Err(e) => {
|
||||
self.pack_post = Some((dimension, None));
|
||||
@@ -6968,6 +7118,12 @@ impl Renderer {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if let Some(post) = post.as_mut() {
|
||||
post.set_paged_maps(
|
||||
(self.shared_material_maps.each_ref())
|
||||
.map(|t| t.as_ref().map(|t| t.create_view(&Default::default()))),
|
||||
);
|
||||
}
|
||||
let report = post.as_ref().map(|p| {
|
||||
let programs = p.programs().into_iter().map(str::to_owned).collect();
|
||||
(programs, p.notes.clone())
|
||||
@@ -7996,6 +8152,41 @@ impl Renderer {
|
||||
let taa = !frame.title
|
||||
&& self.pack_post.as_ref().is_none_or(|p| p.1.is_none())
|
||||
&& self.native.as_ref().is_none_or(|n| n.post.is_none());
|
||||
let hdr_lighting = self.native.is_none()
|
||||
&& self.pack_post.as_ref().is_none_or(|p| p.1.is_none())
|
||||
&& std::env::var_os("HORIZON_HDR_LIGHTING").is_none_or(|v| v != "0");
|
||||
if let Some(hdr) = &mut self.hdr {
|
||||
hdr.lighting = hdr_lighting && hdr.look;
|
||||
}
|
||||
let hdr_written = written_lane(
|
||||
self.hdr.as_ref().is_some_and(|h| h.layered()),
|
||||
self.frame_format,
|
||||
self.shader_pack_kind().is_some(),
|
||||
!frame.title,
|
||||
);
|
||||
if self.hdr_written != hdr_written {
|
||||
log::info!(
|
||||
"Horizon HDR frame: {}",
|
||||
if hdr_written {
|
||||
"written light"
|
||||
} else {
|
||||
"marked"
|
||||
}
|
||||
);
|
||||
}
|
||||
self.hdr_written = hdr_written;
|
||||
let (hdr_settings, hdr_shares) = self
|
||||
.hdr
|
||||
.as_ref()
|
||||
.map_or(([0.0; 4], [0.0; 4]), |h| h.draw_params(!frame.title));
|
||||
self.world_post
|
||||
.light_representation(self.hdr_written.then(|| {
|
||||
(
|
||||
hdr_settings,
|
||||
hdr_shares,
|
||||
self.hdr.as_ref().unwrap().windows(),
|
||||
)
|
||||
}));
|
||||
let jitter = self.world_post.jitter(
|
||||
[self.config.width, self.config.height],
|
||||
taa,
|
||||
@@ -8150,8 +8341,10 @@ impl Renderer {
|
||||
},
|
||||
// Only HDR output reads the solid terrain's lighting alpha.
|
||||
if self.hdr.is_some() { 1.0 } else { 0.0 },
|
||||
0.0,
|
||||
f32::from(u8::from(self.hdr_written)),
|
||||
],
|
||||
hdr_settings,
|
||||
hdr_shares,
|
||||
};
|
||||
self.uploads
|
||||
.buffer(&mut encoder, &self.globals, 0, bytemuck::bytes_of(&globals));
|
||||
@@ -8664,12 +8857,23 @@ impl Renderer {
|
||||
.push_in(group, v.distance, s.page, start, len, [0; 3]);
|
||||
}
|
||||
if let Some(c) = &mut cull {
|
||||
c.drawn(i, v.slot, indices, cull::SECTION_BOX);
|
||||
c.drawn(
|
||||
i,
|
||||
v.slot,
|
||||
indices,
|
||||
cull::section_box(self.sections.bounds(v.slot)),
|
||||
);
|
||||
}
|
||||
} else if let Some(c) = &mut cull {
|
||||
// One test sees the section's draws in both passes.
|
||||
let total: usize = runs.iter().map(|r| r.2).sum();
|
||||
if let Some(test) = c.deferred(i, v.slot, total, indices, cull::SECTION_BOX) {
|
||||
if let Some(test) = c.deferred(
|
||||
i,
|
||||
v.slot,
|
||||
total,
|
||||
indices,
|
||||
cull::section_box(self.sections.bounds(v.slot)),
|
||||
) {
|
||||
for &(group, start, len) in runs {
|
||||
if group == 0 {
|
||||
phase2_opaque += len;
|
||||
@@ -9076,6 +9280,39 @@ impl Renderer {
|
||||
self.uploads.buffer(&mut encoder, core.uniform(), 0, &data);
|
||||
}
|
||||
}
|
||||
// A pack's core/sky and core/stars: the disc with the sky colour, the stars with their
|
||||
// brightness, both turned with the camera only (SkyRenderer's model view).
|
||||
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 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();
|
||||
let projection = jitter * world_projection(fov, view.aspect, 2048.0) * pose;
|
||||
let [r, g, b, ..] = frame.sky.sky_color;
|
||||
let sky =
|
||||
core_sky::uniforms(block, projection, view.view, [r, g, b, 1.0], fog_color, fog);
|
||||
let stars = frame.stars.map(|(rotation, brightness)| {
|
||||
let b = brightness.clamp(0.0, 1.0);
|
||||
core_sky::uniforms(
|
||||
block,
|
||||
projection,
|
||||
view.view * rotation,
|
||||
[b; 4],
|
||||
fog_color,
|
||||
fog,
|
||||
)
|
||||
});
|
||||
core.prepare(&mut self.uploads, &mut encoder, &sky, stars.as_deref());
|
||||
}
|
||||
let skin = frame.skin.filter(|skin| match &mut self.entity_skin {
|
||||
Some(skinner) => skinner.register(&self.device, &mut self.uploads, &mut encoder, skin),
|
||||
None => false,
|
||||
@@ -9109,12 +9346,6 @@ impl Renderer {
|
||||
}
|
||||
// GameRenderer.useImprovedTransparency. A shader pack keeps its own
|
||||
// path.
|
||||
let hdr_lighting = self.native.is_none()
|
||||
&& self.pack_post.as_ref().is_none_or(|p| p.1.is_none())
|
||||
&& std::env::var_os("HORIZON_HDR_LIGHTING").is_none_or(|v| v != "0");
|
||||
if let Some(hdr) = &mut self.hdr {
|
||||
hdr.lighting = hdr_lighting && hdr.look;
|
||||
}
|
||||
let oit_on = self.improved_transparency
|
||||
&& !frame.title
|
||||
&& self.pack_post.as_ref().is_none_or(|p| p.1.is_none())
|
||||
@@ -9421,6 +9652,7 @@ impl Renderer {
|
||||
&mut encoder,
|
||||
jittered * rotation,
|
||||
brightness,
|
||||
&globals,
|
||||
);
|
||||
}
|
||||
self.custom_sky.prepare(
|
||||
@@ -9428,6 +9660,7 @@ impl Renderer {
|
||||
&mut encoder,
|
||||
jittered,
|
||||
&self.custom_sky_draws,
|
||||
&globals,
|
||||
);
|
||||
let celestial_vertices = frame.celestial.len().min(2048 * 4);
|
||||
if frame.end_sky && !frame.title {
|
||||
@@ -9982,6 +10215,12 @@ impl Renderer {
|
||||
view_proj: mirror_view_projection(view_proj, plane).to_cols_array_2d(),
|
||||
inverse_view,
|
||||
fog_ends,
|
||||
hdr_light: [
|
||||
globals.hdr_light[0],
|
||||
globals.hdr_light[1],
|
||||
globals.hdr_light[2],
|
||||
0.0,
|
||||
],
|
||||
..globals
|
||||
};
|
||||
self.uploads.buffer(
|
||||
@@ -10165,8 +10404,16 @@ impl Renderer {
|
||||
{
|
||||
Some((sky, native)) => sky.draw(&mut pass, native),
|
||||
None => {
|
||||
pass.set_pipeline(&self.sky_base);
|
||||
pass.draw(0..3, 0..1);
|
||||
match self.core_sky.as_ref().filter(|c| c.draws_disc()) {
|
||||
Some(core) => {
|
||||
core.draw_disc(&mut pass);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
None => {
|
||||
pass.set_pipeline(&self.sky_base);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
if frame.sky.sunrise_color[3] > 0.001 {
|
||||
pass.set_pipeline(&self.sunrise_pipeline);
|
||||
pass.set_vertex_buffer(0, self.sunrise_buffer.slice(..));
|
||||
@@ -10217,7 +10464,10 @@ impl Renderer {
|
||||
.as_ref()
|
||||
.is_some_and(|n| n.sky.is_some() && !n.vanilla_stars);
|
||||
if !frame.title && frame.stars.is_some() && !pack_draws_sky && !own_stars {
|
||||
self.stars.draw(&mut pass);
|
||||
match self.core_sky.as_ref().filter(|c| c.draws_stars()) {
|
||||
Some(core) => core.draw_stars(&mut pass),
|
||||
None => self.stars.draw(&mut pass),
|
||||
}
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if !frame.title && frame.dark_disc && !pack_draws_sky {
|
||||
@@ -10561,7 +10811,7 @@ impl Renderer {
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
pass.set_pipeline(&self.hdr_entity_mark_pipeline);
|
||||
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
|
||||
}
|
||||
@@ -10600,7 +10850,7 @@ impl Renderer {
|
||||
((base_quads - translucent_item_quads) * 4) as i32,
|
||||
0..1,
|
||||
);
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
// A glowing item in the world (Item sheet quads), as entities are.
|
||||
pass.set_pipeline(&self.hdr_mark_pipeline);
|
||||
pass.draw_indexed(
|
||||
@@ -10655,7 +10905,8 @@ impl Renderer {
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if crumbling_quads > 0 && !oit_on {
|
||||
// Cracks multiply the existing light; scalar OIT coverage cannot carry that factor.
|
||||
if crumbling_quads > 0 && (!oit_on || self.hdr_written) {
|
||||
pass.set_pipeline(&self.crumbling_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
@@ -10684,7 +10935,7 @@ impl Renderer {
|
||||
pass.set_pipeline(&self.lightning_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
self.lightning.draw(&mut pass, &frame.lightning);
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
pass.set_pipeline(&self.hdr_mark_lightning_pipeline);
|
||||
self.lightning.draw(&mut pass, &frame.lightning);
|
||||
}
|
||||
@@ -10724,7 +10975,7 @@ impl Renderer {
|
||||
pass.draw_indexed(first * 6..end * 6, (world_quads * 4) as i32, 0..1);
|
||||
first = end;
|
||||
}
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
// The dragon's eyes and death rays, marked as entities are.
|
||||
pass.set_pipeline(&self.hdr_mark_pipeline);
|
||||
pass.draw_indexed(0..additive_quads * 6, (world_quads * 4) as i32, 0..1);
|
||||
@@ -10831,18 +11082,20 @@ impl Renderer {
|
||||
}
|
||||
};
|
||||
quads(pass, &f.text[s], text_quads, entity_quads);
|
||||
quads(
|
||||
pass,
|
||||
&f.crumbling[s],
|
||||
crumbling_quads,
|
||||
entity_quads + text_quads,
|
||||
);
|
||||
quads(
|
||||
pass,
|
||||
&f.crumbling[s],
|
||||
translucent_crumbling_quads,
|
||||
entity_quads + text_quads + crumbling_quads,
|
||||
);
|
||||
if !self.hdr_written {
|
||||
quads(
|
||||
pass,
|
||||
&f.crumbling[s],
|
||||
crumbling_quads,
|
||||
entity_quads + text_quads,
|
||||
);
|
||||
quads(
|
||||
pass,
|
||||
&f.crumbling[s],
|
||||
translucent_crumbling_quads,
|
||||
entity_quads + text_quads + crumbling_quads,
|
||||
);
|
||||
}
|
||||
quads(
|
||||
pass,
|
||||
&f.swirl[s],
|
||||
@@ -10953,7 +11206,7 @@ impl Renderer {
|
||||
// HDR output's mark on what shines in the stages, as the classic
|
||||
// pass marks it after drawing it: eyes, the dragon's eyes and rays,
|
||||
// firework sparks and lightning.
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
pass.set_pipeline(&self.hdr_mark_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
@@ -11022,7 +11275,7 @@ impl Renderer {
|
||||
((base_quads - translucent_item_quads - translucent_particle_quads) * 4) as i32,
|
||||
0..1,
|
||||
);
|
||||
if self.hdr.is_some() {
|
||||
if self.hdr.is_some() && !self.hdr_written {
|
||||
// Fireworks: their sparks are translucent-layer particles.
|
||||
pass.set_pipeline(&self.hdr_entity_mark_pipeline);
|
||||
pass.draw_indexed(
|
||||
@@ -11062,7 +11315,7 @@ impl Renderer {
|
||||
if outline_edges > 0 && frame.outline_after_terrain && !pack_draws_outline {
|
||||
self.draw_outline(&mut pass, outline_edges as u32, frame.outline_high_contrast);
|
||||
}
|
||||
if translucent_crumbling_quads > 0 && !oit_on {
|
||||
if translucent_crumbling_quads > 0 && (!oit_on || self.hdr_written) {
|
||||
pass.set_pipeline(&self.crumbling_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
@@ -11659,7 +11912,9 @@ impl Renderer {
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(0..overlay.hand_quads * 6, overlay.first_hand_vertex, 0..1);
|
||||
drop(pass);
|
||||
if let Some(hdr) = &self.hdr {
|
||||
if let Some(hdr) = &self.hdr
|
||||
&& !self.hdr_written
|
||||
{
|
||||
hdr.mark_hand(encoder, timing.as_mut());
|
||||
// On the layered frame the hand's marks go to the frame, before they become
|
||||
// light with the world's.
|
||||
@@ -11708,6 +11963,7 @@ impl Renderer {
|
||||
// The layered frame's marks become light: what draws from here draws on light, and
|
||||
// nothing is lifted after.
|
||||
if layered
|
||||
&& !self.hdr_written
|
||||
&& !frame.title
|
||||
&& let Some(hdr) = &self.hdr
|
||||
{
|
||||
@@ -11779,6 +12035,13 @@ impl Renderer {
|
||||
timing.as_mut(),
|
||||
);
|
||||
}
|
||||
// The layered frame's light eases into the peak, before the camera overlays draw on it.
|
||||
if layered
|
||||
&& !frame.title
|
||||
&& let Some(hdr) = &self.hdr
|
||||
{
|
||||
hdr.tonemap(encoder, timing.as_mut());
|
||||
}
|
||||
if !frame.screen.is_empty() {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view: view_target,
|
||||
@@ -12498,7 +12761,7 @@ struct Overlay {
|
||||
hand_view_proj: Mat4,
|
||||
}
|
||||
|
||||
fn world_projection(fov_degrees: f32, aspect: f32, far: f32) -> Mat4 {
|
||||
pub(crate) fn world_projection(fov_degrees: f32, aspect: f32, far: f32) -> Mat4 {
|
||||
Mat4::perspective_rh(fov_degrees.to_radians(), aspect, far, NEAR_PLANE)
|
||||
}
|
||||
|
||||
@@ -12666,6 +12929,28 @@ fn frustum(m: Mat4) -> [Vec4; 6] {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn written_light_requires_the_entire_default_layered_world_lane() {
|
||||
use super::{LINEAR_FRAME, WIDE_FRAME, written_lane};
|
||||
assert!(written_lane(true, LINEAR_FRAME, false, true));
|
||||
for bits in 0..8 {
|
||||
let layered = bits & 1 != 0;
|
||||
let pack = bits & 2 != 0;
|
||||
let world = bits & 4 != 0;
|
||||
assert_eq!(
|
||||
written_lane(layered, LINEAR_FRAME, pack, world),
|
||||
layered && !pack && world
|
||||
);
|
||||
assert!(!written_lane(layered, WIDE_FRAME, pack, world));
|
||||
assert!(!written_lane(
|
||||
layered,
|
||||
wgpu::TextureFormat::Bgra8Unorm,
|
||||
pack,
|
||||
world,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn taa_culling_covers_jittered_view_edges_but_still_rejects_behind_the_camera() {
|
||||
use super::*;
|
||||
@@ -13389,12 +13674,21 @@ mod tests {
|
||||
for path in files {
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
if name == "oit.wgsl" || name == "gpu_occlusion.wgsl" {
|
||||
if name == "oit.wgsl" || name == "gpu_occlusion.wgsl" || name == "hdr_light.wgsl" {
|
||||
// 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 == "terrain.wgsl"
|
||||
|| name == "entity.wgsl"
|
||||
|| name == "stars.wgsl"
|
||||
|| name == "custom_sky.wgsl"
|
||||
{
|
||||
let base = include_str!("hdr_light.wgsl").to_owned() + &text;
|
||||
sources.push((path, crate::overlay::wgsl_constants() + &base));
|
||||
} else if name == "hdr.wgsl" {
|
||||
sources.push((path, super::hdr::shader()));
|
||||
} else if name == "oit_world.wgsl" {
|
||||
let base = include_str!("terrain.wgsl").to_owned() + include_str!("oit.wgsl");
|
||||
let base = super::terrain_shader() + include_str!("oit.wgsl");
|
||||
sources.push((path, base + &text));
|
||||
} else if name == "oit_features.wgsl" {
|
||||
let base = super::entity_shader() + include_str!("oit.wgsl");
|
||||
@@ -13403,7 +13697,7 @@ mod tests {
|
||||
sources.push((path, static_entity_shader()));
|
||||
} else if name == "visbuffer.wgsl" {
|
||||
// Appended to terrain.wgsl (gfx/visbuffer.rs).
|
||||
sources.push((path, include_str!("terrain.wgsl").to_owned() + &text));
|
||||
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");
|
||||
sources.push((path, base + &text));
|
||||
@@ -13414,6 +13708,26 @@ mod tests {
|
||||
sources.push((path, text));
|
||||
}
|
||||
}
|
||||
// The default override specializes written light away. Translate its active form too.
|
||||
let written: Vec<_> = sources
|
||||
.iter()
|
||||
.filter(|(path, _)| {
|
||||
matches!(
|
||||
path.file_name().and_then(|n| n.to_str()),
|
||||
Some("terrain.wgsl" | "entity.wgsl" | "stars.wgsl" | "custom_sky.wgsl")
|
||||
)
|
||||
})
|
||||
.map(|(path, source)| {
|
||||
(
|
||||
path.clone(),
|
||||
source.replace(
|
||||
"override HDR_WRITE_LIGHT: bool = false;",
|
||||
"override HDR_WRITE_LIGHT: bool = true;",
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
sources.extend(written);
|
||||
let mut glsl_checked = 0;
|
||||
// Shaders the OpenGL backend runs must translate: skipping the ones
|
||||
// that fail hides exactly the bug where a shader GL needs cannot
|
||||
@@ -13572,7 +13886,7 @@ mod tests {
|
||||
// `SolidPipeline::plain` builds terrain.wgsl's solid entries with `HDR_LIGHTING`
|
||||
// off on every backend: they must still translate (the other test translates them
|
||||
// with the constants' defaults).
|
||||
let module = naga::front::wgsl::parse_str(include_str!("terrain.wgsl")).unwrap();
|
||||
let module = naga::front::wgsl::parse_str(&super::terrain_shader()).unwrap();
|
||||
let info = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -13718,6 +14032,7 @@ mod tests {
|
||||
translucent_at: (0, u64::from(solid) * 4),
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed_far: false,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
@@ -14073,7 +14388,7 @@ mod tests {
|
||||
#[test]
|
||||
fn distant_tiles_repeat_their_sprite_under_the_clamped_sampler() {
|
||||
// terrain.wgsl's tile_uv, evaluated by naga as a constant expression.
|
||||
let source = include_str!("terrain.wgsl");
|
||||
let source = super::terrain_shader();
|
||||
let body = source.split("fn tile_uv(").nth(1).unwrap();
|
||||
let body = body.split("return ").nth(1).unwrap();
|
||||
let expr = &body[..body.find(';').unwrap()];
|
||||
|
||||
@@ -879,6 +879,7 @@ mod tests {
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed: false,
|
||||
packed_far: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
@@ -1047,7 +1048,15 @@ mod tests {
|
||||
hdr: None,
|
||||
}
|
||||
};
|
||||
let mut flat = flat::Flat::new(&device, &terrain_layout, 4 * GRAIN, 0, pipeline, solid);
|
||||
let mut flat = flat::Flat::new(
|
||||
&device,
|
||||
&terrain_layout,
|
||||
4 * GRAIN,
|
||||
0,
|
||||
false,
|
||||
pipeline,
|
||||
solid,
|
||||
);
|
||||
let stride = std::mem::size_of::<crate::mesher::TerrainVertex>() as u64;
|
||||
let mut translucent = Arena::translucent(&device, stride, 4 * GRAIN);
|
||||
translucent.page_limit = 4 * GRAIN;
|
||||
@@ -1083,6 +1092,7 @@ mod tests {
|
||||
solid: 64,
|
||||
solid_slots: 64 * 4,
|
||||
packed: false,
|
||||
packed_far: false,
|
||||
sides: crate::mesher::Sides::unsorted(64),
|
||||
translucent: 128,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
@@ -1310,6 +1320,7 @@ mod tests {
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed: false,
|
||||
packed_far: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
|
||||
@@ -67,6 +67,7 @@ fn copy(mesh: &SectionMesh) -> SectionMesh {
|
||||
sides: mesh.sides,
|
||||
translucent: mesh.translucent.clone(),
|
||||
packed: mesh.packed.clone(),
|
||||
packed_far: mesh.packed_far,
|
||||
solid_tints: Vec::new(),
|
||||
visibility: mesh.visibility,
|
||||
block_entities: mesh.block_entities.clone(),
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
//! A resource pack's `core/sky` and `core/stars` shaders: the overworld's sky disc and stars drawn
|
||||
//! with the pack's GLSL (`SkyRenderer.renderSkyDisc`, `renderStars`), as a pack that adds a haze
|
||||
//! to the horizon or tints the night sky expects. Without one the sky draws natively (`fs_sky` in
|
||||
//! terrain.wgsl, stars.rs).
|
||||
|
||||
use glam::Mat4;
|
||||
use horizon_shaders::{CoreProgram, Glue};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// `DefaultVertexFormat.POSITION`: the disc's and the stars' vertices.
|
||||
pub(crate) const GLUE: Glue = Glue {
|
||||
inputs: &[("Position", "vec3")],
|
||||
prelude: "layout(location = 0) in vec3 hz_position;",
|
||||
setup: "Position = hz_position;",
|
||||
// 26.3's ProjMat already has wgpu's reversed 0 to 1 depth (Projection: zZeroToOne).
|
||||
finish: "",
|
||||
vertex_id: "int(gl_VertexIndex)",
|
||||
};
|
||||
|
||||
/// Each uniform block's offset in the one buffer (`core_shader::BLOCKS` order).
|
||||
const BLOCK_OFFSETS: [u64; 4] = [0, 256, 512, 768];
|
||||
const BLOCK_SIZES: [u64; 4] = [48, 64, 160, 48];
|
||||
|
||||
/// `SkyRenderer.buildSkyDisc(16)`: a fan around the point 16 above the camera, out to 512 every
|
||||
/// 45 degrees, as a triangle list.
|
||||
pub(crate) fn disc_triangles() -> Vec<[f32; 3]> {
|
||||
let rim: Vec<[f32; 3]> = (-180..=180)
|
||||
.step_by(45)
|
||||
.map(|i: i32| {
|
||||
let a = (i as f32).to_radians();
|
||||
[512.0 * a.cos(), 16.0, 512.0 * a.sin()]
|
||||
})
|
||||
.collect();
|
||||
rim.windows(2)
|
||||
.flat_map(|edge| [[0.0, 16.0, 0.0], edge[0], edge[1]])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One of the sky's pack pipelines with its vertices and uniforms.
|
||||
struct Part {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
group: wgpu::BindGroup,
|
||||
uniform: wgpu::Buffer,
|
||||
vertices: wgpu::Buffer,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl Part {
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
program: &CoreProgram,
|
||||
name: &str,
|
||||
blend: Option<wgpu::BlendState>,
|
||||
triangles: &[[f32; 3]],
|
||||
) -> Option<Self> {
|
||||
// A pack's shader is untrusted: the backend failing to compile it is an internal error.
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
|
||||
let module = |stage, module: &naga::Module| {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&format!("Resource pack {name} ({stage})")),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module.clone())),
|
||||
})
|
||||
};
|
||||
let vertex = module("vertex", &program.vertex);
|
||||
let fragment = module("fragment", &program.fragment);
|
||||
// Vanilla's SKY and STARS pipelines have no samplers: a pack that reads one is not used.
|
||||
let entries: Vec<wgpu::BindGroupLayoutEntry> = (0..4)
|
||||
.map(|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,
|
||||
})
|
||||
.collect();
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some(name),
|
||||
entries: &entries,
|
||||
});
|
||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some(name),
|
||||
size: 1024,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let 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();
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(name),
|
||||
layout: &layout,
|
||||
entries: &resources,
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some(name),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(name),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vertex,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
|
||||
})],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
// The sky is drawn first, behind everything: no depth test, no depth write.
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fragment,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let internal = pollster::block_on(internal.pop());
|
||||
if let Some(e) = internal.or(pollster::block_on(validation.pop())) {
|
||||
log::warn!("the resource pack's {name} shader is not used: {e}");
|
||||
return None;
|
||||
}
|
||||
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(name),
|
||||
contents: bytemuck::cast_slice(triangles),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
Some(Self {
|
||||
pipeline,
|
||||
group,
|
||||
uniform,
|
||||
vertices,
|
||||
count: triangles.len() as u32,
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw(0..self.count, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The packs' sky disc and stars pipelines, each present when a pack replaces that shader.
|
||||
pub(super) struct CoreSky {
|
||||
disc: Option<Part>,
|
||||
stars: Option<Part>,
|
||||
}
|
||||
|
||||
impl CoreSky {
|
||||
/// `None` when neither shader is replaced or neither builds.
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
disc: Option<&CoreProgram>,
|
||||
stars: Option<&CoreProgram>,
|
||||
) -> Option<Self> {
|
||||
let disc = disc.and_then(|program| {
|
||||
// RenderPipelines.SKY: ColorTargetState.DEFAULT, no blending.
|
||||
Part::new(device, format, program, "sky", None, &disc_triangles())
|
||||
});
|
||||
let stars = stars.and_then(|program| {
|
||||
// RenderPipelines.STARS: BlendFunction.OVERLAY (src alpha and one; alpha one, zero).
|
||||
let overlay = wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::SrcAlpha,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::Zero,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
};
|
||||
let triangles = super::stars::star_triangles();
|
||||
Part::new(device, format, program, "stars", Some(overlay), &triangles)
|
||||
});
|
||||
(disc.is_some() || stars.is_some()).then_some(Self { disc, stars })
|
||||
}
|
||||
|
||||
pub(super) fn draws_disc(&self) -> bool {
|
||||
self.disc.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn draws_stars(&self) -> bool {
|
||||
self.stars.is_some()
|
||||
}
|
||||
|
||||
/// Uploads the disc's and the stars' blocks: `sky` and `stars` from [`uniforms`].
|
||||
pub(super) fn prepare(
|
||||
&self,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
sky: &[u8],
|
||||
stars: Option<&[u8]>,
|
||||
) {
|
||||
if let Some(disc) = &self.disc {
|
||||
uploads.buffer(encoder, &disc.uniform, 0, sky);
|
||||
}
|
||||
if let (Some(part), Some(data)) = (&self.stars, stars) {
|
||||
uploads.buffer(encoder, &part.uniform, 0, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// The disc, in place of the native sky colour. The caller sets its own group 0 again after.
|
||||
pub(super) fn draw_disc(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
if let Some(disc) = &self.disc {
|
||||
disc.draw(pass);
|
||||
}
|
||||
}
|
||||
|
||||
/// The stars, in place of the native ones. The caller sets its own group 0 again after.
|
||||
pub(super) fn draw_stars(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
if let Some(stars) = &self.stars {
|
||||
stars.draw(pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The std140 blocks for one sky draw: `Globals`, `Projection` (`projection`, far enough for the
|
||||
/// disc's 512-block rim), `DynamicTransforms` (`model_view`: the camera's rotation, times the
|
||||
/// stars' rotation for them; `color`: the sky colour, or the star brightness in every channel)
|
||||
/// and `Fog` (`fog_color`, `fog`: environmental start and end, render distance start and end,
|
||||
/// sky end, clouds end).
|
||||
pub(super) fn uniforms(
|
||||
globals: [u8; 48],
|
||||
projection: Mat4,
|
||||
model_view: Mat4,
|
||||
color: [f32; 4],
|
||||
fog_color: [f32; 4],
|
||||
fog: [f32; 6],
|
||||
) -> Vec<u8> {
|
||||
let mut data = vec![0f32; 256];
|
||||
data[64..80].copy_from_slice(&projection.to_cols_array());
|
||||
let transforms = 128;
|
||||
data[transforms..transforms + 16].copy_from_slice(&model_view.to_cols_array());
|
||||
data[transforms + 16..transforms + 32].copy_from_slice(&Mat4::IDENTITY.to_cols_array());
|
||||
data[transforms + 32..transforms + 36].copy_from_slice(&color);
|
||||
data[192..196].copy_from_slice(&fog_color);
|
||||
data[196..202].copy_from_slice(&fog);
|
||||
let mut bytes: Vec<u8> = bytemuck::cast_slice(&data).to_vec();
|
||||
bytes[..48].copy_from_slice(&globals);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `buildSkyDisc(16)`: eight triangles from the point above the camera to the rim, which
|
||||
/// starts and ends at x -512 (cos 180°) and passes through z -512, x 512 and z 512.
|
||||
#[test]
|
||||
fn the_disc_is_vanillas_fan_as_triangles() {
|
||||
let triangles = disc_triangles();
|
||||
assert_eq!(triangles.len(), 24);
|
||||
assert!(triangles.chunks(3).all(|t| t[0] == [0.0, 16.0, 0.0]));
|
||||
let rim: Vec<[f32; 3]> = triangles.chunks(3).map(|t| t[1]).collect();
|
||||
let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3);
|
||||
assert!(near(rim[0], [-512.0, 16.0, 0.0]));
|
||||
assert!(near(rim[2], [0.0, 16.0, -512.0]));
|
||||
assert!(near(rim[4], [512.0, 16.0, 0.0]));
|
||||
assert!(near(rim[6], [0.0, 16.0, 512.0]));
|
||||
assert!(near(triangles[23], [-512.0, 16.0, 0.0]));
|
||||
}
|
||||
|
||||
/// The prepared resources (`HORIZON_RESOURCES`, or `.local/horizon-client/<version>`), where
|
||||
/// vanilla's sky shader and its includes are.
|
||||
fn resources() -> std::path::PathBuf {
|
||||
std::env::var_os("HORIZON_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)
|
||||
})
|
||||
}
|
||||
|
||||
/// Vanilla's `sky.vsh` with a pack's fragment shader that paints `ColorModulator` (the sky
|
||||
/// colour) where the disc is: looking at the horizon, the top half of the target is the
|
||||
/// disc, the bottom half stays clear.
|
||||
#[test]
|
||||
fn a_pack_sky_shader_paints_the_disc_above_the_horizon() {
|
||||
let root = resources();
|
||||
let Ok(vertex) =
|
||||
std::fs::read_to_string(root.join("assets/minecraft/shaders/core/sky.vsh"))
|
||||
else {
|
||||
eprintln!("skipping: no prepared resources at {}", root.display());
|
||||
return;
|
||||
};
|
||||
let include = |id: &str| {
|
||||
let (namespace, path) = id.split_once(':')?;
|
||||
std::fs::read_to_string(root.join(format!("assets/{namespace}/{path}"))).ok()
|
||||
};
|
||||
let fragment = "#version 330\n#include <minecraft:dynamictransforms.glsl>\n\
|
||||
layout(location = 0) out vec4 fragColor;\n\
|
||||
void main() {\n fragColor = ColorModulator;\n}\n";
|
||||
let program = horizon_shaders::translate_core(&vertex, fragment, &[], &include, &GLUE)
|
||||
.expect("the sky shaders translate");
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Some((device, queue)) =
|
||||
pollster::block_on(instance.request_adapter(&Default::default()))
|
||||
.ok()
|
||||
.and_then(|adapter| {
|
||||
pollster::block_on(adapter.request_device(&Default::default())).ok()
|
||||
})
|
||||
else {
|
||||
eprintln!("no GPU: skipping");
|
||||
return;
|
||||
};
|
||||
const SIZE: u32 = 64;
|
||||
let format = wgpu::TextureFormat::Rgba8Unorm;
|
||||
let sky = CoreSky::new(&device, format, Some(&program), None).expect("the sky pipeline");
|
||||
let projection = crate::gfx::world_projection(90.0, 1.0, 2048.0);
|
||||
let data = uniforms(
|
||||
[0; 48],
|
||||
projection,
|
||||
Mat4::IDENTITY,
|
||||
[0.25, 0.5, 1.0, 1.0],
|
||||
[0.0; 4],
|
||||
[1e6; 6],
|
||||
);
|
||||
queue.write_buffer(&sky.disc.as_ref().unwrap().uniform, 0, &data);
|
||||
let target = |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: wgpu::TextureUsages::RENDER_ATTACHMENT | usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let color = target(format, wgpu::TextureUsages::COPY_SRC);
|
||||
let depth = target(
|
||||
wgpu::TextureFormat::Depth32Float,
|
||||
wgpu::TextureUsages::empty(),
|
||||
);
|
||||
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()
|
||||
});
|
||||
sky.draw_disc(&mut pass);
|
||||
}
|
||||
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 at = |x: usize, y: usize| pixels[(y * 64 + x) * 4..(y * 64 + x) * 4 + 4].to_vec();
|
||||
// Row 0 is the top: the disc, in the sky colour; the bottom row is below the horizon.
|
||||
let top = at(32, 4);
|
||||
assert!(
|
||||
top.iter()
|
||||
.zip([64u8, 128, 255, 255])
|
||||
.all(|(&p, w)| p.abs_diff(w) <= 1),
|
||||
"{top:?}"
|
||||
);
|
||||
assert_eq!(at(32, 60), vec![0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
//! second pass, draws the sections whose box was seen; the others' draws have
|
||||
//! an instance count of 0.
|
||||
//!
|
||||
//! The picture stays the one without culling: the boxes (grown by a block,
|
||||
//! the farthest a block model reaches out of its cell) are drawn front faces
|
||||
//! The picture stays the one without culling: the boxes around the mesh's
|
||||
//! geometry (padded by a block) are drawn front faces
|
||||
//! only, so a fragment is the box's nearest point on its pixel, and the
|
||||
//! fragment shader runs only where the depth test passes (forced early), so a
|
||||
//! section is left out only where all of its fragments would fail the depth
|
||||
@@ -45,6 +45,15 @@ use std::sync::{
|
||||
const WORDS: u64 = 5 * 4;
|
||||
/// A tested section's box, around its origin: its cell grown by a block.
|
||||
pub const SECTION_BOX: [[f32; 3]; 2] = [[-1.0; 3], [17.0; 3]];
|
||||
/// Mesh bounds already contain `BOUNDS_PAD`; grow them to a block of padding
|
||||
/// in total, keeping the box's faces off the geometry. Without mesh bounds,
|
||||
/// keep the section cell grown by a block, as before.
|
||||
pub fn section_box(bounds: Option<[glam::Vec3; 2]>) -> [[f32; 3]; 2] {
|
||||
bounds.map_or(SECTION_BOX, |[low, high]| {
|
||||
let pad = glam::Vec3::splat(1.0 - crate::mesher::SectionMesh::BOUNDS_PAD);
|
||||
[(low - pad).to_array(), (high + pad).to_array()]
|
||||
})
|
||||
}
|
||||
/// The bit that marks a far tile's slot (`Renderer::tile_cull_slot`): tiles
|
||||
/// count their slots apart from the sections' resident ones.
|
||||
pub const TILE: u32 = 1 << 31;
|
||||
|
||||
@@ -16,8 +16,8 @@ struct Globals {
|
||||
// `place`): the first pass's first draw, both passes' draw counts (the
|
||||
// second's in the high 16 bits), the second pass's first draw. No draws:
|
||||
// phase 1 drew it, and the test only tells whether it is still in sight.
|
||||
// Then its box's lowest and highest corner around the origin (a section's:
|
||||
// -1 and 17, `SECTION_BOX`).
|
||||
// Then its box's lowest and highest corner around the origin: a section's
|
||||
// padded mesh bounds, or -1 and 17 (`SECTION_BOX`) without mesh bounds.
|
||||
struct Test {
|
||||
data: vec4<u32>,
|
||||
low: vec4<f32>,
|
||||
@@ -53,8 +53,8 @@ fn corner(origin: vec3<f32>, low: vec3<f32>, high: vec3<f32>, c: u32) -> vec3<f3
|
||||
);
|
||||
}
|
||||
|
||||
// Each tested section's or far tile's box, grown by a block (block models
|
||||
// reach one block out of their cell), drawn after phase 1 against its depth
|
||||
// Each tested section's or far tile's geometry box, padded by a block,
|
||||
// drawn after phase 1 against its depth
|
||||
// with back faces culled: a front face fragment that passes the depth test
|
||||
// is the box's nearest point on that pixel in front of phase 1's depth, so
|
||||
// the section may show there. A box failing everywhere would have every
|
||||
|
||||
@@ -33,6 +33,9 @@ struct Uniform {
|
||||
brightness: f32,
|
||||
mode: u32,
|
||||
pad: [u32; 2],
|
||||
hdr_settings: [f32; 4],
|
||||
hdr_shares: [f32; 4],
|
||||
hdr_light: [f32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@@ -45,6 +48,8 @@ struct Vertex {
|
||||
|
||||
pub(super) struct CustomSky {
|
||||
pipelines: Vec<wgpu::RenderPipeline>,
|
||||
written_gain: Option<f32>,
|
||||
written_debug: bool,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
sampler: wgpu::Sampler,
|
||||
uniform: wgpu::Buffer,
|
||||
@@ -136,7 +141,10 @@ impl CustomSky {
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Custom sky shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("custom_sky.wgsl").into()),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl"))
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Custom sky layout"),
|
||||
@@ -146,7 +154,11 @@ impl CustomSky {
|
||||
let attributes = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32x4];
|
||||
let pipelines = MODES
|
||||
.iter()
|
||||
.map(|&mode| {
|
||||
.copied()
|
||||
.map(|mode| (mode, false, false))
|
||||
.chain((format == super::LINEAR_FRAME).then_some((Blend::Screen, true, false)))
|
||||
.chain((format == super::LINEAR_FRAME).then_some((Blend::Add, false, true)))
|
||||
.map(|(mode, written_screen, debug_add)| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Custom sky layer"),
|
||||
layout: Some(&pipeline_layout),
|
||||
@@ -174,14 +186,31 @@ impl CustomSky {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_custom_sky"),
|
||||
compilation_options: Default::default(),
|
||||
entry_point: Some(if debug_add {
|
||||
"fs_custom_sky_screen_light"
|
||||
} else {
|
||||
"fs_custom_sky"
|
||||
}),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
// The frame's alpha is HDR output's mark (hdr.wgsl): the sky
|
||||
// under the layers keeps its own.
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: blend(mode),
|
||||
color: if written_screen {
|
||||
wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Constant,
|
||||
..blend(mode)
|
||||
}
|
||||
} else {
|
||||
blend(mode)
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Zero,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
@@ -215,6 +244,8 @@ impl CustomSky {
|
||||
});
|
||||
Self {
|
||||
pipelines,
|
||||
written_gain: None,
|
||||
written_debug: false,
|
||||
layout,
|
||||
sampler,
|
||||
uniform,
|
||||
@@ -295,7 +326,20 @@ impl CustomSky {
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
clip: Mat4,
|
||||
draws: &[Draw],
|
||||
globals: &super::Globals,
|
||||
) {
|
||||
self.written_gain = (globals.hdr_light[3] != 0.0).then(|| {
|
||||
let mark =
|
||||
((1.0 - 0.24 * globals.hdr_light[1]).clamp(0.0, 1.0) * 255.0).round() / 255.0;
|
||||
if globals.hdr_settings[2] > 0.5 && globals.hdr_settings[0] > 1.0 {
|
||||
(1.0 + (globals.hdr_settings[1] - 1.0) * (1.0 - mark) / 0.24).powf(1.0 / 2.2)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
});
|
||||
self.written_debug = self.written_gain.is_some()
|
||||
&& globals.hdr_settings[3] > 0.5
|
||||
&& globals.hdr_settings[2] > 0.5;
|
||||
self.frame.clear();
|
||||
for draw in draws.iter().take(MAX_DRAWS) {
|
||||
if usize::from(draw.image) >= self.groups.len() {
|
||||
@@ -307,6 +351,9 @@ impl CustomSky {
|
||||
brightness: draw.brightness.clamp(0.0, 1.0),
|
||||
mode: mode as u32,
|
||||
pad: [0; 2],
|
||||
hdr_settings: globals.hdr_settings,
|
||||
hdr_shares: globals.hdr_shares,
|
||||
hdr_light: globals.hdr_light,
|
||||
};
|
||||
let offset = STRIDE * self.frame.len() as u64;
|
||||
uploads.buffer(encoder, &self.uniform, offset, bytemuck::bytes_of(&uniform));
|
||||
@@ -323,7 +370,35 @@ impl CustomSky {
|
||||
pub(super) fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
for (i, &(image, mode)) in self.frame.iter().enumerate() {
|
||||
pass.set_pipeline(&self.pipelines[usize::from(mode)]);
|
||||
let screen_debug = usize::from(mode) == 5 && self.written_debug;
|
||||
if screen_debug {
|
||||
// Greyscale debug is not a uniform RGB gain. Attenuate with the original
|
||||
// source first, then add its converted light, without changing the factor.
|
||||
pass.set_pipeline(&self.pipelines[4]);
|
||||
pass.set_bind_group(
|
||||
0,
|
||||
&self.groups[usize::from(image)],
|
||||
&[(STRIDE * i as u64) as u32],
|
||||
);
|
||||
pass.draw(0..36, 0..1);
|
||||
}
|
||||
let index = if screen_debug {
|
||||
10
|
||||
} else if usize::from(mode) == 5 && self.written_gain.is_some() {
|
||||
9
|
||||
} else {
|
||||
usize::from(mode)
|
||||
};
|
||||
if let Some(gain) = self.written_gain {
|
||||
let gain = f64::from(gain);
|
||||
pass.set_blend_constant(wgpu::Color {
|
||||
r: gain,
|
||||
g: gain,
|
||||
b: gain,
|
||||
a: 1.0,
|
||||
});
|
||||
}
|
||||
pass.set_pipeline(&self.pipelines[index]);
|
||||
pass.set_bind_group(
|
||||
0,
|
||||
&self.groups[usize::from(image)],
|
||||
@@ -394,7 +469,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_shader_validates() {
|
||||
let module = naga::front::wgsl::parse_str(include_str!("custom_sky.wgsl")).unwrap();
|
||||
let module = naga::front::wgsl::parse_str(
|
||||
&(include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl")),
|
||||
)
|
||||
.unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -413,4 +491,243 @@ mod tests {
|
||||
assert_eq!(v.position.iter().filter(|c| c.abs() == 1.0).count(), 3);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn written_custom_sky_modes_keep_factors_and_scale_only_screen_source() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let (device, queue) =
|
||||
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let source = include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ include_str!("custom_sky.wgsl")
|
||||
+ r#"
|
||||
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> Out {
|
||||
let p = vec2(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
return Out(vec4(p * 2.0 - 1.0, 0.0, 1.0), vec2(0.5), vec4(0.0, 0.0, 1.0, 1.0));
|
||||
}
|
||||
"#;
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let sky = CustomSky::new(&device, wgpu::TextureFormat::Rgba16Float);
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[Some(&sky.layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let image = device.create_texture_with_data(
|
||||
&queue,
|
||||
&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: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&[204, 102, 51, 128],
|
||||
);
|
||||
let image = image.create_view(&Default::default());
|
||||
let sampler = device.create_sampler(&Default::default());
|
||||
let target = 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: wgpu::TextureFormat::Rgba16Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = target.create_view(&Default::default());
|
||||
// Independent scalar expectation: a sky mark of 206/255 gives this gamma gain.
|
||||
let gain = (1.0f32 + 3.0 * (1.0 - 206.0 / 255.0) / 0.24).powf(1.0 / 2.2);
|
||||
for (mode_index, mode, debug) in MODES
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, m)| (i, m, false))
|
||||
.chain([(5, Blend::Screen, true)])
|
||||
{
|
||||
let uniform = Uniform {
|
||||
clip_from_sky: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
brightness: 0.7,
|
||||
mode: mode_index as u32,
|
||||
pad: [0; 2],
|
||||
hdr_settings: [5.0, 4.0, 1.0, f32::from(u8::from(debug))],
|
||||
hdr_shares: [0.2, 0.5, 0.6, 1.0],
|
||||
hdr_light: [0.8, 0.8, 1.0, 1.0],
|
||||
};
|
||||
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::bytes_of(&uniform),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let color = if debug {
|
||||
blend(Blend::Add)
|
||||
} else if mode == Blend::Screen {
|
||||
wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Constant,
|
||||
..blend(mode)
|
||||
}
|
||||
} else {
|
||||
blend(mode)
|
||||
};
|
||||
let pipeline_for = |entry, color| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("test_vertex"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some(entry),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[("HDR_WRITE_LIGHT", 1.0)],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Rgba16Float,
|
||||
blend: Some(wgpu::BlendState {
|
||||
color,
|
||||
alpha: wgpu::BlendComponent::REPLACE,
|
||||
}),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let pipeline = pipeline_for(
|
||||
if debug {
|
||||
"fs_custom_sky_screen_light"
|
||||
} else {
|
||||
"fs_custom_sky"
|
||||
},
|
||||
color,
|
||||
);
|
||||
let attenuate = debug.then(|| pipeline_for("fs_custom_sky", blend(Blend::Burn)));
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &sky.layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&image),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 2.0,
|
||||
g: 2.0,
|
||||
b: 2.0,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
let g = f64::from(gain);
|
||||
pass.set_blend_constant(wgpu::Color {
|
||||
r: g,
|
||||
g,
|
||||
b: g,
|
||||
a: 1.0,
|
||||
});
|
||||
pass.set_bind_group(0, &group, &[0]);
|
||||
if let Some(attenuate) = &attenuate {
|
||||
pass.set_pipeline(attenuate);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
pass.set_pipeline(&pipeline);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
let readback = 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(
|
||||
target.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &readback,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(256),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
target.size(),
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
readback.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let data = readback.get_mapped_range(..).unwrap();
|
||||
let got = half::f16::from_le_bytes([data[0], data[1]]).to_f32();
|
||||
let (d, t, k) = (2.0, 0.8, 128.0 / 255.0 * 0.7);
|
||||
let expected = 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))
|
||||
.powf(1.0 / 2.2);
|
||||
gain * grey * k + d * (1.0 - t * k)
|
||||
} else {
|
||||
match mode {
|
||||
Blend::Add => d + gain * t * k,
|
||||
Blend::Subtract => d - gain * t * k,
|
||||
Blend::Multiply => d * (1.0 + (t - 1.0) * k),
|
||||
Blend::Dodge => d + d * t * k,
|
||||
Blend::Burn => d * (1.0 - t * k),
|
||||
Blend::Screen => gain * t * k + d * (1.0 - t * k),
|
||||
Blend::Replace => gain * t,
|
||||
Blend::Overlay => 2.0 * d * (0.5 + (t - 0.5) * k),
|
||||
Blend::Alpha => gain * t * k + d * (1.0 - k),
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
(got - expected).abs() < 0.004,
|
||||
"{mode:?} debug {debug}: {got} expected {expected}"
|
||||
);
|
||||
}
|
||||
eprintln!("HDR custom sky GPU ran all nine modes and debug Screen over above-white light");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ struct Layer {
|
||||
brightness: f32,
|
||||
// horizon_optifine::sky::Blend, in gfx/custom_sky.rs's MODES order.
|
||||
mode: u32,
|
||||
pad: vec2<u32>,
|
||||
hdr_settings: vec4<f32>,
|
||||
hdr_shares: vec4<f32>,
|
||||
hdr_light: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> layer: Layer;
|
||||
@group(0) @binding(1) var sky_texture: texture_2d<f32>;
|
||||
@@ -42,20 +46,34 @@ const REPLACE: u32 = 6u;
|
||||
const OVERLAY: u32 = 7u;
|
||||
const ALPHA: u32 = 8u;
|
||||
|
||||
@fragment fn fs_custom_sky(in: Out) -> @location(0) vec4<f32> {
|
||||
fn custom_sky_color(in: Out) -> vec4<f32> {
|
||||
let half = 0.5 / vec2<f32>(textureDimensions(sky_texture));
|
||||
let uv = clamp(in.uv, in.cell.xy + half, in.cell.zw - half);
|
||||
let t = textureSample(sky_texture, sky_sampler, uv);
|
||||
let k = t.a * layer.brightness;
|
||||
var color = t.rgb;
|
||||
if HDR_WRITE_LIGHT && layer.hdr_light.w != 0.0 &&
|
||||
(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);
|
||||
}
|
||||
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(t.rgb, 1.0); }
|
||||
case REPLACE: { return vec4(color, 1.0); }
|
||||
// mix(dst, src, alpha).
|
||||
case ALPHA: { return vec4(t.rgb, k); }
|
||||
case ALPHA: { return vec4(color, k); }
|
||||
// add, subtract, dodge, burn, screen: the layer premultiplied, black leaves it alone.
|
||||
default: { return vec4(t.rgb * k, 1.0); }
|
||||
default: { return vec4(color * k, 1.0); }
|
||||
}
|
||||
}
|
||||
|
||||
@fragment fn fs_custom_sky(in: Out) -> @location(0) vec4<f32> {
|
||||
return custom_sky_color(in);
|
||||
}
|
||||
|
||||
@fragment fn fs_custom_sky_screen_light(in: Out) -> @location(0) vec4<f32> {
|
||||
let color = custom_sky_color(in);
|
||||
return vec4(hdr_written(color.rgb, 1.0 - 0.24 * layer.hdr_light.y, layer.hdr_settings, layer.hdr_shares), color.a);
|
||||
}
|
||||
@@ -106,6 +106,8 @@ pub struct Flat {
|
||||
phase2_calls: wgpu::Buffer,
|
||||
phase2_call_capacity: u64,
|
||||
phase2_call_data: Vec<[u32; 5]>,
|
||||
/// GPU culling's phase 2 uses indirect draws; flat draws without culling do not.
|
||||
indirect_execution: bool,
|
||||
/// The copy of phase 2's draws, and its ranges.
|
||||
phase2_group: Option<(wgpu::BindGroup, u32)>,
|
||||
/// The layout of a page's bind group 1 (shader packs' terrain pipelines share it).
|
||||
@@ -365,6 +367,7 @@ impl Flat {
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
page_vertices: u64,
|
||||
min_sections: usize,
|
||||
indirect_execution: bool,
|
||||
pipeline: impl Fn(
|
||||
&wgpu::PipelineLayout,
|
||||
&str,
|
||||
@@ -489,10 +492,16 @@ impl Flat {
|
||||
device,
|
||||
"Flat section draw calls, phase 2",
|
||||
20,
|
||||
wgpu::BufferUsages::INDIRECT | storage_usage,
|
||||
storage_usage
|
||||
| if indirect_execution {
|
||||
wgpu::BufferUsages::INDIRECT
|
||||
} else {
|
||||
wgpu::BufferUsages::empty()
|
||||
},
|
||||
),
|
||||
phase2_call_capacity: 1,
|
||||
phase2_call_data: Vec::new(),
|
||||
indirect_execution,
|
||||
phase2_group: None,
|
||||
draw_layout,
|
||||
blocks_per_page: page_vertices.div_ceil(GRAIN),
|
||||
@@ -645,6 +654,7 @@ impl Flat {
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) -> bool {
|
||||
self.phase2_group = None;
|
||||
let phase2 = phase2.filter(|_| self.indirect_execution);
|
||||
let phase1_draws = if phase2.is_some() {
|
||||
phase1_draws
|
||||
} else {
|
||||
@@ -753,7 +763,12 @@ impl Flat {
|
||||
device,
|
||||
"Flat section draw calls, phase 2",
|
||||
self.phase2_call_capacity * 20,
|
||||
wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::STORAGE,
|
||||
wgpu::BufferUsages::STORAGE
|
||||
| if self.indirect_execution {
|
||||
wgpu::BufferUsages::INDIRECT
|
||||
} else {
|
||||
wgpu::BufferUsages::empty()
|
||||
},
|
||||
);
|
||||
}
|
||||
if phase2_calls > 0 {
|
||||
|
||||
@@ -72,25 +72,29 @@ fn named(name: Option<&str>) -> Option<Output> {
|
||||
struct Targets {
|
||||
size: [u32; 2],
|
||||
frame: wgpu::Texture,
|
||||
/// The world as `keep_world` kept it; on the layered frame, the frame as the world and the
|
||||
/// hand marked it, for `resolve`.
|
||||
/// The world as `keep_world` kept it; on the layered frame, the frame as `resolve` (the
|
||||
/// world and the hand marked it) or `tonemap` found it.
|
||||
world: wgpu::Texture,
|
||||
coverage: Option<wgpu::Texture>,
|
||||
/// The layered frame's HUD target: what the HUD draws on.
|
||||
hud: Option<wgpu::Texture>,
|
||||
group: wgpu::BindGroup,
|
||||
/// The layered frame's passes' bindings: `resolve`, `keep_world` and `composite`.
|
||||
/// The layered frame's passes' bindings: `resolve` (and `tonemap`), `keep_world` and
|
||||
/// `composite`.
|
||||
passes: Option<[wgpu::BindGroup; 3]>,
|
||||
}
|
||||
|
||||
/// The float frame with HUD coverage (`hud_coverage::supported`, docs/design/hdr-linear-light.md
|
||||
/// "The HUD and menus"): the world's marks become light once, after the world and the hand
|
||||
/// (`resolve`); the camera overlays draw on that light; the HUD draws on a target of its own,
|
||||
/// "The HUD and menus"): the default world draws write light directly; marked fallback frames
|
||||
/// turn their marks into light after the world and the hand (`resolve`); the tone curve eases it into the peak after the post effects (`tonemap`); the
|
||||
/// camera overlays draw on that light; the HUD draws on a target of its own,
|
||||
/// the world clipped to white (`keep_world`); and the last pass composites the HUD over the
|
||||
/// world's light by its coverage (`fs_hdr_layered`), as the menu blur does before it blurs
|
||||
/// (`composite`).
|
||||
struct Layered {
|
||||
resolve: (wgpu::BindGroupLayout, wgpu::RenderPipeline),
|
||||
/// Reads what `resolve` reads.
|
||||
tonemap: wgpu::RenderPipeline,
|
||||
keep: (wgpu::BindGroupLayout, wgpu::RenderPipeline),
|
||||
composite: (wgpu::BindGroupLayout, wgpu::RenderPipeline),
|
||||
/// What `resolve` and `composite` read, written before they run (`begin`).
|
||||
@@ -197,6 +201,10 @@ pub struct Hdr {
|
||||
pub calibration: Option<Calibration>,
|
||||
}
|
||||
|
||||
pub(super) fn shader() -> String {
|
||||
include_str!("../hdr_light.wgsl").to_owned() + include_str!("../hdr.wgsl")
|
||||
}
|
||||
|
||||
impl Hdr {
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
@@ -220,7 +228,7 @@ impl Hdr {
|
||||
let coverage_debug = coverage && debug == Some("coverage");
|
||||
// The layered frame's last pass, else today's.
|
||||
let entry_point = if coverage { "fs_hdr_layered" } else { "fs_hdr" };
|
||||
let mut source = include_str!("../hdr.wgsl").to_owned();
|
||||
let mut source = shader();
|
||||
if coverage_debug {
|
||||
// The debug view wraps the last pass as it is, so the picture under the tint is
|
||||
// exactly the one without it.
|
||||
@@ -329,8 +337,10 @@ impl Hdr {
|
||||
let pipeline = pass(label, &layout, entry, format);
|
||||
(layout, pipeline)
|
||||
};
|
||||
let resolve = with("Horizon HDR marks to light", &[1], true, "fs_resolve");
|
||||
Layered {
|
||||
resolve: with("Horizon HDR marks to light", &[1], true, "fs_resolve"),
|
||||
tonemap: pass("Horizon HDR tone curve", &resolve.0, "fs_tonemap", format),
|
||||
resolve,
|
||||
keep: with("Horizon HDR HUD backdrop", &[0], false, "fs_keep"),
|
||||
composite: with("Horizon HDR composite", &[0, 4, 5], true, "fs_composite"),
|
||||
params: params(),
|
||||
@@ -532,11 +542,33 @@ impl Hdr {
|
||||
}
|
||||
|
||||
/// On the layered frame, after the world and the hand drew and marked it: the marks
|
||||
/// become light in the frame (`fs_resolve`), read from a copy of it in the world target.
|
||||
/// become light in the frame (`fs_resolve`).
|
||||
pub fn resolve(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
self.redraw(encoder, |l| &l.resolve.1, "HDR marks to light", timing);
|
||||
}
|
||||
|
||||
/// On the layered frame, after the post effects and before the camera overlays: the tone
|
||||
/// curve eases the light into the peak (`fs_tonemap`).
|
||||
pub fn tonemap(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
self.redraw(encoder, |l| &l.tonemap, "HDR tone curve", timing);
|
||||
}
|
||||
|
||||
/// The layered frame drawn again by `pipeline`, which reads it from a copy in the world
|
||||
/// target.
|
||||
fn redraw(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
pipeline: impl Fn(&Layered) -> &wgpu::RenderPipeline,
|
||||
label: &'static str,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let (Some(l), Some(t)) = (&self.layered, &self.targets) else {
|
||||
return;
|
||||
@@ -550,14 +582,7 @@ impl Hdr {
|
||||
t.frame.size(),
|
||||
);
|
||||
let view = t.frame.create_view(&Default::default());
|
||||
Self::draw(
|
||||
encoder,
|
||||
&l.resolve.1,
|
||||
group,
|
||||
&view,
|
||||
"HDR marks to light",
|
||||
timing,
|
||||
);
|
||||
Self::draw(encoder, pipeline(l), group, &view, label, timing);
|
||||
}
|
||||
|
||||
/// On the layered frame: the picture so far (`fs_composite`), as the frame stores light,
|
||||
@@ -811,6 +836,11 @@ impl Hdr {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn draw_params(&self, world: bool) -> ([f32; 4], [f32; 4]) {
|
||||
let p = self.params(world, false);
|
||||
([p[1], p[7], p[2], p[3]], p[8..12].try_into().unwrap())
|
||||
}
|
||||
|
||||
/// hdr.wgsl's `Params`.
|
||||
fn params(&self, world: bool, pq: bool) -> [f32; 16] {
|
||||
let paper = self.paper_white.max(1.0);
|
||||
@@ -921,6 +951,99 @@ mod tests {
|
||||
use super::{Hdr, Output, named, pick};
|
||||
use wgpu::{SurfaceColorSpaces as S, TextureFormat as F};
|
||||
|
||||
#[test]
|
||||
fn shared_light_matches_the_original_for_every_mark() {
|
||||
use wgpu::util::DeviceExt;
|
||||
let Some((device, queue)) = gpu() else {
|
||||
eprintln!("HDR equivalence GPU unavailable");
|
||||
return;
|
||||
};
|
||||
// All 8-bit marks, both easing policies, non-grey and over-white colours, look,
|
||||
// debug, headroom and tier shares; half-float neighbours straddle mark boundaries.
|
||||
let source = include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ r#"
|
||||
struct ReferenceParams { peak: f32, scene: f32, world: f32, debug: f32, share: vec4<f32> }
|
||||
var<private> params: ReferenceParams;
|
||||
@group(0) @binding(0) var<storage, read_write> errors: array<u32>;
|
||||
"# + include_str!("hdr_light_reference.txt")
|
||||
+ r#"
|
||||
@compute @workgroup_size(64) fn check(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let i = id.x;
|
||||
let mark = f32(i % 256u) / 255.0;
|
||||
let mode = i / 256u;
|
||||
let eased = (mode & 1u) != 0u;
|
||||
let shows = (mode & 2u) != 0u;
|
||||
params = ReferenceParams(select(1.0, 1000.0 / 203.0, (mode & 4u) != 0u),
|
||||
select(1.0, 4.0, (mode & 8u) != 0u), select(0.0, 1.0, (mode & 16u) != 0u),
|
||||
select(0.0, 1.0, (mode & 32u) != 0u), vec4(0.2, 0.5, 0.6, 1.0));
|
||||
let color = select(vec3(0.9, 0.4, 0.02), vec3(1.4, 0.8, 0.2), (mode & 64u) != 0u);
|
||||
let expected = reference_lit(color, mark, shows, eased);
|
||||
let actual = hdr_mark_light(color, mark, shows, eased,
|
||||
vec4(params.peak, params.scene, params.world, params.debug), params.share);
|
||||
errors[i] = select(0u, 1u, any(abs(expected.light - actual.light) > vec3(1e-6)) || expected.lifted != actual.lifted);
|
||||
let boundary = (f32(i % 255u) + 0.5) / 255.0;
|
||||
for (var step = -1; step <= 1; step++) {
|
||||
let stored = unpack2x16float(pack2x16float(vec2(boundary + f32(step) / 65536.0))).x;
|
||||
let quantized = round(saturate(stored) * 255.0) / 255.0;
|
||||
let want = pow(reference_lit(color, quantized, params.world > 0.5 && params.peak > 1.0, false).light, vec3(1.0 / 2.2));
|
||||
let got = hdr_written(color, stored, vec4(params.peak, params.scene, params.world, params.debug), params.share);
|
||||
errors[i] |= select(0u, 2u, any(abs(want - got) > vec3(1e-6)));
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("shared HDR equivalence"),
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: None,
|
||||
layout: None,
|
||||
module: &shader,
|
||||
entry_point: Some("check"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
let count = 256 * 128;
|
||||
let output = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: &vec![0; count * 4],
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||
});
|
||||
let readback = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: (count * 4) as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pipeline.get_bind_group_layout(0),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: output.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let mut pass = encoder.begin_compute_pass(&Default::default());
|
||||
pass.set_pipeline(&pipeline);
|
||||
pass.set_bind_group(0, &group, &[]);
|
||||
pass.dispatch_workgroups(count as u32 / 64, 1, 1);
|
||||
}
|
||||
encoder.copy_buffer_to_buffer(&output, 0, &readback, 0, (count * 4) as u64);
|
||||
queue.submit([encoder.finish()]);
|
||||
readback.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let data = readback.get_mapped_range(..).unwrap();
|
||||
for (i, bytes) in data.as_chunks::<4>().0.iter().enumerate() {
|
||||
assert_eq!(u32::from_le_bytes(*bytes), 0, "case {i}");
|
||||
}
|
||||
eprintln!(
|
||||
"HDR equivalence GPU ran {count} cases and {} half-float boundary cases",
|
||||
count * 3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_target_resizes_resets_and_shows_in_the_debug_view() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
@@ -1007,14 +1130,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
pub(super) 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()
|
||||
}
|
||||
|
||||
/// Clears all of `texture` to `color`.
|
||||
fn fill(encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture, color: [f64; 4]) {
|
||||
pub(super) fn fill(
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
texture: &wgpu::Texture,
|
||||
color: [f64; 4],
|
||||
) {
|
||||
let view = texture.create_view(&Default::default());
|
||||
let mut attachment = super::super::hud_coverage::attachment(&view);
|
||||
let [r, g, b, a] = color;
|
||||
@@ -1027,7 +1154,7 @@ mod tests {
|
||||
|
||||
/// The first texel of `texture` once `encoder` ran: four half floats, or 10-bit PQ codes
|
||||
/// for HDR10.
|
||||
fn texel(
|
||||
pub(super) fn texel(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
mut encoder: wgpu::CommandEncoder,
|
||||
@@ -1072,7 +1199,8 @@ mod tests {
|
||||
return;
|
||||
};
|
||||
// What the last pass shows of a world pixel nothing covers, from today's frame
|
||||
// (`layered` false) or the layered one, whose marks turn into light before it.
|
||||
// (`layered` false) or the layered one, whose marks turn into light before it, then
|
||||
// pass the tone curve.
|
||||
let shown = |layered: bool, world: [f64; 4]| {
|
||||
let mut hdr =
|
||||
Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, layered, None);
|
||||
@@ -1082,6 +1210,7 @@ mod tests {
|
||||
if layered {
|
||||
hdr.begin(&queue, true);
|
||||
hdr.resolve(&mut encoder, None);
|
||||
hdr.tonemap(&mut encoder, None);
|
||||
hdr.keep_world(&mut encoder, None);
|
||||
super::super::hud_coverage::clear(&mut encoder, &hdr.coverage_view().unwrap());
|
||||
} else {
|
||||
@@ -1090,7 +1219,8 @@ mod tests {
|
||||
let capture = hdr.capture(&device, &queue, &mut encoder, true).unwrap();
|
||||
texel(&device, &queue, encoder, &capture)
|
||||
};
|
||||
// Unmarked, below white and above it; HDR lighting; light sources of each tier.
|
||||
// Unmarked, below white and above it; HDR lighting; light sources of each tier: all up to
|
||||
// the tone curve's knee, which the curve leaves as they are.
|
||||
for world in [
|
||||
[0.5, 0.4, 0.3, 1.0],
|
||||
[1.5, 1.2, 0.6, 1.0],
|
||||
@@ -1112,6 +1242,79 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tone_curve_eases_light_into_the_peak() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
return;
|
||||
};
|
||||
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
(hdr.paper_white, hdr.peak) = (203.0, 1000.0);
|
||||
let headroom = 1000.0 / 203.0;
|
||||
// The frame holding `color` (in gamma, as it stores light) after `passes` ran on it.
|
||||
let after =
|
||||
|hdr: &mut Hdr, color: [f64; 4], passes: &dyn Fn(&Hdr, &mut wgpu::CommandEncoder)| {
|
||||
let frame = hdr.frame(&device, [1, 1]);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
fill(&mut encoder, &frame, color);
|
||||
hdr.begin(&queue, true);
|
||||
passes(hdr, &mut encoder);
|
||||
texel(&device, &queue, encoder, &frame)
|
||||
};
|
||||
let tonemap = |hdr: &Hdr, encoder: &mut wgpu::CommandEncoder| hdr.tonemap(encoder, None);
|
||||
// Linear light in and out, in units of paper white.
|
||||
let eased = |hdr: &mut Hdr, light: [f32; 3]| {
|
||||
let [r, g, b] = light.map(|c| f64::from(c.powf(1.0 / 2.2)));
|
||||
let [r, g, b, a] = after(hdr, [r, g, b, 1.0], &tonemap);
|
||||
assert_eq!(a, 1.0);
|
||||
[r, g, b].map(|c| c.powf(2.2))
|
||||
};
|
||||
let near = |got: f32, want: f32| (got - want).abs() <= 0.005 * want.max(1.0);
|
||||
// Up to the knee (60% of the peak) the light stays exactly as stored.
|
||||
for light in [0.0f32, 0.5, 1.0, 2.0, 2.9] {
|
||||
let g = f64::from(light.powf(1.0 / 2.2));
|
||||
let stored = after(&mut hdr, [g, g * 0.5, 0.0, 1.0], &|_, _| {});
|
||||
assert_eq!(
|
||||
after(&mut hdr, [g, g * 0.5, 0.0, 1.0], &tonemap),
|
||||
stored,
|
||||
"{light}"
|
||||
);
|
||||
}
|
||||
// Past it, BT.2390's EETF in PQ at 203 and 1000 nits, with the source's peak at 2771
|
||||
// nits (13.7 times paper white), worked out apart from the shader.
|
||||
for (light, want) in [
|
||||
(3.5, 3.43791),
|
||||
(4.0, 3.78351),
|
||||
(headroom, 4.23556),
|
||||
(8.0, 4.82043),
|
||||
(12.0, 4.92462),
|
||||
(50.0, headroom),
|
||||
] {
|
||||
let [r, ..] = eased(&mut hdr, [light; 3]);
|
||||
assert!(near(r, want), "{light}: {r}, not {want}");
|
||||
}
|
||||
// It rises, stays under the peak, and the brightest channel scales the others: the
|
||||
// light keeps its hue.
|
||||
let mut last = 0.0;
|
||||
for i in 0..40 {
|
||||
let light = 2.5 * 1.08f32.powi(i);
|
||||
let [r, g, b] = eased(&mut hdr, [light, light / 4.0, 0.0]);
|
||||
assert!(r >= last - 0.001 && r <= headroom * 1.002, "{light}: {r}");
|
||||
assert!(near(g * 4.0, r) && b == 0.0, "{light}: {r} {g} {b}");
|
||||
last = r;
|
||||
}
|
||||
// No headroom (a peak below paper white): a clip at white, channel by channel.
|
||||
hdr.peak = 150.0;
|
||||
let [r, g, b] = eased(&mut hdr, [1.5, 0.5, 0.2]);
|
||||
assert!(r == 1.0 && near(g, 0.5) && near(b, 0.2), "{r} {g} {b}");
|
||||
// HDR lighting's gain stays past the knee until the curve: the marks become light
|
||||
// without easing.
|
||||
hdr.peak = 1000.0;
|
||||
let resolve = |hdr: &Hdr, encoder: &mut wgpu::CommandEncoder| hdr.resolve(encoder, None);
|
||||
let [r, ..] = after(&mut hdr, [1.0, 1.0, 1.0, 0.76], &resolve);
|
||||
let gain = 1.0 + 3.0 * (1.0 - 194.0 / 255.0) / 0.24;
|
||||
assert!(near(r.powf(2.2), gain), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hud_lays_over_the_worlds_light_by_its_coverage() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
@@ -1193,12 +1396,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hdr_shaders_validate() {
|
||||
let entity = crate::overlay::wgsl_constants() + include_str!("../entity.wgsl");
|
||||
let entity = super::super::entity_shader();
|
||||
for source in [
|
||||
include_str!("../hdr.wgsl"),
|
||||
include_str!("../terrain.wgsl"),
|
||||
&super::shader(),
|
||||
&super::super::terrain_shader(),
|
||||
include_str!("../hdr_stats.wgsl"),
|
||||
include_str!("stars.wgsl"),
|
||||
&(include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl")),
|
||||
&entity,
|
||||
] {
|
||||
let module = naga::front::wgsl::parse_str(source).unwrap();
|
||||
@@ -1262,3 +1465,7 @@ mod lighting_tests {
|
||||
assert_eq!(scene_gain(200.0, 480.0), 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "hdr_written_tests.rs"]
|
||||
mod written_tests;
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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.
|
||||
// `eased` for the last pass, which eases it into the peak itself; the layered frame leaves that
|
||||
// to the tone curve (fs_tonemap).
|
||||
struct ReferenceLit {
|
||||
light: vec3<f32>,
|
||||
lifted: bool,
|
||||
};
|
||||
|
||||
fn reference_lit(f: vec3<f32>, a: f32, shows: bool, eased: bool) -> ReferenceLit {
|
||||
var lifted = false;
|
||||
// Light over 1 (a float frame's) shows only where the world does and carries
|
||||
// no mark (alpha 1), up to the peak: light written as light. Marked pixels clip
|
||||
// at white before their lift, as on a unorm frame, so light sources, the sun and
|
||||
// HDR lighting look as they do there; so do the HUD, menus and HDR Lighting off.
|
||||
let c = select(min(f, vec3(1.0)), f, shows && a == 1.0);
|
||||
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
|
||||
var light = pow(c, vec3(2.2));
|
||||
// HDR lighting: alpha between 0.76 and 1 is how much of it the surface gets. Vanilla's
|
||||
// light is albedo times lightmap, so this is the lightmap going past 1. It eases into the
|
||||
// peak from 60% of it, so bright textures keep their detail. Unmarked light (alpha 1)
|
||||
// gets no gain and no knee: the clamp to the peak below is all it needs here.
|
||||
if shows && a >= 0.75 && a < 1.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = 1.0 + (params.scene - 1.0) * (1.0 - a) / 0.24;
|
||||
let v = top * gain;
|
||||
let knee = max(0.6 * params.peak, 1.0);
|
||||
var out = v;
|
||||
if eased && v > knee && params.peak > knee {
|
||||
out = knee + (params.peak - knee) * (1.0 - exp(-(v - knee) / (params.peak - knee)));
|
||||
}
|
||||
light *= select(gain, out / max(top, 1e-4), eased && v > knee);
|
||||
}
|
||||
// The world's alpha is below 0.75 on light sources, the sun and the moon
|
||||
// (terrain.wgsl, entity.wgsl): its band of 0.175 is the tier, the lower the
|
||||
// brighter, and where in the band how much (hdr_mark).
|
||||
if shows && a < 0.75 {
|
||||
let band = u32(clamp(floor((a + 0.005) / 0.175), 0.0, 3.0));
|
||||
let tier = 3u - band;
|
||||
let lift = 1.0 - clamp((a - 0.175 * f32(band)) / 0.165, 0.0, 1.0);
|
||||
if lift > 0.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
var gain = 1.0 + (params.peak - 1.0) * params.share[tier] * lift;
|
||||
if eased {
|
||||
gain = min(gain, params.peak / max(top, 1e-4));
|
||||
}
|
||||
light *= max(gain, 1.0);
|
||||
lifted = true;
|
||||
}
|
||||
}
|
||||
// HORIZON_HDR_DEBUG=1: what isn't lifted turns grey, what is keeps its
|
||||
// colour with a magenta tint, so a lift shows on any colour.
|
||||
if params.debug > 0.5 && params.world > 0.5 {
|
||||
if lifted {
|
||||
light *= vec3(1.0, 0.6, 1.0);
|
||||
} else {
|
||||
light = vec3(dot(light, vec3(0.2126, 0.7152, 0.0722)));
|
||||
}
|
||||
}
|
||||
if eased {
|
||||
light = min(light, vec3(params.peak));
|
||||
}
|
||||
return ReferenceLit(light, lifted);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,818 @@
|
||||
use super::{
|
||||
Hdr, Output,
|
||||
tests::{gpu, texel},
|
||||
};
|
||||
use wgpu::TextureFormat as F;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
const TERRAIN_VERTEX: &str = r#"
|
||||
override TEST_TIER: u32 = 0u;
|
||||
override TEST_OVERLAY: u32 = 0u;
|
||||
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> TerrainWire {
|
||||
let p = vec2(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var o: TerrainOut;
|
||||
o.clip = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
o.uv = vec2(0.5);
|
||||
o.local = vec3(p.x, 0.0, p.y);
|
||||
o.color = vec4(0.8, 0.9, 0.7, select(1.0, 0.5, globals.misc.z > 0.5));
|
||||
o.scene = vec2(15.0);
|
||||
o.spherical = globals.misc.x;
|
||||
o.cylindrical = globals.misc.x;
|
||||
o.visibility = globals.misc.y;
|
||||
o.emission = TEST_TIER;
|
||||
o.emissive_layer = select(0xffffffffu, 1u, TEST_OVERLAY != 0u);
|
||||
return to_wire(o);
|
||||
}
|
||||
"#;
|
||||
const ENTITY_VERTEX: &str = r#"
|
||||
override TEST_TIER: u32 = 0u;
|
||||
override TEST_OVERLAY: u32 = 0u;
|
||||
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> Out {
|
||||
let p = vec2(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var o: Out;
|
||||
o.clip = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
o.uv = vec2(0.5);
|
||||
o.color = vec4(0.8, 0.9, 0.7, 1.0);
|
||||
o.back_color = o.color;
|
||||
o.light_color = vec4(0.7, 0.8, 0.9, 1.0);
|
||||
o.overlay_color = vec4(1.0);
|
||||
o.scene = 0.8;
|
||||
o.tier = TEST_TIER;
|
||||
o.overlay = TEST_OVERLAY;
|
||||
o.wrap = vec2(0.0, 1.0);
|
||||
o.proj = vec4(0.4, 0.6, 0.0, 1.0);
|
||||
o.fog_distance = vec2(globals.misc.x);
|
||||
return o;
|
||||
}
|
||||
"#;
|
||||
|
||||
struct Draw<'a> {
|
||||
device: &'a wgpu::Device,
|
||||
queue: &'a wgpu::Queue,
|
||||
terrain: bool,
|
||||
shader: wgpu::ShaderModule,
|
||||
layout: wgpu::PipelineLayout,
|
||||
bindings: wgpu::BindGroupLayout,
|
||||
group: wgpu::BindGroup,
|
||||
}
|
||||
impl<'a> Draw<'a> {
|
||||
fn render_oit(
|
||||
&self,
|
||||
hdr: &mut Hdr,
|
||||
kind: &str,
|
||||
tier: u32,
|
||||
overlay: u32,
|
||||
written: bool,
|
||||
) -> [f32; 4] {
|
||||
use super::super::oit::{Stage, stage_targets};
|
||||
let source = if self.terrain {
|
||||
super::super::terrain_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl")
|
||||
+ TERRAIN_VERTEX
|
||||
} else {
|
||||
super::super::entity_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_features.wgsl")
|
||||
+ include_str!("../oit_clouds.wgsl")
|
||||
+ ENTITY_VERTEX
|
||||
}
|
||||
.replace(
|
||||
"o.clip = vec4(p * 2.0 - 1.0, 0.0, 1.0)",
|
||||
"o.clip = vec4(p * 2.0 - 1.0, 0.5, 1.0)",
|
||||
);
|
||||
let shader = self
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("actual OIT fragments"),
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let screen_shader = self
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(super::super::terrain_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl")
|
||||
+ TERRAIN_VERTEX)
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let make = |format| {
|
||||
self.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
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&Default::default())
|
||||
};
|
||||
let bounds = make(F::Rgba32Float);
|
||||
let depth = make(F::Depth32Float);
|
||||
let coeff = [make(F::Rgba16Float), make(F::Rgba16Float)];
|
||||
let accumulated = make(F::Rgba16Float);
|
||||
let uniform = self
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&[0.0_f32, 1.0, 0.0, 0.0]),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let frame = hdr.frame(self.device, [1, 1]);
|
||||
let view = frame.create_view(&Default::default());
|
||||
let mut encoder = self.device.create_command_encoder(&Default::default());
|
||||
super::tests::fill(&mut encoder, &frame, [2.5, 1.8, 0.9, 1.0]);
|
||||
let constants = [
|
||||
("HDR_WRITE_LIGHT", f64::from(u8::from(written))),
|
||||
("TEST_TIER", f64::from(tier)),
|
||||
("TEST_OVERLAY", f64::from(overlay)),
|
||||
];
|
||||
for stage in 0..4 {
|
||||
let textures: Vec<_> = match stage {
|
||||
0 => vec![],
|
||||
1 => vec![&bounds],
|
||||
2 => vec![&bounds, &coeff[0], &coeff[1]],
|
||||
_ => vec![&bounds, &coeff[0], &coeff[1], &accumulated],
|
||||
};
|
||||
let mut entries = vec![wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}];
|
||||
entries.extend(
|
||||
textures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| wgpu::BindGroupLayoutEntry {
|
||||
binding: i as u32 + 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}),
|
||||
);
|
||||
let bindings = self
|
||||
.device
|
||||
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &entries,
|
||||
});
|
||||
let mut resources = vec![wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform.as_entire_binding(),
|
||||
}];
|
||||
resources.extend(
|
||||
textures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, t)| wgpu::BindGroupEntry {
|
||||
binding: i as u32 + 1,
|
||||
resource: wgpu::BindingResource::TextureView(t),
|
||||
}),
|
||||
);
|
||||
let group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &bindings,
|
||||
entries: &resources,
|
||||
});
|
||||
let layout = self
|
||||
.device
|
||||
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[Some(&self.bindings), None, Some(&bindings)],
|
||||
..Default::default()
|
||||
});
|
||||
let suffix = ["bounds", "transmittance", "accumulate", "composite"][stage];
|
||||
let entry = if stage == 3 {
|
||||
"fs_oit_composite".to_owned()
|
||||
} else if self.terrain {
|
||||
format!("fs_oit_{suffix}")
|
||||
} else {
|
||||
format!("fs_oit_{kind}_{suffix}")
|
||||
};
|
||||
let targets = if stage == 3 {
|
||||
vec![Some(wgpu::ColorTargetState {
|
||||
format: F::Rgba16Float,
|
||||
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})]
|
||||
} else {
|
||||
stage_targets([Stage::DepthBounds, Stage::Transmittance, Stage::Accumulate][stage])
|
||||
};
|
||||
let shader = if stage == 3 { &screen_shader } else { &shader };
|
||||
let pipeline = self
|
||||
.device
|
||||
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(&entry),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("test_vertex"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: (stage == 3).then_some(wgpu::DepthStencilState {
|
||||
format: F::Depth32Float,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some(&entry),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &targets,
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let outputs = match stage {
|
||||
0 => vec![&bounds],
|
||||
1 => vec![&coeff[0], &coeff[1]],
|
||||
2 => vec![&accumulated],
|
||||
_ => vec![&view],
|
||||
};
|
||||
let attachments: Vec<_> = outputs
|
||||
.iter()
|
||||
.map(|v| {
|
||||
Some(wgpu::RenderPassColorAttachment {
|
||||
view: v,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: if stage == 3 {
|
||||
wgpu::LoadOp::Load
|
||||
} else {
|
||||
wgpu::LoadOp::Clear(if stage == 0 {
|
||||
wgpu::Color {
|
||||
r: -(f32::MAX as f64),
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 0.0,
|
||||
}
|
||||
} else {
|
||||
wgpu::Color::TRANSPARENT
|
||||
})
|
||||
},
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &attachments,
|
||||
depth_stencil_attachment: (stage == 3).then_some(
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &depth,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&pipeline);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_bind_group(2, &group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
texel(self.device, self.queue, encoder, &frame)
|
||||
}
|
||||
fn new(
|
||||
device: &'a wgpu::Device,
|
||||
queue: &'a wgpu::Queue,
|
||||
terrain: bool,
|
||||
globals: &super::super::Globals,
|
||||
alpha: u8,
|
||||
) -> Self {
|
||||
let source = if terrain {
|
||||
super::super::terrain_shader() + TERRAIN_VERTEX
|
||||
} else {
|
||||
super::super::entity_shader() + ENTITY_VERTEX
|
||||
};
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("actual HDR draw shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let mut entries = vec![];
|
||||
for binding in 0..if terrain { 13 } else { 8 } {
|
||||
let sampler = if terrain {
|
||||
binding == 2 || binding == 4
|
||||
} else {
|
||||
binding == 2 || binding == 4 || binding == 6
|
||||
};
|
||||
let ty = if binding == 0 {
|
||||
wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
}
|
||||
} else if sampler {
|
||||
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)
|
||||
} else {
|
||||
wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
multisampled: false,
|
||||
view_dimension: if terrain && binding != 3 && binding != 12 {
|
||||
wgpu::TextureViewDimension::D2Array
|
||||
} else {
|
||||
wgpu::TextureViewDimension::D2
|
||||
},
|
||||
}
|
||||
};
|
||||
entries.push(wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty,
|
||||
count: None,
|
||||
});
|
||||
}
|
||||
let bindings = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &entries,
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[Some(&bindings)],
|
||||
..Default::default()
|
||||
});
|
||||
let uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::bytes_of(globals),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let texture = |layers, data: &[u8]| {
|
||||
device.create_texture_with_data(
|
||||
queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: layers,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: F::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
data,
|
||||
)
|
||||
};
|
||||
let array = texture(2, &[200, 100, 50, alpha, 240, 210, 80, 128]);
|
||||
let array = array.create_view(&wgpu::TextureViewDescriptor {
|
||||
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
let image = texture(1, &[200, 100, 50, alpha]).create_view(&Default::default());
|
||||
let threshold = texture(1, &[0, 0, 0, 0]).create_view(&Default::default());
|
||||
let sampler = device.create_sampler(&Default::default());
|
||||
let resources: Vec<_> = entries
|
||||
.iter()
|
||||
.map(|entry| wgpu::BindGroupEntry {
|
||||
binding: entry.binding,
|
||||
resource: match entry.ty {
|
||||
wgpu::BindingType::Buffer { .. } => uniform.as_entire_binding(),
|
||||
wgpu::BindingType::Sampler(_) => wgpu::BindingResource::Sampler(&sampler),
|
||||
wgpu::BindingType::Texture {
|
||||
view_dimension: wgpu::TextureViewDimension::D2Array,
|
||||
..
|
||||
} => wgpu::BindingResource::TextureView(&array),
|
||||
_ => wgpu::BindingResource::TextureView(if entry.binding == 12 {
|
||||
&threshold
|
||||
} else {
|
||||
&image
|
||||
}),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &bindings,
|
||||
entries: &resources,
|
||||
});
|
||||
Self {
|
||||
device,
|
||||
queue,
|
||||
terrain,
|
||||
shader,
|
||||
layout,
|
||||
bindings,
|
||||
group,
|
||||
}
|
||||
}
|
||||
fn render(
|
||||
&self,
|
||||
hdr: &mut Hdr,
|
||||
written: bool,
|
||||
entry: &str,
|
||||
tier: u32,
|
||||
overlay: u32,
|
||||
background: [f64; 4],
|
||||
) -> [f32; 4] {
|
||||
let constants = [
|
||||
("HDR_WRITE_LIGHT", f64::from(u8::from(written))),
|
||||
("TEST_TIER", f64::from(tier)),
|
||||
("TEST_OVERLAY", f64::from(overlay)),
|
||||
];
|
||||
let pipeline = |entry, mark| {
|
||||
self.device
|
||||
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&self.layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &self.shader,
|
||||
entry_point: Some("test_vertex"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &self.shader,
|
||||
entry_point: Some(entry),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: F::Rgba16Float,
|
||||
blend: Some(if mark || (self.terrain && entry != "fs_translucent") {
|
||||
wgpu::BlendState::REPLACE
|
||||
} else {
|
||||
wgpu::BlendState::ALPHA_BLENDING
|
||||
}),
|
||||
write_mask: if mark {
|
||||
wgpu::ColorWrites::ALPHA
|
||||
} else {
|
||||
wgpu::ColorWrites::ALL
|
||||
},
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let color = pipeline(entry, false);
|
||||
let mark = (!self.terrain && !written && entry == "fs_entity").then(|| {
|
||||
pipeline(
|
||||
if overlay == 4 {
|
||||
"fs_hdr_cloud_mark"
|
||||
} else {
|
||||
"fs_hdr_mark"
|
||||
},
|
||||
true,
|
||||
)
|
||||
});
|
||||
let frame = hdr.frame(self.device, [1, 1]);
|
||||
let view = frame.create_view(&Default::default());
|
||||
let mut encoder = self.device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let [r, g, b, a] = background;
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_pipeline(&color);
|
||||
pass.draw(0..3, 0..1);
|
||||
if let Some(mark) = &mark {
|
||||
pass.set_pipeline(mark);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
if !written {
|
||||
hdr.begin(self.queue, true);
|
||||
hdr.resolve(&mut encoder, None);
|
||||
}
|
||||
texel(self.device, self.queue, encoder, &frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_opaque_writers_match_marks_then_resolve() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
eprintln!("HDR draw GPU unavailable");
|
||||
return;
|
||||
};
|
||||
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
(hdr.paper_white, hdr.peak) = (203.0, 1000.0);
|
||||
let (settings, shares) = hdr.draw_params(true);
|
||||
let mut count = 0;
|
||||
for fog in [0.0, 0.5, 1.0] {
|
||||
for visibility in [1.0, 0.4] {
|
||||
let globals = super::super::Globals {
|
||||
fog_color: [0.2, 0.3, 0.4, 1.0],
|
||||
fog: [0.0, 1.0, 0.0, 1.0],
|
||||
fog_ends: [100.0, 100.0, 0.0, 0.0],
|
||||
misc: [fog, visibility, 0.0, 0.0],
|
||||
filtering: [0.0, 1.0, 0.0, 0.0],
|
||||
hdr_windows: hdr.windows(),
|
||||
hdr_light: [0.8, 0.8, 1.0, 1.0],
|
||||
hdr_settings: settings,
|
||||
hdr_shares: shares,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
for terrain in [true, false] {
|
||||
let draw = Draw::new(&device, &queue, terrain, &globals, 255);
|
||||
for tier in 0..=4 {
|
||||
for overlay in if terrain {
|
||||
vec![0, 1]
|
||||
} else {
|
||||
vec![0, 6, 9, 10, 38]
|
||||
} {
|
||||
let entries = if terrain {
|
||||
vec!["fs_opaque", "fs_solid"]
|
||||
} else {
|
||||
vec!["fs_entity"]
|
||||
};
|
||||
for entry in entries {
|
||||
let marked =
|
||||
draw.render(&mut hdr, false, entry, tier, overlay, [0.0; 4]);
|
||||
let written =
|
||||
draw.render(&mut hdr, true, entry, tier, overlay, [0.0; 4]);
|
||||
// The marked path stores half floats before conversion; the written
|
||||
// one stores after it. Allow two half-float ULPs in gamma light.
|
||||
for i in 0..3 {
|
||||
assert!(
|
||||
(marked[i] - written[i]).abs() <= 0.002 * marked[i].max(1.0),
|
||||
"{terrain} {entry} tier {tier} overlay {overlay} fog {fog} visibility {visibility}: {marked:?} {written:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(written[3], 1.0);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("HDR actual draw GPU ran {count} opaque comparisons");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_translucent_writers_blend_their_own_light() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
eprintln!("HDR blend GPU unavailable");
|
||||
return;
|
||||
};
|
||||
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
let (settings, shares) = hdr.draw_params(true);
|
||||
let globals = super::super::Globals {
|
||||
fog: [0.0, 100.0, 0.0, 100.0],
|
||||
fog_ends: [100.0, 100.0, 0.0, 0.0],
|
||||
misc: [0.0, 1.0, 0.0, 0.0],
|
||||
filtering: [0.0, 1.0, 0.0, 0.0],
|
||||
hdr_windows: hdr.windows(),
|
||||
hdr_light: [0.8, 0.8, 1.0, 1.0],
|
||||
hdr_settings: settings,
|
||||
hdr_shares: shares,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
// Texel alpha 100 is clear glass's kind of texel, below the 0.6 the marked frame's
|
||||
// `fs_translucent_mark` skips: written, it still gets its own light.
|
||||
for (terrain, entry, overlay, tier, tint_alpha, texel_alpha) in [
|
||||
(true, "fs_translucent", 0, 0, 1.0, 180),
|
||||
(true, "fs_translucent", 0, 0, 0.5, 180),
|
||||
(true, "fs_translucent", 0, 0, 1.0, 100),
|
||||
(false, "fs_entity", 0, 3, 1.0, 180),
|
||||
(false, "fs_entity", 4, 0, 1.0, 180),
|
||||
] {
|
||||
let mut globals = globals;
|
||||
globals.misc[2] = if tint_alpha < 1.0 { 1.0 } else { 0.0 };
|
||||
let draw = Draw::new(&device, &queue, terrain, &globals, texel_alpha);
|
||||
let src = draw.render(&mut hdr, true, entry, tier, overlay, [0.0; 4]);
|
||||
let dst = [2.5, 1.8, 0.9, 1.0];
|
||||
let got = draw.render(&mut hdr, true, entry, tier, overlay, dst);
|
||||
let alpha = f32::from(texel_alpha) / 255.0 * tint_alpha;
|
||||
if terrain {
|
||||
// Full block light leaves mark 194/255 whatever the texel's alpha, even when
|
||||
// vertex coverage is halved.
|
||||
let gain = (1.0 + (settings[1] - 1.0) * (1.0 - 194.0 / 255.0) / 0.24).powf(1.0 / 2.2);
|
||||
let expected = 200.0 / 255.0 * 0.8 * gain * alpha;
|
||||
assert!(
|
||||
(src[0] - expected).abs() < 0.002,
|
||||
"coverage {tint_alpha}: {src:?}, expected {expected}"
|
||||
);
|
||||
}
|
||||
for i in 0..3 {
|
||||
let want = src[i] + dst[i] as f32 * (1.0 - alpha);
|
||||
assert!(
|
||||
(got[i] - want).abs() < 0.003,
|
||||
"{entry} {overlay}: {got:?} expected {want}"
|
||||
);
|
||||
}
|
||||
assert!(got[0] > 1.0);
|
||||
}
|
||||
eprintln!("HDR actual blend GPU ran terrain, particle and cloud equations");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmarked_entity_users_keep_their_source_gain() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
return;
|
||||
};
|
||||
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
let (settings, shares) = hdr.draw_params(true);
|
||||
let globals = super::super::Globals {
|
||||
fog: [0.0, 100.0, 0.0, 100.0],
|
||||
fog_ends: [100.0, 100.0, 0.0, 0.0],
|
||||
hdr_windows: hdr.windows(),
|
||||
hdr_light: [1.0, 1.0, 1.0, 1.0],
|
||||
hdr_settings: settings,
|
||||
hdr_shares: shares,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
let draw = Draw::new(&device, &queue, false, &globals, 255);
|
||||
for (entry, overlay) in [
|
||||
("fs_entity_unmarked", 0),
|
||||
("fs_entity_shadow", 0),
|
||||
("fs_energy_swirl", 0),
|
||||
("fs_entity", 11),
|
||||
("fs_entity", 12),
|
||||
] {
|
||||
let raw = draw.render(&mut hdr, true, entry, 0, overlay, [0.0; 4]);
|
||||
let mut hdr_off = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
hdr_off.look = false;
|
||||
let legacy = draw.render(&mut hdr_off, false, entry, 0, overlay, [0.0; 4]);
|
||||
for i in 0..3 {
|
||||
assert!(
|
||||
(raw[i] - legacy[i]).abs() < 0.001,
|
||||
"{entry} {overlay}: {raw:?} {legacy:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additive_texels_below_the_mark_cutoff_keep_their_gain() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
return;
|
||||
};
|
||||
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
let (settings, shares) = hdr.draw_params(true);
|
||||
let globals = super::super::Globals {
|
||||
fog: [0.0, 100.0, 0.0, 100.0],
|
||||
hdr_windows: hdr.windows(),
|
||||
hdr_light: [1.0, 1.0, 1.0, 1.0],
|
||||
hdr_settings: settings,
|
||||
hdr_shares: shares,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
let draw = Draw::new(&device, &queue, false, &globals, 16);
|
||||
let background = [0.0, 0.0, 0.0, 1.0];
|
||||
let marked = draw.render(&mut hdr, false, "fs_celestial", 4, 0, background);
|
||||
let written = draw.render(&mut hdr, true, "fs_celestial", 4, 0, background);
|
||||
// The old mark draw discards this texel; source alpha still contributes its colour.
|
||||
let red = (200.0 / 255.0) * 0.8 * 0.7 * (16.0 / 255.0);
|
||||
assert!(
|
||||
(written[0] - red).abs() < 0.0001,
|
||||
"{written:?}: expected {red}"
|
||||
);
|
||||
for i in 0..3 {
|
||||
assert!(
|
||||
(written[i] - marked[i]).abs() < 0.0001,
|
||||
"{written:?} {marked:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_oit_writer(terrain: bool, kind: &str, overlay: u32, tier: u32, alphas: &[u8]) {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
eprintln!("OIT GPU unavailable");
|
||||
return;
|
||||
};
|
||||
if !adapter
|
||||
.features()
|
||||
.contains(wgpu::Features::FLOAT32_BLENDABLE)
|
||||
{
|
||||
eprintln!("OIT unsupported on {:?}", adapter.get_info());
|
||||
return;
|
||||
}
|
||||
eprintln!(
|
||||
"OIT GPU {:?}: {kind}, alphas {alphas:?}",
|
||||
adapter.get_info()
|
||||
);
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_features: wgpu::Features::FLOAT32_BLENDABLE,
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
|
||||
let (settings, shares) = hdr.draw_params(true);
|
||||
let globals = super::super::Globals {
|
||||
fog: [0.0, 100.0, 0.0, 100.0],
|
||||
fog_ends: [100.0, 100.0, 0.0, 0.0],
|
||||
misc: [0.0, 1.0, 0.0, 0.0],
|
||||
filtering: [0.0, 1.0, 0.0, 0.0],
|
||||
hdr_windows: hdr.windows(),
|
||||
hdr_light: [0.8, 0.8, 1.0, 1.0],
|
||||
hdr_settings: settings,
|
||||
hdr_shares: shares,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
for &alpha in alphas {
|
||||
let draw = Draw::new(&device, &queue, terrain, &globals, alpha);
|
||||
let got = draw.render_oit(&mut hdr, kind, tier, overlay, true);
|
||||
let unconverted = draw.render_oit(&mut hdr, kind, tier, overlay, false);
|
||||
let entry = if terrain {
|
||||
"fs_translucent"
|
||||
} else if kind == "lightning" {
|
||||
"fs_celestial"
|
||||
} else {
|
||||
"fs_entity"
|
||||
};
|
||||
let mut expected = draw.render(
|
||||
&mut hdr,
|
||||
true,
|
||||
entry,
|
||||
tier,
|
||||
overlay,
|
||||
if kind == "lightning" {
|
||||
[0.0; 4]
|
||||
} else {
|
||||
[2.5, 1.8, 0.9, 1.0]
|
||||
},
|
||||
);
|
||||
if kind == "lightning" {
|
||||
for (c, background) in expected[..3].iter_mut().zip([2.5, 1.8, 0.9]) {
|
||||
*c += background;
|
||||
}
|
||||
}
|
||||
for i in 0..3 {
|
||||
assert!(
|
||||
(got[i] - expected[i]).abs() < 0.006,
|
||||
"{kind} alpha {alpha}: {got:?} expected {expected:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(got[3], 1.0);
|
||||
assert!(got[0] > 1.0, "OIT clipped light: {got:?}");
|
||||
assert!(
|
||||
(got[0] - unconverted[0]).abs() > 0.01,
|
||||
"test must distinguish missing conversion: {got:?} {unconverted:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_oit_terrain_writes_its_own_light_including_clear_glass() {
|
||||
check_oit_writer(true, "terrain", 0, 0, &[180, 100]);
|
||||
}
|
||||
#[test]
|
||||
fn actual_oit_particle_writes_its_own_light() {
|
||||
check_oit_writer(false, "entity", 0, 3, &[180]);
|
||||
}
|
||||
#[test]
|
||||
fn actual_oit_cloud_writes_its_own_light() {
|
||||
check_oit_writer(false, "cloud", 4, 0, &[180]);
|
||||
}
|
||||
#[test]
|
||||
fn actual_oit_additive_adds_converted_light_without_coverage() {
|
||||
check_oit_writer(false, "lightning", 0, 4, &[180]);
|
||||
}
|
||||
@@ -446,7 +446,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn coverage_wraps_the_debug_crosshair_without_changing_world_entry_points() {
|
||||
let mut module = naga::front::wgsl::parse_str(include_str!("../terrain.wgsl")).unwrap();
|
||||
let mut module = naga::front::wgsl::parse_str(&crate::gfx::terrain_shader()).unwrap();
|
||||
wrap(
|
||||
&mut module,
|
||||
"fs_debug_line",
|
||||
|
||||
@@ -98,8 +98,24 @@ fn interfaces(source: &str) -> Vec<Interface> {
|
||||
|
||||
/// The shaders as the renderer assembles them, by the `.wgsl` files each is made of.
|
||||
fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
|
||||
let entity = crate::overlay::wgsl_constants() + include_str!("../entity.wgsl");
|
||||
let entity = super::entity_shader();
|
||||
vec![
|
||||
(
|
||||
"HDR",
|
||||
vec!["hdr.wgsl", "hdr_light.wgsl"],
|
||||
super::hdr::shader(),
|
||||
),
|
||||
("terrain", vec!["terrain.wgsl"], super::terrain_shader()),
|
||||
(
|
||||
"stars",
|
||||
vec!["stars.wgsl"],
|
||||
include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl"),
|
||||
),
|
||||
(
|
||||
"custom sky",
|
||||
vec!["custom_sky.wgsl"],
|
||||
include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl"),
|
||||
),
|
||||
("entity", vec!["entity.wgsl"], entity.clone()),
|
||||
(
|
||||
"GPU culling occlusion test",
|
||||
@@ -124,14 +140,14 @@ fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
|
||||
(
|
||||
"improved transparency world",
|
||||
vec!["terrain.wgsl", "oit.wgsl", "oit_world.wgsl"],
|
||||
include_str!("../terrain.wgsl").to_owned()
|
||||
super::terrain_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl"),
|
||||
),
|
||||
(
|
||||
"visibility buffer",
|
||||
vec!["terrain.wgsl", "visbuffer.wgsl"],
|
||||
include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"),
|
||||
super::terrain_shader() + include_str!("../visbuffer.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency world border",
|
||||
|
||||
@@ -309,7 +309,8 @@ pub struct NativeMaterial {
|
||||
/// every name it declares prefixed.
|
||||
fn vanilla_terrain() -> String {
|
||||
prefixed(
|
||||
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
|
||||
&(include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
PREFIX,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,10 +73,15 @@ pub struct WorldDrawn<'a> {
|
||||
pub entities: &'a [Vertex],
|
||||
pub spans: &'a [EntitySpan],
|
||||
/// Which entity each run of `entities` is, recorded every frame (`Frame::entity_motion`):
|
||||
/// the run's first vertex and the entity, `(1 << 32) | id` for a mob or a player and
|
||||
/// `(2 << 32) | id` for a dropped item (their entity ids), 0 for anything else. A run lasts
|
||||
/// until the next. Unlike `spans`, whose ids name a type, these follow one entity from
|
||||
/// frame to frame (API 371).
|
||||
/// the run's first vertex and the entity, `(1 << 32) | id` for a mob or a player,
|
||||
/// `(2 << 32) | id` for a dropped item, `(3 << 32) | id` for another entity (boat,
|
||||
/// minecart, armor stand, item frame, painting, primed TNT, falling block) and
|
||||
/// `(4 << 32) | id` for an item frame's held item (their entity ids), `(5 << 32) | p` for a
|
||||
/// block entity (`p`: its position, x and z in 11 bits each, y in 10), `(6 << 32) | p`
|
||||
/// for a block a piston moves (`p`: the cell it moves to), `7 << 32` for the player's own
|
||||
/// model, 0 for anything else.
|
||||
/// A run lasts until the next. Unlike `spans`, whose ids name a type, these follow one
|
||||
/// entity from frame to frame (API 371).
|
||||
pub motion: &'a [(u32, u64)],
|
||||
/// The shared atlas entity UVs point into.
|
||||
pub atlas: Option<&'a wgpu::Texture>,
|
||||
|
||||
@@ -512,7 +512,8 @@ fn shader_source(
|
||||
format!(
|
||||
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{}{displace}{ENTRIES}",
|
||||
prefixed(
|
||||
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
|
||||
&(include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
PREFIX
|
||||
),
|
||||
super::native_post::line_helpers(api)
|
||||
|
||||
@@ -30,6 +30,7 @@ pub struct NativeSky {
|
||||
/// terrain.wgsl's sky with every name it declares (and `globals`) prefixed.
|
||||
fn vanilla_sky() -> String {
|
||||
let text = [
|
||||
include_str!("../hdr_light.wgsl"),
|
||||
section(TERRAIN, "struct Globals {", "@group(0) @binding(1)"),
|
||||
section(TERRAIN, "fn linear_fog(", "fn apply_fog("),
|
||||
section(TERRAIN, "struct SkyOut {", "// rendertype_lines"),
|
||||
|
||||
@@ -192,7 +192,7 @@ impl Oit {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon improved transparency"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../terrain.wgsl").to_owned()
|
||||
(super::terrain_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl"))
|
||||
.into(),
|
||||
@@ -253,7 +253,14 @@ impl Oit {
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let constants = [("SPRITE_ARRAYS", sprites[0]), ("SPRITE_SPLIT", sprites[1])];
|
||||
let constants = [
|
||||
("SPRITE_ARRAYS", sprites[0]),
|
||||
("SPRITE_SPLIT", sprites[1]),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
),
|
||||
];
|
||||
let add = wgpu::BlendOperation::Add;
|
||||
let depth = |write| {
|
||||
Some(wgpu::DepthStencilState {
|
||||
@@ -464,7 +471,10 @@ impl Oit {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &cloud_shader,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants[2..],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &stage_targets(stage),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
@@ -488,7 +498,14 @@ impl Oit {
|
||||
Stage::Accumulate,
|
||||
),
|
||||
];
|
||||
let features = Features::new(device, &layouts, features[0], features[1], features[2]);
|
||||
let features = Features::new(
|
||||
device,
|
||||
&layouts,
|
||||
features[0],
|
||||
features[1],
|
||||
features[2],
|
||||
format,
|
||||
);
|
||||
Self {
|
||||
uniform,
|
||||
layouts,
|
||||
@@ -912,7 +929,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_terrain_shader_validates() {
|
||||
validate(
|
||||
&(include_str!("../terrain.wgsl").to_owned()
|
||||
&(crate::gfx::terrain_shader()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl")),
|
||||
&[
|
||||
|
||||
@@ -55,6 +55,7 @@ impl Features {
|
||||
entity: &wgpu::BindGroupLayout,
|
||||
weather: &wgpu::BindGroupLayout,
|
||||
border: &wgpu::BindGroupLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
) -> Self {
|
||||
let entity_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon improved transparency features"),
|
||||
@@ -113,6 +114,15 @@ impl Features {
|
||||
bind_group_layouts: &[groups[0], groups[1], Some(&layouts[stage as usize])],
|
||||
..Default::default()
|
||||
});
|
||||
let fragment = if kind.label == "text" && stage == Stage::Accumulate {
|
||||
"fs_oit_text_accumulate".to_owned()
|
||||
} else {
|
||||
format!("fs_oit_{}_{suffix}", kind.name)
|
||||
};
|
||||
let constants = [(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(&format!("OIT {} {suffix}", kind.label)),
|
||||
layout: Some(&layout),
|
||||
@@ -136,8 +146,15 @@ impl Features {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module,
|
||||
entry_point: Some(&format!("fs_oit_{}_{suffix}", kind.name)),
|
||||
compilation_options: Default::default(),
|
||||
entry_point: Some(&fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: if kind.name == "border" {
|
||||
&[]
|
||||
} else {
|
||||
&constants
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
targets: &stage_targets(stage),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
|
||||
@@ -757,6 +757,7 @@ impl PackAtlas {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,8 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
}),
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
// Entities, items and block entities read the shared atlas: its own maps (paged_maps.rs).
|
||||
paged_maps: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +192,7 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,9 @@ pub(super) fn format(f: Format, packed_float: bool) -> wgpu::TextureFormat {
|
||||
_ => rgba,
|
||||
};
|
||||
match (f.kind, f.bits) {
|
||||
(Kind::Unorm, 10) => T::Rgb10a2Unorm,
|
||||
(Kind::Uint, 10) => T::Rgb10a2Uint,
|
||||
(Kind::Snorm, 16) => pick(T::R16Snorm, T::Rg16Snorm, T::Rgba16Snorm),
|
||||
(Kind::Int, 8) => pick(T::R8Sint, T::Rg8Sint, T::Rgba8Sint),
|
||||
(Kind::Int, 16) => pick(T::R16Sint, T::Rg16Sint, T::Rgba16Sint),
|
||||
(Kind::Int, _) => pick(T::R32Sint, T::Rg32Sint, T::Rgba32Sint),
|
||||
|
||||
@@ -52,6 +52,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Paged maps (`paged_maps.rs`) through the translator's `hz_pagedTexture` and the pass's
|
||||
//! binding: a pack reads the map's texels at the colour's coordinates.
|
||||
use super::*;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// The atlas the fixture's coordinates are in, and the one mapped texture's place.
|
||||
const ATLAS: (u32, u32) = (64, 32);
|
||||
const SPRITE: [u32; 4] = [20, 4, 20, 20];
|
||||
|
||||
/// The fixture's normal map texel at texel (x, y) of its texture.
|
||||
fn texel(x: u32, y: u32) -> [u8; 4] {
|
||||
[x as u8 * 10, y as u8 * 10, 7, 255]
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a host GPU; missing adapter is a failure"]
|
||||
fn a_pack_reads_paged_maps_at_the_colours_coordinates() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let root = std::env::temp_dir().join(format!("horizon-paged-pack-{}", std::process::id()));
|
||||
std::fs::create_dir_all(root.join("shaders")).unwrap();
|
||||
std::fs::write(
|
||||
root.join("shaders/gbuffers_terrain.vsh"),
|
||||
"#version 120\nvoid main(){gl_Position=gl_Vertex;}",
|
||||
)
|
||||
.unwrap();
|
||||
// Each pixel reads one atlas texel's centre: in the texture (on four pages), beside it,
|
||||
// on a page without maps, and the specular map, which no texture has.
|
||||
std::fs::write(
|
||||
root.join("shaders/gbuffers_terrain.fsh"),
|
||||
r#"#version 120
|
||||
uniform sampler2D normals;
|
||||
uniform sampler2D specular;
|
||||
vec2 at(float x, float y) { return (vec2(x, y) + 0.5) / vec2(64.0, 32.0); }
|
||||
void main() {
|
||||
int x = int(floor(gl_FragCoord.x));
|
||||
if (x == 0) gl_FragData[0] = texture2D(normals, at(20.0, 4.0));
|
||||
else if (x == 1) gl_FragData[0] = texture2D(normals, at(39.0, 23.0));
|
||||
else if (x == 2) gl_FragData[0] = texture2D(normals, at(31.0, 15.0), 2.0);
|
||||
else if (x == 3) gl_FragData[0] = texture2DLod(normals, at(32.0, 16.0), 3.0);
|
||||
else if (x == 4) gl_FragData[0] = texture2D(normals, at(19.0, 4.0));
|
||||
else if (x == 5) gl_FragData[0] = texture2D(normals, at(50.0, 30.0));
|
||||
else if (x == 6) gl_FragData[0] = texture2D(specular, at(25.0, 10.0));
|
||||
else gl_FragData[0] = texture2D(normals, at(33.0, 5.0));
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let [x0, y0, w, h] = SPRITE;
|
||||
let normal: Vec<u8> = (0..w * h).flat_map(|i| texel(i % w, i / w)).collect();
|
||||
let [normals, specular] = crate::paged_maps::build(
|
||||
ATLAS,
|
||||
&[crate::paged_maps::SpriteMaps {
|
||||
rect: SPRITE,
|
||||
maps: [Some(normal), None],
|
||||
}],
|
||||
16384,
|
||||
);
|
||||
assert!(specular.is_none());
|
||||
let normals = normals.unwrap();
|
||||
let map = device
|
||||
.create_texture_with_data(
|
||||
&queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("Fixture paged normal map"),
|
||||
size: wgpu::Extent3d {
|
||||
width: normals.width,
|
||||
height: normals.height,
|
||||
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: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&normals.pixels,
|
||||
)
|
||||
.create_view(&Default::default());
|
||||
let pack = horizon_shaders::Pack::open(&root).unwrap();
|
||||
let macros = horizon_shaders::Macros::standard();
|
||||
let properties = horizon_shaders::Properties::parse("alphaTest.gbuffers_terrain=off", ¯os);
|
||||
let inputs = horizon_shaders::GameInputs {
|
||||
terrain: Some(horizon_shaders::GbuffersInputs {
|
||||
body: "vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); hz_Vertex=vec4(p*2.0-1.0,0.0,1.0);".into(),
|
||||
paged_maps: true,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &inputs);
|
||||
assert!(post.terrain.is_some(), "{:?}", post.notes);
|
||||
let world = World {
|
||||
sprite_rects: &[],
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: map.clone(),
|
||||
terrain: Some(Geometry {
|
||||
buffers: &[],
|
||||
table: None,
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
ids: None,
|
||||
cull: false,
|
||||
}),
|
||||
block_ids: &[],
|
||||
celestial: None,
|
||||
gui_size: [1, 1],
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
};
|
||||
let mut post = PackPost::new(
|
||||
&device,
|
||||
&queue,
|
||||
&adapter,
|
||||
post,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
world,
|
||||
&Default::default(),
|
||||
)
|
||||
.expect("the pack runs");
|
||||
let mut pass = post
|
||||
.terrain
|
||||
.take()
|
||||
.unwrap_or_else(|| panic!("terrain pipeline must compile: {:?}", post.notes));
|
||||
// As the entity passes are (`paged`), with the maps the renderer hands over.
|
||||
pass.paged = true;
|
||||
post.set_paged_maps([Some(map), None]);
|
||||
post.copied |= 1;
|
||||
post.resize(&device, [8, 1]);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: 256,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Initialize fixture depth"),
|
||||
color_attachments: &[],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &post.targets.as_ref().unwrap().depth,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let group = post.bind(&device, &queue, &mut encoder, &mut pass, [8, 1]);
|
||||
{
|
||||
let mut render = post.begin(&mut encoder, &pass);
|
||||
render.set_pipeline(pass.render());
|
||||
render.set_bind_group(0, &group, &[]);
|
||||
if pass.draw {
|
||||
post.draws.bind(&mut render, "TERRAIN_SOLID");
|
||||
}
|
||||
render.draw(0..3, 0..1);
|
||||
}
|
||||
let output = post.targets.as_ref().unwrap().color[0].as_ref().unwrap()[0].texture();
|
||||
encoder.copy_texture_to_buffer(
|
||||
output.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
bytes_per_row: Some(256),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 8,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let data = read.slice(..).get_mapped_range().unwrap();
|
||||
let local = |x: u32, y: u32| texel(x - x0, y - y0);
|
||||
let expected = [
|
||||
local(20, 4),
|
||||
local(39, 23),
|
||||
local(31, 15),
|
||||
local(32, 16),
|
||||
crate::sprites::FLAT_NORMAL,
|
||||
crate::sprites::FLAT_NORMAL,
|
||||
crate::sprites::NO_SPECULAR,
|
||||
local(33, 5),
|
||||
];
|
||||
for (i, e) in expected.iter().enumerate() {
|
||||
assert_eq!(&data[i * 4..i * 4 + 4], e, "pixel {i}");
|
||||
}
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
@@ -61,6 +61,9 @@ mod shadow_faces_tests;
|
||||
#[cfg(test)]
|
||||
mod r11f_image_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod work_group_fold_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod forced_flip_tests;
|
||||
|
||||
@@ -430,6 +433,9 @@ struct Pass {
|
||||
/// What `texture` reads instead of the block atlas.
|
||||
atlas: Option<wgpu::TextureView>,
|
||||
material_maps: bool,
|
||||
/// Draws the shared atlas's geometry: `normals` and `specular` are its paged maps
|
||||
/// (`PackPost::set_paged_maps`), as its programs were translated to read them.
|
||||
paged: bool,
|
||||
/// Reads `mc_Entity`'s table (`PackPost::block_ids`).
|
||||
ids: bool,
|
||||
/// Bind groups by the sides read (bits 0–15), the buffers whose mip chain is current (bits
|
||||
@@ -674,6 +680,8 @@ pub struct PackPost {
|
||||
custom: horizon_shaders::Custom,
|
||||
atlas: Option<wgpu::TextureView>,
|
||||
material_maps: Option<[wgpu::TextureView; 2]>,
|
||||
/// The shared atlas's normal and specular paged maps (`set_paged_maps`).
|
||||
paged_maps: [Option<wgpu::TextureView>; 2],
|
||||
lightmap: wgpu::TextureView,
|
||||
deferred_pre: u16,
|
||||
composite_pre: u16,
|
||||
@@ -2179,6 +2187,7 @@ fn build_pass(
|
||||
draw,
|
||||
atlas: geometry.and_then(|g| g.atlas.clone()),
|
||||
material_maps: geometry.is_some_and(|g| g.material_maps),
|
||||
paged: false,
|
||||
ids: geometry.is_some_and(|g| g.ids.is_some()),
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
@@ -2319,6 +2328,7 @@ fn build_compute(
|
||||
draw: false,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
paged: false,
|
||||
ids: false,
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
@@ -2357,6 +2367,13 @@ impl Refused {
|
||||
}
|
||||
|
||||
impl PackPost {
|
||||
/// The shared atlas's normal and specular paged maps (`paged_maps.rs`), which the programs
|
||||
/// drawing its geometry read as `normals` and `specular`; set before the first frame (a pass's
|
||||
/// bind groups are kept once made). A missing one reads as the dummy (flat, no specular).
|
||||
pub fn set_paged_maps(&mut self, maps: [Option<wgpu::TextureView>; 2]) {
|
||||
self.paged_maps = maps;
|
||||
}
|
||||
|
||||
/// Builds the pipelines of `post` for a screen of `screen` format. An error when the pack
|
||||
/// can't show anything on the screen: the game turns it off, as a pack that fails to load.
|
||||
///
|
||||
@@ -2780,6 +2797,13 @@ impl PackPost {
|
||||
ids: None,
|
||||
cull: false,
|
||||
});
|
||||
// Programs drawing the shared atlas's geometry read its paged maps (`pack_entities::inputs`).
|
||||
let paged = |pass: Option<Pass>| {
|
||||
pass.map(|mut pass| {
|
||||
pass.paged = true;
|
||||
pass
|
||||
})
|
||||
};
|
||||
let entity_passes = programs.map(|p| {
|
||||
p.zip(entity_geometry.as_ref()).and_then(|(p, g)| {
|
||||
if p.name == "gbuffers_damagedblock" {
|
||||
@@ -2792,15 +2816,15 @@ impl PackPost {
|
||||
ids: g.ids,
|
||||
cull: false,
|
||||
};
|
||||
build(p, Some(&decal))
|
||||
paged(build(p, Some(&decal)))
|
||||
} else {
|
||||
build(p, Some(g))
|
||||
paged(build(p, Some(g)))
|
||||
}
|
||||
})
|
||||
});
|
||||
let shadow_entities = shadow_programs.map(|p| {
|
||||
p.zip(entity_geometry.as_ref())
|
||||
.and_then(|(p, g)| build(p, Some(g)))
|
||||
.and_then(|(p, g)| paged(build(p, Some(g))))
|
||||
});
|
||||
let entities = entity_vertices
|
||||
.filter(|_| (entity_passes.iter().chain(&shadow_entities)).any(Option::is_some))
|
||||
@@ -2843,7 +2867,7 @@ impl PackPost {
|
||||
ids: None,
|
||||
cull: false,
|
||||
};
|
||||
build(p, Some(&g)).map(|pass| (pass, geometry))
|
||||
paged(build(p, Some(&g))).map(|pass| (pass, geometry))
|
||||
});
|
||||
// Translucent held items, drawn from the hand's own buffer.
|
||||
let hand_water = post.hand_water.and_then(|p| {
|
||||
@@ -2857,7 +2881,7 @@ impl PackPost {
|
||||
ids: None,
|
||||
cull: false,
|
||||
};
|
||||
build(p, Some(&g))
|
||||
paged(build(p, Some(&g)))
|
||||
});
|
||||
let weather = post
|
||||
.weather
|
||||
@@ -3296,6 +3320,7 @@ impl PackPost {
|
||||
custom,
|
||||
atlas: world.atlas.as_ref().map(|a| a.0.clone()),
|
||||
material_maps: world.material_maps,
|
||||
paged_maps: [None, None],
|
||||
lightmap: world.lightmap,
|
||||
deferred_pre: mask(&post.deferred_pre),
|
||||
composite_pre: mask(&post.composite_pre),
|
||||
@@ -3656,6 +3681,12 @@ impl PackPost {
|
||||
Source::Dummy if s.dimension == wgpu::TextureViewDimension::D1 => {
|
||||
&self.dummy_1d
|
||||
}
|
||||
Source::Normals if pass.paged => {
|
||||
self.paged_maps[0].as_ref().unwrap_or(&self.dummy_normal)
|
||||
}
|
||||
Source::Specular if pass.paged => {
|
||||
self.paged_maps[1].as_ref().unwrap_or(&self.dummy_black)
|
||||
}
|
||||
Source::Normals => self
|
||||
.material_maps
|
||||
.as_ref()
|
||||
@@ -3756,6 +3787,16 @@ impl PackPost {
|
||||
[n(0), n(1), 1]
|
||||
});
|
||||
let (pipeline, groups) = (pipeline.clone(), groups);
|
||||
// wgpu panics on a dispatch over its limit. The translator reshapes a large
|
||||
// `workGroups`; a `workGroupsRender` that large is not run.
|
||||
let most = device.limits().max_compute_workgroups_per_dimension;
|
||||
if groups.iter().any(|&n| n > most) {
|
||||
log::warn!(
|
||||
"shader pack: {}: {groups:?} work groups is over {most} per dimension; not run",
|
||||
pass.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
for image in (0..self.images.len()).filter(|k| pass.snapshots & 1 << k != 0) {
|
||||
pack_images::snapshot(encoder, &self.images[image]);
|
||||
}
|
||||
@@ -6256,6 +6297,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_is_made_in_its_layout_qualifiers_format() {
|
||||
// `image.x = … RGB10_A2 …` read through `layout(rgb10_a2) image2D x`.
|
||||
for q in [
|
||||
"rgba32f",
|
||||
"rgba16f",
|
||||
"rg16f",
|
||||
"r11f_g11f_b10f",
|
||||
"r32f",
|
||||
"rgba16",
|
||||
"rgb10_a2",
|
||||
"rgba8",
|
||||
"rg8",
|
||||
"r16",
|
||||
"rgba16_snorm",
|
||||
"rgba8_snorm",
|
||||
"r8_snorm",
|
||||
"rgba32i",
|
||||
"r16i",
|
||||
"rgb10_a2ui",
|
||||
"rgba8ui",
|
||||
"r32ui",
|
||||
] {
|
||||
let def = Format::parse(q).unwrap();
|
||||
assert_eq!(Some(pack_images::format(def, true)), image_format(q), "{q}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samplers_read_custom_textures_of_their_type() {
|
||||
use wgpu::TextureSampleType as T;
|
||||
@@ -6301,6 +6370,10 @@ mod destination_tests;
|
||||
#[path = "pack_material_tests.rs"]
|
||||
mod material_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pack_paged_maps_tests.rs"]
|
||||
mod paged_maps_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod rgb_alpha_tests;
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ fn check(sampler: &str, stage: Stage, scene: Scene, expected: [u8; 4]) {
|
||||
let hand = matches!(scene, Scene::Terrain { hand: true, .. });
|
||||
let water = matches!(scene, Scene::Terrain { water: true, .. } | Scene::WaterOnly);
|
||||
let inputs=horizon_shaders::GameInputs {
|
||||
terrain:terrain.then(|| horizon_shaders::GbuffersInputs { declarations:"layout(location=0)in vec3 fixture_position;\n".into(),body:"hz_Vertex=vec4(fixture_position,1.0);hz_Color=vec4(1.0);hz_MultiTexCoord0=vec4(0.0);hz_MultiTexCoord1=vec4(240.0);hz_MultiTexCoord2=vec4(240.0);hz_Normal=vec3(0.0,0.0,1.0);\n".into(),attributes:vec![],after:String::new(),alpha_test:None,item_id_declared:false,far_depth:false }),
|
||||
terrain:terrain.then(|| horizon_shaders::GbuffersInputs { declarations:"layout(location=0)in vec3 fixture_position;\n".into(),body:"hz_Vertex=vec4(fixture_position,1.0);hz_Color=vec4(1.0);hz_MultiTexCoord0=vec4(0.0);hz_MultiTexCoord1=vec4(240.0);hz_MultiTexCoord2=vec4(240.0);hz_Normal=vec3(0.0,0.0,1.0);\n".into(),attributes:vec![],after:String::new(),alpha_test:None,item_id_declared:false,far_depth:false,paged_maps:false }),
|
||||
hand:hand.then(crate::gfx::pack_hand::inputs),..Default::default()
|
||||
};
|
||||
let mut post = gpu.load(
|
||||
@@ -719,6 +719,7 @@ fn shadow_depth_map_reads(
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ fn pack() -> Post {
|
||||
terrain: Some(horizon_shaders::GbuffersInputs {
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
declarations: "layout(location=0)in vec3 fixture_position;\n".into(),
|
||||
body: "hz_Vertex=vec4(fixture_position,1.0);hz_Color=vec4(1.0);\
|
||||
hz_MultiTexCoord0=vec4(0.0);hz_MultiTexCoord1=vec4(240.0);\
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//! A `workGroups` over wgpu's 65535 per dimension runs, and its programs read the work group
|
||||
//! built-ins of the dispatch they declared. The programs are original.
|
||||
use super::destination_tests::Fixture;
|
||||
use super::*;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
fn pack() -> Post {
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut add = |name: &str, source: &str| {
|
||||
zip.start_file(
|
||||
format!("shaders/{name}"),
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(source.as_bytes()).unwrap();
|
||||
};
|
||||
// 70000 x 1 x 2 groups; the last 64 of the second layer each paint one pixel, green when
|
||||
// the built-ins agree with that dispatch.
|
||||
add(
|
||||
"composite.csh",
|
||||
"#version 430\nlayout(local_size_x=1) in;\nconst ivec3 workGroups=ivec3(70000,1,2);\n\
|
||||
layout(rgba8) writeonly uniform image2D colorimg0;\n\
|
||||
void main() {\n uint first = 70000u - 64u;\n\
|
||||
if (gl_WorkGroupID.z != 1u || gl_WorkGroupID.x < first) return;\n\
|
||||
uint k = gl_WorkGroupID.x - first;\n\
|
||||
bool ok = gl_WorkGroupID.y == 0u && gl_NumWorkGroups == uvec3(70000u, 1u, 2u)\n\
|
||||
&& gl_GlobalInvocationID == gl_WorkGroupID;\n\
|
||||
imageStore(colorimg0, ivec2(k % 8u, k / 8u), ok ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1));\n}\n",
|
||||
);
|
||||
add(
|
||||
"final.vsh",
|
||||
"#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n",
|
||||
);
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nuniform sampler2D colortex0;\nin vec2 uv;\n\
|
||||
void main() { gl_FragColor = texelFetch(colortex0, ivec2(gl_FragCoord.xy), 0); }\n",
|
||||
);
|
||||
let pack =
|
||||
horizon_shaders::Pack::from_zip_bytes("original-fold", zip.finish().unwrap().into_inner())
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
horizon_shaders::load_post(&pack, "", &properties, ¯os, &Default::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dispatch_over_the_limit_reads_its_declared_work_groups() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let post = pack();
|
||||
assert!(post.notes.is_empty(), "{:?}", post.notes);
|
||||
let mut post = gpu.build(post);
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
for (k, got) in pixels.iter().enumerate() {
|
||||
assert_eq!(*got, [0, 255, 0, 255], "pixel {k}; notes {:?}", post.notes);
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: true,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +132,7 @@ pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: true,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
}),
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,10 @@ impl<T> Resident<T> {
|
||||
pub fn get_mut(&mut self, slot: u32) -> &mut T {
|
||||
&mut self.items[slot as usize].1
|
||||
}
|
||||
/// Padded mesh bounds of the section in `slot`, relative to its origin.
|
||||
pub fn bounds(&self, slot: u32) -> Option<[Vec3; 2]> {
|
||||
self.bounds[slot as usize]
|
||||
}
|
||||
/// Sections from now on count from `min_y` (a level of another column).
|
||||
pub fn set_min_y(&mut self, min_y: i32) {
|
||||
self.min_y = min_y;
|
||||
|
||||
@@ -46,6 +46,9 @@ pub fn star_triangles() -> Vec<[f32; 3]> {
|
||||
struct Uniform {
|
||||
clip_from_sky: [[f32; 4]; 4],
|
||||
color: [f32; 4],
|
||||
hdr_settings: [f32; 4],
|
||||
hdr_shares: [f32; 4],
|
||||
hdr_light: [f32; 4],
|
||||
}
|
||||
|
||||
pub(super) struct Stars {
|
||||
@@ -93,7 +96,9 @@ impl Stars {
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Stars shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("stars.wgsl").into()),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl")).into(),
|
||||
),
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Stars layout"),
|
||||
@@ -129,7 +134,13 @@ impl Stars {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_stars"),
|
||||
compilation_options: Default::default(),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
// RenderPipelines.STARS: the celestial blend (src alpha, one), with the
|
||||
@@ -165,11 +176,15 @@ impl Stars {
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
clip_from_sky: Mat4,
|
||||
brightness: f32,
|
||||
globals: &super::Globals,
|
||||
) {
|
||||
let b = brightness.clamp(0.0, 1.0);
|
||||
let uniform = Uniform {
|
||||
clip_from_sky: clip_from_sky.to_cols_array_2d(),
|
||||
color: [b; 4],
|
||||
hdr_settings: globals.hdr_settings,
|
||||
hdr_shares: globals.hdr_shares,
|
||||
hdr_light: globals.hdr_light,
|
||||
};
|
||||
uploads.buffer(encoder, &self.uniform, 0, bytemuck::bytes_of(&uniform));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ struct Stars {
|
||||
clip_from_sky: mat4x4<f32>,
|
||||
// starBrightness in every channel (RenderSystem.setShaderColor).
|
||||
color: vec4<f32>,
|
||||
hdr_settings: vec4<f32>,
|
||||
hdr_shares: vec4<f32>,
|
||||
hdr_light: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> stars: Stars;
|
||||
|
||||
@@ -16,5 +19,8 @@ struct Stars {
|
||||
// fully lifted wherever they show at all. Nothing else reads the frame's alpha.
|
||||
@fragment fn fs_stars() -> @location(0) vec4<f32> {
|
||||
let b = stars.color.a;
|
||||
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);
|
||||
}
|
||||
return vec4(stars.color.rgb * b, 0.175);
|
||||
}
|
||||
@@ -8,6 +8,10 @@ pub(super) fn pipelines(
|
||||
) -> [wgpu::RenderPipeline; 3] {
|
||||
std::array::from_fn(|i| {
|
||||
let mut constants = sprite_constants.to_vec();
|
||||
constants.push((
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
));
|
||||
constants.push(("NATIVE_MODEL_ALPHA_CUTOFF", [0.0, 0.1, 0.5][i]));
|
||||
// The ordinary Vertex ABI is unchanged. This material's private payload uses
|
||||
// normal's four bytes for a float z, and glint UV for float normal x/y.
|
||||
|
||||
@@ -65,13 +65,18 @@ impl Visbuffer {
|
||||
let resolve_constants: Vec<(&str, f64)> = constants
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64)])
|
||||
.chain([
|
||||
("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(color == super::LINEAR_FRAME)),
|
||||
),
|
||||
])
|
||||
.collect();
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon visibility buffer"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"))
|
||||
.into(),
|
||||
(super::terrain_shader() + include_str!("../visbuffer.wgsl")).into(),
|
||||
),
|
||||
});
|
||||
let fragment = wgpu::ShaderStages::FRAGMENT;
|
||||
|
||||
@@ -79,6 +79,7 @@ pub struct Post {
|
||||
/// The last TAA frame's unjittered `view_proj` and camera, `None` once a frame ran without it.
|
||||
taa_previous: Option<(DMat4, DVec3)>,
|
||||
taa_this_frame: TaaUniform,
|
||||
light_key: Option<([f32; 4], [f32; 4], [[f32; 4]; 2])>,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
@@ -258,6 +259,19 @@ impl Post {
|
||||
taa_frame: 0,
|
||||
taa_previous: None,
|
||||
taa_this_frame: bytemuck::Zeroable::zeroed(),
|
||||
light_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
// History stores the baked gain as well as the picture. Reject it before jitter records
|
||||
// the next frame whenever that gain or the frame's representation changes.
|
||||
pub(super) fn light_representation(
|
||||
&mut self,
|
||||
key: Option<([f32; 4], [f32; 4], [[f32; 4]; 2])>,
|
||||
) {
|
||||
if self.light_key != key {
|
||||
self.taa_previous = None;
|
||||
self.light_key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,6 +744,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn written_light_changes_reject_history_before_the_next_jitter() {
|
||||
let Some((device, _)) = test_device() else {
|
||||
return;
|
||||
};
|
||||
let mut post = Post::new(&device, wgpu::TextureFormat::Rgba16Float);
|
||||
post.taa = true;
|
||||
if !post.supports_taa() {
|
||||
return;
|
||||
}
|
||||
let key = (
|
||||
[5.0, 4.0, 1.0, 0.0],
|
||||
[0.2, 0.5, 0.6, 1.0],
|
||||
[[0.1; 4], [0.4; 4]],
|
||||
);
|
||||
for key in [Some(key), None, Some(key)] {
|
||||
post.light_representation(key);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.light_representation(key);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
|
||||
}
|
||||
for field in 0..16 {
|
||||
let mut changed = key;
|
||||
match field {
|
||||
0..=3 => changed.0[field] += 0.01,
|
||||
4..=7 => changed.1[field - 4] += 0.01,
|
||||
_ => changed.2[(field - 8) / 4][(field - 8) % 4] += 0.01,
|
||||
}
|
||||
post.light_representation(Some(changed));
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(super) fn pipeline(
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_entity"),
|
||||
entry_point: Some("fs_entity_unmarked"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
@@ -67,7 +67,7 @@ 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);
|
||||
}
|
||||
@fragment fn fs_entity(v:Out)->@location(0) vec4<f32> { return v.color; }
|
||||
@fragment fn fs_entity_unmarked(v:Out)->@location(0) vec4<f32> { return v.color; }
|
||||
"#
|
||||
.into(),
|
||||
),
|
||||
|
||||
@@ -532,6 +532,54 @@ pub fn smooth_gradients_offered() -> bool {
|
||||
SMOOTH_GRADIENTS_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// GPU Culling's setting: off, on where it is offered, or Auto, which has it on only where the
|
||||
/// CPU limits the frame and it makes the frame faster (`autotune::GpuCullingAuto`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum GpuCullingMode {
|
||||
#[default]
|
||||
Off,
|
||||
On,
|
||||
Auto,
|
||||
}
|
||||
impl GpuCullingMode {
|
||||
pub const ALL: [GpuCullingMode; 3] = [
|
||||
GpuCullingMode::Off,
|
||||
GpuCullingMode::On,
|
||||
GpuCullingMode::Auto,
|
||||
];
|
||||
/// The value in `horizon-graphics.txt`: `true` and `false` as the switch wrote them.
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "false",
|
||||
Self::On => "true",
|
||||
Self::Auto => "auto",
|
||||
}
|
||||
}
|
||||
/// The word `horizon-ctl gpu-culling` takes.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::On => "on",
|
||||
Self::Auto => "auto",
|
||||
}
|
||||
}
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "Off",
|
||||
Self::On => "On",
|
||||
Self::Auto => "Auto",
|
||||
}
|
||||
}
|
||||
fn parse(s: &str, default: Self) -> Self {
|
||||
match s {
|
||||
"true" | "on" => Self::On,
|
||||
"false" | "off" => Self::Off,
|
||||
"auto" => Self::Auto,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether GPU Culling (`Graphics::gpu_culling`) can be switched on here, and why not: set when a
|
||||
/// renderer starts, and again when its pass can't be set up or its buffers stop fitting in video
|
||||
/// memory. Where it isn't offered the settings grey its switch out with the reason, and the saved
|
||||
@@ -672,8 +720,9 @@ pub struct Graphics {
|
||||
/// Near solid terrain culled, and its draws listed, on the GPU (`gfx::gpu_culling`), where it
|
||||
/// is offered (`gpu_culling_offer`). Off by default (GPU Culling, Performance graphics); it
|
||||
/// applies at once, and no preset sets it. Every save writes it, so a default turned on later
|
||||
/// needs a new key, as `batchTranslucent` did.
|
||||
pub gpu_culling: bool,
|
||||
/// needs a new key, as `batchTranslucent` did. Auto turns it on only where the CPU limits the
|
||||
/// frame and it makes the frame faster (`autotune`).
|
||||
pub gpu_culling: GpuCullingMode,
|
||||
/// Skip drawing entities hidden behind opaque terrain, synchronously or asynchronously.
|
||||
pub entity_culling: EntityCullingMode,
|
||||
/// Minimum entity population for coarse AABB culling; zero disables it.
|
||||
@@ -684,6 +733,8 @@ pub struct Graphics {
|
||||
/// vanilla's fancy leaves. Ignored with a resource pack that may replace
|
||||
/// the vanilla leaf models ([`Graphics::leaf_culling_effective`]).
|
||||
pub leaf_culling: LeafCullingMode,
|
||||
/// Draw regular ice opaque in the solid pass, without its see-through texture.
|
||||
pub opaque_ice: bool,
|
||||
/// Cache settled shulker-box models.
|
||||
pub cache_shulker_boxes: bool,
|
||||
/// Cache sign-text geometry.
|
||||
@@ -786,11 +837,12 @@ impl Default for Graphics {
|
||||
far_detail: FarDetail::Full,
|
||||
blue_map: crate::bluemap::Settings::default(),
|
||||
cave_culling: true,
|
||||
gpu_culling: false,
|
||||
gpu_culling: GpuCullingMode::Off,
|
||||
entity_culling: EntityCullingMode::Off,
|
||||
entity_culling_precheck: 64,
|
||||
particle_culling: ParticleCullingMode::Off,
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
opaque_ice: false,
|
||||
cache_shulker_boxes: false,
|
||||
// Sign text has used its renderer cache independently of the new model caches.
|
||||
cache_signs: true,
|
||||
@@ -885,13 +937,14 @@ impl Graphics {
|
||||
}
|
||||
"blueMapAutoDetect" => g.blue_map.auto_detect = flag(g.blue_map.auto_detect),
|
||||
"caveCulling" => g.cave_culling = flag(g.cave_culling),
|
||||
"gpuCulling" => g.gpu_culling = flag(g.gpu_culling),
|
||||
"gpuCulling" => g.gpu_culling = GpuCullingMode::parse(value, g.gpu_culling),
|
||||
"entityCulling" => g.entity_culling = EntityCullingMode::parse(value),
|
||||
"entityCullingPrecheck" => {
|
||||
g.entity_culling_precheck = number(g.entity_culling_precheck).min(512)
|
||||
}
|
||||
"particleCulling" => g.particle_culling = ParticleCullingMode::parse(value),
|
||||
"leafCulling" => g.leaf_culling = LeafCullingMode::parse(value),
|
||||
"opaqueIce" => g.opaque_ice = flag(g.opaque_ice),
|
||||
"blockEntityCache" => {
|
||||
let enabled = flag(false);
|
||||
g.cache_shulker_boxes = enabled;
|
||||
@@ -986,7 +1039,7 @@ impl Graphics {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
let mut text = format!(
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nentityCullingPrecheck:{}\nparticleCulling:{}\nleafCulling:{}\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:{}\nhdrUiBrightness:{}\nhdrLighting:{}\nhdrKey:{}\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:{}\nfxaa:{}\ntaa:{}\nupscaling:{}\nupscalingSharpness:{}\nscreenshotQuality:{}\nframeGeneration:{}\nhdr:{}\nhdrPaperWhite:{}\nhdrPeak:{}\nhdrBlackLevel:{}\nhdrUiBrightness:{}\nhdrLighting:{}\nhdrKey:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\nhdrSmoothGradients:{}\ngpuCulling:{}\n",
|
||||
self.preset.name(),
|
||||
self.lod_distance,
|
||||
self.far_detail.name(),
|
||||
@@ -995,6 +1048,7 @@ impl Graphics {
|
||||
self.entity_culling_precheck,
|
||||
self.particle_culling.name(),
|
||||
self.leaf_culling.name(),
|
||||
bool_text(self.opaque_ice),
|
||||
bool_text(self.cache_shulker_boxes),
|
||||
bool_text(self.cache_signs),
|
||||
bool_text(self.cache_decorated_pots),
|
||||
@@ -1041,7 +1095,7 @@ impl Graphics {
|
||||
self.shader_pack,
|
||||
bool_text(self.translucent_batching),
|
||||
bool_text(self.hdr_smooth_gradients),
|
||||
bool_text(self.gpu_culling),
|
||||
self.gpu_culling.key(),
|
||||
);
|
||||
text += &self.blue_map.serialize();
|
||||
text
|
||||
@@ -1181,11 +1235,12 @@ mod tests {
|
||||
servers: Vec::new(),
|
||||
},
|
||||
cave_culling: false,
|
||||
gpu_culling: true,
|
||||
gpu_culling: GpuCullingMode::On,
|
||||
entity_culling: EntityCullingMode::Synchronous,
|
||||
entity_culling_precheck: 128,
|
||||
particle_culling: ParticleCullingMode::Asynchronous,
|
||||
leaf_culling: LeafCullingMode::SolidAggressive,
|
||||
opaque_ice: true,
|
||||
cache_shulker_boxes: true,
|
||||
cache_signs: true,
|
||||
cache_decorated_pots: true,
|
||||
@@ -1271,8 +1326,32 @@ mod tests {
|
||||
assert!(!Graphics::parse("hdr:true\nhdrSmoothGradients:false\n").wide_frame());
|
||||
assert!(Graphics::parse("translucentBatching:false\n").translucent_batching);
|
||||
// GPU Culling is opt-in: only the file or its switch turns it on.
|
||||
assert!(!Graphics::default().gpu_culling);
|
||||
assert!(Graphics::parse("gpuCulling:true\n").gpu_culling);
|
||||
assert_eq!(Graphics::default().gpu_culling, GpuCullingMode::Off);
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:true\n").gpu_culling,
|
||||
GpuCullingMode::On
|
||||
);
|
||||
// Auto is a third value of the same key. A file from before it reads as before, and an
|
||||
// older Horizon reading `auto` finds a value it doesn't know and keeps Off.
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:auto\n").gpu_culling,
|
||||
GpuCullingMode::Auto
|
||||
);
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:false\n").gpu_culling,
|
||||
GpuCullingMode::Off
|
||||
);
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:maybe\n").gpu_culling,
|
||||
GpuCullingMode::Off
|
||||
);
|
||||
for mode in GpuCullingMode::ALL {
|
||||
let g = Graphics {
|
||||
gpu_culling: mode,
|
||||
..Graphics::default()
|
||||
};
|
||||
assert_eq!(Graphics::parse(&g.serialize()).gpu_culling, mode);
|
||||
}
|
||||
// 0 is Auto; anything else is a peak the slider reaches.
|
||||
assert_eq!(Graphics::parse("hdrPeak:0\n").hdr_peak, 0);
|
||||
assert_eq!(Graphics::parse("hdrPeak:50\n").hdr_peak, 200);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -154,61 +154,8 @@ fn calibration_nits(p: vec2<f32>, size: vec2<f32>) -> f32 {
|
||||
return vec4(0.0);
|
||||
}
|
||||
|
||||
// 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.
|
||||
struct Lit {
|
||||
light: vec3<f32>,
|
||||
lifted: bool,
|
||||
};
|
||||
|
||||
fn lit(f: vec3<f32>, a: f32, shows: bool) -> Lit {
|
||||
var lifted = false;
|
||||
// Light over 1 (a float frame's) shows only where the world does and carries
|
||||
// no mark (alpha 1), up to the peak: light written as light. Marked pixels clip
|
||||
// at white before their lift, as on a unorm frame, so light sources, the sun and
|
||||
// HDR lighting look as they do there; so do the HUD, menus and HDR Lighting off.
|
||||
let c = select(min(f, vec3(1.0)), f, shows && a == 1.0);
|
||||
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
|
||||
var light = pow(c, vec3(2.2));
|
||||
// HDR lighting: alpha between 0.76 and 1 is how much of it the surface gets. Vanilla's
|
||||
// light is albedo times lightmap, so this is the lightmap going past 1. It eases into the
|
||||
// peak from 60% of it, so bright textures keep their detail. Unmarked light (alpha 1)
|
||||
// gets no gain and no knee: the clamp to the peak below is all it needs here.
|
||||
if shows && a >= 0.75 && a < 1.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = 1.0 + (params.scene - 1.0) * (1.0 - a) / 0.24;
|
||||
let v = top * gain;
|
||||
let knee = max(0.6 * params.peak, 1.0);
|
||||
var out = v;
|
||||
if v > knee && params.peak > knee {
|
||||
out = knee + (params.peak - knee) * (1.0 - exp(-(v - knee) / (params.peak - knee)));
|
||||
}
|
||||
light *= select(gain, out / max(top, 1e-4), v > knee);
|
||||
}
|
||||
// The world's alpha is below 0.75 on light sources, the sun and the moon
|
||||
// (terrain.wgsl, entity.wgsl): its band of 0.175 is the tier, the lower the
|
||||
// brighter, and where in the band how much (hdr_mark).
|
||||
if shows && a < 0.75 {
|
||||
let band = u32(clamp(floor((a + 0.005) / 0.175), 0.0, 3.0));
|
||||
let tier = 3u - band;
|
||||
let lift = 1.0 - clamp((a - 0.175 * f32(band)) / 0.165, 0.0, 1.0);
|
||||
if lift > 0.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = min(1.0 + (params.peak - 1.0) * params.share[tier] * lift, params.peak / max(top, 1e-4));
|
||||
light *= max(gain, 1.0);
|
||||
lifted = true;
|
||||
}
|
||||
}
|
||||
// HORIZON_HDR_DEBUG=1: what isn't lifted turns grey, what is keeps its
|
||||
// colour with a magenta tint, so a lift shows on any colour.
|
||||
if params.debug > 0.5 && params.world > 0.5 {
|
||||
if lifted {
|
||||
light *= vec3(1.0, 0.6, 1.0);
|
||||
} else {
|
||||
light = vec3(dot(light, vec3(0.2126, 0.7152, 0.0722)));
|
||||
}
|
||||
}
|
||||
return Lit(min(light, vec3(params.peak)), lifted);
|
||||
fn lit(f: vec3<f32>, a: f32, shows: bool, eased: bool) -> Lit {
|
||||
return hdr_mark_light(f, a, shows, eased, vec4(params.peak, params.scene, params.world, params.debug), params.share);
|
||||
}
|
||||
|
||||
// Calibration: above its controls the test picture, straight in nits, past
|
||||
@@ -258,19 +205,69 @@ fn display(light: vec3<f32>) -> vec4<f32> {
|
||||
// hand writes its own alpha there (Hdr::hand_light_view): 1, or a light source it holds.
|
||||
let shows = params.world > 0.5 && params.peak > 1.0
|
||||
&& (hand || (!covered && shows_world(f.rgb, w.rgb)));
|
||||
var light = lit(f.rgb, w.a, shows).light;
|
||||
var light = lit(f.rgb, w.a, shows, true).light;
|
||||
// The HUD and menus at their own brightness, by how much they cover.
|
||||
light *= mix(1.0, params.ui, ui);
|
||||
return display(light);
|
||||
}
|
||||
|
||||
// Hdr::resolve, after the world and the hand: the marks they left (`world` holds the frame as
|
||||
// they drew it) become light in the frame, in gamma as the frame stores it, alpha 1. What draws
|
||||
// after it (camera overlays, the menu blur) draws on light, and nothing is lifted later.
|
||||
// they drew it) become light in the frame, in gamma as the frame stores it, alpha 1, past the
|
||||
// peak if they lift it there (the tone curve eases it in). What draws after it (camera overlays,
|
||||
// the menu blur) draws on light, and nothing is lifted later.
|
||||
@fragment fn fs_resolve(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let w = frame_texel(textureLoad(world, vec2<i32>(pos.xy), 0));
|
||||
let shows = params.world > 0.5 && params.peak > 1.0;
|
||||
return vec4(pow(lit(w.rgb, w.a, shows).light, vec3(1.0 / 2.2)), 1.0);
|
||||
return vec4(pow(lit(w.rgb, w.a, shows, false).light, vec3(1.0 / 2.2)), 1.0);
|
||||
}
|
||||
|
||||
// The tone curve's knee: up to it light stays as it is. Where HDR lighting eased into the peak
|
||||
// before the curve (`lit`), 60% of it, so bright textures keep their detail; at least paper white.
|
||||
fn tone_knee(headroom: f32) -> f32 {
|
||||
return max(0.6 * headroom, 1.0);
|
||||
}
|
||||
|
||||
// The tone curve on light whose brightest channel is `top`, in units of paper white `paper`
|
||||
// nits: what to scale the light by, so it ends at most `headroom` (above 1). ITU-R BT.2390's
|
||||
// EETF in PQ, without the black level lift (`display` does that): the same up to its knee, then
|
||||
// a Hermite spline that reaches the peak level, at the source's peak. That peak is where the knee
|
||||
// lands at `tone_knee`, PQ's 10 000 nits at most; light past it shows at the peak. The brightest
|
||||
// channel sets the scale for all three, so light keeps its hue as it eases (lava stays orange)
|
||||
// and no channel passes the peak.
|
||||
fn tone_scale(top: f32, headroom: f32, paper: f32) -> f32 {
|
||||
if top <= tone_knee(headroom) {
|
||||
return 1.0;
|
||||
}
|
||||
let peak = pq(vec3(headroom * paper)).x;
|
||||
let source = min(3.0 * peak - 2.0 * pq(vec3(tone_knee(headroom) * paper)).x, 1.0);
|
||||
let e = pq(vec3(top * paper)).x / source;
|
||||
let max_lum = peak / source;
|
||||
let ks = 1.5 * max_lum - 0.5;
|
||||
if e >= 1.0 {
|
||||
return headroom / top;
|
||||
}
|
||||
if e <= ks {
|
||||
return 1.0;
|
||||
}
|
||||
let t = (e - ks) / (1.0 - ks);
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
let p = (2.0 * t3 - 3.0 * t2 + 1.0) * ks + (t3 - 2.0 * t2 + t) * (1.0 - ks) + (3.0 * t2 - 2.0 * t3) * max_lum;
|
||||
return pq_nits(vec3(p * source)).x / paper / top;
|
||||
}
|
||||
|
||||
// Hdr::tonemap, after the post effects and before the camera overlays (docs/design/
|
||||
// hdr-linear-light.md, `[tonemap]`): the light (`world` holds the frame) eased into the peak, in
|
||||
// gamma, where scaling the light by s scales the stored colour by s^(1/2.2). Light up to the
|
||||
// knee stays exactly as stored. With no headroom, a clip at white.
|
||||
@fragment fn fs_tonemap(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let g = textureLoad(world, vec2<i32>(pos.xy), 0).rgb;
|
||||
if params.peak <= 1.0 {
|
||||
return vec4(min(g, vec3(1.0)), 1.0);
|
||||
}
|
||||
let top = pow(max(max(g.r, max(g.g, g.b)), 0.0), 2.2);
|
||||
let s = tone_scale(top, params.peak, params.paper * 80.0);
|
||||
return vec4(select(g * pow(s, 1.0 / 2.2), g, s == 1.0), 1.0);
|
||||
}
|
||||
|
||||
// Hdr::keep_world: the HUD's backdrop, the world clipped to white with alpha 0, the frame as
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
override HDR_WRITE_LIGHT: bool = false;
|
||||
|
||||
// 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.
|
||||
// `eased` for the last pass, which eases it into the peak itself; the layered frame leaves that
|
||||
// to the tone curve (fs_tonemap).
|
||||
struct Lit {
|
||||
light: vec3<f32>,
|
||||
lifted: bool,
|
||||
};
|
||||
|
||||
fn hdr_mark_light(f: vec3<f32>, a: f32, shows: bool, eased: bool, settings: vec4<f32>, shares: vec4<f32>) -> Lit {
|
||||
var lifted = false;
|
||||
// Light over 1 (a float frame's) shows only where the world does and carries
|
||||
// no mark (alpha 1), up to the peak: light written as light. Marked pixels clip
|
||||
// at white before their lift, as on a unorm frame, so light sources, the sun and
|
||||
// HDR lighting look as they do there; so do the HUD, menus and HDR Lighting off.
|
||||
let c = select(min(f, vec3(1.0)), f, shows && a == 1.0);
|
||||
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
|
||||
var light = pow(c, vec3(2.2));
|
||||
// HDR lighting: alpha between 0.76 and 1 is how much of it the surface gets. Vanilla's
|
||||
// light is albedo times lightmap, so this is the lightmap going past 1. It eases into the
|
||||
// peak from 60% of it, so bright textures keep their detail. Unmarked light (alpha 1)
|
||||
// gets no gain and no knee: the clamp to the peak below is all it needs here.
|
||||
if shows && a >= 0.75 && a < 1.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = 1.0 + (settings.y - 1.0) * (1.0 - a) / 0.24;
|
||||
let v = top * gain;
|
||||
let knee = max(0.6 * settings.x, 1.0);
|
||||
var out = v;
|
||||
if eased && v > knee && settings.x > knee {
|
||||
out = knee + (settings.x - knee) * (1.0 - exp(-(v - knee) / (settings.x - knee)));
|
||||
}
|
||||
light *= select(gain, out / max(top, 1e-4), eased && v > knee);
|
||||
}
|
||||
// The world's alpha is below 0.75 on light sources, the sun and the moon
|
||||
// (terrain.wgsl, entity.wgsl): its band of 0.175 is the tier, the lower the
|
||||
// brighter, and where in the band how much (hdr_mark).
|
||||
if shows && a < 0.75 {
|
||||
let band = u32(clamp(floor((a + 0.005) / 0.175), 0.0, 3.0));
|
||||
let tier = 3u - band;
|
||||
let lift = 1.0 - clamp((a - 0.175 * f32(band)) / 0.165, 0.0, 1.0);
|
||||
if lift > 0.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
var gain = 1.0 + (settings.x - 1.0) * shares[tier] * lift;
|
||||
if eased {
|
||||
gain = min(gain, settings.x / max(top, 1e-4));
|
||||
}
|
||||
light *= max(gain, 1.0);
|
||||
lifted = true;
|
||||
}
|
||||
}
|
||||
// HORIZON_HDR_DEBUG=1: what isn't lifted turns grey, what is keeps its
|
||||
// colour with a magenta tint, so a lift shows on any colour.
|
||||
if settings.w > 0.5 && settings.z > 0.5 {
|
||||
if lifted {
|
||||
light *= vec3(1.0, 0.6, 1.0);
|
||||
} else {
|
||||
light = vec3(dot(light, vec3(0.2126, 0.7152, 0.0722)));
|
||||
}
|
||||
}
|
||||
if eased {
|
||||
light = min(light, vec3(settings.x));
|
||||
}
|
||||
return Lit(light, lifted);
|
||||
}
|
||||
|
||||
// Marks are decoded at the precision of the original 8-bit frame, even in a float target.
|
||||
fn hdr_quantized_mark(mark: f32) -> f32 {
|
||||
return round(saturate(mark) * 255.0) / 255.0;
|
||||
}
|
||||
|
||||
fn hdr_written(color: vec3<f32>, mark: f32, settings: vec4<f32>, shares: vec4<f32>) -> vec3<f32> {
|
||||
let shows = settings.z > 0.5 && settings.x > 1.0;
|
||||
return pow(hdr_mark_light(max(color, vec3(0.0)), hdr_quantized_mark(mark), shows, false, settings, shares).light, vec3(1.0 / 2.2));
|
||||
}
|
||||
@@ -100,12 +100,18 @@ pub struct HudAtlas {
|
||||
/// 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<horizon_shaders::CoreProgram>>,
|
||||
/// The packs' `core/sky` and `core/stars`, when a pack replaces them.
|
||||
pub sky_shader: Option<std::sync::Arc<horizon_shaders::CoreProgram>>,
|
||||
pub stars_shader: Option<std::sync::Arc<horizon_shaders::CoreProgram>>,
|
||||
/// The packs' OptiFine random entity textures, whose variants this atlas packs.
|
||||
pub random_textures: horizon_optifine::random_textures::RandomTextures,
|
||||
/// Their emissive overlays (`_e`), packed here too.
|
||||
pub emissive: std::sync::Arc<horizon_optifine::emissive::Emissive>,
|
||||
/// ETF's blinking frames (`_blink`), packed here too.
|
||||
pub blinks: horizon_optifine::blink::Blinks,
|
||||
/// Of the shared atlas's textures, their LabPBR normal and specular maps as paged maps
|
||||
/// (`paged_maps.rs`), read while loading for shader packs (`sprites::labpbr_maps`).
|
||||
pub material_maps: std::sync::Arc<[Option<crate::paged_maps::PagedMap>; 2]>,
|
||||
white: Rect,
|
||||
}
|
||||
/// Atlas rows reserved for glyphs rasterized at run time.
|
||||
@@ -888,6 +894,10 @@ impl HudAtlas {
|
||||
&[],
|
||||
&crate::gfx::core_lines::GLUE,
|
||||
);
|
||||
let sky =
|
||||
|name| pack_text::load_core(root, defaults, name, &[], &crate::gfx::core_sky::GLUE);
|
||||
atlas.sky_shader = sky("sky");
|
||||
atlas.stars_shader = sky("stars");
|
||||
Ok(atlas)
|
||||
}
|
||||
/// Empty slots written at run time: map textures (game/maps.rs), the
|
||||
@@ -967,6 +977,7 @@ impl HudAtlas {
|
||||
revision: Self::next_revision(),
|
||||
pixels: Vec::new(),
|
||||
packed: None,
|
||||
material_maps: Default::default(),
|
||||
galactic_widths: [0; 128],
|
||||
width: 1,
|
||||
height: 1,
|
||||
@@ -981,6 +992,8 @@ impl HudAtlas {
|
||||
glint_armor: (Vec::new(), 1, 1),
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
random_textures: Default::default(),
|
||||
emissive: Arc::default(),
|
||||
blinks: Default::default(),
|
||||
@@ -1029,6 +1042,7 @@ impl HudAtlas {
|
||||
(glyph_top, height - glyph_top),
|
||||
white,
|
||||
),
|
||||
material_maps: Default::default(),
|
||||
galactic_widths: galactic_widths(&sprites, &pixels, width, height),
|
||||
pixels,
|
||||
packed: None,
|
||||
@@ -1045,6 +1059,8 @@ impl HudAtlas {
|
||||
white,
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
random_textures: Default::default(),
|
||||
emissive: Default::default(),
|
||||
blinks: Default::default(),
|
||||
@@ -1192,9 +1208,21 @@ impl HudAtlas {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The title panorama and the New World preset pictures load on their own. The
|
||||
// Realms world-slot pictures are drawn from this atlas (vanilla draws their
|
||||
// textures full size): the three faces and `presets/isles.png` stay, costing
|
||||
// one 1024-tall shelf.
|
||||
files.retain(|p| {
|
||||
let s = p.to_string_lossy();
|
||||
!s.contains("title/background") && !s.contains("presets")
|
||||
let s = p.to_string_lossy().replace('\\', "/");
|
||||
let slot_picture = [
|
||||
"textures/gui/title/background/panorama_0.png",
|
||||
"textures/gui/title/background/panorama_2.png",
|
||||
"textures/gui/title/background/panorama_3.png",
|
||||
"textures/gui/presets/isles.png",
|
||||
]
|
||||
.iter()
|
||||
.any(|kept| s.ends_with(kept));
|
||||
slot_picture || (!s.contains("title/background") && !s.contains("presets"))
|
||||
});
|
||||
// OptiFine random entity variants, under the names their scan gives them (those
|
||||
// outside `textures/` are `<ns>:optifine/random/...`).
|
||||
@@ -1269,61 +1297,87 @@ impl HudAtlas {
|
||||
extra.sort();
|
||||
files.extend(extra);
|
||||
}
|
||||
type Decoded = (PackedImage, Option<TextureFlags>, Option<(u32, u32)>, bool);
|
||||
let decode_one = |path: &PathBuf| -> Option<Decoded> {
|
||||
let name = if let Some(name) = named.get(path) {
|
||||
name.clone()
|
||||
} else {
|
||||
match path.strip_prefix(&textures) {
|
||||
Ok(rel) => rel.with_extension("").to_string_lossy().replace('\\', "/"),
|
||||
Err(_) => {
|
||||
let rel = path.strip_prefix(&assets).ok()?.with_extension("");
|
||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
||||
let (namespace, rest) = rel.split_once("/textures/")?;
|
||||
format!("{namespace}:{rest}")
|
||||
// A texture's LabPBR maps, as decoded (width, height, RGBA8), for shader packs.
|
||||
type Maps = [Option<(u32, u32, Vec<u8>)>; 2];
|
||||
let read_maps = full && crate::sprites::labpbr_maps();
|
||||
type Decoded = (
|
||||
PackedImage,
|
||||
Option<TextureFlags>,
|
||||
Option<(u32, u32)>,
|
||||
bool,
|
||||
Option<Box<Maps>>,
|
||||
);
|
||||
let decode_one =
|
||||
|path: &PathBuf| -> Option<Decoded> {
|
||||
let name = if let Some(name) = named.get(path) {
|
||||
name.clone()
|
||||
} else {
|
||||
match path.strip_prefix(&textures) {
|
||||
Ok(rel) => rel.with_extension("").to_string_lossy().replace('\\', "/"),
|
||||
Err(_) => {
|
||||
let rel = path.strip_prefix(&assets).ok()?.with_extension("");
|
||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
||||
let (namespace, rest) = rel.split_once("/textures/")?;
|
||||
format!("{namespace}:{rest}")
|
||||
}
|
||||
}
|
||||
};
|
||||
// LabPBR's maps beside a texture are the world's (sprites.rs), not images to draw.
|
||||
if let Some(base) = name.strip_suffix("_n").or_else(|| name.strip_suffix("_s"))
|
||||
&& textures.join(format!("{base}.png")).is_file()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let image = decode(path)
|
||||
.map_err(|e| log::warn!("skipped GUI texture {}: {e}", path.display()))
|
||||
.ok()?;
|
||||
let mcmeta = horizon_core::prepared_files::read_to_string(
|
||||
&path.with_extension("png.mcmeta"),
|
||||
)
|
||||
.ok();
|
||||
let animation = animation_of(mcmeta.as_deref());
|
||||
// An animated strip keeps its first frame.
|
||||
let frames =
|
||||
animation.is_some() || name.starts_with("block/") || name.starts_with("item/");
|
||||
let (w, h, pixels) = if frames {
|
||||
first_frame(image, animation.as_ref())
|
||||
} else {
|
||||
image
|
||||
};
|
||||
// The default texture's size, when a pack's differs from it.
|
||||
let gui_size = default_textures
|
||||
.as_ref()
|
||||
.and_then(|d| png_size(&d.join(format!("{name}.png"))))
|
||||
.map(|(dw, dh)| {
|
||||
let side = dw.min(dh);
|
||||
if frames { (side, side) } else { (dw, dh) }
|
||||
})
|
||||
.filter(|&size| size != (w, h));
|
||||
let flags = mcmeta.as_deref().and_then(TextureFlags::from_mcmeta);
|
||||
let animated = frames && animation.is_some();
|
||||
let (gw, gh) = gui_size.unwrap_or((w, h));
|
||||
let maps = read_maps
|
||||
.then(|| {
|
||||
let stem = path.file_stem()?.to_str()?;
|
||||
let maps: Maps = ["_n", "_s"].map(|suffix| {
|
||||
decode(&path.with_file_name(format!("{stem}{suffix}.png"))).ok()
|
||||
});
|
||||
maps.iter().any(Option::is_some).then(|| Box::new(maps))
|
||||
})
|
||||
.flatten();
|
||||
// The Realms world-slot pictures are 1024-tall GUI textures drawn as
|
||||
// they are; the cap that keeps other GUI textures small does not
|
||||
// apply to them.
|
||||
let tall_picture = matches!(
|
||||
name.as_str(),
|
||||
"gui/title/background/panorama_0"
|
||||
| "gui/title/background/panorama_2"
|
||||
| "gui/title/background/panorama_3"
|
||||
| "gui/presets/isles"
|
||||
);
|
||||
(gw <= 1024 && (gh <= 512 || tall_picture || name.starts_with("font/")))
|
||||
.then_some(((name, w, h, pixels), flags, gui_size, animated, maps))
|
||||
};
|
||||
// LabPBR's maps beside a texture are the world's (sprites.rs), not images to draw.
|
||||
if let Some(base) = name.strip_suffix("_n").or_else(|| name.strip_suffix("_s"))
|
||||
&& textures.join(format!("{base}.png")).is_file()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let image = decode(path)
|
||||
.map_err(|e| log::warn!("skipped GUI texture {}: {e}", path.display()))
|
||||
.ok()?;
|
||||
let mcmeta =
|
||||
horizon_core::prepared_files::read_to_string(&path.with_extension("png.mcmeta"))
|
||||
.ok();
|
||||
let animation = animation_of(mcmeta.as_deref());
|
||||
// An animated strip keeps its first frame.
|
||||
let frames =
|
||||
animation.is_some() || name.starts_with("block/") || name.starts_with("item/");
|
||||
let (w, h, pixels) = if frames {
|
||||
first_frame(image, animation.as_ref())
|
||||
} else {
|
||||
image
|
||||
};
|
||||
// The default texture's size, when a pack's differs from it.
|
||||
let gui_size = default_textures
|
||||
.as_ref()
|
||||
.and_then(|d| png_size(&d.join(format!("{name}.png"))))
|
||||
.map(|(dw, dh)| {
|
||||
let side = dw.min(dh);
|
||||
if frames { (side, side) } else { (dw, dh) }
|
||||
})
|
||||
.filter(|&size| size != (w, h));
|
||||
let flags = mcmeta.as_deref().and_then(TextureFlags::from_mcmeta);
|
||||
let animated = frames && animation.is_some();
|
||||
let (gw, gh) = gui_size.unwrap_or((w, h));
|
||||
(gw <= 1024 && (gh <= 512 || name.starts_with("font/"))).then_some((
|
||||
(name, w, h, pixels),
|
||||
flags,
|
||||
gui_size,
|
||||
animated,
|
||||
))
|
||||
};
|
||||
let threads = std::thread::available_parallelism().map_or(2, |n| n.get());
|
||||
let chunk = files.len().div_ceil(threads).max(1);
|
||||
let decoded: Vec<_> = std::thread::scope(|scope| {
|
||||
@@ -1342,7 +1396,11 @@ impl HudAtlas {
|
||||
let mut gui_sizes = HashMap::new();
|
||||
let mut animated: Vec<String> = Vec::new();
|
||||
let mut images: Vec<PackedImage> = Vec::with_capacity(decoded.len());
|
||||
for (image, image_flags, gui_size, is_animated) in decoded {
|
||||
let mut maps: HashMap<String, Box<Maps>> = HashMap::new();
|
||||
for (image, image_flags, gui_size, is_animated, image_maps) in decoded {
|
||||
if let Some(m) = image_maps {
|
||||
maps.insert(image.0.clone(), m);
|
||||
}
|
||||
if let Some(f) = image_flags {
|
||||
flags.insert(image.0.clone(), f);
|
||||
}
|
||||
@@ -1517,6 +1575,28 @@ impl HudAtlas {
|
||||
}
|
||||
sprite_animations.push((rect, animation));
|
||||
}
|
||||
// Each mapped texture's maps at its size in the atlas, paged.
|
||||
let material_maps = if maps.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
let mapped: Vec<crate::paged_maps::SpriteMaps> = maps
|
||||
.iter()
|
||||
.filter_map(|(name, m)| {
|
||||
let rect = sprites.get(name)?;
|
||||
let (tw, th) = rect.texels((width, height));
|
||||
let x = (rect.u0 * width as f32).round() as u32;
|
||||
let y = (rect.v0 * height as f32).round() as u32;
|
||||
Some(crate::paged_maps::SpriteMaps {
|
||||
rect: [x, y, tw, th],
|
||||
maps: m.each_ref().map(|map| {
|
||||
let (mw, mh, pixels) = map.as_ref()?;
|
||||
crate::paged_maps::fit(pixels, (*mw, *mh), (tw, th))
|
||||
}),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
crate::paged_maps::build((width, height), &mapped, MAX_ATLAS_SIZE)
|
||||
};
|
||||
let white = sprites["horizon/white"];
|
||||
let glint = Self::glint_texture(root, defaults, "enchanted_glint_item");
|
||||
let glint_armor = Self::glint_texture(root, defaults, "enchanted_glint_armor");
|
||||
@@ -1531,6 +1611,7 @@ impl HudAtlas {
|
||||
);
|
||||
Ok(Self {
|
||||
revision: Self::next_revision(),
|
||||
material_maps: std::sync::Arc::new(material_maps),
|
||||
galactic_widths: galactic_widths(&sprites, &pixels, width, height),
|
||||
pixels,
|
||||
packed: None,
|
||||
@@ -1546,6 +1627,8 @@ impl HudAtlas {
|
||||
glint_armor,
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
random_textures,
|
||||
emissive,
|
||||
blinks,
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod assets;
|
||||
pub mod atlas_sources;
|
||||
pub mod audio;
|
||||
pub mod auth;
|
||||
pub mod autotune;
|
||||
pub mod ban_notice;
|
||||
pub mod bidi;
|
||||
pub mod biome;
|
||||
@@ -84,6 +85,7 @@ pub mod pack_screen;
|
||||
pub mod packs;
|
||||
pub mod pad_cursor;
|
||||
pub mod pad_glyph;
|
||||
pub mod paged_maps;
|
||||
pub mod par;
|
||||
pub mod particles;
|
||||
pub mod poi_sections;
|
||||
@@ -126,6 +128,7 @@ pub mod test_block;
|
||||
pub mod test_instance_block;
|
||||
pub mod testgen;
|
||||
pub mod title_edition;
|
||||
pub mod touch;
|
||||
pub mod trims;
|
||||
pub mod ui;
|
||||
pub mod vault;
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
};
|
||||
pub mod cache;
|
||||
mod mesh;
|
||||
pub mod packed;
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
@@ -559,7 +560,8 @@ pub(crate) fn state_kinds(facts: &BlockFacts) -> Vec<u8> {
|
||||
}
|
||||
/// Blocks that fill their cell but neither draw solid nor collide as a full
|
||||
/// block: far terrain draws them as blocks, or patches of powder snow leave
|
||||
/// holes in mountains (and paths and fields dips).
|
||||
/// holes in mountains (and paths and fields dips). Slabs use the same
|
||||
/// full-cell approximation as stairs, so distant slab builds remain visible.
|
||||
fn nearly_full(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
@@ -568,6 +570,7 @@ fn nearly_full(name: &str) -> bool {
|
||||
| "minecraft:farmland"
|
||||
| "minecraft:honey_block"
|
||||
) || name.ends_with("_stairs")
|
||||
|| name.ends_with("_slab")
|
||||
|| name.contains("fence")
|
||||
|| name.contains("_wall")
|
||||
|| name.contains("bars")
|
||||
@@ -1414,7 +1417,13 @@ impl Lod {
|
||||
&edges,
|
||||
&input.finer,
|
||||
);
|
||||
// Sorts the quads by facing side, and clears
|
||||
// the compact layout: a far tile has no tints
|
||||
// for `mesher::packed`, so its own records
|
||||
// (`lod::packed`) are laid out here, from the
|
||||
// quads in the order the sort left them.
|
||||
mesh.sort_solid();
|
||||
crate::lod::packed::lay_out(&mut mesh);
|
||||
let [ox, _, oz] = job.key.origin();
|
||||
let size = job.key.size();
|
||||
mesh.keep = keep_finer
|
||||
@@ -3977,6 +3986,73 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires prepared models/textures in HORIZON_TEST_RESOURCES"]
|
||||
fn slab_materials_survive_chunk_summaries_and_draw_far_faces() -> crate::Result<()> {
|
||||
use crate::{assets::Assets, biome::Biomes, light::LightTables, mesher::MeshTables};
|
||||
let root = std::path::PathBuf::from(
|
||||
std::env::var_os("HORIZON_TEST_RESOURCES").expect("HORIZON_TEST_RESOURCES"),
|
||||
);
|
||||
let assets = Arc::new(Assets::load(&root)?);
|
||||
let render = Arc::new(assets.reload(&root)?);
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let context = WorkerContext {
|
||||
light: Arc::new(LightTables::load(&root, assets.registry, &facts)?),
|
||||
tables: Arc::new(MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
crate::models::load_visibility(&root, assets.registry)?,
|
||||
)?),
|
||||
assets: assets.clone(),
|
||||
render,
|
||||
biomes: Arc::new(Biomes::load(&root)?),
|
||||
};
|
||||
let kinds = state_kinds(&facts);
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
let mut mesher = mesh::Mesher::default();
|
||||
let mut materials = FxHashSet::default();
|
||||
// Real top, bottom, double and waterlogged states: a waterlogged slab
|
||||
// must keep its material rather than become a column of plain water.
|
||||
for (i, f) in facts.states.iter().enumerate() {
|
||||
if !f.name.ends_with("_slab") {
|
||||
continue;
|
||||
}
|
||||
materials.insert(f.name.as_ref());
|
||||
let state = BlockStateId(i as u32);
|
||||
chunk.set(3, 90, 3, state.0);
|
||||
let columns = summarize(&chunk, &kinds);
|
||||
let column = &columns[3 * 16 + 3];
|
||||
assert_eq!(column.spans.len(), 1, "{} state {i}", f.name);
|
||||
assert_eq!(column.spans[0].state, state, "{} state {i}", f.name);
|
||||
assert_eq!((column.spans[0].min, column.spans[0].max), (90, 91));
|
||||
assert_eq!(Column::reduced(&columns, 0, 0, 4).spans, column.spans);
|
||||
let mesh = mesher.mesh(
|
||||
TileKey {
|
||||
x: 0,
|
||||
z: 0,
|
||||
level: 0,
|
||||
},
|
||||
&columns.iter().collect::<Vec<_>>(),
|
||||
&FxHashSet::default(),
|
||||
None,
|
||||
&context,
|
||||
&std::array::from_fn(|_| Vec::new()),
|
||||
&[],
|
||||
);
|
||||
// Like stairs, far slabs use the existing full-cell approximation.
|
||||
assert_eq!(mesh.solid.len(), 6 * 4, "{} state {i}", f.name);
|
||||
assert!(mesh.translucent.is_empty(), "{} state {i}", f.name);
|
||||
}
|
||||
for name in [
|
||||
"minecraft:smooth_stone_slab",
|
||||
"minecraft:oak_slab",
|
||||
"minecraft:cut_copper_slab",
|
||||
] {
|
||||
assert!(materials.contains(name), "missing fixture {name}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires prepared models/textures in HORIZON_TEST_RESOURCES"]
|
||||
fn real_mesh_clips_near_chunks_and_preserves_water_and_edited_surfaces() -> crate::Result<()> {
|
||||
use crate::{
|
||||
assets::Assets, biome::Biomes, light::LightTables, mesher::MeshTables,
|
||||
|
||||
@@ -1386,6 +1386,52 @@ mod tests {
|
||||
let p = |i: usize| glam::Vec3::from(q[i].pos);
|
||||
(p(1) - p(0)).cross(p(2) - p(0))
|
||||
}
|
||||
/// Every solid quad the mesher emits packs into a far record, and the
|
||||
/// record gives the quad back bit for bit (`lod::packed`).
|
||||
///
|
||||
/// `lod::packed`'s own tests check its records against its own copy of what
|
||||
/// `emit_quad` does, which would pass just as well if the two had drifted
|
||||
/// apart. This checks them against the real mesher, over terrain that makes
|
||||
/// faces of every axis and winding: a hill of varied heights, a material
|
||||
/// boundary, a light boundary and water.
|
||||
#[test]
|
||||
fn every_quad_the_mesher_emits_packs_and_comes_back() {
|
||||
let mut columns = columns();
|
||||
for (i, column) in columns.iter_mut().enumerate() {
|
||||
let (x, z) = (i % CELLS, i / CELLS);
|
||||
let top = 4 + (x as i16 % 5) * 2 + (z as i16 % 3);
|
||||
let state = if (x + z) % 7 == 0 { 3 } else { 1 };
|
||||
let light = if x % 4 == 0 { 16 } else { 240 };
|
||||
column.spans = vec![
|
||||
Span {
|
||||
light,
|
||||
..span(0, top, state)
|
||||
},
|
||||
// Water over the lower ground, for the fluid faces too.
|
||||
span(top, 12.max(top + 1), 2),
|
||||
]
|
||||
.into();
|
||||
}
|
||||
let mesh = mesh(&columns);
|
||||
let quads = mesh.solid.as_chunks::<4>().0;
|
||||
assert!(
|
||||
quads.len() > 50,
|
||||
"{} quads is too few to say much",
|
||||
quads.len()
|
||||
);
|
||||
for (i, quad) in quads.iter().enumerate() {
|
||||
let words = crate::lod::packed::pack(quad)
|
||||
.unwrap_or_else(|| panic!("quad {i} of {} did not pack", quads.len()));
|
||||
let back = crate::lod::packed::corners(words)
|
||||
.unwrap_or_else(|| panic!("quad {i}'s record names no face"));
|
||||
assert_eq!(
|
||||
bytemuck::cast::<_, [[u32; 6]; 4]>(back),
|
||||
bytemuck::cast::<_, [[u32; 6]; 4]>(*quad),
|
||||
"quad {i} came back changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_solid_cube_merges_to_six_faces_with_block_scaled_uvs() {
|
||||
let mut columns = columns();
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
//! The compact layout of a far tile's solid quads in the arena: one 24-byte
|
||||
//! record each instead of four 24-byte vertices.
|
||||
//!
|
||||
//! A far tile's quads are not blocks' faces -- they are merged rectangles of a
|
||||
//! voxel column's exposed side, hundreds of blocks wide -- so none of them fits
|
||||
//! `mesher::packed`'s record, whose coordinates are a block grid of 16. They
|
||||
//! are far more regular than a chunk's quads instead: a rectangle in an
|
||||
//! axis-aligned plane, with one colour, one light and one sprite layer across
|
||||
//! all four corners, and UVs that `lod::mesh::emit_quad` derives from the
|
||||
//! positions. So a record holds what that function was given, and
|
||||
//! terrain.wgsl's `far_corner` does what it did:
|
||||
//!
|
||||
//! - word 0: the colour (RGBA8), the same for all four corners;
|
||||
//! - word 1: the sprite layer, and the light in the high half;
|
||||
//! - word 2: the plane, the coordinate all four corners share, as an `i32`;
|
||||
//! - word 3: the rectangle's low edge along the face's first axis, as an `i32`;
|
||||
//! - word 4: its low edge along the second, as an `i32`;
|
||||
//! - word 5: the plane's axis, whether the corners are reversed, the
|
||||
//! rectangle's width and height, and `FULL` clear.
|
||||
//!
|
||||
//! Nothing here reasons about which record is right for a quad: `pack` builds
|
||||
//! one, rebuilds the four vertices from it with the same code the shader
|
||||
//! mirrors, and keeps it only if they come back bit for bit. A quad no record
|
||||
//! reproduces -- a fractional coordinate, a rectangle too wide, a face whose
|
||||
//! UVs were scaled down to fit (`lod::mesh::MAX_PIECES`) -- is kept as its four
|
||||
//! vertices, exactly as `mesher::packed` keeps one.
|
||||
|
||||
use crate::mesher::{TerrainVertex, packed};
|
||||
|
||||
/// Word 5's bit of a quad kept as four vertices; word 0 is then the slots from
|
||||
/// the record to its first vertex. The same bit `mesher::packed` uses, so the
|
||||
/// two layouts place their records and spilled vertices alike.
|
||||
pub use crate::mesher::packed::FULL;
|
||||
|
||||
/// The draws' base vertex of a compact far mesh, above `packed::PACKED_BASE`:
|
||||
/// `FAR_BASE + 4 * slot + corner` is a corner of the quad whose record is at
|
||||
/// `slot`. terrain.wgsl's `section_corner` reads the layout off the index, so
|
||||
/// a far tile and a chunk section can share one arena and one pipeline.
|
||||
pub const FAR_BASE: u32 = 1 << 29;
|
||||
|
||||
/// Whether far tiles meshed from now on get this layout: `HORIZON_FAR_PACK=0`
|
||||
/// stops them and leaves chunk sections' own layout alone, so the two can be
|
||||
/// told apart in one build. `HORIZON_PACKED_QUADS=0` stops both.
|
||||
pub fn far_packing() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var("HORIZON_FAR_PACK").as_deref() != Ok("0"))
|
||||
}
|
||||
|
||||
/// The widest rectangle a record's 11-bit width and height hold. `lod::mesh`
|
||||
/// splits a face at `MAX_FACE` = 1024 blocks unless it scales its UVs instead,
|
||||
/// and a scaled face does not pack, so this is never the binding limit.
|
||||
const MAX_EXTENT: i64 = (1 << 11) - 1;
|
||||
|
||||
/// Which axis a face's plane lies on, and what `lod::mesh::emit_quad` makes of
|
||||
/// the other two. The names are that function's: `u` is the rectangle's first
|
||||
/// axis and `v` its second, and the UV of a corner is `[u, -v]` of it.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Axis {
|
||||
/// Up and Down: the plane is y, u is x and v is z.
|
||||
Y,
|
||||
/// North and South: the plane is z, u is x and v is y.
|
||||
Z,
|
||||
/// West and East: the plane is x, u is z and v is y.
|
||||
X,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
const ALL: [Self; 3] = [Self::Y, Self::Z, Self::X];
|
||||
|
||||
fn bits(self) -> u32 {
|
||||
match self {
|
||||
Self::Y => 0,
|
||||
Self::Z => 1,
|
||||
Self::X => 2,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn from_bits(bits: u32) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|a| a.bits() == bits)
|
||||
}
|
||||
}
|
||||
|
||||
/// A face as `lod::mesh::emit_quad` was given it, which is all a record holds.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Face {
|
||||
axis: Axis,
|
||||
/// Whether the corners go round the other way, as they do for Down, South
|
||||
/// and East (that function's `points.reverse()`).
|
||||
reversed: bool,
|
||||
plane: i32,
|
||||
u0: i32,
|
||||
v0: i32,
|
||||
du: u32,
|
||||
dv: u32,
|
||||
layer: u16,
|
||||
light: u16,
|
||||
color: u32,
|
||||
}
|
||||
|
||||
impl Face {
|
||||
/// The four vertices of it, as `lod::mesh::emit_quad` writes them. Kept
|
||||
/// here and not shared with that function because it is what the shader
|
||||
/// mirrors: the two have to agree, and `a_record_rebuilds_what_the_mesher_
|
||||
/// emitted` is what holds them to it.
|
||||
fn vertices(self) -> [TerrainVertex; 4] {
|
||||
let (p, u0, v0) = (self.plane as f32, self.u0 as f32, self.v0 as f32);
|
||||
let u1 = u0 + self.du as f32;
|
||||
let v1 = v0 + self.dv as f32;
|
||||
let mut points = match self.axis {
|
||||
Axis::Y => [[u0, p, v1], [u1, p, v1], [u1, p, v0], [u0, p, v0]],
|
||||
Axis::Z => [[u0, v1, p], [u1, v1, p], [u1, v0, p], [u0, v0, p]],
|
||||
Axis::X => [[p, v1, u1], [p, v1, u0], [p, v0, u0], [p, v0, u1]],
|
||||
};
|
||||
if self.reversed {
|
||||
points.reverse();
|
||||
}
|
||||
let uvs = points.map(|pos| match self.axis {
|
||||
Axis::Y => [pos[0], -pos[2]],
|
||||
Axis::Z => [pos[0], -pos[1]],
|
||||
Axis::X => [pos[2], -pos[1]],
|
||||
});
|
||||
let corner = [0, 1].map(|i| {
|
||||
uvs.iter()
|
||||
.map(|uv| uv[i])
|
||||
.fold(f32::INFINITY, f32::min)
|
||||
.floor()
|
||||
});
|
||||
let mut out = [TerrainVertex::tiled([0.0; 3], [0.0; 2], 0, 0, 0); 4];
|
||||
for ((to, pos), uv) in out.iter_mut().zip(points).zip(uvs) {
|
||||
*to = TerrainVertex::tiled(
|
||||
pos,
|
||||
[uv[0] - corner[0], uv[1] - corner[1]],
|
||||
self.layer,
|
||||
self.color,
|
||||
self.light,
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn words(self) -> [u32; 6] {
|
||||
[
|
||||
self.color,
|
||||
u32::from(self.layer) | u32::from(self.light) << 16,
|
||||
self.plane as u32,
|
||||
self.u0 as u32,
|
||||
self.v0 as u32,
|
||||
self.axis.bits() | u32::from(self.reversed) << 2 | self.du << 3 | self.dv << 14,
|
||||
]
|
||||
}
|
||||
|
||||
/// The face a record names, or `None` for a record that spills its quad's
|
||||
/// vertices (`FULL`) or that does not name an axis.
|
||||
#[cfg(test)]
|
||||
fn from_words(w: [u32; 6]) -> Option<Self> {
|
||||
if w[5] & FULL != 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
axis: Axis::from_bits(w[5] & 3)?,
|
||||
reversed: (w[5] >> 2) & 1 != 0,
|
||||
plane: w[2] as i32,
|
||||
u0: w[3] as i32,
|
||||
v0: w[4] as i32,
|
||||
du: (w[5] >> 3) & 0x7ff,
|
||||
dv: (w[5] >> 14) & 0x7ff,
|
||||
layer: w[1] as u16,
|
||||
light: (w[1] >> 16) as u16,
|
||||
color: w[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A whole number in `i32`, or `None`.
|
||||
fn whole(v: f32) -> Option<i32> {
|
||||
(v.fract() == 0.0 && (f32::from(i16::MIN) * 16.0..=f32::from(i16::MAX) * 16.0).contains(&v))
|
||||
.then_some(v as i32)
|
||||
}
|
||||
|
||||
/// `quad`'s record, if one gives back its four vertices exactly.
|
||||
pub fn pack(quad: &[TerrainVertex; 4]) -> Option<[u32; 6]> {
|
||||
let first = quad[0];
|
||||
// One colour, one light and one layer across the four corners, which is
|
||||
// what lets a record hold them once.
|
||||
if quad
|
||||
.iter()
|
||||
.any(|v| (v.color, v.light, v.layer) != (first.color, first.light, first.layer))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let lo = [0, 1, 2].map(|k| quad.iter().map(|v| v.pos[k]).fold(f32::INFINITY, f32::min));
|
||||
let hi = [0, 1, 2].map(|k| {
|
||||
quad.iter()
|
||||
.map(|v| v.pos[k])
|
||||
.fold(f32::NEG_INFINITY, f32::max)
|
||||
});
|
||||
// Every axis and winding the mesher can have emitted; the round trip below
|
||||
// says which one it was, so nothing here has to work it out.
|
||||
for axis in Axis::ALL {
|
||||
let (plane, u, v) = match axis {
|
||||
Axis::Y => (1, 0, 2),
|
||||
Axis::Z => (2, 0, 1),
|
||||
Axis::X => (0, 2, 1),
|
||||
};
|
||||
if lo[plane] != hi[plane] {
|
||||
continue;
|
||||
}
|
||||
let (Some(plane), Some(u0), Some(v0)) = (whole(lo[plane]), whole(lo[u]), whole(lo[v]))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let (Some(u1), Some(v1)) = (whole(hi[u]), whole(hi[v])) else {
|
||||
continue;
|
||||
};
|
||||
let (du, dv) = (i64::from(u1 - u0), i64::from(v1 - v0));
|
||||
if du > MAX_EXTENT || dv > MAX_EXTENT {
|
||||
continue;
|
||||
}
|
||||
for reversed in [false, true] {
|
||||
let face = Face {
|
||||
axis,
|
||||
reversed,
|
||||
plane,
|
||||
u0,
|
||||
v0,
|
||||
du: du as u32,
|
||||
dv: dv as u32,
|
||||
layer: first.layer,
|
||||
light: first.light,
|
||||
color: first.color,
|
||||
};
|
||||
let words = face.words();
|
||||
// The record is kept only where it gives the quad back exactly, so
|
||||
// a quad this layout cannot hold is spilled rather than altered.
|
||||
if bytemuck::cast::<_, [[u32; 6]; 4]>(face.vertices())
|
||||
== bytemuck::cast::<_, [[u32; 6]; 4]>(*quad)
|
||||
{
|
||||
return Some(words);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The four corners a record names, as the shader rebuilds them; `None` for a
|
||||
/// record that spills its quad's vertices. For the tests that hold this layout
|
||||
/// and `lod::mesh` to each other.
|
||||
#[cfg(test)]
|
||||
pub fn corners(words: [u32; 6]) -> Option<[TerrainVertex; 4]> {
|
||||
Face::from_words(words).map(Face::vertices)
|
||||
}
|
||||
|
||||
/// Lays a far tile's solid quads out compactly in `mesh.packed`, where the
|
||||
/// renderer reads that layout. Call it after `SectionMesh::sort_solid`, which
|
||||
/// reorders the quads and clears whatever layout was there.
|
||||
pub fn lay_out(mesh: &mut crate::mesher::SectionMesh) {
|
||||
if !packed::packing() || !far_packing() {
|
||||
return;
|
||||
}
|
||||
mesh.packed = layout(&mesh.solid);
|
||||
mesh.packed_far = !mesh.packed.is_empty();
|
||||
}
|
||||
|
||||
/// A far tile's solid quads laid out compactly: a record each in mesh order,
|
||||
/// then the vertices of the quads no record fits. Empty where that would not
|
||||
/// be smaller than the vertices.
|
||||
pub fn layout(solid: &[TerrainVertex]) -> Vec<[u32; 6]> {
|
||||
let quads = solid.as_chunks::<4>().0;
|
||||
let records: Vec<Option<[u32; 6]>> = quads.iter().map(pack).collect();
|
||||
packed::place(&records, quads)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The four corners as bits: `TerrainVertex` has no `PartialEq`, and bit
|
||||
/// identity is the guarantee the layout makes in any case.
|
||||
fn words_of(quad: [TerrainVertex; 4]) -> [[u32; 6]; 4] {
|
||||
bytemuck::cast(quad)
|
||||
}
|
||||
|
||||
fn face() -> Face {
|
||||
Face {
|
||||
axis: Axis::Y,
|
||||
reversed: false,
|
||||
plane: 71,
|
||||
u0: -48,
|
||||
v0: 112,
|
||||
du: 16,
|
||||
dv: 7,
|
||||
layer: 3,
|
||||
light: 0xF0F0,
|
||||
color: 0xFF_80_40_20,
|
||||
}
|
||||
}
|
||||
|
||||
/// Every axis and winding survives the words, so `from_words` and the
|
||||
/// shader's `far_corner` read what `words` wrote.
|
||||
#[test]
|
||||
fn a_face_survives_its_record() {
|
||||
for axis in Axis::ALL {
|
||||
for reversed in [false, true] {
|
||||
let want = Face {
|
||||
axis,
|
||||
reversed,
|
||||
..face()
|
||||
};
|
||||
let got = Face::from_words(want.words()).expect("a record names a face");
|
||||
assert_eq!(
|
||||
words_of(got.vertices()),
|
||||
words_of(want.vertices()),
|
||||
"{axis:?} {reversed}"
|
||||
);
|
||||
assert_eq!((got.axis, got.reversed), (axis, reversed));
|
||||
assert_eq!((got.plane, got.u0, got.v0), (want.plane, want.u0, want.v0));
|
||||
assert_eq!((got.du, got.dv), (want.du, want.dv));
|
||||
assert_eq!(
|
||||
(got.layer, got.light, got.color),
|
||||
(want.layer, want.light, want.color)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The widest and the most distant rectangle a record claims to hold comes
|
||||
/// back exactly, so the bit fields are wide enough for what they say.
|
||||
#[test]
|
||||
fn the_extremes_a_record_claims_come_back() {
|
||||
let want = Face {
|
||||
du: MAX_EXTENT as u32,
|
||||
dv: MAX_EXTENT as u32,
|
||||
plane: i32::from(i16::MAX),
|
||||
u0: -8_000_000,
|
||||
v0: 8_000_000,
|
||||
layer: u16::MAX,
|
||||
light: u16::MAX,
|
||||
color: u32::MAX,
|
||||
..face()
|
||||
};
|
||||
let got = Face::from_words(want.words()).expect("a record names a face");
|
||||
assert_eq!((got.du, got.dv), (want.du, want.dv));
|
||||
assert_eq!((got.plane, got.u0, got.v0), (want.plane, want.u0, want.v0));
|
||||
assert_eq!(
|
||||
(got.layer, got.light, got.color),
|
||||
(want.layer, want.light, want.color)
|
||||
);
|
||||
}
|
||||
|
||||
/// Any quad the mesher could have emitted packs, and unpacks to the very
|
||||
/// vertices it emitted: this is the whole guarantee the layout makes.
|
||||
#[test]
|
||||
fn a_record_rebuilds_what_the_mesher_emitted() {
|
||||
let mut packed = 0;
|
||||
for axis in Axis::ALL {
|
||||
for reversed in [false, true] {
|
||||
for (du, dv) in [(1, 1), (16, 1), (1, 16), (37, 91), (1024, 1024)] {
|
||||
let want = Face {
|
||||
axis,
|
||||
reversed,
|
||||
du,
|
||||
dv,
|
||||
..face()
|
||||
};
|
||||
let quad = want.vertices();
|
||||
let words = pack(&quad).expect("the mesher's own quad packs");
|
||||
let back = Face::from_words(words).expect("a record names a face");
|
||||
assert_eq!(
|
||||
words_of(back.vertices()),
|
||||
words_of(quad),
|
||||
"{axis:?} {reversed} {du}x{dv}"
|
||||
);
|
||||
packed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(packed, 30, "every case packed");
|
||||
}
|
||||
|
||||
/// A quad this layout cannot hold is refused rather than altered: the
|
||||
/// corners disagreeing on a colour, a fractional coordinate, and a
|
||||
/// rectangle wider than the width field.
|
||||
#[test]
|
||||
fn a_quad_no_record_fits_is_refused() {
|
||||
let mut uneven = face().vertices();
|
||||
uneven[2].color ^= 1;
|
||||
assert!(pack(&uneven).is_none(), "corners differ in colour");
|
||||
|
||||
let mut fraction = face().vertices();
|
||||
fraction[0].pos[0] += 0.5;
|
||||
assert!(pack(&fraction).is_none(), "a fractional coordinate");
|
||||
|
||||
let wide = Face {
|
||||
du: MAX_EXTENT as u32 + 1,
|
||||
..face()
|
||||
};
|
||||
assert!(pack(&wide.vertices()).is_none(), "wider than the field");
|
||||
}
|
||||
|
||||
/// A tile of quads that all pack takes a quarter of the slots its vertices
|
||||
/// would, which is the point of the layout.
|
||||
#[test]
|
||||
fn a_tile_that_packs_takes_a_quarter_of_the_slots() {
|
||||
let mut solid = Vec::new();
|
||||
for n in 0..64 {
|
||||
solid.extend(
|
||||
Face {
|
||||
u0: n * 4,
|
||||
..face()
|
||||
}
|
||||
.vertices(),
|
||||
);
|
||||
}
|
||||
let out = layout(&solid);
|
||||
assert_eq!(out.len(), solid.len() / 4, "a slot a quad, none spilled");
|
||||
for (quad, record) in solid.as_chunks::<4>().0.iter().zip(&out) {
|
||||
let face = Face::from_words(*record).expect("a record names a face");
|
||||
assert_eq!(words_of(face.vertices()), words_of(*quad),);
|
||||
}
|
||||
}
|
||||
|
||||
/// A tile whose quads mostly spill keeps the vertices and gives up on the
|
||||
/// layout rather than growing: `place` is what decides, and this holds it
|
||||
/// to the same rule `mesher::packed` follows.
|
||||
#[test]
|
||||
fn a_tile_that_mostly_spills_keeps_its_vertices() {
|
||||
let mut solid = Vec::new();
|
||||
for n in 0..64 {
|
||||
let mut quad = Face {
|
||||
u0: n * 4,
|
||||
..face()
|
||||
}
|
||||
.vertices();
|
||||
quad[1].color ^= 1;
|
||||
solid.extend(quad);
|
||||
}
|
||||
assert!(layout(&solid).is_empty(), "no layout smaller than vertices");
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
//! keeps 7 archives a day, the newest with the highest index.
|
||||
//!
|
||||
//! Messages go through the `log` facade. Horizon's own still print their plain text on stderr,
|
||||
//! as they did before the log existed; other crates' errors only reach the file (their
|
||||
//! warnings are mostly the Vulkan loader's startup chatter).
|
||||
//! as they did before the log existed. Other crates, including native mods and their helper
|
||||
//! crates, reach the file through INFO too, with their target prefixed to identify the source.
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
@@ -424,14 +424,18 @@ fn ours(target: &str) -> bool {
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, meta: &log::Metadata) -> bool {
|
||||
meta.level() <= log::Level::Info
|
||||
&& (ours(meta.target()) || meta.level() <= log::Level::Error)
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
let message = record.args().to_string();
|
||||
let horizon = ours(record.target());
|
||||
let message = if horizon {
|
||||
record.args().to_string()
|
||||
} else {
|
||||
format!("[{}] {}", record.target(), record.args())
|
||||
};
|
||||
let current = std::thread::current();
|
||||
let thread = match current.name() {
|
||||
Some("main") => self.main_thread,
|
||||
@@ -460,7 +464,7 @@ impl log::Log for Logger {
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
let stderr = ours(record.target()).then(|| message.clone());
|
||||
let stderr = horizon.then(|| message.clone());
|
||||
self.send(thread, level, &message, stderr);
|
||||
if gate == Gate::Last {
|
||||
self.write(
|
||||
@@ -652,6 +656,91 @@ pub fn newest_crash_report(home: &Path) -> Option<PathBuf> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn native_mod_records_reach_the_file_with_their_targets() {
|
||||
use log::Log;
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("horizon-native-log-test-{}", std::process::id()));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let (out, receive) = mpsc::channel();
|
||||
let writer = Writer {
|
||||
file: Some(open_latest(&dir).unwrap().unwrap()),
|
||||
dir: dir.clone(),
|
||||
date: Local::now().date_naive(),
|
||||
};
|
||||
let handle = std::thread::spawn(move || writer.run(receive));
|
||||
let logger = Logger {
|
||||
out,
|
||||
recent: Mutex::new(VecDeque::new()),
|
||||
main_thread: "Render thread",
|
||||
floods: Mutex::new(Floods::default()),
|
||||
};
|
||||
for (target, level, message) in [
|
||||
("meridian_horizon::save", log::Level::Warn, "save failed"),
|
||||
("meridian_core::map", log::Level::Info, "map opened"),
|
||||
("wgpu_core::device", log::Level::Error, "device lost"),
|
||||
("meridian_core::map", log::Level::Debug, "debug detail"),
|
||||
("meridian_core::map", log::Level::Trace, "trace detail"),
|
||||
] {
|
||||
logger.log(
|
||||
&log::Record::builder()
|
||||
.args(format_args!("{message}"))
|
||||
.level(level)
|
||||
.target(target)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
logger.flush();
|
||||
drop(logger);
|
||||
handle.join().unwrap();
|
||||
let text = fs::read_to_string(dir.join("latest.log")).unwrap();
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
assert!(
|
||||
text.contains("[meridian_horizon::save] save failed"),
|
||||
"{text}"
|
||||
);
|
||||
assert!(text.contains("[meridian_core::map] map opened"), "{text}");
|
||||
assert!(text.contains("[wgpu_core::device] device lost"), "{text}");
|
||||
assert!(!text.contains("debug detail"));
|
||||
assert!(!text.contains("trace detail"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizon_stderr_and_external_file_records_stay_separate() {
|
||||
use log::Log;
|
||||
let (out, receive) = mpsc::channel();
|
||||
let logger = Logger {
|
||||
out,
|
||||
recent: Mutex::new(VecDeque::new()),
|
||||
main_thread: "Render thread",
|
||||
floods: Mutex::new(Floods::default()),
|
||||
};
|
||||
for target in ["horizon_client::game", "meridian_core", "wgpu_hal::vulkan"] {
|
||||
logger.log(
|
||||
&log::Record::builder()
|
||||
.args(format_args!("diagnostic"))
|
||||
.level(log::Level::Info)
|
||||
.target(target)
|
||||
.build(),
|
||||
);
|
||||
let Out::Write {
|
||||
stderr,
|
||||
record: (_, _, message),
|
||||
..
|
||||
} = receive.try_recv().unwrap()
|
||||
else {
|
||||
panic!("a diagnostic must write a record")
|
||||
};
|
||||
if ours(target) {
|
||||
assert_eq!(stderr.as_deref(), Some("diagnostic"));
|
||||
assert_eq!(message, "diagnostic");
|
||||
} else {
|
||||
assert!(stderr.is_none());
|
||||
assert_eq!(message, format!("[{target}] diagnostic"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_player_dir_keeps_the_shared_files_in_the_game_folder() {
|
||||
let (game, player) = (Path::new("/games/shared"), Path::new("/games/players/alex"));
|
||||
|
||||
@@ -347,6 +347,10 @@ pub struct SectionMesh {
|
||||
/// model order before.
|
||||
pub sides: Sides,
|
||||
pub translucent: Vec<TerrainVertex>,
|
||||
/// Whether `packed` holds far tile records (`lod::packed`) rather than
|
||||
/// section records (`mesher::packed`): the two are read apart, by the base
|
||||
/// vertex their draws use.
|
||||
pub packed_far: bool,
|
||||
/// `solid` in the compact layout (`packed`), when the renderer reads it and it is
|
||||
/// smaller; empty otherwise. Made by `sort_solid`.
|
||||
pub packed: Vec<[u32; 6]>,
|
||||
@@ -379,6 +383,7 @@ impl Default for SectionMesh {
|
||||
sides: Sides::default(),
|
||||
translucent: Vec::new(),
|
||||
packed: Vec::new(),
|
||||
packed_far: false,
|
||||
solid_tints: Vec::new(),
|
||||
visibility: ALL_VISIBLE,
|
||||
block_entities: Vec::new(),
|
||||
@@ -413,6 +418,7 @@ impl SectionMesh {
|
||||
self.solid.clear();
|
||||
self.translucent.clear();
|
||||
self.packed.clear();
|
||||
self.packed_far = false;
|
||||
self.solid_tints.clear();
|
||||
self.block_entities.clear();
|
||||
self.lights.clear();
|
||||
@@ -442,6 +448,7 @@ impl SectionMesh {
|
||||
self.sides = sides::sort_tinted(&mut self.solid, &mut self.solid_tints);
|
||||
self.bounds = self.compute_bounds();
|
||||
self.packed.clear();
|
||||
self.packed_far = false;
|
||||
if packed::packing() && quads > 0 && self.solid_tints.len() == quads {
|
||||
self.packed = packed::layout(&self.solid, &self.solid_tints);
|
||||
}
|
||||
@@ -652,7 +659,11 @@ struct Info {
|
||||
/// and a full collision shape. The only block leaf culling treats as solid.
|
||||
opaque_cube: bool,
|
||||
blocks_flow: bool,
|
||||
/// `instanceof IceBlock` (frosted ice too): fluids read it.
|
||||
ice: bool,
|
||||
/// Regular ice, the block the Opaque Ice option draws opaque; frosted ice
|
||||
/// keeps its see-through, cracking look.
|
||||
opaque_ice: bool,
|
||||
sturdy: u8,
|
||||
/// Occlusion face per direction: 0 empty, 1 the full-block singleton, 2 partial.
|
||||
face: [u8; 6],
|
||||
@@ -844,6 +855,7 @@ impl MeshTables {
|
||||
),
|
||||
blocks_flow: assets.blocks_fluid_flow.contains(&*s.name),
|
||||
ice: s.has_class("IceBlock"),
|
||||
opaque_ice: s.has_class("IceBlock") && !s.has_class("FrostedIceBlock"),
|
||||
sturdy: s.sturdy_full,
|
||||
face,
|
||||
cover,
|
||||
@@ -1251,6 +1263,8 @@ fn row_masks_match_cell_definition() {
|
||||
#[cfg(test)]
|
||||
mod leaf_culling_tests;
|
||||
#[cfg(test)]
|
||||
mod opaque_ice_tests;
|
||||
#[cfg(test)]
|
||||
mod reuse_tests;
|
||||
#[cfg(test)]
|
||||
mod tint_tests;
|
||||
@@ -1580,6 +1594,8 @@ pub struct Mesher<'a> {
|
||||
pub smooth_lighting: bool,
|
||||
/// The `cutoutLeaves` option (fancy graphics); off makes leaves opaque.
|
||||
pub cutout_leaves: bool,
|
||||
/// The `opaqueIce` option: regular ice draws opaque in the solid pass.
|
||||
pub opaque_ice: bool,
|
||||
/// Sprite layers a shader pack moves into the translucent pass, by layer.
|
||||
pub translucent_layers: &'a [bool],
|
||||
/// What the worker does with leaf faces inside a canopy.
|
||||
@@ -1705,6 +1721,9 @@ impl Mesher<'_> {
|
||||
let other = &info[neighbor as usize];
|
||||
// LeavesBlock.skipRendering with opaque leaves.
|
||||
let opaque_leaves = !self.cutout_leaves && state.leaves && other.leaves;
|
||||
// Opaque Ice: faces between two ice blocks are hidden, as they are
|
||||
// between two opaque blocks.
|
||||
let opaque_ice = self.opaque_ice && state.opaque_ice && other.opaque_ice;
|
||||
let culled_leaf = match self.leaf_culling {
|
||||
LeafCullingMode::Hollow | LeafCullingMode::Hidden => {
|
||||
state.leaves
|
||||
@@ -1722,6 +1741,7 @@ impl Mesher<'_> {
|
||||
_ => false,
|
||||
};
|
||||
visible[d as usize] = !opaque_leaves
|
||||
&& !opaque_ice
|
||||
&& !culled_leaf
|
||||
&& self
|
||||
.visibility
|
||||
@@ -1950,9 +1970,11 @@ impl Mesher<'_> {
|
||||
// The face's own UVs, already packed: no per-vertex rounding.
|
||||
std::array::from_fn(|i| (vertex(tint, i, face.uv_fixed[i]), q.uv[i]))
|
||||
};
|
||||
// Opaque leaves (forceOpaque) and leaves a leaf-culling mode proved internal draw in the
|
||||
// solid layer whatever their sprite.
|
||||
let solid_leaves = !self.cutout_leaves && state.leaves || interior_leaf;
|
||||
// Opaque leaves (forceOpaque), leaves a leaf-culling mode proved internal and Opaque
|
||||
// Ice draw in the solid layer whatever their sprite.
|
||||
let force_solid = !self.cutout_leaves && state.leaves
|
||||
|| interior_leaf
|
||||
|| self.opaque_ice && state.opaque_ice;
|
||||
let own = TileLayer::Layer {
|
||||
layer: face.layer as u16,
|
||||
translucent: face.translucent,
|
||||
@@ -1972,8 +1994,8 @@ impl Mesher<'_> {
|
||||
TileLayer::Skip | TileLayer::Default => own,
|
||||
};
|
||||
match pick {
|
||||
Pick::Keep => self.quad(out, solid_leaves, own, corners.map(|c| c.0), tint),
|
||||
Pick::Tile(t) => self.quad(out, solid_leaves, tile(t), corners.map(|c| c.0), tint),
|
||||
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::Quarters(tiles) => {
|
||||
let cuts = cuts(&corners);
|
||||
for s in cuts.windows(2) {
|
||||
@@ -1982,7 +2004,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, solid_leaves, tile(tiles[quarter]), piece, tint);
|
||||
self.quad(out, force_solid, tile(tiles[quarter]), piece, tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2031,17 +2053,17 @@ impl Mesher<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// One terrain quad with sprite `tile`, in the pass its alpha needs (`solid_leaves`: no
|
||||
/// One terrain quad with sprite `tile`, in the pass its alpha needs (`force_solid`: no
|
||||
/// alpha cutout whatever the sprite).
|
||||
fn quad(
|
||||
&self,
|
||||
out: &mut SectionMesh,
|
||||
solid_leaves: bool,
|
||||
force_solid: bool,
|
||||
tile: TileLayer,
|
||||
v: [TerrainVertex; 4],
|
||||
tint: Option<u32>,
|
||||
) {
|
||||
self.quad_as(out, solid_leaves, tile, v, true, tint);
|
||||
self.quad_as(out, force_solid, tile, v, true, tint);
|
||||
}
|
||||
/// Whether [`Mesher::quad`] draws `tile` in the translucent pass.
|
||||
fn translucent(&self, tile: TileLayer) -> bool {
|
||||
@@ -2065,7 +2087,7 @@ impl Mesher<'_> {
|
||||
fn quad_as(
|
||||
&self,
|
||||
out: &mut SectionMesh,
|
||||
solid_leaves: bool,
|
||||
force_solid: bool,
|
||||
tile: TileLayer,
|
||||
mut v: [TerrainVertex; 4],
|
||||
by_sprite: bool,
|
||||
@@ -2082,14 +2104,16 @@ impl Mesher<'_> {
|
||||
if self.tables.is_animated(layer) {
|
||||
out.note_sprite_layer(layer);
|
||||
}
|
||||
let translucent = if by_sprite {
|
||||
let translucent = if force_solid {
|
||||
false
|
||||
} else if by_sprite {
|
||||
self.translucent(tile)
|
||||
} else {
|
||||
translucent
|
||||
};
|
||||
// Quads over opaque texels draw in the solid layer: no alpha cutout (a dirt path's side
|
||||
// skips its clear top row). All four vertices share the layer.
|
||||
let layer = if opaque && !translucent || solid_leaves {
|
||||
let layer = if opaque && !translucent || force_solid {
|
||||
layer | OPAQUE_LAYER
|
||||
} else {
|
||||
layer
|
||||
|
||||
@@ -343,6 +343,7 @@ mod tests {
|
||||
opaque_cube: opaque,
|
||||
blocks_flow: false,
|
||||
ice: false,
|
||||
opaque_ice: false,
|
||||
sturdy: 0,
|
||||
face: [0; 6],
|
||||
cover: [0.0; 6],
|
||||
|
||||
@@ -83,6 +83,7 @@ fn mesh(
|
||||
blend_radius: 0,
|
||||
smooth_lighting: false,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: mode,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Opaque Ice through the real mesher: regular ice moves from the translucent
|
||||
//! pass to the solid pass without its texture alpha, keeps its faces toward
|
||||
//! air, loses the faces between two ice blocks, and leaves other translucent
|
||||
//! blocks alone. Needs prepared resources, like the other mesher geometry tests.
|
||||
use super::*;
|
||||
use crate::{
|
||||
assets::{Assets, RenderAssets},
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
chunk::Chunk,
|
||||
models,
|
||||
};
|
||||
|
||||
struct Fixture {
|
||||
assets: Assets,
|
||||
biomes: Biomes,
|
||||
render: RenderAssets,
|
||||
visibility: horizon_world::block_render::FaceVisibility,
|
||||
tables: MeshTables,
|
||||
}
|
||||
|
||||
fn fixture() -> Fixture {
|
||||
let root = std::env::var_os("HORIZON_MESH_TEST_RESOURCES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client/26.3")
|
||||
});
|
||||
let assets = Assets::load(&root).unwrap();
|
||||
let facts = BlockFacts::load(&root, assets.registry).unwrap();
|
||||
let biomes = Biomes::load(&root).unwrap();
|
||||
let render = assets.reload(&root).unwrap();
|
||||
let visibility = models::load_visibility(&root, assets.registry).unwrap();
|
||||
let tables = MeshTables::new(&assets, &render, &facts, visibility.clone()).unwrap();
|
||||
Fixture {
|
||||
assets,
|
||||
biomes,
|
||||
render,
|
||||
visibility,
|
||||
tables,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(f: &Fixture, name: &str) -> u32 {
|
||||
f.assets.registry.default_state(name).unwrap().0
|
||||
}
|
||||
|
||||
fn mesh(f: &Fixture, chunk: &Chunk, opaque_ice: bool) -> SectionMesh {
|
||||
let region = [[Some(chunk); 3]; 3];
|
||||
let mut mesher = Mesher {
|
||||
nether_shading: false,
|
||||
shader_pack_lighting: false,
|
||||
separate_ao: false,
|
||||
shader_pack: false,
|
||||
assets: &f.assets,
|
||||
render: &f.render,
|
||||
tables: &f.tables,
|
||||
biomes: &f.biomes,
|
||||
visibility: f.visibility.clone(),
|
||||
zoom: BiomeZoom::new(0),
|
||||
blend_radius: 0,
|
||||
smooth_lighting: false,
|
||||
cutout_leaves: true,
|
||||
opaque_ice,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
mesher.mesh(®ion, 0)
|
||||
}
|
||||
|
||||
const BASE: i32 = MIN_Y + 4;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn off_keeps_regular_ice_in_the_translucent_pass() {
|
||||
let f = fixture();
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
chunk.set(8, BASE, 8, state(&f, "minecraft:ice"));
|
||||
let mesh = mesh(&f, &chunk, false);
|
||||
assert!(mesh.solid.is_empty());
|
||||
assert_eq!(mesh.translucent.len(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn on_draws_regular_ice_opaque_in_the_solid_pass() {
|
||||
let f = fixture();
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
chunk.set(8, BASE, 8, state(&f, "minecraft:ice"));
|
||||
let mesh = mesh(&f, &chunk, true);
|
||||
assert!(mesh.translucent.is_empty());
|
||||
let quads = mesh.solid.as_chunks::<4>().0;
|
||||
assert_eq!(quads.len(), 6);
|
||||
for quad in quads {
|
||||
assert!(
|
||||
quad.iter().all(|v| v.layer & OPAQUE_LAYER != 0),
|
||||
"an opaque ice quad kept its texture alpha"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn on_culls_the_face_between_two_ice_blocks() {
|
||||
let f = fixture();
|
||||
let ice = state(&f, "minecraft:ice");
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
chunk.set(7, BASE, 8, ice);
|
||||
chunk.set(8, BASE, 8, ice);
|
||||
let mesh = mesh(&f, &chunk, true);
|
||||
assert!(mesh.translucent.is_empty());
|
||||
// Two cubes, six faces each, minus the face they share.
|
||||
assert_eq!(mesh.solid.len(), 10 * 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn on_leaves_other_translucent_blocks_alone() {
|
||||
let f = fixture();
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
chunk.set(4, BASE, 8, state(&f, "minecraft:ice"));
|
||||
chunk.set(12, BASE, 8, state(&f, "minecraft:slime_block"));
|
||||
let mesh = mesh(&f, &chunk, true);
|
||||
assert_eq!(mesh.solid.len(), 24, "only the ice cube is solid");
|
||||
assert!(
|
||||
mesh.translucent.len() >= 24,
|
||||
"the slime cube is still translucent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn on_leaves_frosted_ice_translucent() {
|
||||
let f = fixture();
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
chunk.set(8, BASE, 8, state(&f, "minecraft:frosted_ice"));
|
||||
let mesh = mesh(&f, &chunk, true);
|
||||
assert!(mesh.solid.is_empty());
|
||||
assert_eq!(mesh.translucent.len(), 24);
|
||||
}
|
||||
@@ -208,6 +208,18 @@ pub fn layout(solid: &[TerrainVertex], tints: &[u32]) -> Vec<[u32; 6]> {
|
||||
.zip(tints)
|
||||
.map(|(q, &t)| pack_quad(q, t))
|
||||
.collect();
|
||||
place(&records, quads)
|
||||
}
|
||||
|
||||
/// A mesh's solid range from a record per quad, `None` where no record fits:
|
||||
/// the records in mesh order, padded to a multiple of 4 slots, then the four
|
||||
/// vertices of each quad without one, whose record holds the distance to them
|
||||
/// (`FULL`). Empty where that would not be smaller than the vertices, which is
|
||||
/// what makes the layout never a loss.
|
||||
///
|
||||
/// Shared with `lod::packed`, whose records hold a far tile's quads instead but
|
||||
/// spill and pad the same way.
|
||||
pub fn place(records: &[Option<[u32; 6]>], quads: &[[TerrainVertex; 4]]) -> Vec<[u32; 6]> {
|
||||
let n = quads.len();
|
||||
let packed = records.iter().filter(|r| r.is_some()).count();
|
||||
let full = n - packed;
|
||||
@@ -229,7 +241,7 @@ pub fn layout(solid: &[TerrainVertex], tints: &[u32]) -> Vec<[u32; 6]> {
|
||||
}));
|
||||
}
|
||||
out.resize(n + pad, [0; 6]);
|
||||
for (q, record) in quads.iter().zip(&records) {
|
||||
for (q, record) in quads.iter().zip(records) {
|
||||
if record.is_none() {
|
||||
out.extend(
|
||||
q.iter()
|
||||
|
||||
@@ -181,6 +181,7 @@ fn a_shader_pack_lights_the_faces_itself() {
|
||||
blend_radius: 0,
|
||||
smooth_lighting: false,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
@@ -237,6 +238,7 @@ fn a_shader_pack_lights_every_nether_face_like_an_up_face() {
|
||||
blend_radius: 0,
|
||||
smooth_lighting: false,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
@@ -289,6 +291,7 @@ fn separate_ao_passes_ao_in_alpha_without_darkening_rgb() {
|
||||
blend_radius: 0,
|
||||
smooth_lighting: true,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
@@ -364,6 +367,7 @@ fn reused_mesh_matches_fresh_geometry_across_sections_and_resource_reload() {
|
||||
blend_radius: 2,
|
||||
smooth_lighting: reload == 0,
|
||||
cutout_leaves: reload == 0,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
@@ -513,6 +517,7 @@ fn a_shader_pack_s_still_water_top_starts_at_its_south_west_corner() {
|
||||
blend_radius: 0,
|
||||
smooth_lighting: true,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
|
||||
@@ -92,6 +92,7 @@ fn info(solid: bool) -> Info {
|
||||
opaque_cube: solid,
|
||||
blocks_flow: false,
|
||||
ice: false,
|
||||
opaque_ice: false,
|
||||
sturdy: 0,
|
||||
face: [0; 6],
|
||||
cover: [0.0; 6],
|
||||
|
||||
@@ -21,7 +21,11 @@ fn oit_cloud_color(in: Out, front: bool, fade: bool) -> vec4<f32> {
|
||||
return oit_add_transmittance(1.0 - color.a, in.clip.z, in.clip.xy);
|
||||
}
|
||||
@fragment fn fs_oit_cloud_accumulate(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let color = oit_cloud_color(in, front, true);
|
||||
var color = oit_cloud_color(in, front, true);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
color = vec4(hdr_written(color.rgb, hdr_scene_alpha(globals.hdr_light.y),
|
||||
globals.hdr_settings, globals.hdr_shares), color.a);
|
||||
}
|
||||
let seen = oit_sample_transmittance(vec2<i32>(in.clip.xy),
|
||||
oit_normalize_depth(in.clip.z, in.clip.xy), 1.0 - color.a);
|
||||
return vec4(color.rgb * color.a, color.a) * seen;
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
return oit_transmittance_fragment(entity_color(in, front).a, in.clip);
|
||||
}
|
||||
@fragment fn fs_oit_entity_accumulate(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
return oit_accumulate_fragment(entity_written_color(in, front), in.clip);
|
||||
}
|
||||
// Text keeps its source gain, as fs_entity_unmarked does in the forward pass.
|
||||
@fragment fn fs_oit_text_accumulate(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
return oit_accumulate_fragment(entity_color(in, front), in.clip);
|
||||
}
|
||||
// `OIT_ENTITY_SHADOW`.
|
||||
@@ -56,6 +60,9 @@
|
||||
return OitCoefficients(vec4(0.0), vec4(0.0));
|
||||
}
|
||||
@fragment fn fs_oit_lightning_accumulate(in: Out) -> @location(0) vec4<f32> {
|
||||
let color = celestial_color(in);
|
||||
var color = celestial_color(in);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
color = vec4(hdr_written(color.rgb, hdr_entity_mark(in), globals.hdr_settings, globals.hdr_shares), color.a);
|
||||
}
|
||||
return oit_accumulate_additive(vec4(color.rgb, min(0.99, color.a)), 0.0, in.clip);
|
||||
}
|
||||
@@ -9,19 +9,25 @@ fn oit_terrain_color(in: TerrainOut) -> vec4<f32> {
|
||||
}
|
||||
// Stage 1: (-nearest, furthest, nearest device depth, nearest opaque device
|
||||
// depth), combined by max blending.
|
||||
@fragment fn fs_oit_bounds(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
@fragment fn fs_oit_bounds(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
let color = oit_terrain_color(in);
|
||||
let linear = oit_linear_depth(in.clip.z);
|
||||
let opaque = select(0.0, in.clip.z, color.a > OIT_FULLY_OPAQUE_ALPHA);
|
||||
return vec4(-linear, linear, in.clip.z, opaque);
|
||||
}
|
||||
// Stage 2.
|
||||
@fragment fn fs_oit_transmittance(in: TerrainOut) -> OitCoefficients {
|
||||
@fragment fn fs_oit_transmittance(wire: TerrainWire) -> OitCoefficients {
|
||||
let in = from_wire(wire);
|
||||
let color = oit_terrain_color(in);
|
||||
return oit_add_transmittance(1.0 - color.a, in.clip.z, in.clip.xy);
|
||||
}
|
||||
// Stage 3: premultiplied colour, weighted by the transmittance in front.
|
||||
@fragment fn fs_oit_accumulate(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
@fragment fn fs_oit_accumulate(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
hdr_normal = normalize(cross(dpdy(in.local), dpdx(in.local)));
|
||||
}
|
||||
let color = oit_terrain_color(in);
|
||||
let seen = oit_sample_transmittance(vec2<i32>(in.clip.xy),
|
||||
oit_normalize_depth(in.clip.z, in.clip.xy), 1.0 - color.a);
|
||||
@@ -30,6 +36,13 @@ fn oit_terrain_color(in: TerrainOut) -> vec4<f32> {
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
// The fog colour is premultiplied by the same weight.
|
||||
let rgb = mix(weighted.rgb, globals.fog_color.rgb * weighted.a, f * globals.fog_color.a);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
let mark = hdr_scene_alpha(mix(hdr_scene(in.scene.x, in.scene.y, hdr_facing()),
|
||||
globals.hdr_light.y, f * globals.fog_color.a));
|
||||
// Convert the surface before weighting; its mark never belongs to the light behind it.
|
||||
let fogged = mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a);
|
||||
return vec4(hdr_written(fogged, mark, globals.hdr_settings, globals.hdr_shares) * weighted.a, weighted.a);
|
||||
}
|
||||
return vec4(rgb, weighted.a);
|
||||
}
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ impl Opt {
|
||||
Opt::PadRepeatInterval => "horizon.options.repeat_interval",
|
||||
Opt::PadInvertX => "horizon.options.invert_x",
|
||||
Opt::PadInvertY => "horizon.options.invert_y",
|
||||
Opt::PadCraftOnServers => "horizon.options.craft_on_servers",
|
||||
Opt::PadToggleSneak => "horizon.options.sneak_button",
|
||||
Opt::PadMoveDeadzone => "horizon.options.move_deadzone",
|
||||
Opt::PadLookDeadzone => "horizon.options.look_deadzone",
|
||||
@@ -849,6 +850,7 @@ impl Opt {
|
||||
}),
|
||||
Opt::PadInvertX => on_off(s.controller.invert_x),
|
||||
Opt::PadInvertY => on_off(s.controller.invert_y),
|
||||
Opt::PadCraftOnServers => on_off(s.controller.craft_on_servers),
|
||||
Opt::PadToggleSneak => tr(if s.controller.toggle_sneak {
|
||||
"options.key.toggle"
|
||||
} else {
|
||||
@@ -1156,6 +1158,9 @@ impl Opt {
|
||||
}
|
||||
Opt::PadInvertX => s.controller.invert_x = !s.controller.invert_x,
|
||||
Opt::PadInvertY => s.controller.invert_y = !s.controller.invert_y,
|
||||
Opt::PadCraftOnServers => {
|
||||
s.controller.craft_on_servers = !s.controller.craft_on_servers
|
||||
}
|
||||
Opt::PadToggleSneak => s.controller.toggle_sneak = !s.controller.toggle_sneak,
|
||||
Opt::PadNintendoButtons => {
|
||||
s.controller.nintendo_letters = !s.controller.nintendo_letters
|
||||
@@ -1654,7 +1659,7 @@ pub struct OptionsUi {
|
||||
pub minecart_improvements: bool,
|
||||
/// Scroll the selected language into the middle on the next layout.
|
||||
center_language: bool,
|
||||
pub graphics: crate::graphics_ui::GraphicsUi,
|
||||
graphics: shell::Shell<crate::graphics_ui::GraphicsScreen>,
|
||||
pub controller: crate::controller_ui::ControllerUi,
|
||||
/// Horizon Extras, on the settings shell.
|
||||
extras: shell::Shell<crate::extras_ui::ExtrasScreen>,
|
||||
@@ -2095,9 +2100,6 @@ impl OptionsUi {
|
||||
.pack_screen
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.search_focused()),
|
||||
Page::Graphics | Page::GraphicsAdvanced | Page::GraphicsHdr => {
|
||||
self.graphics.typing()
|
||||
}
|
||||
Page::Controller | Page::PadBinds => self.controller.typing(),
|
||||
_ if Self::shader_page(self.page()).is_some() => self.shader.typing(),
|
||||
_ => match self.shell() {
|
||||
@@ -2210,9 +2212,6 @@ impl OptionsUi {
|
||||
if page == Page::Packs {
|
||||
self.pack_screen = None;
|
||||
}
|
||||
if let Some(at) = Self::graphics_page(page) {
|
||||
self.graphics.open(at);
|
||||
}
|
||||
if let Some(at) = Self::controller_page(page) {
|
||||
self.controller.open(at);
|
||||
}
|
||||
@@ -2222,6 +2221,9 @@ impl OptionsUi {
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
shell.open();
|
||||
}
|
||||
if let Some(at) = Self::graphics_page(page) {
|
||||
self.graphics.show(at);
|
||||
}
|
||||
if page == Page::SplitSetup {
|
||||
self.split_setup.reset();
|
||||
}
|
||||
@@ -2238,12 +2240,14 @@ impl OptionsUi {
|
||||
fn shell(&self) -> Option<&dyn shell::Hosted> {
|
||||
match self.page() {
|
||||
Page::Extras => Some(&self.extras),
|
||||
page if Self::graphics_page(page).is_some() => Some(&self.graphics),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn shell_mut(&mut self) -> Option<&mut dyn shell::Hosted> {
|
||||
match self.page() {
|
||||
Page::Extras => Some(&mut self.extras),
|
||||
page if Self::graphics_page(page).is_some() => Some(&mut self.graphics),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -2252,6 +2256,11 @@ impl OptionsUi {
|
||||
pub fn reveal(&mut self, s: &Settings, size: (f32, f32), rect: [f32; 4]) -> Option<f32> {
|
||||
self.shell_mut()?.reveal(s, size, rect)
|
||||
}
|
||||
/// The window's size, for the graphics screen's vanilla rows that step through its
|
||||
/// resolutions: before each input that can press one.
|
||||
fn graphics_framebuffer(&mut self) {
|
||||
self.graphics.screen.framebuffer = self.framebuffer;
|
||||
}
|
||||
/// What a shell screen's outputs do to these options.
|
||||
fn shell_events(&mut self, s: &mut Settings, outputs: Vec<shell::Output>) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
@@ -2259,43 +2268,17 @@ impl OptionsUi {
|
||||
match output {
|
||||
shell::Output::Event(event) => events.push(event),
|
||||
shell::Output::BindKey(name) => self.selected_key = Some(name),
|
||||
shell::Output::Push(page) => self.push(page),
|
||||
shell::Output::Push(page) => {
|
||||
if page == Page::HdrCalibration {
|
||||
self.calibration = Some(crate::hdr_calibration::Calibration::new(s));
|
||||
}
|
||||
self.push(page);
|
||||
}
|
||||
shell::Output::Done => events.extend(self.back(s)),
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
/// What the graphics screen's actions do to these options.
|
||||
fn graphics_events(
|
||||
&mut self,
|
||||
s: &mut Settings,
|
||||
actions: Vec<crate::graphics_ui::Action>,
|
||||
) -> Vec<Event> {
|
||||
use crate::graphics_ui::Action;
|
||||
let mut events = Vec::new();
|
||||
for action in actions {
|
||||
match action {
|
||||
Action::ButtonClick => events.push(Event::ButtonClick),
|
||||
Action::Changed => events.push(Event::Changed),
|
||||
Action::SliderDrag => events.push(Event::SliderDrag),
|
||||
Action::BindKey(name) => self.selected_key = Some(name),
|
||||
Action::Calibrate => {
|
||||
self.calibration = Some(crate::hdr_calibration::Calibration::new(s));
|
||||
self.push(Page::HdrCalibration);
|
||||
}
|
||||
// The Shaders page's Shader Packs...: the list of either kind, as the page
|
||||
// opens it from anywhere.
|
||||
Action::ShaderPacks => {
|
||||
events.push(Event::ListShaderPacks);
|
||||
s.shader_options = None;
|
||||
events.push(Event::OpenShaderOptions);
|
||||
self.push(Page::ShaderPacks);
|
||||
}
|
||||
Action::Done => events.extend(self.back(s)),
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
/// What the controller screen's actions do to these options.
|
||||
fn controller_events(
|
||||
&mut self,
|
||||
@@ -3704,11 +3687,6 @@ impl OptionsUi {
|
||||
c.draw(batch, atlas, cursor);
|
||||
return;
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
self.graphics.binding = self.selected_key;
|
||||
self.graphics.draw(s, batch, atlas, cursor, self.in_world);
|
||||
return;
|
||||
}
|
||||
if Self::controller_page(self.page()).is_some() {
|
||||
self.controller.binding = self.selected_pad_action;
|
||||
self.controller.draw(s, batch, atlas, cursor, self.in_world);
|
||||
@@ -3813,22 +3791,16 @@ impl OptionsUi {
|
||||
}
|
||||
match &wd.kind {
|
||||
WKind::Button { label, active, act } => {
|
||||
if matches!(act, Act::Done) {
|
||||
ui::action_button(
|
||||
batch,
|
||||
atlas,
|
||||
crate::widgets::Action::Done,
|
||||
wd.x,
|
||||
wd.y,
|
||||
wd.w,
|
||||
wd.h,
|
||||
label,
|
||||
*active,
|
||||
hover,
|
||||
);
|
||||
} else {
|
||||
ui::button(batch, atlas, wd.x, wd.y, wd.w, wd.h, label, *active, hover);
|
||||
}
|
||||
ui::focused_button(
|
||||
batch,
|
||||
atlas,
|
||||
[wd.x, wd.y, wd.w, wd.h],
|
||||
label,
|
||||
*active,
|
||||
pointer_over,
|
||||
is_focused,
|
||||
matches!(act, Act::Done).then_some(crate::widgets::Action::Done),
|
||||
);
|
||||
crate::widgets::last_narratable(|n| {
|
||||
narrate_widget(n, s, wd, hover, list_key, framebuffer);
|
||||
});
|
||||
@@ -4077,21 +4049,6 @@ impl OptionsUi {
|
||||
let outcome = c.click((x, y), size, drag);
|
||||
return self.calibration_outcome(s, outcome);
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
// Waiting on a key: a mouse button binds, as on Key Binds.
|
||||
if self.selected_key.is_some() && !drag {
|
||||
let button = if right {
|
||||
winit::event::MouseButton::Right
|
||||
} else {
|
||||
winit::event::MouseButton::Left
|
||||
};
|
||||
return self.bind_mouse(s, button);
|
||||
}
|
||||
let actions =
|
||||
self.graphics
|
||||
.click(s, (x, y), size, framebuffer, drag, right, self.shift);
|
||||
return self.graphics_events(s, actions);
|
||||
}
|
||||
if self.shell().is_some() {
|
||||
// Waiting on a key: a mouse button binds, as on Key Binds.
|
||||
if self.selected_key.is_some() && !drag {
|
||||
@@ -4103,6 +4060,8 @@ impl OptionsUi {
|
||||
return self.bind_mouse(s, button);
|
||||
}
|
||||
let shift = self.shift;
|
||||
self.framebuffer = framebuffer;
|
||||
self.graphics_framebuffer();
|
||||
let outputs = self.shell_mut().map_or_else(Vec::new, |shell| {
|
||||
shell.click(s, (x, y), size, drag, right, shift)
|
||||
});
|
||||
@@ -4244,6 +4203,11 @@ impl OptionsUi {
|
||||
if wd.list && (y < top || y >= bottom) {
|
||||
continue;
|
||||
}
|
||||
// AbstractWidget.mouseClicked rejects inactive buttons before the
|
||||
// container can focus them.
|
||||
if matches!(wd.kind, WKind::Button { active: false, .. }) {
|
||||
return events;
|
||||
}
|
||||
// A click focuses the widget for its highlight only: vanilla's wheel
|
||||
// steps a keyboard-focused widget from anywhere, not a clicked one.
|
||||
self.focus = Some(focus_id(wd_idx, &wd));
|
||||
@@ -4385,8 +4349,7 @@ impl OptionsUi {
|
||||
let slider = self.dragging.take().is_some();
|
||||
let (shader_slider, shader_pack) = self.shader.release();
|
||||
let shell_slider = self.shell_mut().is_some_and(|shell| shell.release());
|
||||
let graphics_slider =
|
||||
self.graphics.release() | self.controller.release() | shader_slider | shell_slider;
|
||||
let graphics_slider = self.controller.release() | shader_slider | shell_slider;
|
||||
let calibration_slider = self.calibration.as_mut().is_some_and(|c| c.release());
|
||||
let split_drop = self.split_setup.release();
|
||||
if slider || graphics_slider || calibration_slider {
|
||||
@@ -4453,7 +4416,6 @@ impl OptionsUi {
|
||||
/// wheel wherever the cursor is. Focus left by a click does not count
|
||||
/// (`focus_from_keyboard`): honouring it toggled the clicked option on every
|
||||
/// later wheel event, from anywhere on the screen, while the list never scrolled.
|
||||
/// The graphics screen has its own wheel (`GraphicsUi::wheel`).
|
||||
pub fn wheel(
|
||||
&mut self,
|
||||
s: &mut Settings,
|
||||
@@ -4467,14 +4429,11 @@ impl OptionsUi {
|
||||
if self.resource_error.is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.graphics_framebuffer();
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
let outputs = shell.wheel(s, size, cursor, amount);
|
||||
return self.shell_events(s, outputs);
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
let actions = self.graphics.wheel(s, size, cursor, framebuffer, amount);
|
||||
return self.graphics_events(s, actions);
|
||||
}
|
||||
if self.page() == Page::Studio {
|
||||
self.studio.wheel(amount);
|
||||
return Vec::new();
|
||||
@@ -4566,10 +4525,6 @@ impl OptionsUi {
|
||||
shell.scroll(s, size, amount);
|
||||
return;
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
self.graphics.scroll(s, size, amount);
|
||||
return;
|
||||
}
|
||||
if self.page() == Page::Studio {
|
||||
self.studio.wheel(amount);
|
||||
return;
|
||||
@@ -4766,8 +4721,6 @@ impl OptionsUi {
|
||||
if shell.typing() {
|
||||
shell.type_text(text);
|
||||
}
|
||||
} else if Self::graphics_page(self.page()).is_some() && self.graphics.typing() {
|
||||
self.graphics.type_text(text);
|
||||
} else if Self::controller_page(self.page()).is_some() && self.controller.typing() {
|
||||
self.controller.type_text(text);
|
||||
} else if Self::shader_page(self.page()).is_some() && self.shader.typing() {
|
||||
@@ -4806,9 +4759,6 @@ impl OptionsUi {
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
return shell.shortcut(shortcut, clipboard);
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
return self.graphics.shortcut(shortcut, clipboard);
|
||||
}
|
||||
if Self::controller_page(self.page()).is_some() {
|
||||
return self.controller.shortcut(shortcut, clipboard);
|
||||
}
|
||||
@@ -4925,26 +4875,13 @@ impl OptionsUi {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
let size = (self.size.0.max(480.0), self.size.1.max(240.0));
|
||||
let fb = [self.framebuffer[0].max(480), self.framebuffer[1].max(240)];
|
||||
let (actions, handled) = self.graphics.key_modifiers(s, key, text, shift, size, fb);
|
||||
let mut events = self.graphics_events(s, actions);
|
||||
if key == K::Escape {
|
||||
events.extend(self.back(s));
|
||||
return events;
|
||||
}
|
||||
if handled {
|
||||
return events;
|
||||
}
|
||||
return events;
|
||||
}
|
||||
// The size it was drawn at; before the first draw, one its keys can lay out at.
|
||||
let gui = if self.size.0 > 0.0 && self.size.1 > 0.0 {
|
||||
self.size
|
||||
} else {
|
||||
MIN_KEY_GUI
|
||||
};
|
||||
self.graphics_framebuffer();
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
let outputs = shell.key(s, key, text, shift, gui);
|
||||
return self.shell_events(s, outputs);
|
||||
@@ -6119,6 +6056,104 @@ mod tests {
|
||||
assert_eq!(s.file.fullscreen_resolution, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_cursor_leaves_a_clicked_option_while_its_focus_stays() {
|
||||
use crate::widgets::{Cursor, take_cursor};
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Video);
|
||||
let size = (480.0, 480.0);
|
||||
let (widgets, top, bottom, _) = ui.layout(&s, size.0, size.1);
|
||||
let button = widgets
|
||||
.iter()
|
||||
.find(|wd| {
|
||||
matches!(
|
||||
wd.kind,
|
||||
WKind::Button {
|
||||
active: true,
|
||||
act: Act::Cycle(_),
|
||||
..
|
||||
}
|
||||
) && (!wd.list || wd.y >= top && wd.y + wd.h <= bottom)
|
||||
})
|
||||
.expect("visible cycle button");
|
||||
let over = (button.x + 1.0, button.y + 1.0);
|
||||
ui.click(
|
||||
&mut s,
|
||||
&atlas,
|
||||
over.0,
|
||||
over.1,
|
||||
size,
|
||||
[480, 480],
|
||||
false,
|
||||
false,
|
||||
);
|
||||
ui.release();
|
||||
assert!(
|
||||
ui.focus.is_some(),
|
||||
"the accepted click keeps vanilla's focus"
|
||||
);
|
||||
assert!(!ui.focus_from_keyboard);
|
||||
let _ = take_cursor();
|
||||
ui.draw(&s, &mut HudBatch::new([480, 480]), &atlas, over);
|
||||
assert_eq!(take_cursor(), Cursor::PointingHand);
|
||||
ui.draw(&s, &mut HudBatch::new([480, 480]), &atlas, (-10.0, -10.0));
|
||||
assert_eq!(take_cursor(), Cursor::Arrow, "focus is not mouse hover");
|
||||
assert!(
|
||||
ui.focus.is_some(),
|
||||
"moving the pointer does not clear focus"
|
||||
);
|
||||
ui.focus_from_keyboard = true;
|
||||
ui.draw(&s, &mut HudBatch::new([480, 480]), &atlas, (-10.0, -10.0));
|
||||
assert_eq!(
|
||||
take_cursor(),
|
||||
Cursor::Arrow,
|
||||
"keyboard focus is not mouse hover either"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_cursor_leaves_a_disabled_option_without_focusing_it() {
|
||||
use crate::widgets::{Cursor, take_cursor};
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::KeyBinds);
|
||||
let size = (480.0, 480.0);
|
||||
let (widgets, top, bottom, _) = ui.layout(&s, size.0, size.1);
|
||||
let button = widgets
|
||||
.iter()
|
||||
.find(|wd| {
|
||||
matches!(wd.kind, WKind::Button { active: false, .. })
|
||||
&& (!wd.list || wd.y >= top && wd.y + wd.h <= bottom)
|
||||
})
|
||||
.expect("visible disabled button");
|
||||
let over = (button.x + 1.0, button.y + 1.0);
|
||||
let _ = take_cursor();
|
||||
ui.draw(&s, &mut HudBatch::new([480, 480]), &atlas, over);
|
||||
assert_eq!(take_cursor(), Cursor::NotAllowed);
|
||||
assert!(
|
||||
ui.click(
|
||||
&mut s,
|
||||
&atlas,
|
||||
over.0,
|
||||
over.1,
|
||||
size,
|
||||
[480, 480],
|
||||
false,
|
||||
false
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
ui.focus.is_none(),
|
||||
"an inactive widget rejects the click before focus"
|
||||
);
|
||||
ui.draw(&s, &mut HudBatch::new([480, 480]), &atlas, (-10.0, -10.0));
|
||||
assert_eq!(take_cursor(), Cursor::Arrow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clicked_extras_tooltip_leaves_with_the_pointer() {
|
||||
let atlas = atlas();
|
||||
@@ -7634,7 +7669,7 @@ mod tests {
|
||||
/// Packs... opens the pack list, listed afresh, and Done comes back to that page.
|
||||
#[test]
|
||||
fn the_graphics_shaders_page_opens_the_shader_pack_list() {
|
||||
use crate::graphics_ui::{H, Item, Target};
|
||||
use crate::graphics_ui::{H, Item};
|
||||
use winit::keyboard::KeyCode;
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
@@ -7643,15 +7678,15 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
ui.show(Page::Graphics);
|
||||
ui.graphics.open(crate::graphics_ui::Page::Shaders);
|
||||
let packs = Target::Item(Item::Horizon(H::ShaderPacks));
|
||||
ui.graphics.show(crate::graphics_ui::Page::Shaders);
|
||||
let packs = Item::Horizon(H::ShaderPacks);
|
||||
assert!(
|
||||
(0..40).any(|_| {
|
||||
ui.key(&mut s, &atlas, KeyCode::Tab, None);
|
||||
ui.graphics.focus == Some(packs)
|
||||
ui.graphics.focused_row() == Some(packs)
|
||||
}),
|
||||
"{:?}",
|
||||
ui.graphics.focus
|
||||
ui.graphics.focused()
|
||||
);
|
||||
let events = ui.key(&mut s, &atlas, KeyCode::Enter, None);
|
||||
assert_eq!(ui.page(), Page::ShaderPacks);
|
||||
@@ -7660,7 +7695,10 @@ mod tests {
|
||||
assert!(events.contains(&Event::OpenShaderOptions), "{events:?}");
|
||||
ui.back(&mut s);
|
||||
assert_eq!(ui.page(), Page::Graphics);
|
||||
assert_eq!(ui.graphics.page(), crate::graphics_ui::Page::Shaders);
|
||||
assert_eq!(
|
||||
ui.graphics.open_page(),
|
||||
Some(crate::graphics_ui::Page::Shaders)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7883,14 +7921,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_graphics_screens_wheel_scrolls_even_over_a_slider_being_changed() {
|
||||
use crate::graphics_ui::{H, Item, Target};
|
||||
use crate::graphics_ui::{H, Item};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Graphics);
|
||||
ui.graphics.show(crate::graphics_ui::Page::Performance);
|
||||
let size = (640.0, 360.0);
|
||||
s.horizon.chunk_updates = 16;
|
||||
ui.graphics.focus = Some(Target::Item(Item::Horizon(H::ChunkUpdates)));
|
||||
ui.graphics.can_change_value = true;
|
||||
let chunk_updates = Item::Horizon(H::ChunkUpdates);
|
||||
// Tab onto it: the arrows change it.
|
||||
while ui.graphics.focused_row() != Some(chunk_updates) {
|
||||
shell::Hosted::key(&mut ui.graphics, &mut s, K::Tab, None, false, size);
|
||||
}
|
||||
assert!(ui.graphics.changes_value());
|
||||
let before = ui.graphics.scroll_amount();
|
||||
ui.wheel(&mut s, size, (320.0, 150.0), [1920, 1080], -1.0);
|
||||
assert_eq!(s.horizon.chunk_updates, 16);
|
||||
@@ -7920,29 +7964,21 @@ mod tests {
|
||||
// 2. Horizon Graphics page through OptionsUi:
|
||||
// Tab onto ChunkUpdates slider, ArrowRight adjusts value, Space toggles can_change_value
|
||||
ui.show(Page::Graphics);
|
||||
ui.graphics.open(crate::graphics_ui::Page::Performance);
|
||||
ui.graphics.show(crate::graphics_ui::Page::Performance);
|
||||
s.horizon.chunk_updates = 10;
|
||||
let chunk_updates = crate::graphics_ui::Item::Horizon(crate::graphics_ui::H::ChunkUpdates);
|
||||
ui.size = size;
|
||||
ui.framebuffer = [854, 480];
|
||||
|
||||
// Tab into controls
|
||||
for _ in 0..80 {
|
||||
ui.key(&mut s, &atlas, K::Tab, None);
|
||||
if ui.graphics.focus
|
||||
== Some(crate::graphics_ui::Target::Item(
|
||||
crate::graphics_ui::Item::Horizon(crate::graphics_ui::H::ChunkUpdates),
|
||||
))
|
||||
{
|
||||
if ui.graphics.focused_row() == Some(chunk_updates) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
ui.graphics.focus,
|
||||
Some(crate::graphics_ui::Target::Item(
|
||||
crate::graphics_ui::Item::Horizon(crate::graphics_ui::H::ChunkUpdates)
|
||||
))
|
||||
);
|
||||
assert!(ui.graphics.can_change_value);
|
||||
assert_eq!(ui.graphics.focused_row(), Some(chunk_updates));
|
||||
assert!(ui.graphics.changes_value());
|
||||
|
||||
// Right arrow steps ChunkUpdates to 11
|
||||
let events = ui.key(&mut s, &atlas, K::ArrowRight, None);
|
||||
@@ -7951,7 +7987,7 @@ mod tests {
|
||||
|
||||
// Space toggles can_change_value to false
|
||||
ui.key(&mut s, &atlas, K::Space, None);
|
||||
assert!(!ui.graphics.can_change_value);
|
||||
assert!(!ui.graphics.changes_value());
|
||||
|
||||
// ArrowRight does not change ChunkUpdates when can_change_value is false
|
||||
ui.key(&mut s, &atlas, K::ArrowRight, None);
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! The shared atlas's LabPBR maps (`_n`, `_s` beside an item's, entity's or block's texture),
|
||||
//! for shader packs: a map the size of the atlas would be mostly empty (only some textures have
|
||||
//! maps) and as large as the atlas itself, so each is a paged map instead. The atlas is cut into
|
||||
//! 16x16 pages; only pages holding a mapped texture get one in the map's pool, found through a
|
||||
//! page table at the map's start. Packs read the maps with the colour's coordinates, as for
|
||||
//! terrain: the translator turns their reads into `hz_pagedTexture` (horizon-shaders,
|
||||
//! `PAGED_READ`, which documents the layout this writes).
|
||||
|
||||
/// A page's side, in texels.
|
||||
pub const PAGE: u32 = 16;
|
||||
/// Texels before the page table: the atlas's size, and the map's value where it has none.
|
||||
const HEADER: u32 = 2;
|
||||
|
||||
/// One paged map, RGBA8.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PagedMap {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub pixels: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A texture's maps: its place in the atlas (x, y, width, height, in texels) and its normal and
|
||||
/// specular images at that size (RGBA8), each where it has one.
|
||||
pub struct SpriteMaps {
|
||||
pub rect: [u32; 4],
|
||||
pub maps: [Option<Vec<u8>>; 2],
|
||||
}
|
||||
|
||||
/// The normal and specular paged maps of an atlas of `size` texels, each `None` where no texture
|
||||
/// has that map; `max_side` is the GPU's largest texture side.
|
||||
pub fn build(size: (u32, u32), sprites: &[SpriteMaps], max_side: u32) -> [Option<PagedMap>; 2] {
|
||||
[
|
||||
(0, crate::sprites::FLAT_NORMAL),
|
||||
(1, crate::sprites::NO_SPECULAR),
|
||||
]
|
||||
.map(|(kind, default)| {
|
||||
let mapped = sprites
|
||||
.iter()
|
||||
.filter_map(|s| s.maps[kind].as_deref().map(|m| (s.rect, m)));
|
||||
build_one(size, mapped, default, max_side)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_one<'a>(
|
||||
(width, height): (u32, u32),
|
||||
sprites: impl Iterator<Item = ([u32; 4], &'a [u8])>,
|
||||
default: [u8; 4],
|
||||
max_side: u32,
|
||||
) -> Option<PagedMap> {
|
||||
let (pages_x, pages_y) = (width.div_ceil(PAGE), height.div_ceil(PAGE));
|
||||
// Each atlas page's place in the pool, plus one (0: none).
|
||||
let mut table = vec![0u32; (pages_x * pages_y) as usize];
|
||||
let mut pool: Vec<Vec<u8>> = Vec::new();
|
||||
let blank: Vec<u8> = default.repeat((PAGE * PAGE) as usize);
|
||||
for ([x, y, w, h], map) in sprites {
|
||||
if w == 0 || h == 0 || map.len() < (w * h * 4) as usize || x + w > width || y + h > height {
|
||||
continue;
|
||||
}
|
||||
for py in y / PAGE..=(y + h - 1) / PAGE {
|
||||
for px in x / PAGE..=(x + w - 1) / PAGE {
|
||||
let entry = &mut table[(py * pages_x + px) as usize];
|
||||
if *entry == 0 {
|
||||
pool.push(blank.clone());
|
||||
*entry = pool.len() as u32;
|
||||
}
|
||||
let page = &mut pool[*entry as usize - 1];
|
||||
// The sprite's texels on this page.
|
||||
let (x0, x1) = (x.max(px * PAGE), (x + w).min((px + 1) * PAGE));
|
||||
let (y0, y1) = (y.max(py * PAGE), (y + h).min((py + 1) * PAGE));
|
||||
for ty in y0..y1 {
|
||||
let src = (((ty - y) * w + (x0 - x)) * 4) as usize;
|
||||
let dst = (((ty - py * PAGE) * PAGE + (x0 - px * PAGE)) * 4) as usize;
|
||||
let len = ((x1 - x0) * 4) as usize;
|
||||
page[dst..dst + len].copy_from_slice(&map[src..src + len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if pool.is_empty() || pool.len() >= 1 << 24 {
|
||||
return None;
|
||||
}
|
||||
let entries = HEADER + pages_x * pages_y;
|
||||
// The narrowest map width (whole pages) that keeps it within the GPU's side.
|
||||
let (side, rows, map_height) = [1024, 2048, 4096, 8192, 16384]
|
||||
.into_iter()
|
||||
.filter(|&s| s <= max_side)
|
||||
.map(|s| {
|
||||
let rows = entries.div_ceil(s).next_multiple_of(PAGE);
|
||||
let per_row = s / PAGE;
|
||||
(s, rows, rows + (pool.len() as u32).div_ceil(per_row) * PAGE)
|
||||
})
|
||||
.find(|&(_, _, h)| h <= max_side)?;
|
||||
let mut pixels = vec![0u8; (side * map_height * 4) as usize];
|
||||
let header = [
|
||||
width as u8,
|
||||
(width >> 8) as u8,
|
||||
height as u8,
|
||||
(height >> 8) as u8,
|
||||
];
|
||||
pixels[..4].copy_from_slice(&header);
|
||||
pixels[4..8].copy_from_slice(&default);
|
||||
for (i, &k) in table.iter().enumerate() {
|
||||
let at = (HEADER as usize + i) * 4;
|
||||
pixels[at..at + 4].copy_from_slice(&[k as u8, (k >> 8) as u8, (k >> 16) as u8, 255]);
|
||||
}
|
||||
let per_row = side / PAGE;
|
||||
for (k, page) in pool.iter().enumerate() {
|
||||
let k = k as u32;
|
||||
let (ox, oy) = (k % per_row * PAGE, rows + k / per_row * PAGE);
|
||||
for row in 0..PAGE {
|
||||
let dst = (((oy + row) * side + ox) * 4) as usize;
|
||||
let src = (row * PAGE * 4) as usize;
|
||||
pixels[dst..dst + (PAGE * 4) as usize]
|
||||
.copy_from_slice(&page[src..src + (PAGE * 4) as usize]);
|
||||
}
|
||||
}
|
||||
Some(PagedMap {
|
||||
width: side,
|
||||
height: map_height,
|
||||
pixels,
|
||||
})
|
||||
}
|
||||
|
||||
/// `map` (RGBA8, `mw` by `mh`) at `w` by `h`, nearest: a map drawn for a strip of animation
|
||||
/// frames keeps its first, the colour's shape from its top.
|
||||
pub fn fit(map: &[u8], (mw, mh): (u32, u32), (w, h): (u32, u32)) -> Option<Vec<u8>> {
|
||||
if mw == 0 || mh == 0 || w == 0 || h == 0 || map.len() < (mw * mh * 4) as usize {
|
||||
return None;
|
||||
}
|
||||
// The first frame of a strip as tall as the colour's frame is wide, scaled.
|
||||
let frame_h = (u64::from(mw) * u64::from(h) / u64::from(w)).clamp(1, u64::from(mh)) as u32;
|
||||
let mut out = Vec::with_capacity((w * h * 4) as usize);
|
||||
for y in 0..h {
|
||||
let sy = (u64::from(y) * u64::from(frame_h) / u64::from(h)) as u32;
|
||||
for x in 0..w {
|
||||
let sx = (u64::from(x) * u64::from(mw) / u64::from(w)) as u32;
|
||||
let at = ((sy * mw + sx) * 4) as usize;
|
||||
out.extend_from_slice(&map[at..at + 4]);
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// What `PAGED_READ` returns at texel (x, y) of the atlas.
|
||||
fn read(map: &PagedMap, (width, height): (u32, u32), (x, y): (u32, u32)) -> [u8; 4] {
|
||||
let texel = |at: u32| {
|
||||
let at = (at * 4) as usize;
|
||||
[0, 1, 2, 3].map(|c| map.pixels[at + c])
|
||||
};
|
||||
let at = |x: u32, y: u32| texel(y * map.width + x);
|
||||
let h = texel(0);
|
||||
assert_eq!(
|
||||
(
|
||||
u32::from(h[0]) | u32::from(h[1]) << 8,
|
||||
u32::from(h[2]) | u32::from(h[3]) << 8
|
||||
),
|
||||
(width, height)
|
||||
);
|
||||
let pages = (width.div_ceil(PAGE), height.div_ceil(PAGE));
|
||||
let e = texel(2 + (y / PAGE) * pages.0 + x / PAGE);
|
||||
let k = u32::from(e[0]) | u32::from(e[1]) << 8 | u32::from(e[2]) << 16;
|
||||
if k == 0 {
|
||||
return texel(1);
|
||||
}
|
||||
let rows = (2 + pages.0 * pages.1)
|
||||
.div_ceil(map.width)
|
||||
.next_multiple_of(16);
|
||||
let per_row = map.width / 16;
|
||||
at(
|
||||
(k - 1) % per_row * 16 + x % 16,
|
||||
rows + (k - 1) / per_row * 16 + y % 16,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapped_textures_read_back_and_the_rest_reads_the_default() {
|
||||
let size = (4096, 2048);
|
||||
// A 20x20 texture across four pages, and one without a specular map.
|
||||
let normal: Vec<u8> = (0..20 * 20)
|
||||
.flat_map(|i: u32| [i as u8, (i >> 8) as u8, 7, 255])
|
||||
.collect();
|
||||
let sprites = [
|
||||
SpriteMaps {
|
||||
rect: [30, 40, 20, 20],
|
||||
maps: [Some(normal.clone()), Some(vec![9; 20 * 20 * 4])],
|
||||
},
|
||||
SpriteMaps {
|
||||
rect: [1000, 1000, 16, 16],
|
||||
maps: [Some(vec![3; 16 * 16 * 4]), None],
|
||||
},
|
||||
];
|
||||
let [normals, specular] = build(size, &sprites, 16384);
|
||||
let (normals, specular) = (normals.unwrap(), specular.unwrap());
|
||||
for y in 0..20 {
|
||||
for x in 0..20 {
|
||||
let i = ((y * 20 + x) * 4) as usize;
|
||||
assert_eq!(read(&normals, size, (30 + x, 40 + y)), normal[i..i + 4]);
|
||||
assert_eq!(read(&specular, size, (30 + x, 40 + y)), [9; 4]);
|
||||
}
|
||||
}
|
||||
// The texels beside it on its pages, and pages without a map.
|
||||
assert_eq!(read(&normals, size, (29, 40)), crate::sprites::FLAT_NORMAL);
|
||||
assert_eq!(read(&normals, size, (2000, 5)), crate::sprites::FLAT_NORMAL);
|
||||
assert_eq!(
|
||||
read(&specular, size, (1005, 1005)),
|
||||
crate::sprites::NO_SPECULAR
|
||||
);
|
||||
assert_eq!(read(&normals, size, (1005, 1005)), [3; 4]);
|
||||
// Five pages: a table of 2 + 256 x 128 entries, 33 rows at 1024 wide (48, whole pages),
|
||||
// then a row of pages.
|
||||
assert_eq!((normals.width, normals.height), (1024, 64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_mapped_makes_no_map() {
|
||||
let sprites = [SpriteMaps {
|
||||
rect: [0, 0, 16, 16],
|
||||
maps: [None, None],
|
||||
}];
|
||||
assert_eq!(build((1024, 1024), &sprites, 16384), [None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_map_fits_its_texture() {
|
||||
// A 2x map is sampled down; a strip of frames keeps its first.
|
||||
let map: Vec<u8> = (0..4 * 8).flat_map(|i: u8| [i, 0, 0, 255]).collect();
|
||||
let fitted = fit(&map, (4, 8), (2, 2)).unwrap();
|
||||
assert_eq!(
|
||||
fitted.chunks(4).map(|p| p[0]).collect::<Vec<_>>(),
|
||||
[0, 2, 8, 10]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,33 @@ pub fn entity() -> LegacyRandom {
|
||||
create()
|
||||
}
|
||||
|
||||
/// `RandomSupport.SEED_UNIQUIFIER`'s start and step.
|
||||
const UNIQUIFIER_START: i64 = 8_682_522_807_148_012;
|
||||
const UNIQUIFIER_STEP: i64 = 1_181_783_497_276_652_981;
|
||||
|
||||
thread_local! {
|
||||
/// This thread's uniquifier under `HORIZON_TEST_SEEDS`.
|
||||
static THREAD_UNIQUIFIER: std::cell::Cell<i64> = const { std::cell::Cell::new(UNIQUIFIER_START) };
|
||||
}
|
||||
|
||||
/// `RandomSupport.generateUniqueSeed`. Under `HORIZON_STILL` (screenshots that
|
||||
/// come out the same every run) the clock is left out, so the seeds repeat run
|
||||
/// to run, in the order they are taken.
|
||||
/// to run, in the order they are taken. Under `HORIZON_TEST_SEEDS` the clock is
|
||||
/// 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.
|
||||
pub fn unique_seed() -> i64 {
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
static UNIQUIFIER: AtomicI64 = AtomicI64::new(8_682_522_807_148_012);
|
||||
static UNIQUIFIER: AtomicI64 = AtomicI64::new(UNIQUIFIER_START);
|
||||
if test_seeds() {
|
||||
return THREAD_UNIQUIFIER.with(|u| {
|
||||
u.set(u.get().wrapping_mul(UNIQUIFIER_STEP));
|
||||
u.get()
|
||||
});
|
||||
}
|
||||
let mut current = UNIQUIFIER.load(Ordering::Relaxed);
|
||||
let next = loop {
|
||||
let next = current.wrapping_mul(1_181_783_497_276_652_981);
|
||||
let next = current.wrapping_mul(UNIQUIFIER_STEP);
|
||||
match UNIQUIFIER.compare_exchange(current, next, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => break next,
|
||||
Err(v) => current = v,
|
||||
@@ -47,6 +65,30 @@ fn still() -> bool {
|
||||
*STILL.get_or_init(|| std::env::var("HORIZON_STILL").is_ok_and(|v| v == "1"))
|
||||
}
|
||||
|
||||
/// `HORIZON_TEST_SEEDS=1`, a dev switch for parity harnesses, read once.
|
||||
fn test_seeds() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| {
|
||||
let on = std::env::var("HORIZON_TEST_SEEDS").is_ok_and(|v| v == "1");
|
||||
if on {
|
||||
log::warn!(
|
||||
"HORIZON_TEST_SEEDS=1: unseeded randoms repeat run to run (parity harnesses only)"
|
||||
);
|
||||
}
|
||||
on
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
if !test_seeds() {
|
||||
return Err("setting the seed uniquifier needs HORIZON_TEST_SEEDS=1");
|
||||
}
|
||||
THREAD_UNIQUIFIER.with(|u| u.set(value));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -840,6 +840,7 @@ pub enum Opt {
|
||||
PadRepeatInterval,
|
||||
PadInvertX,
|
||||
PadInvertY,
|
||||
PadCraftOnServers,
|
||||
PadToggleSneak,
|
||||
PadMoveDeadzone,
|
||||
PadLookDeadzone,
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
//!
|
||||
//! A new screen implements [`Screen`], describing its rows from the settings, and [`Shell`] does
|
||||
//! everything else: drawing, clicks, one scroll path, a controller's focus and its declared
|
||||
//! buttons, keys and each row's description (`extras_ui` is the smallest). `graphics_ui`,
|
||||
//! `controller_ui` and `shader_ui` still drive the parts below by hand.
|
||||
//! buttons, keys and each row's description (`extras_ui` is the smallest; `graphics_ui` has
|
||||
//! pages, sliders, costs and Undo). `controller_ui` and `shader_ui` still drive the parts below
|
||||
//! by hand.
|
||||
use crate::{
|
||||
graphics_style as style,
|
||||
hud::{HudAtlas, HudBatch},
|
||||
@@ -38,6 +39,15 @@ pub(crate) struct Columns {
|
||||
const NOTE_LINES: usize = 2;
|
||||
/// The height of a line of description.
|
||||
const NOTE_LINE_H: f32 = 10.0;
|
||||
/// The strip's text from its left edge, and its first line (the name) from its top.
|
||||
const DOCK_INSET: f32 = 12.0;
|
||||
const DOCK_NAME_Y: f32 = 8.0;
|
||||
/// A cost's square before the name: its side, how far down it sits, and the name after it.
|
||||
const COST_SQUARE: f32 = 4.0;
|
||||
const COST_SQUARE_Y: f32 = 10.0;
|
||||
const COST_NAME_X: f32 = 8.0;
|
||||
/// What the strip keeps clear at its right of a name with its cost.
|
||||
const COST_LINE_SLACK: f32 = 32.0;
|
||||
/// A taller strip leaves the list at least this high.
|
||||
const MIN_LIST_H: f32 = 60.0;
|
||||
impl Columns {
|
||||
@@ -745,7 +755,6 @@ pub(crate) enum Value {
|
||||
Choice(String),
|
||||
/// A slider: left and right step it (`steps`: its stops, else a pixel of the track), a press
|
||||
/// on the track sets it (a controller's focus does the same).
|
||||
#[allow(dead_code)] // No screen on the shell has one yet; `controller_ui` will (#11487).
|
||||
Slider {
|
||||
position: f32,
|
||||
steps: Option<u32>,
|
||||
@@ -772,6 +781,10 @@ pub(crate) struct Row<R> {
|
||||
pub(crate) chosen: Option<String>,
|
||||
/// Can be used now (a setting of a switch that is off can't).
|
||||
pub(crate) live: bool,
|
||||
/// Why it can't be used now: shown in the strip, greyed, instead of what it does.
|
||||
pub(crate) why: Option<String>,
|
||||
/// What it costs, beside its name in the strip.
|
||||
pub(crate) cost: Option<style::Cost>,
|
||||
}
|
||||
impl<R> Row<R> {
|
||||
pub(crate) fn new(id: R, name: String, value: Value) -> Self {
|
||||
@@ -782,6 +795,8 @@ impl<R> Row<R> {
|
||||
about: String::new(),
|
||||
chosen: None,
|
||||
live: true,
|
||||
why: None,
|
||||
cost: None,
|
||||
}
|
||||
}
|
||||
pub(crate) fn about(mut self, about: String) -> Self {
|
||||
@@ -792,6 +807,16 @@ impl<R> Row<R> {
|
||||
self.live = live;
|
||||
self
|
||||
}
|
||||
/// Greyed, saying why, while there is a reason it can't be used.
|
||||
pub(crate) fn blocked(mut self, why: Option<String>) -> Self {
|
||||
self.live = why.is_none();
|
||||
self.why = why;
|
||||
self
|
||||
}
|
||||
pub(crate) fn cost(mut self, cost: style::Cost) -> Self {
|
||||
self.cost = Some(cost);
|
||||
self
|
||||
}
|
||||
/// Its whole caption ("Name: value"), for the narrator and the widget tree.
|
||||
fn label(&self, value: Option<&str>) -> String {
|
||||
match value {
|
||||
@@ -885,6 +910,28 @@ pub(crate) trait Screen {
|
||||
) -> Option<Vec<Output>> {
|
||||
None
|
||||
}
|
||||
/// Whether the screen has Undo, which puts back what it changed (`undo`) since it opened.
|
||||
fn undoes(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Undo: put back, from the settings as the screen opened (`before`), what it changes.
|
||||
fn undo(&mut self, _s: &mut Settings, _before: &Settings) {}
|
||||
/// Whether what a row did (its outputs) is a change Undo puts back.
|
||||
fn changes(&self, outputs: &[Output]) -> bool {
|
||||
changes_settings(outputs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `outputs` say a setting changed, or a key is being bound to one.
|
||||
pub(crate) fn changes_settings(outputs: &[Output]) -> bool {
|
||||
use crate::options_ui::Event;
|
||||
let changed = |output: &Output| {
|
||||
matches!(
|
||||
output,
|
||||
Output::BindKey(_) | Output::Event(Event::Changed | Event::SliderDrag)
|
||||
)
|
||||
};
|
||||
outputs.iter().any(changed)
|
||||
}
|
||||
|
||||
/// A fingerprint of `parts` for [`Screen::stamp`]: their `Debug` text, hashed as it is written,
|
||||
@@ -958,6 +1005,9 @@ pub(crate) trait Hosted {
|
||||
fn typing(&self) -> bool;
|
||||
/// A key or the pad moved the focus away: the search field stops taking the keys.
|
||||
fn blur_search(&mut self);
|
||||
/// Nothing has the focus: a touch moved off it, say.
|
||||
#[allow(dead_code)] // For #11846's `OptionsUi::touch_start`, which cleared Graphics' focus.
|
||||
fn clear_focus(&mut self);
|
||||
/// Text typed into the search field (the keyboard, a controller's on-screen keyboard).
|
||||
fn type_text(&mut self, text: &str);
|
||||
/// Ctrl+A/C/V/X in the search field.
|
||||
@@ -970,10 +1020,11 @@ pub(crate) trait Hosted {
|
||||
|
||||
/// What the keyboard or a controller can focus.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Target<P, R> {
|
||||
pub(crate) enum Target<P, R> {
|
||||
Row(R),
|
||||
Page(P),
|
||||
Search,
|
||||
Undo,
|
||||
Done,
|
||||
}
|
||||
|
||||
@@ -1022,6 +1073,10 @@ pub(crate) struct Shell<S: Screen> {
|
||||
waiting: Option<&'static str>,
|
||||
/// The rows as last built, and what for.
|
||||
built: Option<(Built<S::Page, S::Row>, Rc<View<S::Page, S::Row>>)>,
|
||||
/// The settings as the screen found them, for Undo (`Screen::undoes`), and whether it
|
||||
/// changed them since.
|
||||
before: Option<Box<Settings>>,
|
||||
changed: bool,
|
||||
}
|
||||
impl<S: Screen + Default> Default for Shell<S> {
|
||||
fn default() -> Self {
|
||||
@@ -1041,6 +1096,8 @@ impl<S: Screen + Default> Default for Shell<S> {
|
||||
picked: None,
|
||||
waiting: None,
|
||||
built: None,
|
||||
before: None,
|
||||
changed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1084,6 +1141,34 @@ impl<S: Screen> Shell<S> {
|
||||
fn rebuild(&mut self) {
|
||||
self.built = None;
|
||||
}
|
||||
/// A row did `outputs`: it reads anew, and Undo has something to put back if they changed it.
|
||||
fn did(&mut self, outputs: &[Output]) {
|
||||
self.rebuild();
|
||||
// Only a screen with Undo has something for it to put back.
|
||||
if self.screen.undoes() && self.screen.changes(outputs) {
|
||||
self.changed = true;
|
||||
}
|
||||
}
|
||||
/// Remember the settings before the first change, for Undo.
|
||||
fn keep(&mut self, s: &Settings) {
|
||||
if self.screen.undoes() && self.before.is_none() {
|
||||
self.before = Some(Box::new(s.clone()));
|
||||
}
|
||||
}
|
||||
/// Undo, gone once it has put the settings back.
|
||||
fn undo(&mut self, s: &mut Settings) -> Vec<Output> {
|
||||
if let Some(before) = self.before.take() {
|
||||
self.screen.undo(s, &before);
|
||||
self.before = Some(before);
|
||||
}
|
||||
self.changed = false;
|
||||
self.rebuild();
|
||||
vec![CLICK, Output::Event(crate::options_ui::Event::Changed)]
|
||||
}
|
||||
/// Open at `page`.
|
||||
pub(crate) fn show(&mut self, page: S::Page) {
|
||||
self.page = Some(page);
|
||||
}
|
||||
/// The screen's rows: the open page's or, while searching, every page's whose name or
|
||||
/// description has the search in it, under their pages' names. The row being changed stays
|
||||
/// whatever it reads now.
|
||||
@@ -1211,14 +1296,14 @@ impl<S: Screen> Shell<S> {
|
||||
};
|
||||
self.dragging = Some(row.id);
|
||||
self.screen.slide(s, row.id, position);
|
||||
self.rebuild();
|
||||
self.did(&[DRAGGED]);
|
||||
vec![DRAGGED]
|
||||
}
|
||||
_ => {
|
||||
self.can_change_value = false;
|
||||
let mut outputs = vec![CLICK];
|
||||
outputs.extend(self.screen.press(s, row.id, back));
|
||||
self.rebuild();
|
||||
self.did(&outputs);
|
||||
outputs
|
||||
}
|
||||
}
|
||||
@@ -1245,6 +1330,9 @@ impl<S: Screen> Shell<S> {
|
||||
out.push((Target::Row(id), cols.row(y)));
|
||||
}
|
||||
}
|
||||
if self.changed {
|
||||
out.push((Target::Undo, cols.button(1)));
|
||||
}
|
||||
out.push((Target::Done, cols.button(0)));
|
||||
out
|
||||
}
|
||||
@@ -1340,10 +1428,14 @@ impl<S: Screen> Shell<S> {
|
||||
current: Option<S::Page>,
|
||||
) {
|
||||
let cols = &view.cols;
|
||||
let [x, y, ..] = cols.dock;
|
||||
let [x, y, w, _] = cols.dock;
|
||||
let left = x + DOCK_INSET;
|
||||
let top = y + DOCK_NAME_Y;
|
||||
style::panel(batch, atlas, cols.dock);
|
||||
let (name, about) = match described.map(|i| &view.rows[i]) {
|
||||
Some(row) => (row.name.as_str(), row.described()),
|
||||
let row = described.map(|i| &view.rows[i]);
|
||||
let why = row.and_then(|row| row.why.as_deref().filter(|_| !row.live));
|
||||
let (name, about) = match row {
|
||||
Some(row) => (row.name.as_str(), why.unwrap_or(row.described())),
|
||||
None => {
|
||||
let Some(page) = view.pages.iter().find(|p| Some(p.0) == current) else {
|
||||
return;
|
||||
@@ -1351,11 +1443,30 @@ impl<S: Screen> Shell<S> {
|
||||
(page.1.as_str(), page.2.as_str())
|
||||
}
|
||||
};
|
||||
let name = Text::literal(name.to_owned());
|
||||
batch.text_styled(atlas, &name, x + 12.0, y + 8.0, style::WHITE, true);
|
||||
match row.and_then(|row| row.cost) {
|
||||
// Its cost in its colour, after a square of it.
|
||||
Some(cost) => {
|
||||
let line = format!("{name} · {}", lang::get(cost.label()));
|
||||
let line = style::fit(atlas, &line, w - COST_LINE_SLACK);
|
||||
let line = Text::literal(line).with_color(cost.rgb());
|
||||
let square = [left, y + COST_SQUARE_Y, COST_SQUARE, COST_SQUARE];
|
||||
style::fill(batch, atlas, square, style::cost_color(cost));
|
||||
let after = left + COST_NAME_X;
|
||||
batch.text_styled(atlas, &line, after, top, style::WHITE, true);
|
||||
}
|
||||
None => {
|
||||
let name = Text::literal(name.to_owned());
|
||||
batch.text_styled(atlas, &name, left, top, style::WHITE, true);
|
||||
}
|
||||
}
|
||||
// Labelled for the widget tree: what the strip says is what a test or an agent reads.
|
||||
crate::widgets::record(about, cols.dock, false);
|
||||
dock_note(batch, atlas, cols, about.to_owned(), style::GREY);
|
||||
let color = if why.is_some() {
|
||||
style::MUTED
|
||||
} else {
|
||||
style::GREY
|
||||
};
|
||||
dock_note(batch, atlas, cols, about.to_owned(), color);
|
||||
}
|
||||
}
|
||||
impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
@@ -1372,6 +1483,7 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
waiting: Option<&'static str>,
|
||||
) {
|
||||
self.waiting = waiting;
|
||||
self.keep(s);
|
||||
let view = self.view(s, (batch.width, batch.height));
|
||||
let cols = view.cols;
|
||||
backdrop(batch, atlas, in_world);
|
||||
@@ -1422,6 +1534,14 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
crate::widgets::focus_link(cols.search(), true, row);
|
||||
}
|
||||
self.draw_dock(batch, atlas, &view, described, current);
|
||||
// Undo, over Done, only once there is something to undo.
|
||||
if self.changed {
|
||||
let undo = cols.button(1);
|
||||
let label = Text::literal(lang::get("Undo"));
|
||||
let flags = (self.shows_focus(Target::Undo), false);
|
||||
crate::widgets::declare_action(crate::widgets::Action::Apply, undo, true);
|
||||
foot_button(batch, atlas, undo, &label, cursor, flags);
|
||||
}
|
||||
let done = cols.button(0);
|
||||
let label = Text::literal(lang::get("Done"));
|
||||
let flags = (self.shows_focus(Target::Done), true);
|
||||
@@ -1437,6 +1557,7 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
right: bool,
|
||||
shift: bool,
|
||||
) -> Vec<Output> {
|
||||
self.keep(s);
|
||||
let view = self.view(s, size);
|
||||
let cols = view.cols;
|
||||
let at = |[rx, ry, rw, rh]: [f32; 4]| ui::hovered((x, y), rx, ry, rw, rh);
|
||||
@@ -1454,7 +1575,7 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
}
|
||||
if let Some(row) = self.dragging {
|
||||
self.screen.slide(s, row, cols.slider_at(x));
|
||||
self.rebuild();
|
||||
self.did(&[DRAGGED]);
|
||||
return vec![DRAGGED];
|
||||
}
|
||||
return Vec::new();
|
||||
@@ -1487,6 +1608,11 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
self.focus = Some(Target::Done);
|
||||
return vec![CLICK, Output::Done];
|
||||
}
|
||||
if self.changed && at(cols.button(1)) {
|
||||
// Undo goes away with the change it undid.
|
||||
self.focus = None;
|
||||
return self.undo(s);
|
||||
}
|
||||
for (i, page) in self.tabs(&view) {
|
||||
if at(cols.page(i)) && self.has(page, &view) {
|
||||
self.focus = Some(Target::Page(page));
|
||||
@@ -1534,7 +1660,7 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
if let Some(row) = self.row_at(&view, at).filter(|row| row.live)
|
||||
&& let Some(outputs) = self.screen.wheel_step(s, row.id, amount > 0.0)
|
||||
{
|
||||
self.rebuild();
|
||||
self.did(&outputs);
|
||||
return outputs;
|
||||
}
|
||||
}
|
||||
@@ -1557,6 +1683,7 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
size: (f32, f32),
|
||||
) -> Vec<Output> {
|
||||
use winit::keyboard::KeyCode as K;
|
||||
self.keep(s);
|
||||
if key == K::Escape {
|
||||
self.can_change_value = false;
|
||||
return vec![Output::Done];
|
||||
@@ -1608,7 +1735,7 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
let width = view.cols.track().1 + 8.0;
|
||||
let next = slider_step(position, steps, width, key == K::ArrowRight);
|
||||
self.screen.slide(s, row.id, next);
|
||||
self.rebuild();
|
||||
self.did(&[DRAGGED]);
|
||||
return vec![DRAGGED];
|
||||
}
|
||||
if matches!(
|
||||
@@ -1644,6 +1771,11 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
Some(row) => self.activate(s, row, shift, None),
|
||||
None => Vec::new(),
|
||||
},
|
||||
Some(Target::Undo) => {
|
||||
// Undo goes away with the change it undid: Done has the focus.
|
||||
self.focus = Some(Target::Done);
|
||||
self.undo(s)
|
||||
}
|
||||
Some(Target::Done) => vec![CLICK, Output::Done],
|
||||
Some(Target::Search) | None => Vec::new(),
|
||||
}
|
||||
@@ -1656,6 +1788,10 @@ impl<S: Screen + Default> Hosted for Shell<S> {
|
||||
self.focus = None;
|
||||
}
|
||||
}
|
||||
fn clear_focus(&mut self) {
|
||||
self.focus = None;
|
||||
self.can_change_value = false;
|
||||
}
|
||||
fn type_text(&mut self, text: &str) {
|
||||
if !text.is_empty() {
|
||||
self.search.insert_text(text);
|
||||
@@ -1699,6 +1835,71 @@ impl<S: Screen> Shell<S> {
|
||||
pub(crate) fn scroll_amount(&self) -> f32 {
|
||||
self.scroll.amount
|
||||
}
|
||||
/// What the shell's own focus is on.
|
||||
pub(crate) fn focused(&self) -> Option<Target<S::Page, S::Row>> {
|
||||
self.focus
|
||||
}
|
||||
/// Put the focus there, as a key would.
|
||||
pub(crate) fn set_focus(&mut self, focus: Option<Target<S::Page, S::Row>>) {
|
||||
self.focus = focus;
|
||||
self.keyed = true;
|
||||
}
|
||||
/// Left and Right move the focused slider.
|
||||
pub(crate) fn changes_value(&self) -> bool {
|
||||
self.can_change_value
|
||||
}
|
||||
/// Undo shows: the screen changed something since it opened.
|
||||
pub(crate) fn changed(&self) -> bool {
|
||||
self.changed
|
||||
}
|
||||
/// The scrollbar is held.
|
||||
pub(crate) fn scroller_held(&self) -> bool {
|
||||
self.scroll.held()
|
||||
}
|
||||
/// What the search field holds now, set as typing would.
|
||||
pub(crate) fn set_search(&mut self, text: &str) {
|
||||
self.search.set_value(text);
|
||||
self.searched();
|
||||
}
|
||||
/// The page shown while nothing is searched, as opened or picked.
|
||||
pub(crate) fn open_page(&self) -> Option<S::Page> {
|
||||
self.page
|
||||
}
|
||||
/// The list's lines, in order, and its content's height.
|
||||
pub(crate) fn lines_at(
|
||||
&mut self,
|
||||
s: &Settings,
|
||||
size: (f32, f32),
|
||||
) -> (Vec<Line<S::Page, usize>>, f32) {
|
||||
let view = self.view(s, size);
|
||||
(view.lines.clone(), view.content)
|
||||
}
|
||||
/// Each row of the list, in order, with its page.
|
||||
pub(crate) fn listed(&mut self, s: &Settings, size: (f32, f32)) -> Vec<(S::Page, S::Row)> {
|
||||
let view = self.view(s, size);
|
||||
let lines = view.lines.iter();
|
||||
lines
|
||||
.filter_map(|l| Some((l.page, view.rows[l.setting?].id)))
|
||||
.collect()
|
||||
}
|
||||
/// The page the plate is headed with.
|
||||
pub(crate) fn current_page(&mut self, s: &Settings, size: (f32, f32)) -> Option<S::Page> {
|
||||
let view = self.view(s, size);
|
||||
self.current(&view)
|
||||
}
|
||||
/// Whether `row` is all in the list's view now.
|
||||
pub(crate) fn in_view(&mut self, s: &Settings, size: (f32, f32), row: S::Row) -> bool {
|
||||
let view = self.view(s, size);
|
||||
let cols = view.cols;
|
||||
let mut placed = self.placed(&view);
|
||||
placed.any(|(r, y)| r.id == row && y >= cols.top && y + style::ROW <= cols.bottom)
|
||||
}
|
||||
/// Whether Tab reaches `row` now.
|
||||
pub(crate) fn tab_reaches(&mut self, s: &Settings, size: (f32, f32), row: S::Row) -> bool {
|
||||
let view = self.view(s, size);
|
||||
let focusables = self.focusables(&view, false);
|
||||
focusables.iter().any(|(t, _)| *t == Target::Row(row))
|
||||
}
|
||||
/// The row the shell's own focus is on.
|
||||
pub(crate) fn focused_row(&self) -> Option<S::Row> {
|
||||
match self.focus {
|
||||
@@ -1740,6 +1941,7 @@ mod tests {
|
||||
const FIRST_FILLER: u32 = 10;
|
||||
const FILLERS: u32 = 40;
|
||||
const GREY: u32 = 100;
|
||||
const WHY: &str = "Not here.";
|
||||
impl Screen for Probe {
|
||||
type Page = u8;
|
||||
type Row = u32;
|
||||
@@ -1768,7 +1970,9 @@ mod tests {
|
||||
..Row::new(CHOICE, "Paint".to_owned(), Value::Choice(value.to_owned()))
|
||||
.about("Picks a colour.".to_owned())
|
||||
},
|
||||
Row::new(GREY, "Greyed".to_owned(), Value::Press).live(false),
|
||||
Row::new(GREY, "Greyed".to_owned(), Value::Press)
|
||||
.about("Does a thing.".to_owned())
|
||||
.blocked(Some(WHY.to_owned())),
|
||||
];
|
||||
rows.extend((0..FILLERS).map(|i| {
|
||||
Row::new(FIRST_FILLER + i, format!("Filler {i}"), Value::Press)
|
||||
@@ -1792,13 +1996,21 @@ mod tests {
|
||||
},
|
||||
]
|
||||
}
|
||||
fn press(&mut self, _: &mut Settings, row: u32, back: bool) -> Vec<Output> {
|
||||
fn press(&mut self, s: &mut Settings, row: u32, back: bool) -> Vec<Output> {
|
||||
self.pressed.push((row, back));
|
||||
if row == CHOICE {
|
||||
self.choice = (self.choice + 1) % CHOICES.len();
|
||||
match row {
|
||||
CHOICE => self.choice = (self.choice + 1) % CHOICES.len(),
|
||||
FIRST_FILLER => s.fov += 1.0,
|
||||
_ => {}
|
||||
}
|
||||
vec![CHANGED]
|
||||
}
|
||||
fn undoes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn undo(&mut self, s: &mut Settings, before: &Settings) {
|
||||
s.fov = before.fov;
|
||||
}
|
||||
fn slide(&mut self, _: &mut Settings, _: u32, position: f32) {
|
||||
self.slider = position;
|
||||
}
|
||||
@@ -1898,6 +2110,85 @@ mod tests {
|
||||
assert_eq!(shell.screen.pressed, [(cut, false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_appears_after_a_change_and_puts_back_what_the_screen_opened_with() {
|
||||
let mut s = Settings::default();
|
||||
let mut shell = Shell::<Probe>::default();
|
||||
let undo = Columns::of(SIZE.0, SIZE.1).button(1);
|
||||
let on_undo = (undo[0] + 2.0, undo[1] + 2.0);
|
||||
// Nothing changed: there is no Undo to press.
|
||||
assert!(
|
||||
shell
|
||||
.click(&mut s, on_undo, SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
let at = middle_of(&mut shell, FIRST_FILLER, SIZE);
|
||||
shell.click(&mut s, at, SIZE, false, false, false);
|
||||
assert_eq!(s.fov, 71.0);
|
||||
// Another option changed meanwhile isn't the screen's to put back.
|
||||
s.chat_opacity = 0.25;
|
||||
assert_eq!(
|
||||
shell.click(&mut s, on_undo, SIZE, false, false, false),
|
||||
vec![CLICK, CHANGED]
|
||||
);
|
||||
assert_eq!((s.fov, s.chat_opacity), (70.0, 0.25));
|
||||
assert!(
|
||||
shell
|
||||
.click(&mut s, on_undo, SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
// From the keyboard: Tab reaches it, Enter undoes and leaves the focus on Done.
|
||||
shell.click(&mut s, at, SIZE, false, false, false);
|
||||
let reached = (0..100).any(|_| {
|
||||
shell.key(&mut s, K::Tab, None, false, SIZE);
|
||||
shell.focus == Some(Target::Undo)
|
||||
});
|
||||
assert!(reached);
|
||||
assert_eq!(
|
||||
shell.key(&mut s, K::Enter, None, false, SIZE),
|
||||
vec![CLICK, CHANGED]
|
||||
);
|
||||
assert_eq!(s.fov, 70.0);
|
||||
assert_eq!(shell.focus, Some(Target::Done));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_strip_says_why_a_greyed_row_cant_be_used() {
|
||||
let atlas = atlas();
|
||||
let s = Settings::default();
|
||||
let mut shell = Shell::<Probe>::default();
|
||||
let mut batch = HudBatch::new([1708, 960]);
|
||||
let size = (batch.width, batch.height);
|
||||
let strip = |batch: &mut HudBatch, shell: &mut Shell<Probe>, at| {
|
||||
shell.draw(&s, batch, &atlas, at, false, None);
|
||||
crate::widgets::end_frame(crate::widgets::generation() + 1);
|
||||
let dock = Columns::of(size.0, size.1).dock;
|
||||
let labelled = crate::widgets::last_labelled();
|
||||
let found = labelled.iter().find(|w| [w.x, w.y, w.w, w.h] == dock);
|
||||
found.map(|w| w.label.clone())
|
||||
};
|
||||
let grey = middle_of(&mut shell, GREY, size);
|
||||
assert_eq!(strip(&mut batch, &mut shell, grey).as_deref(), Some(WHY));
|
||||
let choice = middle_of(&mut shell, CHOICE, size);
|
||||
assert_eq!(
|
||||
strip(&mut batch, &mut shell, choice).as_deref(),
|
||||
Some(CHOICES[0].1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn growing_the_window_keeps_the_scroll_within_the_list() {
|
||||
let s = Settings::default();
|
||||
let mut shell = Shell::<Probe>::default();
|
||||
shell.scroll(&s, SIZE, -1000.0);
|
||||
assert!(shell.scroll.amount > 0.0);
|
||||
let tall = (SIZE.0, 4000.0);
|
||||
let view = shell.view(&s, tall);
|
||||
assert_eq!(shell.scroll.amount, 0.0);
|
||||
let first = shell.placed(&view).next().map(|(_, y)| y).unwrap();
|
||||
assert!(first >= view.cols.top);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_scroll_path_moves_the_list_for_wheel_and_stick_alike() {
|
||||
let mut s = Settings::default();
|
||||
|
||||
@@ -20,6 +20,7 @@ use std::{
|
||||
pub const MASTER: usize = 0;
|
||||
pub const MUSIC: usize = 1;
|
||||
pub const RECORDS: usize = 2;
|
||||
pub const WEATHER: usize = 3;
|
||||
pub const NEUTRAL: usize = 6;
|
||||
pub const PLAYERS: usize = 7;
|
||||
pub const AMBIENT: usize = 8;
|
||||
@@ -77,7 +78,7 @@ impl SoundEntity {
|
||||
}
|
||||
|
||||
/// The tickable instance kinds Horizon plays (`TickableSoundInstance`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Ticking {
|
||||
/// Not tickable.
|
||||
No,
|
||||
@@ -112,6 +113,24 @@ pub enum Ticking {
|
||||
/// A native mod's music layer (`MusicLayers`), whose volume its owner
|
||||
/// sets each tick.
|
||||
MusicLayer,
|
||||
/// `DirectionalSoundInstance`: ten blocks from the camera toward
|
||||
/// `Vec3.directionFromRotation(x_angle, y_angle)`.
|
||||
Directional { x_angle: f32, y_angle: f32 },
|
||||
}
|
||||
/// `DirectionalSoundInstance.setPosition`: the camera plus
|
||||
/// `Vec3.directionFromRotation(xAngle, yAngle).scale(10.0)`.
|
||||
fn directional_position(camera: [f64; 3], x_angle: f32, y_angle: f32) -> [f64; 3] {
|
||||
use horizon_core::geometry::trig;
|
||||
let to_rad = std::f32::consts::PI / 180.0;
|
||||
let y = -y_angle * to_rad - std::f32::consts::PI;
|
||||
let (y_cos, y_sin) = (trig::cos(f64::from(y)), trig::sin(f64::from(y)));
|
||||
let x = -x_angle * to_rad;
|
||||
let (x_cos, x_sin) = (-trig::cos(f64::from(x)), trig::sin(f64::from(x)));
|
||||
[
|
||||
camera[0] + f64::from(y_sin * x_cos) * 10.0,
|
||||
camera[1] + f64::from(x_sin) * 10.0,
|
||||
camera[2] + f64::from(y_cos * x_cos) * 10.0,
|
||||
]
|
||||
}
|
||||
/// What a minecart's sound instances read about it.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -170,6 +189,8 @@ pub struct TickContext {
|
||||
pub player_fall_flying: bool,
|
||||
/// The mob the player rides, when it is not a minecart.
|
||||
pub ridden: Option<RiddenSound>,
|
||||
/// `mainCamera().position()`.
|
||||
pub camera_position: [f64; 3],
|
||||
}
|
||||
/// `AbstractSoundInstance` and its subclasses' fields.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -588,6 +609,31 @@ impl SoundInstance {
|
||||
s.ticking = Ticking::Elytra { time: 0 };
|
||||
s
|
||||
}
|
||||
/// `new DirectionalSoundInstance(event, source, random, camera, xAngle,
|
||||
/// yAngle)`: volume and pitch 1, heard everywhere alike.
|
||||
pub fn directional(
|
||||
id: &str,
|
||||
source: usize,
|
||||
random: LegacyRandom,
|
||||
camera: [f64; 3],
|
||||
x_angle: f32,
|
||||
y_angle: f32,
|
||||
) -> Self {
|
||||
let mut s = Self::new(
|
||||
id,
|
||||
source,
|
||||
1.0,
|
||||
1.0,
|
||||
random,
|
||||
false,
|
||||
0,
|
||||
Attenuation::None,
|
||||
directional_position(camera, x_angle, y_angle),
|
||||
false,
|
||||
);
|
||||
s.ticking = Ticking::Directional { x_angle, y_angle };
|
||||
s
|
||||
}
|
||||
pub fn tickable(&self) -> bool {
|
||||
self.ticking != Ticking::No
|
||||
}
|
||||
@@ -762,6 +808,12 @@ impl SoundInstance {
|
||||
self.volume = 1.0;
|
||||
self.pitch = 1.0;
|
||||
}
|
||||
Ticking::Directional { x_angle, y_angle } => {
|
||||
// DirectionalSoundInstance.tick: setPosition.
|
||||
let (x_angle, y_angle) = (*x_angle, *y_angle);
|
||||
self.position = directional_position(ctx.camera_position, x_angle, y_angle);
|
||||
self.attenuation = Attenuation::None;
|
||||
}
|
||||
Ticking::GuardianAttack { guardian } => {
|
||||
let id = *guardian;
|
||||
// The client's guardian never has a target of its own, so
|
||||
|
||||
@@ -274,6 +274,7 @@ struct MeshSettings {
|
||||
blend_radius: i32,
|
||||
smooth_lighting: bool,
|
||||
cutout_leaves: bool,
|
||||
opaque_ice: bool,
|
||||
translucent_layers: Arc<[bool]>,
|
||||
leaf_culling: LeafCullingMode,
|
||||
}
|
||||
@@ -380,6 +381,33 @@ impl MeshPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The chunks `wanted` in the order they decorate: by rank, lowest first. No
|
||||
/// two share a rank, so only the chunks without one yet need their distance:
|
||||
/// those no ticket reached (forced and extra areas) are ranked after every
|
||||
/// other, nearest first. The pass that pops them stops once the workers are
|
||||
/// busy, usually long before the last, so a heap orders only what it takes. A
|
||||
/// server with players spread over the map holds tens of thousands of proto
|
||||
/// chunks; measuring and sorting them all every tick took a fifth of its tick.
|
||||
fn decoration_queue(
|
||||
order: &mut DecorationOrder,
|
||||
wanted: impl Iterator<Item = (i32, i32)>,
|
||||
distance: impl Fn(i32, i32) -> i64,
|
||||
) -> std::collections::BinaryHeap<std::cmp::Reverse<(u64, i32, i32)>> {
|
||||
let mut ranked = Vec::new();
|
||||
let mut fresh = Vec::new();
|
||||
for (x, z) in wanted {
|
||||
match order.rank(x, z) {
|
||||
Some(rank) => ranked.push(std::cmp::Reverse((rank, x, z))),
|
||||
None => fresh.push((distance(x, z), x, z)),
|
||||
}
|
||||
}
|
||||
fresh.sort_unstable();
|
||||
for (_, x, z) in fresh {
|
||||
ranked.push(std::cmp::Reverse((order.rank_or_append(x, z), x, z)));
|
||||
}
|
||||
std::collections::BinaryHeap::from(ranked)
|
||||
}
|
||||
|
||||
/// Finished jobs, from the workers to the game thread, in the order they
|
||||
/// finished. Not an `mpsc` channel: its receiver waits for a sender it finds
|
||||
/// half way through a send, and a worker descheduled there on a busy machine
|
||||
@@ -533,6 +561,8 @@ pub struct Streaming {
|
||||
/// The `ao` and `cutoutLeaves` options.
|
||||
pub smooth_lighting: bool,
|
||||
pub cutout_leaves: bool,
|
||||
/// Draw regular ice opaque in the solid pass (`Mesher::opaque_ice`).
|
||||
pub opaque_ice: bool,
|
||||
/// Sprite layers a shader pack draws in the translucent pass (`[[materials]]
|
||||
/// translucent`), by layer; empty without one.
|
||||
pub translucent_layers: Arc<[bool]>,
|
||||
@@ -563,6 +593,9 @@ pub struct Streaming {
|
||||
pub extra: Vec<((i32, i32), i32)>,
|
||||
/// `extra`'s squares as a grid, made again when `extra` changes.
|
||||
cover: Option<ExtraCover>,
|
||||
/// Set while `update` runs, after it refreshed `cover`: nothing else can
|
||||
/// change `extra` meanwhile, so `cover` answers without comparing them.
|
||||
cover_current: bool,
|
||||
/// Mesh the sections for drawing (false on a server thread).
|
||||
pub mesh: bool,
|
||||
/// The pregeneration band (`set_band`).
|
||||
@@ -997,6 +1030,7 @@ impl<G: ChunkGenerator> Jobs<G> {
|
||||
blend_radius: settings.blend_radius,
|
||||
smooth_lighting: settings.smooth_lighting,
|
||||
cutout_leaves: settings.cutout_leaves,
|
||||
opaque_ice: settings.opaque_ice,
|
||||
translucent_layers: &settings.translucent_layers,
|
||||
leaf_culling: settings.leaf_culling,
|
||||
assets: &context.assets,
|
||||
@@ -1111,6 +1145,7 @@ impl Streaming {
|
||||
biome_blend: 2,
|
||||
smooth_lighting: true,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: Arc::from([]),
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
light_tables,
|
||||
@@ -1123,6 +1158,7 @@ impl Streaming {
|
||||
relight_resent: false,
|
||||
extra: Vec::new(),
|
||||
cover: None,
|
||||
cover_current: false,
|
||||
mesh: true,
|
||||
band: None,
|
||||
band_ranked: None,
|
||||
@@ -1263,6 +1299,7 @@ impl Streaming {
|
||||
self.meshed.remove(&(x, z));
|
||||
self.lighting.remove(&(x, z));
|
||||
world.dirty_sections.take_column(x, z);
|
||||
world.entities_pending.remove(&(x, z));
|
||||
chunks.extend(world.chunks.remove(&(x, z)));
|
||||
}
|
||||
chunks
|
||||
@@ -1403,6 +1440,7 @@ impl Streaming {
|
||||
.block_names
|
||||
.extend(std::mem::take(&mut chunk.block_names));
|
||||
let entities = std::mem::take(&mut chunk.entities);
|
||||
world.entities_pending.insert((chunk.x, chunk.z));
|
||||
if !entities.is_empty() {
|
||||
world.loaded_entities.push(((chunk.x, chunk.z), entities));
|
||||
}
|
||||
@@ -1626,6 +1664,7 @@ impl Streaming {
|
||||
return (uploads, Vec::new());
|
||||
}
|
||||
ExtraCover::refresh(&mut self.cover, &self.extra);
|
||||
self.cover_current = true;
|
||||
// The sweeps below walk every loaded column and proto chunk. Run them
|
||||
// at once when the view moves and every eighth tick otherwise: a
|
||||
// column can stay a little past the edge without anyone noticing.
|
||||
@@ -1734,6 +1773,7 @@ impl Streaming {
|
||||
if let Some(chunk) = world.chunks.remove(key) {
|
||||
removed.push(chunk);
|
||||
}
|
||||
world.entities_pending.remove(key);
|
||||
self.lit.remove(key);
|
||||
self.meshed.remove(key);
|
||||
self.lighting.remove(key);
|
||||
@@ -1834,7 +1874,7 @@ impl Streaming {
|
||||
extra: &self.extra,
|
||||
forced: &self.forced,
|
||||
mod_forced: &self.mod_forced,
|
||||
cover: ExtraCover::matching(&self.cover, &self.extra),
|
||||
cover: ExtraCover::current(&self.cover, &self.extra, self.cover_current),
|
||||
band: self.band.as_ref(),
|
||||
};
|
||||
let (protos, generating) = (&self.protos, &self.generating);
|
||||
@@ -1866,29 +1906,24 @@ impl Streaming {
|
||||
})
|
||||
})
|
||||
};
|
||||
let keys: Vec<(i32, i32)> = self
|
||||
.protos
|
||||
.iter()
|
||||
.filter(|(k, p)| p.is_some() && wants(self, k.0, k.1))
|
||||
.map(|(k, _)| *k)
|
||||
.collect();
|
||||
let mut wanted: Vec<(u64, i64, i32, i32)> = keys
|
||||
.into_iter()
|
||||
.map(|(x, z)| (0, self.distance(x, z), x, z))
|
||||
.collect();
|
||||
// Chunks no ticket reached (forced and extra areas) go last, nearest first.
|
||||
wanted.sort_unstable_by_key(|w| w.1);
|
||||
for w in &mut wanted {
|
||||
w.0 = self.decoration_order.rank_or_append(w.2, w.3);
|
||||
}
|
||||
wanted.sort_unstable();
|
||||
// Out for the call: ranking reads the streamer's ranges.
|
||||
let mut order = std::mem::take(&mut self.decoration_order);
|
||||
let mut wanted = decoration_queue(
|
||||
&mut order,
|
||||
self.protos
|
||||
.iter()
|
||||
.filter(|&(&(x, z), proto)| proto.is_some() && wants(self, x, z))
|
||||
.map(|(&k, _)| k),
|
||||
|x, z| self.distance(x, z),
|
||||
);
|
||||
self.decoration_order = order;
|
||||
// Most wanted chunks are blocked, and the same neighbours are
|
||||
// asked about for each of them: what `waits_for` says is kept for
|
||||
// the pass (nothing it reads changes in it but `decorated`, which
|
||||
// updates the entry). Without it, scheduling at render distance 32
|
||||
// took most of the thread that runs it while the world loaded.
|
||||
let waiting: std::cell::RefCell<FxHashMap<(i32, i32), bool>> = Default::default();
|
||||
for (_, _, x, z) in wanted {
|
||||
while let Some(std::cmp::Reverse((_, x, z))) = wanted.pop() {
|
||||
if self.decorating >= workers * 2 {
|
||||
break;
|
||||
}
|
||||
@@ -1993,6 +2028,7 @@ impl Streaming {
|
||||
// A server draws nothing: its sections are never meshed.
|
||||
if !self.mesh {
|
||||
world.dirty_sections.clear();
|
||||
self.cover_current = false;
|
||||
return (uploads, removed);
|
||||
}
|
||||
// Meshing once the column and its neighbours are lit, and the column
|
||||
@@ -2013,7 +2049,7 @@ impl Streaming {
|
||||
extra: &self.extra,
|
||||
forced: &self.forced,
|
||||
mod_forced: &self.mod_forced,
|
||||
cover: ExtraCover::matching(&self.cover, &self.extra),
|
||||
cover: ExtraCover::current(&self.cover, &self.extra, self.cover_current),
|
||||
band: self.band.as_ref(),
|
||||
};
|
||||
self.waiting.retain(|&(x, sy, z)| {
|
||||
@@ -2093,6 +2129,7 @@ impl Streaming {
|
||||
blend_radius: self.biome_blend,
|
||||
smooth_lighting: self.smooth_lighting,
|
||||
cutout_leaves: self.cutout_leaves,
|
||||
opaque_ice: self.opaque_ice,
|
||||
translucent_layers: self.translucent_layers.clone(),
|
||||
leaf_culling: self.leaf_culling,
|
||||
},
|
||||
@@ -2100,6 +2137,7 @@ impl Streaming {
|
||||
);
|
||||
}
|
||||
}
|
||||
self.cover_current = false;
|
||||
(uploads, removed)
|
||||
}
|
||||
fn schedule_generation(&mut self, world: &World, workers: usize) {
|
||||
@@ -2110,7 +2148,7 @@ impl Streaming {
|
||||
extra: &self.extra,
|
||||
forced: &self.forced,
|
||||
mod_forced: &self.mod_forced,
|
||||
cover: ExtraCover::matching(&self.cover, &self.extra),
|
||||
cover: ExtraCover::current(&self.cover, &self.extra, self.cover_current),
|
||||
band: self.band.as_ref(),
|
||||
},
|
||||
world,
|
||||
@@ -2244,7 +2282,7 @@ impl Streaming {
|
||||
extra: &self.extra,
|
||||
forced: &self.forced,
|
||||
mod_forced: &self.mod_forced,
|
||||
cover: ExtraCover::matching(&self.cover, &self.extra),
|
||||
cover: ExtraCover::current(&self.cover, &self.extra, self.cover_current),
|
||||
band: self.band.as_ref(),
|
||||
}
|
||||
}
|
||||
@@ -2615,6 +2653,20 @@ impl ExtraCover {
|
||||
) -> Option<&'a ExtraCover> {
|
||||
cover.as_ref().filter(|c| c.extra == extra)
|
||||
}
|
||||
/// [`Self::matching`], told whether `cover` is known to match: the
|
||||
/// comparison reads every area, and `update` asks for each column it
|
||||
/// looks at (a server holds an area per player).
|
||||
fn current<'a>(
|
||||
cover: &'a Option<ExtraCover>,
|
||||
extra: &[((i32, i32), i32)],
|
||||
known: bool,
|
||||
) -> Option<&'a ExtraCover> {
|
||||
if known {
|
||||
debug_assert!(cover.as_ref().is_none_or(|c| c.extra == extra));
|
||||
return cover.as_ref();
|
||||
}
|
||||
Self::matching(cover, extra)
|
||||
}
|
||||
/// Rebuild the coverage index if `extra` changed; none for one area or none
|
||||
/// (a scan is as quick).
|
||||
fn refresh(cover: &mut Option<ExtraCover>, extra: &[((i32, i32), i32)]) {
|
||||
@@ -3173,6 +3225,64 @@ static NO_CHUNKS: std::sync::LazyLock<HashSet<(i32, i32)>> = std::sync::LazyLock
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The queue hands out the wanted chunks in the order the full sort did
|
||||
/// (every chunk measured, sorted by distance, the new ones ranked in that
|
||||
/// order, then all sorted by rank), and ranks the new ones the same way,
|
||||
/// whatever order the chunks are listed in.
|
||||
#[test]
|
||||
fn decoration_queue_pops_in_the_order_a_full_sort_gives() {
|
||||
// Distinct for every chunk, so the full sort has one answer.
|
||||
let distance = |x: i32, z: i32| i64::from(x) * 100_000 + i64::from(z);
|
||||
let full_sort = |order: &mut DecorationOrder, wanted: &[(i32, i32)]| {
|
||||
let mut w: Vec<(u64, i64, i32, i32)> = wanted
|
||||
.iter()
|
||||
.map(|&(x, z)| (0, distance(x, z), x, z))
|
||||
.collect();
|
||||
w.sort_unstable_by_key(|w| w.1);
|
||||
for w in &mut w {
|
||||
w.0 = order.rank_or_append(w.2, w.3);
|
||||
}
|
||||
w.sort_unstable();
|
||||
w.into_iter().map(|w| (w.2, w.3)).collect::<Vec<_>>()
|
||||
};
|
||||
let ranked = |order: &mut DecorationOrder| {
|
||||
order.reassign((0, 0), 6, |x, z| (x + z).rem_euclid(5) == 0);
|
||||
};
|
||||
let (mut fast, mut full) = (DecorationOrder::default(), DecorationOrder::default());
|
||||
ranked(&mut fast);
|
||||
ranked(&mut full);
|
||||
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
|
||||
let mut shuffled = |mut keys: Vec<(i32, i32)>| {
|
||||
for i in (1..keys.len()).rev() {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
keys.swap(i, (seed % (i as u64 + 1)) as usize);
|
||||
}
|
||||
keys
|
||||
};
|
||||
// Some of the player's ranked chunks, and an extra area's that no
|
||||
// ticket reached; the second time, more of the area, the first part
|
||||
// ranked by then.
|
||||
let mut wanted: Vec<(i32, i32)> = (-6i32..=6)
|
||||
.flat_map(|x| (-6..=6).map(move |z| (x, z)))
|
||||
.filter(|&(x, z)| (x * 7 + z).rem_euclid(3) != 0)
|
||||
.collect();
|
||||
wanted.extend((20..=24).flat_map(|x| (-3..=3).map(move |z| (x, z))));
|
||||
for round in 0..2 {
|
||||
if round == 1 {
|
||||
wanted.extend((25..=27).flat_map(|x| (-3..=3).map(move |z| (x, z))));
|
||||
}
|
||||
let wanted = shuffled(wanted.clone());
|
||||
let mut queue = decoration_queue(&mut fast, wanted.iter().copied(), distance);
|
||||
let popped: Vec<(i32, i32)> = std::iter::from_fn(|| queue.pop())
|
||||
.map(|std::cmp::Reverse((_, x, z))| (x, z))
|
||||
.collect();
|
||||
assert_eq!(popped, full_sort(&mut full, &wanted), "round {round}");
|
||||
assert_eq!(fast.ordered(), full.ordered(), "round {round}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resent_column_keeps_its_sections_but_takes_new_biomes() {
|
||||
let mut old = Chunk::empty(2, -3);
|
||||
|
||||
@@ -40,7 +40,10 @@ struct Globals {
|
||||
hdr_windows: array<vec4<f32>, 2>,
|
||||
// HDR lighting: x how strongly the sky lights the world now (`Sky::sky_light_factor`),
|
||||
// y what the sky and fog get: the same, but none under water; z 1 with HDR output on.
|
||||
// w: this entire frame writes light, so alpha is coverage rather than a mark.
|
||||
hdr_light: vec4<f32>,
|
||||
hdr_settings: vec4<f32>,
|
||||
hdr_shares: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> globals: Globals;
|
||||
@group(0) @binding(1) var sprites: texture_2d_array<f32>;
|
||||
@@ -199,26 +202,73 @@ fn sprite_at(tiled: vec2<f32>, layer: u32, du: vec2<f32>, dv: vec2<f32>) -> vec4
|
||||
return sample_nearest(uv, layer, pixel, du, dv, texel_screen, tiled_face);
|
||||
}
|
||||
|
||||
// The terrain stages' values, as the code reads them. What crosses from the vertex stage to the
|
||||
// fragment stage is `TerrainWire`: the same values in fewer interpolant locations.
|
||||
struct TerrainOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) @interpolate(flat) layer: u32,
|
||||
@location(2) color: vec4<f32>,
|
||||
@location(3) spherical: f32,
|
||||
@location(4) cylindrical: f32,
|
||||
clip: vec4<f32>,
|
||||
uv: vec2<f32>,
|
||||
layer: u32,
|
||||
color: vec4<f32>,
|
||||
spherical: f32,
|
||||
cylindrical: f32,
|
||||
// How the face shines in HDR output (mesher.rs's `EMISSION_BITS`): 0 not
|
||||
// at all, 1 to 4 the tier with its bright parts, 5 to 8 the tier all over.
|
||||
@location(5) @interpolate(flat) emission: u32,
|
||||
emission: u32,
|
||||
// HDR lighting: block and sky light, as levels 0 to 15.
|
||||
@location(11) scene: vec2<f32>,
|
||||
@location(6) @interpolate(flat) fade: u32,
|
||||
scene: vec2<f32>,
|
||||
fade: u32,
|
||||
// A distant tile's tile-relative position, for its fade (`faded`).
|
||||
@location(7) local: vec3<f32>,
|
||||
local: vec3<f32>,
|
||||
// terrain.vsh's chunkVisibility: the section's fade-in, from 16 blocks out.
|
||||
@location(8) visibility: f32,
|
||||
visibility: f32,
|
||||
// The camera-relative height, for the water mirror's cut at the plane.
|
||||
@location(9) height: f32,
|
||||
@location(10) @interpolate(flat) emissive_layer: u32,
|
||||
height: f32,
|
||||
emissive_layer: u32,
|
||||
}
|
||||
// `TerrainOut` in 5 interpolant locations instead of 12. A GPU's vertex stage pays for each
|
||||
// location it exports (AMD's parameter cache gives one a whole slot, however few of its four
|
||||
// components are used): on an RX 580 the 12 cost 3.2 ns per quad and these 5 cost 1.6. Every
|
||||
// component is still interpolated on its own (and the integers stay flat), so each pixel gets
|
||||
// the very values the separate locations gave it.
|
||||
struct TerrainWire {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
// The colour.
|
||||
@location(0) a: vec4<f32>,
|
||||
// `local` and `visibility`.
|
||||
@location(1) b: vec4<f32>,
|
||||
// `uv` and `scene`.
|
||||
@location(2) c: vec4<f32>,
|
||||
// `spherical`, `cylindrical` and `height`.
|
||||
@location(3) d: vec4<f32>,
|
||||
// `layer`, `emission`, `fade` and `emissive_layer`.
|
||||
@location(4) @interpolate(flat) e: vec4<u32>,
|
||||
}
|
||||
fn to_wire(o: TerrainOut) -> TerrainWire {
|
||||
var w: TerrainWire;
|
||||
w.clip = o.clip;
|
||||
w.a = o.color;
|
||||
w.b = vec4(o.local, o.visibility);
|
||||
w.c = vec4(o.uv, o.scene);
|
||||
w.d = vec4(o.spherical, o.cylindrical, o.height, 0.0);
|
||||
w.e = vec4(o.layer, o.emission, o.fade, o.emissive_layer);
|
||||
return w;
|
||||
}
|
||||
fn from_wire(w: TerrainWire) -> TerrainOut {
|
||||
var o: TerrainOut;
|
||||
o.clip = w.clip;
|
||||
o.color = w.a;
|
||||
o.local = w.b.xyz;
|
||||
o.visibility = w.b.w;
|
||||
o.uv = w.c.xy;
|
||||
o.scene = w.c.zw;
|
||||
o.spherical = w.d.x;
|
||||
o.cylindrical = w.d.y;
|
||||
o.height = w.d.z;
|
||||
o.layer = w.e.x;
|
||||
o.emission = w.e.y;
|
||||
o.fade = w.e.z;
|
||||
o.emissive_layer = w.e.w;
|
||||
return o;
|
||||
}
|
||||
// Whether a fading tile or section leaves this pixel out (gfx/lod.rs's
|
||||
// `fade_code`, `fade_out_code`). It fades in, or out, chunk by chunk: each
|
||||
@@ -333,9 +383,9 @@ fn fade_code(w: f32) -> u32 {
|
||||
@vertex fn vs_terrain(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
@location(5) origin: vec4<f32>) -> TerrainWire {
|
||||
return to_wire(terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w));
|
||||
}
|
||||
// Flat section draws (gfx/flat.rs): one draw per arena page over the frame's
|
||||
// index list. A section's vertices fill whole 256-vertex blocks of its page:
|
||||
@@ -346,10 +396,10 @@ fn fade_code(w: f32) -> u32 {
|
||||
@builtin(vertex_index) vertex: u32,
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>
|
||||
) -> TerrainOut {
|
||||
) -> TerrainWire {
|
||||
let origin = slot_origins[block_slots[vertex >> 8u]];
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
return to_wire(terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w));
|
||||
}
|
||||
// Solid terrain draws in two passes, each over its own part of every mesh's quads
|
||||
// (`mesher::Part`): first the quads with no cutout that are not fading, through
|
||||
@@ -370,38 +420,38 @@ fn left_out() -> TerrainOut {
|
||||
@vertex fn vs_terrain_opaque(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
if !opaque_quad(layer, origin.w) { return left_out(); }
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
@location(5) origin: vec4<f32>) -> TerrainWire {
|
||||
if !opaque_quad(layer, origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w));
|
||||
}
|
||||
@vertex fn vs_terrain_cutout(
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
if opaque_quad(layer, origin.w) { return left_out(); }
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
@location(5) origin: vec4<f32>) -> TerrainWire {
|
||||
if opaque_quad(layer, origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w));
|
||||
}
|
||||
@vertex fn vs_terrain_flat_opaque(
|
||||
@builtin(vertex_index) vertex: u32,
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>
|
||||
) -> TerrainOut {
|
||||
) -> TerrainWire {
|
||||
let origin = slot_origins[block_slots[vertex >> 8u]];
|
||||
if !opaque_quad(layer, origin.w) { return left_out(); }
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
if !opaque_quad(layer, origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w));
|
||||
}
|
||||
@vertex fn vs_terrain_flat_cutout(
|
||||
@builtin(vertex_index) vertex: u32,
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>
|
||||
) -> TerrainOut {
|
||||
) -> TerrainWire {
|
||||
let origin = slot_origins[block_slots[vertex >> 8u]];
|
||||
if opaque_quad(layer, origin.w) { return left_out(); }
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
if opaque_quad(layer, origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w));
|
||||
}
|
||||
// Solid section draws that read their vertices from the arena page (gfx.rs's `pulled`
|
||||
// pipelines; `mesher/packed.rs`): a draw's vertex indices below PACKED_BASE name vertices
|
||||
@@ -411,6 +461,12 @@ fn left_out() -> TerrainOut {
|
||||
// Binding 19: visbuffer.wgsl, in the same module, takes 2 to 18 of its own group 1.
|
||||
@group(1) @binding(19) var<storage, read> arena_page: array<vec2<u32>>;
|
||||
const PACKED_BASE: u32 = 0x10000000u;
|
||||
// Far tiles' own compact layout (`lod/packed.rs`), read apart from a section's by the base
|
||||
// its draws use: from FAR_BASE on, a corner of the quad whose far record is at that slot.
|
||||
const FAR_BASE: u32 = 0x20000000u;
|
||||
// `TerrainVertex::tiled`: a far tile's UVs are in blocks from the face's corner, this many
|
||||
// fixed-point steps each (`mesher::TILED_UV_STEPS`).
|
||||
const TILED_UV_STEPS: f32 = 32.0;
|
||||
// A vertex's words: position bits, then UV, layer with light, colour.
|
||||
struct ArenaCorner {
|
||||
p: vec3<u32>,
|
||||
@@ -471,6 +527,39 @@ fn packed_corner(lo: vec3<u32>, hi: vec3<u32>, corner: u32) -> ArenaCorner {
|
||||
return ArenaCorner(bitcast<vec3<u32>>(vec3<f32>(pos)),
|
||||
vec3((u << 10u) | (v << 26u), (hi.x & 0xffffu) | (packed_light << 16u), color));
|
||||
}
|
||||
// `lod::packed::Face::vertices`: corner `corner` of the far record whose words are lo (0 to
|
||||
// 2) and hi (3 to 5). A far quad is a rectangle in an axis-aligned plane with one colour,
|
||||
// one light and one layer, so the record holds the rectangle and this rebuilds the four
|
||||
// corners and the UVs `lod::mesh::emit_quad` derived from them. The two have to agree, and
|
||||
// `lod::mesh`'s `every_quad_the_mesher_emits_packs_and_comes_back` is what holds them to it.
|
||||
fn far_corner(lo: vec3<u32>, hi: vec3<u32>, corner: u32) -> ArenaCorner {
|
||||
let axis = hi.z & 3u;
|
||||
let reversed = (hi.z >> 2u) & 1u;
|
||||
let plane = f32(bitcast<i32>(lo.z));
|
||||
let u0 = f32(bitcast<i32>(hi.x));
|
||||
let v0 = f32(bitcast<i32>(hi.y));
|
||||
let u1 = u0 + f32((hi.z >> 3u) & 0x7ffu);
|
||||
let v1 = v0 + f32((hi.z >> 14u) & 0x7ffu);
|
||||
// `points.reverse()`, which is what the mesher does for Down, South and East.
|
||||
let c = select(corner, 3u - corner, reversed == 1u);
|
||||
let far_v = select(v0, v1, c <= 1u);
|
||||
// Axis X runs its u the other way round, as that function's `points` do.
|
||||
let first = c == 0u || c == 3u;
|
||||
let far_u = select(select(u1, u0, first), select(u0, u1, first), axis == 2u);
|
||||
// 0: the plane is y, u is x and v is z. 1: the plane is z, u is x, v is y.
|
||||
// 2: the plane is x, u is z and v is y.
|
||||
var pos = vec3(far_u, plane, far_v);
|
||||
if axis == 1u {
|
||||
pos = vec3(far_u, far_v, plane);
|
||||
} else if axis == 2u {
|
||||
pos = vec3(plane, far_v, far_u);
|
||||
}
|
||||
// The UV is `[u, -v]` of the face counted from its corner, which is `[u0, -v1]`: every
|
||||
// coordinate is whole, so the mesher's `floor` of the least corner is the corner itself.
|
||||
let uv = vec2<u32>(vec2(far_u - u0, v1 - far_v) * TILED_UV_STEPS);
|
||||
// Word 1 is the layer with the light, word 0 the colour, as a vertex holds them.
|
||||
return ArenaCorner(bitcast<vec3<u32>>(pos), vec3(uv.x | (uv.y << 16u), lo.y, lo.x));
|
||||
}
|
||||
// A solid draw's vertex `vertex`, and the slot its block is found by (`block_slots`, 256
|
||||
// slots a block): the vertex's own, or its quad's record's. `packed`: it has a record.
|
||||
struct SectionCorner {
|
||||
@@ -482,6 +571,18 @@ fn section_corner(vertex: u32) -> SectionCorner {
|
||||
if vertex < PACKED_BASE {
|
||||
return SectionCorner(arena_vertex(vertex), vertex, false);
|
||||
}
|
||||
if vertex >= FAR_BASE {
|
||||
let far_slot = (vertex - FAR_BASE) >> 2u;
|
||||
let a = arena_page[far_slot * 3u];
|
||||
let b = arena_page[far_slot * 3u + 1u];
|
||||
let c = arena_page[far_slot * 3u + 2u];
|
||||
// `FULL`: the quad's four vertices, word 0 slots on, as a section's spill.
|
||||
if (c.y & 0x80000000u) != 0u {
|
||||
return SectionCorner(arena_vertex(far_slot + a.x + (vertex & 3u)), far_slot, true);
|
||||
}
|
||||
return SectionCorner(far_corner(vec3(a.x, a.y, b.x), vec3(b.y, c.x, c.y), vertex & 3u),
|
||||
far_slot, true);
|
||||
}
|
||||
let slot = (vertex - PACKED_BASE) >> 2u;
|
||||
let corner = vertex & 3u;
|
||||
let a = arena_page[slot * 3u];
|
||||
@@ -511,37 +612,37 @@ fn corner_vertex(c: ArenaCorner, origin: vec4<f32>) -> TerrainOut {
|
||||
unorm8x4(c.r.z), light, origin.xyz, origin.w);
|
||||
}
|
||||
@vertex fn vs_section(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
return corner_vertex(section_corner(vertex).c, origin);
|
||||
@location(5) origin: vec4<f32>) -> TerrainWire {
|
||||
return to_wire(corner_vertex(section_corner(vertex).c, origin));
|
||||
}
|
||||
@vertex fn vs_section_opaque(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
@location(5) origin: vec4<f32>) -> TerrainWire {
|
||||
let s = section_corner(vertex);
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(corner_vertex(s.c, origin));
|
||||
}
|
||||
@vertex fn vs_section_cutout(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
@location(5) origin: vec4<f32>) -> TerrainWire {
|
||||
let s = section_corner(vertex);
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(corner_vertex(s.c, origin));
|
||||
}
|
||||
@vertex fn vs_section_flat(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
@vertex fn vs_section_flat(@builtin(vertex_index) vertex: u32) -> TerrainWire {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
return corner_vertex(s.c, origin);
|
||||
return to_wire(corner_vertex(s.c, origin));
|
||||
}
|
||||
@vertex fn vs_section_flat_opaque(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
@vertex fn vs_section_flat_opaque(@builtin(vertex_index) vertex: u32) -> TerrainWire {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(corner_vertex(s.c, origin));
|
||||
}
|
||||
@vertex fn vs_section_flat_cutout(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
@vertex fn vs_section_flat_cutout(@builtin(vertex_index) vertex: u32) -> TerrainWire {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return to_wire(left_out()); }
|
||||
return to_wire(corner_vertex(s.c, origin));
|
||||
}
|
||||
// HDR lighting: how far a surface's light carries it above paper white, 0 to 1. Only the
|
||||
// strongest light does, so dark stays as dark as in SDR; sky light by how the face meets the sun.
|
||||
@@ -620,11 +721,13 @@ fn hdr_mark(tier: u32, y: f32) -> f32 {
|
||||
let lift = smoothstep(globals.hdr_windows[0][t], globals.hdr_windows[1][t], y);
|
||||
return 0.175 * f32(3u - t) + 0.165 * (1.0 - lift);
|
||||
}
|
||||
@fragment fn fs_solid(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
@fragment fn fs_solid(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
return solid_color(in);
|
||||
}
|
||||
// `fs_solid` for the opaque pass (`opaque_quad`): no fade, no cutout, no discard.
|
||||
@fragment fn fs_opaque(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
@fragment fn fs_opaque(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
if HDR_LIGHTING && globals.hdr_light.z != 0.0 {
|
||||
hdr_normal = normalize(cross(dpdy(in.local), dpdx(in.local)));
|
||||
}
|
||||
@@ -635,7 +738,8 @@ fn hdr_mark(tier: u32, y: f32) -> f32 {
|
||||
}
|
||||
// The water mirror (gfx.rs's reflection pass): what lies below the water plane
|
||||
// (`fog_ends.w`, camera-relative) would show above it in the mirror, so it is cut away.
|
||||
@fragment fn fs_solid_mirror(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
@fragment fn fs_solid_mirror(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
if in.height < globals.fog_ends.w { discard; }
|
||||
return solid_color(in);
|
||||
}
|
||||
@@ -656,7 +760,15 @@ fn solid_color(in: TerrainOut) -> vec4<f32> {
|
||||
}
|
||||
// The rest of a solid pixel's colour once it is kept: emission and fog.
|
||||
fn solid_finish(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>) -> vec4<f32> {
|
||||
return solid_finish_at(in, texel, color, dpdx(in.uv), dpdy(in.uv));
|
||||
return solid_light(solid_finish_at(in, texel, color, dpdx(in.uv), dpdy(in.uv)));
|
||||
}
|
||||
// A finished solid pixel as the frame keeps it: its mark in alpha, or on the written lane its
|
||||
// light, alpha 1.
|
||||
fn solid_light(finished: vec4<f32>) -> vec4<f32> {
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(finished.rgb, finished.a, globals.hdr_settings, globals.hdr_shares), 1.0);
|
||||
}
|
||||
return finished;
|
||||
}
|
||||
// `solid_finish` with the UV's screen gradients given (`sprite_at`).
|
||||
fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<f32>,
|
||||
@@ -703,7 +815,8 @@ fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<
|
||||
// HDR lighting on what's mostly opaque among translucent faces (water), drawn again after
|
||||
// them with only alpha written: `fs_translucent` blends its alpha as coverage, so a lake's
|
||||
// value would be diluted. Clear glass leaves what's behind it, light sources included, alone.
|
||||
@fragment fn fs_translucent_mark(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
@fragment fn fs_translucent_mark(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
let dx = dpdx(in.local);
|
||||
let dy = dpdy(in.local);
|
||||
hdr_normal = normalize(cross(dy, dx));
|
||||
@@ -716,11 +829,26 @@ fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<
|
||||
// Water, glass and other see-through faces draw whole as they fade in, the
|
||||
// layer drawn first in front (gfx.rs's water tiers): dithered, the faded-out
|
||||
// pixels showed the layer behind through the one in front, twice the water.
|
||||
@fragment fn fs_translucent(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
var color = sprite(in.uv, in.layer & 0x7fffu) * in.color;
|
||||
@fragment fn fs_translucent(wire: TerrainWire) -> @location(0) vec4<f32> {
|
||||
let in = from_wire(wire);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
hdr_normal = normalize(cross(dpdy(in.local), dpdx(in.local)));
|
||||
}
|
||||
let texel = sprite(in.uv, in.layer & 0x7fffu);
|
||||
var color = texel * in.color;
|
||||
color = mix(globals.fog_color * vec4(1.0, 1.0, 1.0, color.a), color, in.visibility);
|
||||
if color.a < 0.01 { discard; }
|
||||
return apply_fog(color, in.spherical, in.cylindrical);
|
||||
let finished = apply_fog(color, in.spherical, in.cylindrical);
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
// The surface's own light, whatever its texel's alpha: what is behind it is light already.
|
||||
// (The marked frame keeps one mark a pixel, so there `fs_translucent_mark` skips clear
|
||||
// texels to leave what is behind them its own.)
|
||||
let fogged = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w)) * globals.fog_color.a;
|
||||
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;
|
||||
}
|
||||
|
||||
// Native still-fluid boxes use block sprites and lightmap without terrain AO/emission
|
||||
@@ -786,7 +914,8 @@ struct NativeModelOut {
|
||||
+ max(0.0, dot(light1, in.normal))) * 0.6 + 0.4);
|
||||
color = vec4(color.rgb * shade, color.a);
|
||||
}
|
||||
return apply_fog(color, in.spherical, in.cylindrical);
|
||||
let finished = apply_fog(color, in.spherical, in.cylindrical);
|
||||
return vec4(finished.rgb, select(finished.a, 1.0, HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0));
|
||||
}
|
||||
|
||||
// Sky (SkyRenderer): the 10-vertex sky disc fan at y = 16 (radius 512) in
|
||||
@@ -919,12 +1048,19 @@ fn sky_along_ray_base(d: vec3<f32>) -> vec3<f32> {
|
||||
}
|
||||
return color;
|
||||
}
|
||||
fn sky_finished(color: vec3<f32>) -> vec4<f32> {
|
||||
let mark = 1.0 - 0.24 * globals.hdr_light.y;
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(color, mark, globals.hdr_settings, globals.hdr_shares), 1.0);
|
||||
}
|
||||
return vec4(color, mark);
|
||||
}
|
||||
@fragment fn fs_sky(in: SkyOut) -> @location(0) vec4<f32> {
|
||||
// `hdr_scene_alpha`, spelled out: this part of the file also goes into shader packs' skies.
|
||||
return vec4(sky_along_ray(view_ray(in.ndc)), 1.0 - 0.24 * globals.hdr_light.y);
|
||||
return sky_finished(sky_along_ray(view_ray(in.ndc)));
|
||||
}
|
||||
@fragment fn fs_sky_base(in: SkyOut) -> @location(0) vec4<f32> {
|
||||
return vec4(sky_along_ray_base(view_ray(in.ndc)), 1.0 - 0.24 * globals.hdr_light.y);
|
||||
return sky_finished(sky_along_ray_base(view_ray(in.ndc)));
|
||||
}
|
||||
// The sunrise/sunset fan as geometry: the clip position is projected by the
|
||||
// CPU with the sky's unjittered view, and the apex weight (1 at the apex, 0 at
|
||||
@@ -937,7 +1073,7 @@ struct SunriseOut {
|
||||
return SunriseOut(clip, weight);
|
||||
}
|
||||
@fragment fn fs_sunrise(in: SunriseOut) -> @location(0) vec4<f32> {
|
||||
return vec4(globals.sunrise.rgb, in.weight * globals.sunrise.a);
|
||||
return vec4(sky_finished(globals.sunrise.rgb).rgb, in.weight * globals.sunrise.a);
|
||||
}
|
||||
// The first-person hand's depth clear inside the world pass: `vs_sky`'s triangle lies at the
|
||||
// far plane, written over the whole depth target with no colour.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! On-screen touch controls for touch-screen devices (Android first). Nothing
|
||||
//! here runs unless a touch surface is in use.
|
||||
pub mod layout;
|
||||
pub use layout::{Control, Mode, Placement, Settings};
|
||||
@@ -0,0 +1,429 @@
|
||||
// Adapted from Starling (https://github.com/starling-client/starling, `android` branch at
|
||||
// 150a414b09a1, `crates/horizon-client-render/src/touch/layout.rs`), written by Vasysik and the
|
||||
// Starling contributors. Copyright (c) 2026 Ludovic Marie and the Horizon contributors, under the
|
||||
// same MIT license as Horizon (LICENSE at the repository root).
|
||||
|
||||
//! The touch layout: which on-screen controls exist, where each one sits for
|
||||
//! each control scheme, and the player's saved choices. Coordinates are
|
||||
//! normalized to the surface; sizes are fractions of its height, so a button
|
||||
//! stays square on wide phones and on tablets. Nothing here reads input or
|
||||
//! draws: this is the file the touch controls and their editor share.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
/// How the player moves and aims.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Mode {
|
||||
/// A joystick moves; attack and use act on the crosshair.
|
||||
#[default]
|
||||
Crosshair,
|
||||
/// A joystick moves; tapping the world attacks or uses what was tapped.
|
||||
Tap,
|
||||
/// A D-pad moves; tapping the world acts on it (or the crosshair, split).
|
||||
Dpad,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub const ALL: [Self; 3] = [Self::Crosshair, Self::Tap, Self::Dpad];
|
||||
/// The four schemes a settings screen offers: mode, `split_controls`, label.
|
||||
pub const SCHEMES: [(Self, bool, &'static str); 4] = [
|
||||
(Self::Crosshair, false, "Joystick & aim crosshair"),
|
||||
(Self::Tap, false, "Joystick & tap to interact"),
|
||||
(Self::Dpad, false, "D-Pad & tap to interact"),
|
||||
(Self::Dpad, true, "D-Pad & aim crosshair"),
|
||||
];
|
||||
|
||||
/// This mode's row in [`Settings::layouts`].
|
||||
pub fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// The built-in controls. The order is the index into a layout row and into
|
||||
/// `button_visibility`, so new controls go at the end.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Control {
|
||||
Move,
|
||||
Sprint,
|
||||
Jump,
|
||||
Sneak,
|
||||
Attack,
|
||||
Use,
|
||||
Chat,
|
||||
Pause,
|
||||
Settings,
|
||||
Inventory,
|
||||
Debug,
|
||||
Perspective,
|
||||
Offhand,
|
||||
Shield,
|
||||
}
|
||||
|
||||
/// How many built-in controls a layout row holds.
|
||||
pub const CONTROLS: usize = Control::ALL.len();
|
||||
|
||||
impl Control {
|
||||
pub const ALL: [Self; 14] = [
|
||||
Self::Move,
|
||||
Self::Sprint,
|
||||
Self::Jump,
|
||||
Self::Sneak,
|
||||
Self::Attack,
|
||||
Self::Use,
|
||||
Self::Chat,
|
||||
Self::Pause,
|
||||
Self::Settings,
|
||||
Self::Inventory,
|
||||
Self::Debug,
|
||||
Self::Perspective,
|
||||
Self::Offhand,
|
||||
Self::Shield,
|
||||
];
|
||||
|
||||
/// The name the layout editor shows.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Move => "Movement",
|
||||
Self::Sprint => "Sprint",
|
||||
Self::Jump => "Jump / fly up",
|
||||
Self::Sneak => "Sneak / fly down",
|
||||
Self::Attack => "Attack / mine",
|
||||
Self::Use => "Use / place",
|
||||
Self::Chat => "Chat",
|
||||
Self::Pause => "Pause",
|
||||
Self::Settings => "Touch settings",
|
||||
Self::Inventory => "Inventory",
|
||||
Self::Debug => "Debug overlay (F3)",
|
||||
Self::Perspective => "Camera perspective (F5)",
|
||||
Self::Offhand => "Swap hands",
|
||||
Self::Shield => "Raise shield",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where one control sits: its centre (0..1 of the surface), its size (a
|
||||
/// fraction of the surface height) and its opacity.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Placement {
|
||||
pub center: [f32; 2],
|
||||
pub size: f32,
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub const fn new(x: f32, y: f32, size: f32) -> Self {
|
||||
Self {
|
||||
center: [x, y],
|
||||
size,
|
||||
opacity: 0.65,
|
||||
}
|
||||
}
|
||||
|
||||
/// The square control as `[x, y, w, h]` in normalized surface coordinates,
|
||||
/// for a surface `aspect` (width / height) wide.
|
||||
pub fn rect(self, aspect: f32) -> [f32; 4] {
|
||||
let w = self.size / aspect.max(0.1);
|
||||
[
|
||||
self.center[0] - w / 2.0,
|
||||
self.center[1] - self.size / 2.0,
|
||||
w,
|
||||
self.size,
|
||||
]
|
||||
}
|
||||
|
||||
/// Brings a value read from disk back on screen and into range.
|
||||
pub fn sanitize(&mut self) {
|
||||
self.size = finite_or(self.size, 0.15).clamp(0.045, 0.45);
|
||||
self.opacity = finite_or(self.opacity, 0.65).clamp(0.0, 1.0);
|
||||
for c in &mut self.center {
|
||||
*c = finite_or(*c, 0.5).clamp(0.025, 0.975);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `button_visibility` values: follow the scheme, always shown, always hidden.
|
||||
pub const FOLLOW_SCHEME: u8 = 0;
|
||||
pub const SHOWN: u8 = 1;
|
||||
pub const HIDDEN: u8 = 2;
|
||||
|
||||
/// The player's touch layout, saved as JSON. Missing fields take their
|
||||
/// defaults, so later options can be added without resetting a saved file.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub mode: Mode,
|
||||
/// With [`Mode::Dpad`]: attack and use act on the crosshair.
|
||||
pub split_controls: bool,
|
||||
/// Mirrors every control left to right.
|
||||
pub left_handed: bool,
|
||||
/// With [`Mode::Dpad`]: the jump and sneak buttons trade places.
|
||||
pub swap_jump_sneak: bool,
|
||||
pub sprint_button: bool,
|
||||
/// Sprint by pushing the joystick past its rim instead of with a button.
|
||||
pub sprint_with_joystick: bool,
|
||||
pub offhand_button: bool,
|
||||
pub shield_button: bool,
|
||||
/// Per control: [`FOLLOW_SCHEME`], [`SHOWN`] or [`HIDDEN`].
|
||||
pub button_visibility: [u8; CONTROLS],
|
||||
/// One row of placements per [`Mode`], indexed by [`Control`].
|
||||
pub layouts: [[Placement; CONTROLS]; 3],
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
let row = [
|
||||
Placement::new(0.13, 0.76, 0.32), // Move
|
||||
Placement::new(0.72, 0.78, 0.105), // Sprint
|
||||
Placement::new(0.92, 0.78, 0.16), // Jump
|
||||
Placement::new(0.81, 0.86, 0.115), // Sneak
|
||||
Placement::new(0.80, 0.64, 0.16), // Attack
|
||||
Placement::new(0.92, 0.52, 0.16), // Use
|
||||
Placement::new(0.46, 0.055, 0.075), // Chat
|
||||
Placement::new(0.50, 0.055, 0.075), // Pause
|
||||
Placement::new(0.54, 0.055, 0.075), // Settings
|
||||
Placement::new(0.73, 0.95, 0.075), // Inventory
|
||||
Placement::new(0.42, 0.055, 0.075), // Debug
|
||||
Placement::new(0.58, 0.055, 0.075), // Perspective
|
||||
Placement::new(0.80, 0.96, 0.075), // Offhand
|
||||
Placement::new(0.92, 0.36, 0.12), // Shield
|
||||
];
|
||||
let mut layouts = [row; 3];
|
||||
// The D-pad sits where the joystick does; sneak becomes its centre.
|
||||
layouts[Mode::Dpad.index()][Control::Sneak as usize] = Placement::new(0.13, 0.76, 0.10);
|
||||
layouts[Mode::Dpad.index()][Control::Jump as usize] = Placement::new(0.90, 0.78, 0.14);
|
||||
Self {
|
||||
mode: Mode::Crosshair,
|
||||
split_controls: false,
|
||||
left_handed: false,
|
||||
swap_jump_sneak: false,
|
||||
sprint_button: true,
|
||||
sprint_with_joystick: false,
|
||||
offhand_button: false,
|
||||
shield_button: true,
|
||||
button_visibility: [FOLLOW_SCHEME; CONTROLS],
|
||||
layouts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Reads `path`; a missing or unreadable file gives the defaults.
|
||||
pub fn load(path: &Path) -> Self {
|
||||
let mut settings: Self = std::fs::read(path)
|
||||
.ok()
|
||||
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
|
||||
.unwrap_or_default();
|
||||
settings.sanitize();
|
||||
settings
|
||||
}
|
||||
|
||||
/// Writes `path` through a temporary file, so a crash never leaves half a layout.
|
||||
pub fn save(&self, path: &Path) -> std::io::Result<()> {
|
||||
let temp = path.with_extension("json.tmp");
|
||||
std::fs::write(&temp, serde_json::to_vec_pretty(self)?)?;
|
||||
std::fs::rename(temp, path)
|
||||
}
|
||||
|
||||
pub fn sanitize(&mut self) {
|
||||
// Only the D-pad has a split variant (see `Mode::SCHEMES`).
|
||||
if self.mode != Mode::Dpad {
|
||||
self.split_controls = false;
|
||||
}
|
||||
for v in &mut self.button_visibility {
|
||||
if *v > HIDDEN {
|
||||
*v = FOLLOW_SCHEME;
|
||||
}
|
||||
}
|
||||
for p in self.layouts.iter_mut().flatten() {
|
||||
p.sanitize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether attack and use act on the crosshair rather than on a tap.
|
||||
pub fn crosshair(&self) -> bool {
|
||||
self.mode == Mode::Crosshair || self.split_controls
|
||||
}
|
||||
|
||||
pub fn visible(&self, c: Control) -> bool {
|
||||
match self.button_visibility[c as usize] {
|
||||
SHOWN => return true,
|
||||
HIDDEN => return false,
|
||||
_ => {}
|
||||
}
|
||||
match c {
|
||||
Control::Offhand => self.offhand_button,
|
||||
Control::Shield => self.shield_button && !self.visible(Control::Use),
|
||||
Control::Attack | Control::Use => self.crosshair(),
|
||||
Control::Sprint => {
|
||||
self.sprint_button && self.mode != Mode::Dpad && !self.sprint_with_joystick
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The layout slot a control is drawn in (jump and sneak may trade places).
|
||||
pub fn slot(&self, c: Control) -> usize {
|
||||
let swap = self.mode == Mode::Dpad && self.swap_jump_sneak;
|
||||
match c {
|
||||
Control::Jump if swap => Control::Sneak as usize,
|
||||
Control::Sneak if swap => Control::Jump as usize,
|
||||
_ => c as usize,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a control is in the current scheme, mirrored for left hands.
|
||||
pub fn placement(&self, c: Control) -> Placement {
|
||||
let mut p = self.layouts[self.mode.index()][self.slot(c)];
|
||||
if self.left_handed {
|
||||
p.center[0] = 1.0 - p.center[0];
|
||||
}
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `p` lies in the rectangle `[x, y, w, h]` (edges included).
|
||||
pub fn contains([x, y, w, h]: [f32; 4], p: [f32; 2]) -> bool {
|
||||
(x..=x + w).contains(&p[0]) && (y..=y + h).contains(&p[1])
|
||||
}
|
||||
|
||||
fn finite_or(value: f32, fallback: f32) -> f32 {
|
||||
if value.is_finite() { value } else { fallback }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_file(name: &str) -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("horizon-touch-{name}-{}.json", std::process::id()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_indices_match_layout_rows_and_schemes() {
|
||||
for (i, mode) in Mode::ALL.into_iter().enumerate() {
|
||||
assert_eq!(mode.index(), i);
|
||||
}
|
||||
for (mode, split, _) in Mode::SCHEMES {
|
||||
assert!(!split || mode == Mode::Dpad);
|
||||
}
|
||||
for (i, c) in Control::ALL.into_iter().enumerate() {
|
||||
assert_eq!(c as usize, i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheme_decides_attack_use_sprint_and_shield() {
|
||||
let mut s = Settings::default();
|
||||
assert!(s.visible(Control::Attack) && s.visible(Control::Use));
|
||||
assert!(s.visible(Control::Sprint));
|
||||
// The shield button only stands in when Use is hidden.
|
||||
assert!(!s.visible(Control::Shield));
|
||||
s.mode = Mode::Tap;
|
||||
assert!(!s.visible(Control::Attack) && !s.visible(Control::Use));
|
||||
assert!(s.visible(Control::Shield));
|
||||
s.mode = Mode::Dpad;
|
||||
assert!(!s.visible(Control::Sprint));
|
||||
s.split_controls = true;
|
||||
assert!(s.crosshair() && s.visible(Control::Attack));
|
||||
s.mode = Mode::Crosshair;
|
||||
s.sprint_with_joystick = true;
|
||||
assert!(!s.visible(Control::Sprint));
|
||||
assert!(!s.visible(Control::Offhand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_visibility_overrides_the_scheme_and_survives_a_save() {
|
||||
let mut s = Settings {
|
||||
mode: Mode::Tap,
|
||||
..Settings::default()
|
||||
};
|
||||
s.button_visibility[Control::Attack as usize] = SHOWN;
|
||||
s.button_visibility[Control::Chat as usize] = HIDDEN;
|
||||
assert!(s.visible(Control::Attack));
|
||||
assert!(!s.visible(Control::Chat));
|
||||
let file = temp_file("visibility");
|
||||
s.save(&file).unwrap();
|
||||
let read = Settings::load(&file);
|
||||
std::fs::remove_file(&file).unwrap();
|
||||
assert_eq!(read, s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_bad_or_partial_files_fall_back_to_defaults() {
|
||||
assert_eq!(Settings::load(&temp_file("absent")), Settings::default());
|
||||
let file = temp_file("bad");
|
||||
std::fs::write(&file, b"{ not json").unwrap();
|
||||
assert_eq!(Settings::load(&file), Settings::default());
|
||||
std::fs::write(&file, br#"{"mode":"dpad","left_handed":true}"#).unwrap();
|
||||
let read = Settings::load(&file);
|
||||
std::fs::remove_file(&file).unwrap();
|
||||
assert_eq!(read.mode, Mode::Dpad);
|
||||
assert!(read.left_handed);
|
||||
assert_eq!(read.layouts, Settings::default().layouts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_brings_controls_back_on_screen() {
|
||||
let mut s = Settings {
|
||||
mode: Mode::Tap,
|
||||
split_controls: true,
|
||||
..Settings::default()
|
||||
};
|
||||
s.button_visibility[0] = 9;
|
||||
s.layouts[0][0] = Placement {
|
||||
center: [f32::NAN, 7.0],
|
||||
size: 0.0,
|
||||
opacity: f32::INFINITY,
|
||||
};
|
||||
s.sanitize();
|
||||
assert!(!s.split_controls);
|
||||
assert_eq!(s.button_visibility[0], FOLLOW_SCHEME);
|
||||
let p = s.layouts[0][0];
|
||||
assert_eq!(p.center, [0.5, 0.975]);
|
||||
assert_eq!(p.size, 0.045);
|
||||
assert_eq!(p.opacity, 0.65);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_hands_mirror_and_dpad_can_swap_jump_and_sneak() {
|
||||
let mut s = Settings::default();
|
||||
let right = s.placement(Control::Jump).center;
|
||||
s.left_handed = true;
|
||||
assert_eq!(
|
||||
s.placement(Control::Jump).center,
|
||||
[1.0 - right[0], right[1]]
|
||||
);
|
||||
s.left_handed = false;
|
||||
s.mode = Mode::Dpad;
|
||||
s.swap_jump_sneak = true;
|
||||
let row = s.layouts[Mode::Dpad.index()];
|
||||
assert_eq!(s.placement(Control::Jump), row[Control::Sneak as usize]);
|
||||
assert_eq!(s.placement(Control::Sneak), row[Control::Jump as usize]);
|
||||
// Only the D-pad swaps.
|
||||
s.mode = Mode::Crosshair;
|
||||
assert_eq!(s.slot(Control::Jump), Control::Jump as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controls_stay_square_on_wide_surfaces() {
|
||||
let p = Placement::new(0.5, 0.5, 0.2);
|
||||
let [x, y, w, h] = p.rect(2.0);
|
||||
assert_eq!([w, h], [0.1, 0.2]);
|
||||
assert!(contains([x, y, w, h], [0.5, 0.5]));
|
||||
assert!(contains([x, y, w, h], [x + w, y + h]));
|
||||
assert!(!contains([x, y, w, h], [0.56, 0.5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_controls_are_on_screen_in_every_mode() {
|
||||
let s = Settings::default();
|
||||
for row in s.layouts {
|
||||
for p in row {
|
||||
let mut clamped = p;
|
||||
clamped.sanitize();
|
||||
assert_eq!(clamped, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,25 @@ pub fn blit_sprite(
|
||||
draw_sprite(batch, *r, nine_slice_metadata(sprite), [x, y, w, h], color);
|
||||
}
|
||||
}
|
||||
/// `GuiGraphicsExtractor.blit(location, x, y, u, v, width, height, textureWidth,
|
||||
/// textureHeight)`: a `src` region of a sprite — in its own texture's units, as the
|
||||
/// u/v/width/height arguments are — stretched over `w` × `h`.
|
||||
pub fn blit_sprite_region(
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
sprite: &str,
|
||||
src: [f32; 4],
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
color: u32,
|
||||
) {
|
||||
if let Some(r) = atlas.sprites.get(sprite) {
|
||||
let size = [r.w as f32, r.h as f32];
|
||||
batch.rect_uv(x, y, w, h, sub(*r, size, src), color);
|
||||
}
|
||||
}
|
||||
fn draw_sprite(
|
||||
batch: &mut HudBatch,
|
||||
r: Rect,
|
||||
@@ -575,6 +594,7 @@ pub fn action_button(
|
||||
label,
|
||||
active,
|
||||
hover,
|
||||
false,
|
||||
room,
|
||||
|rect| crate::widgets::Narratable::button(label.plain(), rect, active),
|
||||
);
|
||||
@@ -602,10 +622,46 @@ pub fn button_as(
|
||||
label,
|
||||
active,
|
||||
hover,
|
||||
false,
|
||||
0.0,
|
||||
narration,
|
||||
);
|
||||
}
|
||||
/// An options button whose focus highlights it without changing the mouse cursor.
|
||||
/// `AbstractWidget.handleCursor` tests hover, not `isHoveredOrFocused`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn focused_button(
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
rect: [f32; 4],
|
||||
label: &Text,
|
||||
active: bool,
|
||||
hover: bool,
|
||||
focused: bool,
|
||||
action: Option<crate::widgets::Action>,
|
||||
) {
|
||||
let room = if let Some(action) = action {
|
||||
crate::widgets::declare_action(action, rect, active);
|
||||
if active {
|
||||
crate::widgets::action_glyph_room()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
button_with_room(
|
||||
batch,
|
||||
atlas,
|
||||
rect,
|
||||
label,
|
||||
active,
|
||||
hover,
|
||||
focused,
|
||||
room,
|
||||
|rect| crate::widgets::Narratable::button(label.plain(), rect, active),
|
||||
);
|
||||
}
|
||||
/// A button whose label keeps `room` free at its left.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn button_with_room(
|
||||
@@ -615,6 +671,7 @@ fn button_with_room(
|
||||
label: &Text,
|
||||
active: bool,
|
||||
hover: bool,
|
||||
focused: bool,
|
||||
room: f32,
|
||||
narration: impl FnOnce([f32; 4]) -> crate::widgets::Narratable,
|
||||
) {
|
||||
@@ -624,11 +681,11 @@ fn button_with_room(
|
||||
if active && plain.trim().is_empty() {
|
||||
crate::widgets::target([x, y, w, h]);
|
||||
}
|
||||
crate::widgets::narratable(|| narration([x, y, w, h]).hovered(hover));
|
||||
crate::widgets::narratable(|| narration([x, y, w, h]).hovered(hover || focused));
|
||||
crate::widgets::widget_cursor(hover, active);
|
||||
let sprite = if !active {
|
||||
"gui/sprites/widget/button_disabled"
|
||||
} else if hover {
|
||||
} else if hover || focused {
|
||||
"gui/sprites/widget/button_highlighted"
|
||||
} else {
|
||||
"gui/sprites/widget/button"
|
||||
@@ -1258,6 +1315,29 @@ impl Caret {
|
||||
rect: [f32; 4],
|
||||
focused: bool,
|
||||
focused_at: Instant,
|
||||
) {
|
||||
self.draw_color(
|
||||
batch,
|
||||
atlas,
|
||||
(id, value, max),
|
||||
rect,
|
||||
focused,
|
||||
focused_at,
|
||||
rgba(224, 224, 224, 255),
|
||||
);
|
||||
}
|
||||
/// [`Self::draw`] with the text in `color` (`EditBox.setTextColor`): the
|
||||
/// settings tab turns an invalid Realm name red.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw_color(
|
||||
&mut self,
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
(id, value, max): (crate::clipboard::FieldId, &str, usize),
|
||||
rect: [f32; 4],
|
||||
focused: bool,
|
||||
focused_at: Instant,
|
||||
color: u32,
|
||||
) {
|
||||
let mut idle;
|
||||
let edit = if focused {
|
||||
@@ -1270,6 +1350,7 @@ impl Caret {
|
||||
};
|
||||
[edit.x, edit.y, edit.width, edit.height] = rect;
|
||||
edit.focused = focused;
|
||||
edit.text_color = color;
|
||||
if edit.focused_at < focused_at {
|
||||
edit.focused_at = focused_at;
|
||||
}
|
||||
|
||||
@@ -423,5 +423,5 @@ fn vis_corner(position: vec3<u32>, rest: vec3<u32>, origin: vec4<f32>) -> Terrai
|
||||
let texel = vis_sprite(t.uv, t.layer & 0x7fffu, du, dv);
|
||||
var color = texel * t.color;
|
||||
color = mix(globals.fog_color * vec4(1.0, 1.0, 1.0, color.a), color, t.visibility);
|
||||
return solid_finish_at(t, texel, color, du, dv);
|
||||
return solid_light(solid_finish_at(t, texel, color, du, dv));
|
||||
}
|
||||
@@ -75,6 +75,10 @@ pub struct World {
|
||||
pub light_edits: Vec<((i32, i32, i32), u32, u32)>,
|
||||
/// Saved entities of chunks that just loaded, spawned by the game.
|
||||
pub loaded_entities: Vec<((i32, i32), Vec<horizon_world::nbt::Tag>)>,
|
||||
/// Chunks loaded since the level's last pass, their entity sections not
|
||||
/// ready yet (`PersistentEntitySectionManager`'s PENDING), empty ones too:
|
||||
/// the pass ends by readying them (`processPendingLoads`).
|
||||
pub entities_pending: FxHashSet<(i32, i32)>,
|
||||
/// Chunks that just came into this level from a worker, and whether each
|
||||
/// was generated rather than read from the save: the game tells native mods.
|
||||
pub loaded_chunks: Vec<(i32, i32, bool)>,
|
||||
@@ -86,7 +90,7 @@ pub struct World {
|
||||
/// interaction range.
|
||||
pub chest_openers: FxHashMap<(i32, i32, i32), (i32, f64)>,
|
||||
/// `BaseContainerBlockEntity.name`: custom names of container block entities.
|
||||
pub block_names: FxHashMap<(i32, i32, i32), String>,
|
||||
pub block_names: FxHashMap<(i32, i32, i32), horizon_world::nbt::Tag>,
|
||||
/// States (by id) whose writes are recorded in `watched_writes` (sculk
|
||||
/// blocks, which need a block entity).
|
||||
pub watched: Vec<bool>,
|
||||
@@ -214,6 +218,7 @@ impl World {
|
||||
dirty_light: Default::default(),
|
||||
light_edits: Vec::new(),
|
||||
loaded_entities: Vec::new(),
|
||||
entities_pending: FxHashSet::default(),
|
||||
loaded_chunks: Vec::new(),
|
||||
modified: FxHashSet::default(),
|
||||
attachments: Default::default(),
|
||||
|
||||
@@ -147,9 +147,10 @@ fn sunrise_sectors_match_reference_on_real_gpu() {
|
||||
#[test]
|
||||
fn native_pack_sky_keeps_sunrise_helpers_private() {
|
||||
use wgsl_prefix::{TERRAIN, prefixed, section};
|
||||
// Use the same complete terrain sections and prefixer as native_sky.rs.
|
||||
// Use the same shared helper, complete terrain sections and prefixer as native_sky.rs.
|
||||
let sky = prefixed(
|
||||
&[
|
||||
include_str!("../src/hdr_light.wgsl"),
|
||||
section(TERRAIN, "struct Globals {", "@group(0) @binding(1)"),
|
||||
section(TERRAIN, "fn linear_fog(", "fn apply_fog("),
|
||||
section(TERRAIN, "struct SkyOut {", "// rendertype_lines"),
|
||||
@@ -179,7 +180,10 @@ fn native_pack_sky_keeps_sunrise_helpers_private() {
|
||||
|
||||
#[test]
|
||||
fn complete_terrain_shader_still_validates_for_fallback_sky() {
|
||||
let module = naga::front::wgsl::parse_str(wgsl_prefix::TERRAIN).unwrap();
|
||||
let module = naga::front::wgsl::parse_str(
|
||||
&(include_str!("../src/hdr_light.wgsl").to_owned() + wgsl_prefix::TERRAIN),
|
||||
)
|
||||
.unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
|
||||
@@ -459,5 +459,38 @@
|
||||
"horizon.extras.horizon_me.tooltip": "Mich als Horizon-Spieler zeigen: andere Horizon-Spieler sehen das Logo neben deinem Namen. An, sagt der Dienst jedem, der nach dir fragt, dass du in den letzten 3 Stunden Horizon gespielt hast; aus, meldet er dich nie.",
|
||||
"horizon.tempban.list.empty": "Es gibt keine befristeten Sperren",
|
||||
"horizon.tempban.list.count": "Es gibt %s befristete Sperre(n):",
|
||||
"horizon.tempban.list.entry": "%1$s wurde von %2$s gesperrt, %3$s (bis %4$s)"
|
||||
"horizon.tempban.list.entry": "%1$s wurde von %2$s gesperrt, %3$s (bis %4$s)",
|
||||
"horizon.studio.timeline.freecam_on": "Freie Kamera: EIN",
|
||||
"horizon.studio.timeline.freecam_off": "Freie Kamera: AUS",
|
||||
"horizon.studio.timeline.freecam_short": "Cam",
|
||||
"horizon.studio.timeline.freecam_active_short": "Cam ✓",
|
||||
"horizon.studio.timeline.add_keyframe_short": "+ Bild",
|
||||
"horizon.studio.timeline.delete_keyframe_short": "− Löschen",
|
||||
"horizon.studio.timeline.split": "Teilen",
|
||||
"horizon.studio.timeline.marker": "Markierung %s",
|
||||
"horizon.studio.timeline.seconds": "%s s",
|
||||
"horizon.studio.timeline.scene_count": "%s Szenen",
|
||||
"horizon.studio.timeline.scene_title": "Szene %s",
|
||||
"horizon.studio.timeline.sequence": "Sequenz",
|
||||
"horizon.studio.topbar.title": "✦ Studio",
|
||||
"horizon.studio.topbar.file": "Datei",
|
||||
"horizon.studio.topbar.camera": "Kamera",
|
||||
"horizon.studio.topbar.view": "Ansicht",
|
||||
"horizon.studio.topbar.file_short": "Datei",
|
||||
"horizon.studio.topbar.camera_short": "Cam",
|
||||
"horizon.studio.topbar.view_short": "Ans.",
|
||||
"horizon.studio.topbar.game_pause_menu": "⏸ Pausenmenü",
|
||||
"horizon.studio.topbar.pause_short": "Pause",
|
||||
"horizon.studio.topbar.open_recordings": "📁 Aufnahmeordner öffnen",
|
||||
"horizon.studio.topbar.close_replay": "✕ Wiedergabe schließen",
|
||||
"horizon.studio.topbar.exit": "Beenden",
|
||||
"horizon.studio.topbar.toggle_freecam": "Freie Kamera umschalten",
|
||||
"horizon.studio.topbar.reset_to_player": "👤 Auf Spieler zurücksetzen",
|
||||
"horizon.studio.topbar.unlock_freecam": "🔓 Freie Kamera entsperren",
|
||||
"horizon.studio.topbar.lock_to_spline": "🔒 An Pfad fixieren",
|
||||
"horizon.studio.topbar.add_keyframe": "➕ Schlüsselbild hinzufügen",
|
||||
"horizon.studio.topbar.add_marker": "Markierung hinzufügen",
|
||||
"horizon.studio.topbar.clear_keyframes": "🗑 Schlüsselbilder löschen",
|
||||
"horizon.studio.topbar.timeline_hud": "⏱ Zeitleiste",
|
||||
"horizon.studio.topbar.split_scene": "Szene teilen"
|
||||
}
|
||||
@@ -63,6 +63,8 @@
|
||||
"horizon.options.look_sensitivity": "Look Sensitivity",
|
||||
"horizon.options.invert_x": "Invert Look X",
|
||||
"horizon.options.invert_y": "Invert Look Y",
|
||||
"horizon.options.craft_on_servers": "Quick Craft on Servers",
|
||||
"horizon.controller.direct_craft": "Quick Craft",
|
||||
"horizon.options.sneak_button": "Sneak Button",
|
||||
"horizon.options.move_deadzone": "Move Deadzone",
|
||||
"horizon.options.look_deadzone": "Look Deadzone",
|
||||
@@ -313,6 +315,7 @@
|
||||
"How far away mobs and items are drawn, as a share of normal.": "How far away mobs and items are drawn, as a share of normal.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "Skips terrain hidden behind closed sections. Off costs a lot.",
|
||||
"Culls nearby terrain on the GPU: less CPU at high render distance, more GPU.": "Culls nearby terrain on the GPU: less CPU at high render distance, more GPU.",
|
||||
"Auto: tries it where your CPU limits the frame, keeps it if frames get faster.": "Auto: tries it where your CPU limits the frame, keeps it if frames get faster.",
|
||||
"Hides entities behind opaque blocks.": "Hides entities behind opaque blocks.",
|
||||
"No entity occlusion culling.": "No entity occlusion culling.",
|
||||
"CPU occlusion test runs this frame.": "CPU occlusion test runs this frame.",
|
||||
@@ -575,6 +578,8 @@
|
||||
"Reuses statue geometry until its block state changes.": "Reuses statue geometry until its block state changes.",
|
||||
"Leaf Culling": "Leaf Culling",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "Every leaf face. Vanilla's fancy leaves.",
|
||||
"Opaque Ice": "Opaque Ice",
|
||||
"Draws ice in the solid pass, without its see-through texture.": "Draws ice in the solid pass, without its see-through texture.",
|
||||
"Distant Plant Dither": "Distant Plant Dither",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "Keeps fine plants in high-res packs from fading far away.",
|
||||
"Screenshot Quality": "Screenshot Quality",
|
||||
@@ -975,6 +980,7 @@
|
||||
"A DualSense's light shows your health, from green through amber to red.": "A DualSense's light shows your health, from green through amber to red.",
|
||||
"A DualSense's light changes with the dimension you're in.": "A DualSense's light changes with the dimension you're in.",
|
||||
"A DualSense keeps its own light.": "A DualSense keeps its own light.",
|
||||
"Lets LT + A craft on servers other than your own worlds. Some servers count it as a macro.": "Lets LT + A craft on servers other than your own worlds. Some servers count it as a macro.",
|
||||
"Show the buttons for what you can do, while you play with a controller.": "Show the buttons for what you can do, while you play with a controller.",
|
||||
"How fast the stick moves the crosshair in inventories.": "How fast the stick moves the crosshair in inventories.",
|
||||
"How fast the right stick scrolls a list.": "How fast the right stick scrolls a list.",
|
||||
@@ -1006,5 +1012,38 @@
|
||||
"horizon.recipe_transfer.missing": "Missing Items",
|
||||
"horizon.recipe_transfer.no_room": "Inventory full",
|
||||
"horizon.recipe_transfer.unavailable": "Recipe does not fit this menu",
|
||||
"horizon.recipe_transfer.unsupported": "Server requires a known recipe"
|
||||
"horizon.recipe_transfer.unsupported": "Server requires a known recipe",
|
||||
"horizon.studio.timeline.freecam_on": "Freecam: ON",
|
||||
"horizon.studio.timeline.freecam_off": "Freecam: OFF",
|
||||
"horizon.studio.timeline.freecam_short": "Cam",
|
||||
"horizon.studio.timeline.freecam_active_short": "Cam ✓",
|
||||
"horizon.studio.timeline.add_keyframe_short": "+ Key",
|
||||
"horizon.studio.timeline.delete_keyframe_short": "− Del",
|
||||
"horizon.studio.timeline.split": "Split",
|
||||
"horizon.studio.timeline.marker": "Marker %s",
|
||||
"horizon.studio.timeline.seconds": "%s s",
|
||||
"horizon.studio.timeline.scene_count": "%s Scenes",
|
||||
"horizon.studio.timeline.scene_title": "Scene %s",
|
||||
"horizon.studio.timeline.sequence": "Sequence",
|
||||
"horizon.studio.topbar.title": "✦ Studio",
|
||||
"horizon.studio.topbar.file": "File",
|
||||
"horizon.studio.topbar.camera": "Camera",
|
||||
"horizon.studio.topbar.view": "View",
|
||||
"horizon.studio.topbar.file_short": "File",
|
||||
"horizon.studio.topbar.camera_short": "Cam",
|
||||
"horizon.studio.topbar.view_short": "View",
|
||||
"horizon.studio.topbar.game_pause_menu": "⏸ Pause Menu",
|
||||
"horizon.studio.topbar.pause_short": "Pause",
|
||||
"horizon.studio.topbar.open_recordings": "📁 Open Recordings Folder",
|
||||
"horizon.studio.topbar.close_replay": "✕ Close Replay",
|
||||
"horizon.studio.topbar.exit": "Exit",
|
||||
"horizon.studio.topbar.toggle_freecam": "Toggle Freecam",
|
||||
"horizon.studio.topbar.reset_to_player": "👤 Reset to Player",
|
||||
"horizon.studio.topbar.unlock_freecam": "🔓 Unlock Freecam",
|
||||
"horizon.studio.topbar.lock_to_spline": "🔒 Lock to Spline",
|
||||
"horizon.studio.topbar.add_keyframe": "➕ Add Keyframe",
|
||||
"horizon.studio.topbar.add_marker": "Add Marker",
|
||||
"horizon.studio.topbar.clear_keyframes": "🗑 Clear Keyframes",
|
||||
"horizon.studio.topbar.timeline_hud": "⏱ Timeline",
|
||||
"horizon.studio.topbar.split_scene": "Split Scene"
|
||||
}
|
||||
@@ -196,10 +196,10 @@
|
||||
"horizon.pages.split_profiles": "Кто играет?",
|
||||
"horizon.pages.split_profiles.tooltip": "Кто играет: профиль, хранимый в этой папке игры.",
|
||||
"horizon.shader_options.profile": "Профиль",
|
||||
"Particle Culling": "Particle Culling",
|
||||
"Draws every particle.": "Draws every particle.",
|
||||
"Skips offscreen, subpixel and terrain-hidden particles.": "Skips offscreen, subpixel and terrain-hidden particles.",
|
||||
"Skips offscreen, subpixel and terrain-hidden particles with less render-thread work.": "Skips offscreen, subpixel and terrain-hidden particles with less render-thread work.",
|
||||
"Particle Culling": "Отсечение частиц",
|
||||
"Draws every particle.": "Рисует каждую частицу.",
|
||||
"Skips offscreen, subpixel and terrain-hidden particles.": "Пропускает частицы за экраном, субпиксельные и скрытые ландшафтом.",
|
||||
"Skips offscreen, subpixel and terrain-hidden particles with less render-thread work.": "Пропускает частицы за экраном, субпиксельные и скрытые ландшафтом с меньшей нагрузкой на поток рендеринга.",
|
||||
"General": "Общие",
|
||||
"Advanced": "Дополнительно",
|
||||
"Undo": "Отменить",
|
||||
|
||||
@@ -225,6 +225,7 @@ fn mesher<'a>(context: &'a WorkerContext, seed: i64) -> Mesher<'a> {
|
||||
blend_radius: 2,
|
||||
smooth_lighting: true,
|
||||
cutout_leaves: true,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
assets: &context.assets,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Environment attribute layer stacks for tools/test_environment_system_parity.py: per case, a
|
||||
//! dimension's attributes, the biome layer and the weather layers, sampled at a position whose
|
||||
//! biome mix comes from the Gaussian sampler over a test grid of biomes.
|
||||
use horizon_world::environment_attributes::{ATTRIBUTES, AttributeMap, Kind, Value};
|
||||
use horizon_world::environment_system::{BiomeBlend, Builder, Conditions, gaussian_sample};
|
||||
use horizon_world::environment_timeline::Timeline;
|
||||
use serde_json::Value as Json;
|
||||
use std::{collections::HashMap, error::Error, fmt::Write};
|
||||
|
||||
fn bits(f: f32) -> String {
|
||||
format!("{:08x}", f.to_bits())
|
||||
}
|
||||
|
||||
fn format(value: &Value) -> String {
|
||||
match value {
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Float(f) => bits(*f),
|
||||
Value::Rgb(v) => v.map(bits).join(" "),
|
||||
Value::Argb(v) => v.map(bits).join(" "),
|
||||
Value::Other(json) => json.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's test grid: a cell's biome is picked by a hash of its coordinates.
|
||||
fn cell(grid: &[String], x: i32, y: i32, z: i32) -> &str {
|
||||
let hash = (x
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(y)
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(z))
|
||||
& i32::MAX;
|
||||
&grid[hash as usize % grid.len()]
|
||||
}
|
||||
|
||||
fn run(
|
||||
maps: &HashMap<String, AttributeMap>,
|
||||
timelines: &[Timeline],
|
||||
case: &Json,
|
||||
output: &mut String,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let strings = |key: &str| -> Vec<String> {
|
||||
case[key]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|v| v.as_str().map(str::to_owned))
|
||||
.collect()
|
||||
};
|
||||
let grid = strings("grid");
|
||||
let position: Vec<f64> = case["position"]
|
||||
.as_array()
|
||||
.ok_or("position")?
|
||||
.iter()
|
||||
.filter_map(Json::as_f64)
|
||||
.collect();
|
||||
let map = |id: &str| maps.get(id).ok_or_else(|| format!("no map {id}"));
|
||||
|
||||
let mut builder = Builder::default();
|
||||
builder.constant_layer(map(case["dimension"].as_str().ok_or("dimension")?)?);
|
||||
let biomes = strings("biomes")
|
||||
.iter()
|
||||
.map(|id| map(id))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
builder.biome_layer(biomes);
|
||||
if case["timelines"].as_bool() == Some(true) {
|
||||
for timeline in timelines {
|
||||
builder.timeline_layer(timeline);
|
||||
}
|
||||
}
|
||||
if case["weather"].as_bool() == Some(true) {
|
||||
builder.weather_layers();
|
||||
}
|
||||
let system = builder.build();
|
||||
|
||||
let quart = |c: f64| (c / 4.0).floor() as i32;
|
||||
let here = map(cell(
|
||||
&grid,
|
||||
quart(position[0]),
|
||||
quart(position[1]),
|
||||
quart(position[2]),
|
||||
))?;
|
||||
let mut blend = BiomeBlend::default();
|
||||
let blended = case["blend"].as_bool() == Some(true);
|
||||
if blended {
|
||||
let scaled = [position[0] * 0.25, position[1] * 0.25, position[2] * 0.25];
|
||||
gaussian_sample(
|
||||
scaled,
|
||||
|x, y, z| &maps[cell(&grid, x, y, z)],
|
||||
|w, m| blend.accumulate(w, m),
|
||||
);
|
||||
}
|
||||
let biome = |_full: bool| Some(here);
|
||||
let conditions = Conditions {
|
||||
rain: case["rain"].as_f64().ok_or("rain")? as f32,
|
||||
thunder: case["thunder"].as_f64().ok_or("thunder")? as f32,
|
||||
blend: blended.then_some(&blend),
|
||||
biome: &biome,
|
||||
clock: &|_| case["ticks"].as_i64().unwrap_or(0),
|
||||
};
|
||||
for attribute in ATTRIBUTES.iter().filter(|a| a.kind != Kind::Other) {
|
||||
let value = system.value(attribute, &conditions).ok_or("no value")?;
|
||||
let dimension = system
|
||||
.dimension_value(attribute, &conditions)
|
||||
.ok_or("no value")?;
|
||||
writeln!(
|
||||
output,
|
||||
"minecraft:{}\t{}\t{}",
|
||||
attribute.id,
|
||||
format(&value),
|
||||
format(&dimension)
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1])?;
|
||||
let mut lines = input.lines();
|
||||
let data: Json = serde_json::from_str(lines.next().ok_or("no data")?)?;
|
||||
let mut maps = HashMap::new();
|
||||
let mut timelines = Vec::new();
|
||||
for (id, json) in data.as_object().ok_or("data")? {
|
||||
if id.starts_with("timeline:") {
|
||||
timelines.push(Timeline::parse(json).map_err(|e| format!("{id}: {e}"))?);
|
||||
continue;
|
||||
}
|
||||
maps.insert(
|
||||
id.clone(),
|
||||
AttributeMap::parse(json).map_err(|e| format!("{id}: {e}"))?,
|
||||
);
|
||||
}
|
||||
let mut output = String::new();
|
||||
for line in lines {
|
||||
run(&maps, &timelines, &serde_json::from_str(line)?, &mut output)?;
|
||||
}
|
||||
std::fs::write(&args[2], output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Easings and timeline tracks for tools/test_environment_timeline_parity.py.
|
||||
use horizon_world::environment_attributes::{Entry, Value, attribute};
|
||||
use horizon_world::environment_timeline::{Easing, Track};
|
||||
use std::{error::Error, fmt::Write};
|
||||
|
||||
fn bits(f: f32) -> String {
|
||||
format!("{:08x}", f.to_bits())
|
||||
}
|
||||
|
||||
fn format(value: &Value) -> String {
|
||||
match value {
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Float(f) => bits(*f),
|
||||
Value::Rgb(v) => v.map(bits).join(" "),
|
||||
Value::Argb(v) => v.map(bits).join(" "),
|
||||
Value::Other(json) => json.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ease(json: &str, x: f32) -> Result<String, Box<dyn Error>> {
|
||||
Ok(match Easing::parse(&serde_json::from_str(json)?) {
|
||||
Ok(easing) => bits(easing.apply(x)),
|
||||
Err(_) => "error".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn track(
|
||||
id: &str,
|
||||
period: &str,
|
||||
json: &str,
|
||||
base: &str,
|
||||
ticks: &str,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
let attribute = attribute(id).ok_or("unknown attribute")?;
|
||||
let base = match Entry::parse(attribute, &serde_json::from_str(base)?)? {
|
||||
Entry::Override(value) => value,
|
||||
_ => return Err("the base is not a plain value".into()),
|
||||
};
|
||||
let period = if period == "-" {
|
||||
None
|
||||
} else {
|
||||
Some(period.parse()?)
|
||||
};
|
||||
let Ok(track) = Track::parse(attribute, &serde_json::from_str(json)?) else {
|
||||
return Ok("error".into());
|
||||
};
|
||||
if let Some(p) = period
|
||||
&& track.validate_period(p).is_err()
|
||||
{
|
||||
return Ok("error".into());
|
||||
}
|
||||
let sampler = track.sampler(period);
|
||||
let values: Vec<String> = ticks
|
||||
.split(',')
|
||||
.map(|t| t.parse::<i64>().map(|t| format(&sampler.apply(&base, t))))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(values.join(";"))
|
||||
}
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut output = String::new();
|
||||
for line in std::fs::read_to_string(&args[1])?.lines() {
|
||||
let f: Vec<&str> = line.split('\t').collect();
|
||||
let result = match f[0] {
|
||||
"ease" => ease(f[1], f[2].parse()?)?,
|
||||
"track" => track(f[1], f[2], f[3], f[4], f[5])?,
|
||||
_ => return Err("unknown case".into()),
|
||||
};
|
||||
writeln!(output, "{result}")?;
|
||||
}
|
||||
std::fs::write(&args[2], output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Dropped-item environmental damage, without a game or a server.
|
||||
use horizon_client::{
|
||||
assets::Assets, blocks::BlockFacts, chunk::Chunk, entities::ItemEntity, items::Items,
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::geometry::Vec3;
|
||||
use horizon_world::loot::PackOverlay;
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let root = Path::new(args.get(1).ok_or("missing resources")?);
|
||||
let assets = Assets::load(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let items = Items::load(root, assets.registry, &facts, &PackOverlay::NONE)?;
|
||||
let mut world = World::new(facts.air, 1);
|
||||
world.chunks.insert((0, 0), Arc::new(Chunk::empty(0, 0)));
|
||||
let mut output = String::new();
|
||||
for line in fs::read_to_string(args.get(2).ok_or("missing inputs")?)?.lines() {
|
||||
let f: Vec<_> = line.split('\t').collect();
|
||||
if f.len() != 6 {
|
||||
return Err("expected six tab-separated columns".into());
|
||||
}
|
||||
let position = Vec3::new(f[2].parse()?, f[3].parse()?, f[4].parse()?);
|
||||
let block = if f[5] == "empty" {
|
||||
"minecraft:cauldron"
|
||||
} else {
|
||||
"minecraft:lava_cauldron"
|
||||
};
|
||||
let secondary = match f[5] {
|
||||
"pair" => "minecraft:lava_cauldron",
|
||||
"mixed" => "minecraft:lava",
|
||||
_ => "minecraft:air",
|
||||
};
|
||||
world.set(
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
assets
|
||||
.registry
|
||||
.default_state(secondary)
|
||||
.ok_or("missing secondary block")?,
|
||||
)?;
|
||||
world.set(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
assets
|
||||
.registry
|
||||
.default_state(block)
|
||||
.ok_or("missing cauldron")?,
|
||||
)?;
|
||||
let mut item = ItemEntity {
|
||||
rotation: [0.0; 2],
|
||||
join: Default::default(),
|
||||
health: 5,
|
||||
fire_ticks: 0,
|
||||
id: 1,
|
||||
stack: horizon_client::item_data::parse_stack(f[1])?.ok_or("empty fixture stack")?,
|
||||
thrower: None,
|
||||
target: None,
|
||||
position,
|
||||
previous: position,
|
||||
velocity: Vec3::ZERO,
|
||||
on_ground: false,
|
||||
age: 0,
|
||||
pickup_delay: 0,
|
||||
bob_offset: 0.0,
|
||||
ticks: 0,
|
||||
needs_sync: false,
|
||||
stuck: Vec3::ZERO,
|
||||
was_touching_water: false,
|
||||
no_gravity: true,
|
||||
vertical_collision: false,
|
||||
horizontal_collision: false,
|
||||
};
|
||||
output.push_str(f[0]);
|
||||
for _ in 0..2 {
|
||||
if item.health > 0 {
|
||||
item.environment_tick(&world, &facts, assets.registry, &items);
|
||||
}
|
||||
output.push_str(&format!(
|
||||
"\t{},{},{}",
|
||||
item.health,
|
||||
item.fire_ticks,
|
||||
item.health <= 0
|
||||
));
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
fs::write(args.get(3).ok_or("missing output")?, output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -48,6 +48,8 @@ mod entity_particles_probe;
|
||||
mod entity_sound_packet_fixture;
|
||||
mod environment_attribute_modifier_fixture;
|
||||
mod environment_fixture;
|
||||
mod environment_system_fixture;
|
||||
mod environment_timeline_fixture;
|
||||
mod falling_block_fixture;
|
||||
mod finalize_spawn_fixture;
|
||||
mod fire_starter_sound_probe;
|
||||
@@ -66,6 +68,7 @@ mod input_probe;
|
||||
mod interaction_input_fixture;
|
||||
mod lan_probe;
|
||||
mod late_entity_pair_fixture;
|
||||
mod lava_cauldron_item_fixture;
|
||||
mod light_parity;
|
||||
mod lore_tooltip_fixture;
|
||||
mod marker_particles_probe;
|
||||
@@ -144,6 +147,7 @@ probes! {
|
||||
stack_mining_probe,
|
||||
cushion_placement_probe,
|
||||
cauldron_fixture,
|
||||
lava_cauldron_item_fixture,
|
||||
chat_screen_fixture,
|
||||
chest_pair_fixture,
|
||||
chest_load_fixture,
|
||||
@@ -163,6 +167,8 @@ probes! {
|
||||
entity_particles_probe,
|
||||
environment_attribute_modifier_fixture,
|
||||
environment_fixture,
|
||||
environment_system_fixture,
|
||||
environment_timeline_fixture,
|
||||
finalize_spawn_fixture,
|
||||
fire_starter_sound_probe,
|
||||
zombie_cure_sound_probe,
|
||||
|
||||
@@ -1443,7 +1443,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
// What a copper golem did to chests, before the next mob ticks.
|
||||
for e in &events[from.min(events.len())..] {
|
||||
match e {
|
||||
MobEvent::ChestOpen { chest, open } => {
|
||||
MobEvent::ChestOpen { chest, open, .. } => {
|
||||
let o = world
|
||||
.chest_openers
|
||||
.entry((chest[0], chest[1], chest[2]))
|
||||
|
||||
Loaded 100 of 576 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user