mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 17:19:10 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2325ba52d4 |
No files matched your search
@@ -25,8 +25,7 @@
|
||||
|
||||
<!-- "none", or for each project you read or copied from: its link at the commit, its license (only MIT,
|
||||
BSD-2-Clause, BSD-3-Clause, Apache-2.0, ISC, zlib, 0BSD, MIT-0, Unlicense or CC0-1.0), and what you took.
|
||||
CONTRIBUTING.md "Only original code". Shader packs: also the sections of docs/cleanroom/shader-packs-spec.md,
|
||||
the documentation pages and the packs you used. -->
|
||||
CONTRIBUTING.md "Only original code". -->
|
||||
|
||||
## Self-check
|
||||
|
||||
|
||||
@@ -14,4 +14,3 @@
|
||||
84875247 Online5277 # added by Ludovic, 2026-10-27 (his top three by merged pull requests)
|
||||
86072227 levzzz5154 # added by Ludovic, 2026-10-28 (Horizon Team; author of horizon-mixins)
|
||||
87478350 Qwerner88880 # added by Ludovic, 2026-10-31 (Discord vladislav_hagen; native-mod hooks)
|
||||
169198624 itarqos5 # added by Ludovic, 2026-11-08 (Discord Literal; Horizon Team)
|
||||
@@ -291,11 +291,6 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo run --locked -p xtask -- package "$BIN/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
|
||||
# Windows x64: Microsoft's DXC, pinned and checksummed, which DirectX 12 uses on integrated GPUs
|
||||
# (gfx/dx12_compiler.rs), with its licence. ARM64 keeps FXC (THIRD_PARTY_NOTICES.md says why).
|
||||
if [[ "${{ matrix.name }}" == windows ]]; then
|
||||
python tools/ci/fetch_dxc.py "dist/${{ matrix.dir }}"
|
||||
fi
|
||||
cd dist
|
||||
rm -f "${{ matrix.asset }}"
|
||||
case "${{ matrix.asset }}" in
|
||||
|
||||
@@ -152,23 +152,23 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
|
||||
| Subsystem | Files |
|
||||
|---|---|
|
||||
| Game loop, tick, frame | `game.rs`: `fn run`, `fn tick`, `fn tick_world`, `fn render`; G: `window_handler.rs` (`impl ApplicationHandler for Game`: `window_event`, `about_to_wait`); L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
|
||||
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs`, `screen_backgrounds.rs` (panorama, menu blur, border wall) |
|
||||
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `world_entry.rs`, `postprocess.rs`; game.rs `fn update_world` |
|
||||
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
|
||||
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
|
||||
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
|
||||
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
|
||||
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
|
||||
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
|
||||
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
|
||||
| Player actions | game.rs (`fn use_hand`, `fn tick_mining`, `fn break_block`); G: `item_actions.rs` (`fn use_item`), `melee.rs` (`fn attack`), `hand.rs`, `combat.rs`, `consume.rs`, `using_item.rs` (an item in use, tick by tick), `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
|
||||
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs`, `inventory_input.rs` (keys and clicks over a container), `inventory_slots.rs` (slot layout and drawing) |
|
||||
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
|
||||
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs` |
|
||||
| Commands | parse: crate `horizon-command` (`args.rs`, `selector.rs`, `tree_*.rs`, `vanilla.rs`); run: G: `commands.rs` (dispatch) + `commands/*.rs` (`admin`, `execute`, `items`, `players`, `world_cmds`...); L: `scoreboard_cmd.rs`, `data_cmd.rs`; `horizon-client-core`: `nbt_path.rs`, `scoreboard.rs`; G: `functions.rs`, `command_blocks.rs` |
|
||||
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `settings_shell.rs` (Horizon's settings screens on it: `extras_ui.rs`, `graphics_ui.rs`, `controller_ui.rs`), `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `hud_kit/` (Horizon overlays' `Room` and `Plate`, [docs/ui-kit.md](docs/ui-kit.md)), `pad_glyph.rs` (controller glyphs, by what the button does), `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `loading_screen.rs` (the loading screen, the pre-game `App`), `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs`, `creative_screen.rs` |
|
||||
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs` |
|
||||
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
|
||||
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
|
||||
| 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`, `pad_craft.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
|
||||
| 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); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
|
||||
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; 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` |
|
||||
| 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)) |
|
||||
@@ -191,7 +191,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
`levels.rs` swaps whole dimensions in the same way. Changes reach clients through the block log, level
|
||||
events and the entity tracker (`lan/tracking.rs`, `game/lan_entities.rs`).
|
||||
- Shared read-only data: `Resources` (`res`: blocks, items, recipes, mob types, biomes) and `PackResources`
|
||||
(`pack`), both in game.rs, their methods in `game/resource_tables.rs`.
|
||||
(`pack`), both in game.rs.
|
||||
|
||||
## Single player runs on a server thread
|
||||
|
||||
|
||||
+1
-7
@@ -61,10 +61,6 @@ another project's source may also be read and used, **only when that project's l
|
||||
"source available" licenses, all rights reserved (OptiFine), and any project with no license or an unclear one.
|
||||
Don't read, copy, translate or paraphrase their source, and don't send code an AI tool wrote from it. Being able
|
||||
to download a mod or a pack does not give you the right to read its files.
|
||||
- **Java's own library is GPL too.** OpenJDK's source (`java.lang.Math`, `StrictMath`, `FdLibm`, `HashMap`, the
|
||||
sorts, `Double.toString`, HotSpot's intrinsics...) is GPL-2.0, so it is off limits like the rest. Reproduce Java's
|
||||
behaviour from its API documentation (the Javadoc) and from what a JVM returns, or from a permissive original such
|
||||
as Sun's fdlibm C sources (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); see #6741.
|
||||
- **Check the license yourself.** Read the LICENSE file at the commit you use and the header of each file you take:
|
||||
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one at
|
||||
your commit counts. In doubt, don't: ask in #contributors first.
|
||||
@@ -76,9 +72,7 @@ another project's source may also be read and used, **only when that project's l
|
||||
|
||||
User documentation and file formats are fine to read, and packs and mods are fine to run and observe, whatever their
|
||||
license. Shader packs are a clean-room rewrite of the pack format, produced from a written description of it (see
|
||||
#1561): the [specification and its provenance log](docs/cleanroom/README.md) are public. They carry no foreign
|
||||
source, and the rules above still apply to anything you read while working on them. A shader-pack pull request
|
||||
names its sources: the sections of that specification, the documentation pages and the packs it ran.
|
||||
#1561); they carry no foreign source, and the rules above still apply to anything you read while working on them.
|
||||
|
||||
## Before asking for review
|
||||
|
||||
|
||||
Generated
-2
@@ -1588,7 +1588,6 @@ dependencies = [
|
||||
"horizon-mod-api-gen",
|
||||
"horizon-narrator",
|
||||
"horizon-optifine",
|
||||
"horizon-polytone",
|
||||
"horizon-protocol",
|
||||
"horizon-shader-api",
|
||||
"horizon-shaders",
|
||||
@@ -1776,7 +1775,6 @@ dependencies = [
|
||||
name = "horizon-polytone"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+2
-89
@@ -1,36 +1,5 @@
|
||||
# Third-party notices
|
||||
|
||||
## Code removed for its license
|
||||
|
||||
Horizon once shipped three pieces of code under MIT that came from projects whose licenses don't allow that. All
|
||||
three are gone from main. They are listed here for anyone who holds an older commit, tag or release: in those
|
||||
versions, that code is not covered by Horizon's MIT license. It stays under its authors' copyright and its original
|
||||
license.
|
||||
|
||||
- **Shader packs written from Iris's source.** `crates/horizon-shaderpack` and the render stages that used it were
|
||||
written from the source of [Iris](https://github.com/IrisShaders/Iris) (LGPL-3.0, copyright the Iris contributors).
|
||||
They were added from 15 October 2026 (`b22583d057`), shipped in the game releases `v2026.10.16.02` to `v0.2.46`,
|
||||
and were removed on 24 October 2026 ([#1561](https://github.com/horizon-rust/horizon/pull/1561)) after a developer
|
||||
of Iris showed us. Shader packs came back in `crates/horizon-shaders`, first written from a description of the
|
||||
pack format.
|
||||
- **Math functions translated from OpenJDK.** In `crates/horizon-core/src/jmath.rs`, `fdlibm::{log, log10, cbrt,
|
||||
hypot}` were translated from OpenJDK's `java/lang/FdLibm.java` and `round_f32` from its `java/lang/Math.java`
|
||||
(GPL-2.0 with the Classpath Exception), and `aarch64::{sin, cos}` from HotSpot's `macroAssembler_aarch64_trig.cpp`
|
||||
(GPL-2.0); copyright Oracle and/or its affiliates, and Cavium for that file. The `fdlibm` and `aarch64` functions
|
||||
were added on 13 October 2026 (`558d140fca`, first released in `v2026.10.13`). `round_f32` was added on 14 October
|
||||
2026 as `java_round` in `crates/horizon-client/src/mipmap.rs` (`95db0fce9d`) and moved into `jmath.rs` on 3
|
||||
November ([#7770](https://github.com/horizon-rust/horizon/pull/7770)). All of them were replaced in
|
||||
[#11944](https://github.com/horizon-rust/horizon/pull/11944) by code written without OpenJDK's sources, from Sun's
|
||||
fdlibm C sources, the Java SE API documentation and a description of the behaviour, and checked against a JVM
|
||||
([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
|
||||
- **Java's `HashMap` translated from OpenJDK.** `crates/horizon-world/src/java_hash_map.rs`, which reproduces the
|
||||
iteration order of `java.util.HashMap`, was translated from OpenJDK's `java/util/HashMap.java` (GPL-2.0 with the
|
||||
Classpath Exception; copyright Oracle and/or its affiliates). It was added on 7 October 2026 (`118e93ea51` and
|
||||
`194db0dbb4`, first released in `v2026.10.12`). It was replaced in
|
||||
[#12043](https://github.com/horizon-rust/horizon/pull/12043) by code written without OpenJDK's sources, from a
|
||||
description of the behaviour, the Java SE API documentation and the textbook red-black tree, and checked against a
|
||||
JVM ([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
|
||||
|
||||
## Catnip / Ponder
|
||||
|
||||
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
|
||||
@@ -46,11 +15,8 @@ original classes, game shaders, textures or prepared resources are distributed h
|
||||
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
|
||||
developers, with upstream fixes applied
|
||||
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124),
|
||||
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). Its GL backend also reports
|
||||
the context's sampled textures and samplers per shader stage (usually 32 on desktop GL,
|
||||
where WebGL2's minimum 16 was reported before), so a pipeline layout the driver accepts is
|
||||
not refused. It is licensed under MIT or Apache-2.0; both licenses are in that folder
|
||||
(`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). It is licensed under MIT or
|
||||
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
|
||||
## naga
|
||||
|
||||
@@ -63,32 +29,6 @@ variable's array specifier does not change later variables.
|
||||
It also reads a struct member outside a constant expression through a pointer to the member,
|
||||
not out of a loaded copy of the whole struct, so a member an atomic function uses can be read.
|
||||
|
||||
## DirectX Shader Compiler
|
||||
|
||||
The Windows x64 game folder holds `dxcompiler.dll`, Microsoft's DirectX Shader Compiler
|
||||
[v1.9.2609](https://github.com/microsoft/DirectXShaderCompiler/releases/tag/v1.9.2609), unmodified: the
|
||||
`bin/x64/dxcompiler.dll` of `dxc_2026_09_29.zip` (zip SHA-256
|
||||
`ad31b1fc8443175d204f77a611fdb3ef2ec42759bdc2f1167368de24a4a7e7f1`, DLL SHA-256
|
||||
`0689e2fee0a1c9df8662a59680469f24e2a3ad7ce6bbbcc9284f7a83172bfc59`, 22,274,872 bytes). The release build downloads
|
||||
it and checks both checksums (`tools/ci/fetch_dxc.py`); it is not in this repository. DirectX 12 loads it only on
|
||||
integrated GPUs (`crates/horizon-client-render/src/gfx/dx12_compiler.rs`,
|
||||
[#8513](https://github.com/horizon-rust/horizon/issues/8513)); every other GPU compiles with FXC, part of Windows.
|
||||
|
||||
Copyright (c) Microsoft Corporation. It is not covered by Horizon's MIT license. It is distributed under the
|
||||
"Microsoft Software License Terms, Microsoft DirectX Shader Compiler" (`LICENSE-MS.txt` of that release). Its
|
||||
Distributable Code terms are the Windows SDK's: end users get terms that protect the code and Microsoft at least as
|
||||
much, and the distributor indemnifies Microsoft. The project accepted them on 2026-11-08. Its open-source part is also under the University of Illinois/NCSA license (`LICENSE-LLVM.txt`). Both
|
||||
files ship beside the DLL as `DXC-LICENSE-MS.txt` and `DXC-LICENSE-LLVM.txt`, with `DXC-NOTICE.txt`
|
||||
(`packaging/windows/`), which tells players those terms apply and that deleting the DLL brings back FXC.
|
||||
|
||||
- Only x64: the Windows SDK's list of distributable files
|
||||
([REDIST.TXT lists](https://learn.microsoft.com/en-us/legal/windows-sdk/redist), updated 2024-10-21) names
|
||||
`Redist\D3D\x64\dxcompiler.dll` and `dxil.dll` and their x86 copies, for classic Windows applications (Horizon is
|
||||
one), and no ARM64 copy. The release zip's own `LICENSE-MS.txt` refers to a list of distributable files that the zip
|
||||
doesn't contain, so the SDK's list is the one followed. The Windows ARM64 build ships no DXC and compiles with FXC.
|
||||
- No `dxil.dll`: since DXC 1.8.2505 the compiler signs shaders with its own validator and no longer loads it, and
|
||||
wgpu never does.
|
||||
|
||||
## libmimalloc-sys and mimalloc
|
||||
|
||||
`third_party/libmimalloc-sys` is [libmimalloc-sys](https://github.com/purpleprotocol/mimalloc_rust)
|
||||
@@ -316,30 +256,3 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## fdlibm
|
||||
|
||||
`crates/horizon-core/src/jmath/fdlibm.rs` and the `fdlibm_asin`, `fdlibm_atan` and
|
||||
`fdlibm_atan2` functions in `crates/horizon-core/src/geometry/trig.rs` are Rust ports of
|
||||
[fdlibm](https://www.netlib.org/fdlibm/) 5.3, Sun's freely distributable math library, whose
|
||||
results the Java SE API specifies for `StrictMath`. `fdlibm.rs` ports `e_log.c`, `e_log10.c`,
|
||||
`s_cbrt.c` and `e_hypot.c`, and, with fused multiply-adds, `s_sin.c`, `s_cos.c`, `k_sin.c`,
|
||||
`k_cos.c` and `e_rem_pio2.c`; `trig.rs` ports `e_asin.c`, `s_atan.c` and `e_atan2.c`. This
|
||||
notice also heads both files:
|
||||
|
||||
```
|
||||
Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
|
||||
|
||||
Developed at SunSoft, a Sun Microsystems, Inc. business.
|
||||
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).
|
||||
@@ -917,46 +917,6 @@ impl NumericProperty {
|
||||
/// (`None` removes a prototype component).
|
||||
pub type Components = BTreeMap<String, Value>;
|
||||
|
||||
/// A stack's components read in place: its patch first, then its item's defaults. Reading one copies
|
||||
/// nothing, where [`ItemModels::components`] clones the defaults for a stack with a patch (an enchanted
|
||||
/// sword's are a few dozen JSON values, on every draw of it).
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ComponentsView<'a> {
|
||||
prototype: &'a Components,
|
||||
patch: &'a BTreeMap<String, Option<Value>>,
|
||||
}
|
||||
|
||||
impl<'a> ComponentsView<'a> {
|
||||
pub fn get(&self, key: &str) -> Option<&'a Value> {
|
||||
match self.patch.get(key) {
|
||||
Some(patched) => patched.as_ref(),
|
||||
None => self.prototype.get(key),
|
||||
}
|
||||
}
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.get(key).is_some()
|
||||
}
|
||||
/// Components with no patch: `components` as they are.
|
||||
pub fn of(components: &'a Components) -> Self {
|
||||
static NO_PATCH: BTreeMap<String, Option<Value>> = BTreeMap::new();
|
||||
Self {
|
||||
prototype: components,
|
||||
patch: &NO_PATCH,
|
||||
}
|
||||
}
|
||||
/// The merged map, for what needs one whole.
|
||||
pub fn to_map(&self) -> Components {
|
||||
let mut out = self.prototype.clone();
|
||||
for (key, value) in self.patch {
|
||||
match value {
|
||||
Some(v) => out.insert(key.clone(), v.clone()),
|
||||
None => out.remove(key),
|
||||
};
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// An item stack as the resolver reads it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct StackView {
|
||||
@@ -1127,15 +1087,14 @@ impl ItemModels {
|
||||
if stack.patch.is_empty() {
|
||||
return std::borrow::Cow::Borrowed(prototype);
|
||||
}
|
||||
std::borrow::Cow::Owned(self.components_view(stack).to_map())
|
||||
}
|
||||
/// The stack's components, read in place (see [`ComponentsView`]).
|
||||
pub fn components_view<'a>(&'a self, stack: &'a StackView) -> ComponentsView<'a> {
|
||||
static NONE: Components = BTreeMap::new();
|
||||
ComponentsView {
|
||||
prototype: self.prototypes.get(&stack.item).unwrap_or(&NONE),
|
||||
patch: &stack.patch,
|
||||
let mut out = prototype.clone();
|
||||
for (key, value) in &stack.patch {
|
||||
match value {
|
||||
Some(v) => out.insert(key.clone(), v.clone()),
|
||||
None => out.remove(key),
|
||||
};
|
||||
}
|
||||
std::borrow::Cow::Owned(out)
|
||||
}
|
||||
/// `ItemModelResolver.updateForTopItem`.
|
||||
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
|
||||
@@ -1153,7 +1112,7 @@ impl ItemModels {
|
||||
ctx: &Context,
|
||||
env: &dyn ItemEnv,
|
||||
) {
|
||||
let components = self.components_view(stack);
|
||||
let components = self.components(stack);
|
||||
let Some(Value::String(model_id)) = components.get("minecraft:item_model") else {
|
||||
return;
|
||||
};
|
||||
@@ -1163,7 +1122,7 @@ impl ItemModels {
|
||||
let mut state = Resolve {
|
||||
models: self,
|
||||
stack,
|
||||
components,
|
||||
components: &components,
|
||||
ctx,
|
||||
env,
|
||||
};
|
||||
@@ -1181,7 +1140,7 @@ impl ItemModels {
|
||||
self.properties(stack).2
|
||||
}
|
||||
fn properties(&self, stack: &StackView) -> (bool, bool, f32) {
|
||||
match self.components_view(stack).get("minecraft:item_model") {
|
||||
match self.components(stack).get("minecraft:item_model") {
|
||||
Some(Value::String(id)) => identifier(id)
|
||||
.ok()
|
||||
.and_then(|id| self.definitions.get(&id))
|
||||
@@ -1200,7 +1159,7 @@ impl ItemModels {
|
||||
struct Resolve<'a> {
|
||||
models: &'a ItemModels,
|
||||
stack: &'a StackView,
|
||||
components: ComponentsView<'a>,
|
||||
components: &'a Components,
|
||||
ctx: &'a Context<'a>,
|
||||
env: &'a dyn ItemEnv,
|
||||
}
|
||||
@@ -1496,9 +1455,9 @@ impl Resolve<'_> {
|
||||
fn numeric(&self, property: &NumericProperty) -> f32 {
|
||||
let owner = self.ctx.owner;
|
||||
match property {
|
||||
NumericProperty::BundleFullness => self
|
||||
.env
|
||||
.bundle_fullness(self.stack, &self.components.to_map()),
|
||||
NumericProperty::BundleFullness => {
|
||||
self.env.bundle_fullness(self.stack, self.components)
|
||||
}
|
||||
NumericProperty::Compass {
|
||||
wobble,
|
||||
target,
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(super) fn decode(m: &Map<String, Value>) -> Result<Value> {
|
||||
/// with a matching level. An empty predicate list still needs the component.
|
||||
pub(super) fn matches(
|
||||
predicate: &Value,
|
||||
components: ComponentsView<'_>,
|
||||
components: &Components,
|
||||
env: &dyn ItemEnv,
|
||||
) -> Option<bool> {
|
||||
let (id, expected) = predicate.as_object()?.iter().next()?;
|
||||
@@ -143,17 +143,13 @@ mod tests {
|
||||
json!({"minecraft:density":0}),
|
||||
)]);
|
||||
assert_eq!(
|
||||
matches(
|
||||
&json!({"minecraft:enchantments":[{}]}),
|
||||
ComponentsView::of(&components),
|
||||
&NoEnv
|
||||
),
|
||||
matches(&json!({"minecraft:enchantments":[{}]}), &components, &NoEnv),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
matches(
|
||||
&json!({"minecraft:enchantments":[{"enchantments":"minecraft:density"}]}),
|
||||
ComponentsView::of(&components),
|
||||
&components,
|
||||
&NoEnv
|
||||
),
|
||||
Some(false)
|
||||
@@ -170,21 +166,12 @@ mod tests {
|
||||
"minecraft:enchantments".into(),
|
||||
json!({"minecraft:density":3}),
|
||||
)]);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &Tags),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &NoEnv),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(matches(&predicate, &components, &Tags), Some(true));
|
||||
assert_eq!(matches(&predicate, &components, &NoEnv), Some(false));
|
||||
components.insert(
|
||||
"minecraft:enchantments".into(),
|
||||
json!({"minecraft:density":2}),
|
||||
);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &Tags),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(matches(&predicate, &components, &Tags), Some(false));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn identifier_adds_default_namespace() {
|
||||
@@ -112,49 +111,3 @@ fn custom_condition_validates_its_name_and_both_branches() {
|
||||
assert!(matches!(node("example:charged", empty).unwrap(),
|
||||
ItemModelNode::Condition { property: ConditionProperty::Custom(name), .. } if name == "example:charged"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
|
||||
let mut models = ItemModels::default();
|
||||
models.prototypes.insert(
|
||||
"minecraft:diamond_sword".into(),
|
||||
Components::from_iter([
|
||||
(
|
||||
"minecraft:item_model".into(),
|
||||
json!("minecraft:diamond_sword"),
|
||||
),
|
||||
("minecraft:max_damage".into(), json!(1561)),
|
||||
("minecraft:rarity".into(), json!("common")),
|
||||
]),
|
||||
);
|
||||
let mut stack = StackView::new("minecraft:diamond_sword", 1);
|
||||
stack.patch.insert(
|
||||
"minecraft:enchantments".into(),
|
||||
Some(json!({"minecraft:sharpness": 5})),
|
||||
);
|
||||
stack
|
||||
.patch
|
||||
.insert("minecraft:rarity".into(), Some(json!("rare")));
|
||||
stack.patch.insert("minecraft:max_damage".into(), None);
|
||||
let view = models.components_view(&stack);
|
||||
assert_eq!(view.get("minecraft:rarity"), Some(&json!("rare")));
|
||||
assert_eq!(view.get("minecraft:max_damage"), None, "a removed default");
|
||||
assert_eq!(
|
||||
view.get("minecraft:item_model"),
|
||||
Some(&json!("minecraft:diamond_sword"))
|
||||
);
|
||||
assert!(view.contains_key("minecraft:enchantments"));
|
||||
assert!(!view.contains_key("minecraft:custom_name"));
|
||||
// Every key reads as the merged map has it.
|
||||
let merged = models.components(&stack);
|
||||
for key in [
|
||||
"minecraft:rarity",
|
||||
"minecraft:max_damage",
|
||||
"minecraft:item_model",
|
||||
"minecraft:enchantments",
|
||||
"minecraft:custom_name",
|
||||
] {
|
||||
assert_eq!(view.get(key), merged.get(key), "{key}");
|
||||
}
|
||||
assert_eq!(view.to_map(), *merged);
|
||||
}
|
||||
@@ -290,12 +290,12 @@ pub fn normalize_quaternion(q: [f32; 4]) -> [f32; 4] {
|
||||
[x * inv, y * inv, z * inv, w * inv]
|
||||
}
|
||||
/// JOML `Math.sin(float)` without fast math.
|
||||
pub fn joml_sin(rad: f32) -> f32 {
|
||||
fn joml_sin(rad: f32) -> f32 {
|
||||
f64::from(rad).sin() as f32
|
||||
}
|
||||
/// JOML `Math.cosFromSin(float, float)` without fast math: the magnitude from
|
||||
/// the sine, the sign from the angle's quadrant (exact zeros at right angles).
|
||||
pub fn cos_from_sin(sin: f32, angle: f32) -> f32 {
|
||||
fn cos_from_sin(sin: f32, angle: f32) -> f32 {
|
||||
let cos = (1.0 - sin * sin).sqrt();
|
||||
let tau = (std::f64::consts::PI * 2.0) as f32;
|
||||
let a = angle + std::f32::consts::FRAC_PI_2;
|
||||
|
||||
@@ -217,10 +217,9 @@ pub fn stack(tag: &Tag) -> Option<Tag> {
|
||||
Some(Tag::compound(out))
|
||||
}
|
||||
/// `ItemStackTemplate.CODEC`: an item id or `{id, count?, components?}`.
|
||||
/// Encoding uses the primary record form, including for a bare item id.
|
||||
fn template(tag: &Tag) -> Option<Tag> {
|
||||
match tag {
|
||||
Tag::String(_) => Some(Tag::compound([("id", tag.clone())])),
|
||||
Tag::String(_) => Some(tag.clone()),
|
||||
Tag::Compound(_) => {
|
||||
let mut out = vec![("id", string(&text(tag.get("id"))?))];
|
||||
if let Some(count) = int(tag.get("count")).filter(|c| *c != 1) {
|
||||
@@ -1012,69 +1011,3 @@ mod tests {
|
||||
assert_eq!(items[1].get("count"), Some(&Tag::Int(5)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pot_template_tests {
|
||||
use super::*;
|
||||
use horizon_world::nbt::parse_snbt;
|
||||
|
||||
#[test]
|
||||
fn decorated_pot_string_template_load_save_uses_item_compound() {
|
||||
let parse = |s: &str| parse_snbt(s).expect("valid input NBT").0;
|
||||
let input = |front: &str| {
|
||||
let mut tag = parse(
|
||||
r#"{id:"minecraft:decorated_pot",x:4,y:200,z:4,sherds:{back:{id:"minecraft:brick"},right:{id:"minecraft:archer_pottery_sherd"}},item:{id:"minecraft:wheat",count:3,components:{"minecraft:custom_name":"H290 item"}}}"#,
|
||||
);
|
||||
let Tag::Compound(root) = &mut tag else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(Tag::Compound(sides)) = root.get_mut(&"sherds".into()) else {
|
||||
unreachable!()
|
||||
};
|
||||
sides.insert("front".into(), parse(front));
|
||||
tag
|
||||
};
|
||||
let other_fields = |loaded: &Tag, saved: &Tag| {
|
||||
assert_eq!(saved.get("id"), loaded.get("id"));
|
||||
for key in ["x", "y", "z", "item"] {
|
||||
assert_eq!(saved.get(key), loaded.get(key), "{key}");
|
||||
}
|
||||
let loaded_sides = loaded.get("sherds").unwrap();
|
||||
let saved_sides = saved.get("sherds").unwrap();
|
||||
for side in ["back", "right"] {
|
||||
assert_eq!(saved_sides.get(side), loaded_sides.get(side), "{side}");
|
||||
}
|
||||
assert_eq!(saved_sides.get("left"), None);
|
||||
};
|
||||
let plain = r#"{id:"minecraft:angler_pottery_sherd"}"#;
|
||||
let defaults = r#"{id:"minecraft:angler_pottery_sherd",count:1,components:{}}"#;
|
||||
let rich = r#"{id:"minecraft:angler_pottery_sherd",count:3,components:{"minecraft:custom_data":{h290:1},"minecraft:custom_name":"H290 sherd"}}"#;
|
||||
// Actual canonical load/save entry, not the private template helper.
|
||||
for (front, expected) in [(plain, plain), (defaults, plain), (rich, rich)] {
|
||||
let loaded = input(front);
|
||||
let original = loaded.clone();
|
||||
let saved = canonical(&loaded).expect("actual decorated-pot codec");
|
||||
other_fields(&loaded, &saved);
|
||||
assert_eq!(
|
||||
saved.get("sherds").unwrap().get("front"),
|
||||
Some(&parse(expected))
|
||||
);
|
||||
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
|
||||
assert_eq!(loaded, original, "read-only canonicalization");
|
||||
}
|
||||
println!(
|
||||
"H290 actual canonical compound/default/count/components, other sides/item/name and idempotency controls admitted"
|
||||
);
|
||||
let loaded = input(r#""minecraft:angler_pottery_sherd""#);
|
||||
let original = loaded.clone();
|
||||
let saved = canonical(&loaded).expect("actual decorated-pot codec");
|
||||
other_fields(&loaded, &saved);
|
||||
assert_eq!(loaded, original, "read-only canonicalization");
|
||||
assert_eq!(
|
||||
saved.get("sherds").unwrap().get("front"),
|
||||
Some(&parse(plain)),
|
||||
"string template must save the primary item compound",
|
||||
);
|
||||
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
|
||||
}
|
||||
}
|
||||
@@ -412,11 +412,10 @@ pub fn to_text_with(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// TranslatableContents.decompose: Language.getOrDefault(key, fallback), so the
|
||||
// fallback is the template itself, never looked up as another key.
|
||||
let translated = Text::translate(key, &args);
|
||||
match o.get("fallback").and_then(Value::as_str) {
|
||||
Some(fallback) => Text::translate_with_fallback(key, fallback, &args),
|
||||
None => Text::translate(key, &args),
|
||||
Some(fallback) if translated.plain() == key => Text::translate(fallback, &args),
|
||||
_ => translated,
|
||||
}
|
||||
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
|
||||
resolve(sel)
|
||||
@@ -514,43 +513,6 @@ 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]
|
||||
|
||||
@@ -46,21 +46,6 @@ thread_local! {
|
||||
}
|
||||
/// The current language's `bidirectional`, read for every drawn string.
|
||||
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
|
||||
/// Counts the languages set (`set`): another count is another language, so text looked up
|
||||
/// before it may read otherwise.
|
||||
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
thread_local! {
|
||||
/// Counts the scopes this thread entered and left (`scoped`), which no other thread sees.
|
||||
static SCOPE_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
/// Which language text looks up in now, on this thread, as a count that changes whenever that
|
||||
/// can: both counts only grow, so their sum changes when either does.
|
||||
pub fn generation() -> u64 {
|
||||
GENERATION.load(Ordering::Relaxed) + SCOPE_GENERATION.get()
|
||||
}
|
||||
fn scope_changed() {
|
||||
SCOPE_GENERATION.set(SCOPE_GENERATION.get() + 1);
|
||||
}
|
||||
|
||||
/// A source of resource files: the prepared root plus packs, highest priority last.
|
||||
pub trait ResourceSource {
|
||||
@@ -539,7 +524,6 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
|
||||
pub fn set(language: Language) {
|
||||
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
|
||||
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
|
||||
GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
pub fn current() -> Option<Arc<Language>> {
|
||||
if let Some(language) = SCOPE.with(|s| s.borrow().as_ref().map(|s| s.language.clone())) {
|
||||
@@ -559,7 +543,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
SCOPE.with(|s| *s.borrow_mut() = self.0.take());
|
||||
scope_changed();
|
||||
}
|
||||
}
|
||||
let outer = SCOPE.with(|s| {
|
||||
@@ -569,7 +552,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
|
||||
})
|
||||
});
|
||||
let _restore = Restore(outer);
|
||||
scope_changed();
|
||||
let result = f();
|
||||
let missed = SCOPE.with(|s| {
|
||||
s.borrow_mut()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! 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.
|
||||
@@ -48,6 +47,26 @@ 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,31 +89,6 @@ 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)]
|
||||
|
||||
@@ -14,8 +14,6 @@ pub enum PistonPart {
|
||||
Entity,
|
||||
State,
|
||||
Properties,
|
||||
/// Any other block entity's whole compound (`BlockEntity.saveWithFullMetadata`).
|
||||
BlockEntity,
|
||||
}
|
||||
|
||||
struct Pretty {
|
||||
@@ -257,20 +255,6 @@ fn piston_hash_order(
|
||||
entries.sort_by_key(|(k, _)| (hash_bucket(c.len(), k), std::cmp::Reverse(*k)));
|
||||
}
|
||||
PistonPart::State => return java_hash_order(c),
|
||||
PistonPart::BlockEntity => {
|
||||
// saveAdditional's fields, then `components`, then saveMetadata's
|
||||
// id and position; the fields keep the default order among them.
|
||||
const METADATA: [&str; 5] = ["components", "id", "x", "y", "z"];
|
||||
let mut entries = java_hash_order(c);
|
||||
entries.sort_by_key(|(k, _)| {
|
||||
let key = string_of(k);
|
||||
(
|
||||
hash_bucket(c.len(), k),
|
||||
METADATA.iter().position(|m| *m == key).map_or(0, |i| i + 1),
|
||||
)
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
@@ -329,14 +313,6 @@ pub fn java_hash_order(
|
||||
"NoBasePlate",
|
||||
"Marker",
|
||||
"Pose",
|
||||
// Mannequin.addAdditionalSaveData.
|
||||
"profile",
|
||||
"hidden_layers",
|
||||
"main_hand",
|
||||
"pose",
|
||||
"immovable",
|
||||
"description",
|
||||
"hide_description",
|
||||
];
|
||||
// BlockEntity.saveMetadata writes the position after the entity's data.
|
||||
const AFTER: &[&str] = &["x", "y", "z"];
|
||||
@@ -421,21 +397,6 @@ mod tests {
|
||||
assert_eq!(pretty(&tag).plain(), ordinary);
|
||||
}
|
||||
#[test]
|
||||
fn mannequin_compound_orders_pose_before_immovable_in_shared_bucket() {
|
||||
let tag = parse_snbt(
|
||||
r#"{AbsorptionAmount:0.0f,Air:300s,Brain:{memories:{}},DeathTime:0s,FallFlying:0b,Fire:0s,Health:20.0f,HurtTime:0s,Invulnerable:0b,Motion:[0.0d,0.0d,0.0d],OnGround:0b,PortalCooldown:0,Pos:[2.5d,100.0d,2.5d],Rotation:[0.0f,0.0f],UUID:[I;369,0,0,1],attributes:[],current_impulse_context_reset_grace_time:0,description:{text:"Guard"},hide_description:1b,immovable:1b,pose:"crouching",profile:{name:"alex"}}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.0;
|
||||
let rendered = pretty(&tag).plain();
|
||||
let pose_idx = rendered.find("pose: \"crouching\"").unwrap();
|
||||
let immovable_idx = rendered.find("immovable: 1b").unwrap();
|
||||
assert!(
|
||||
pose_idx < immovable_idx,
|
||||
"expected `pose` before `immovable` in bucket 5, got: {rendered}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn compact_form_matches_vanilla() {
|
||||
assert_eq!(
|
||||
rt("{b:1b,a:\"x\",c:[1,2],d:1.5f,e:2d,f:3L,g:4s}"),
|
||||
@@ -480,18 +441,4 @@ mod tests {
|
||||
let long = parse_snbt("{l:[{a:1},{b:2}]}").unwrap().0;
|
||||
assert_eq!(pretty(&long).plain(), "{l: [{a: 1}, {b: 2}]}");
|
||||
}
|
||||
#[test]
|
||||
fn block_entity_metadata_follows_its_fields() {
|
||||
// A conduit's `/data get block`, as vanilla 26.3 printed it: `Target`
|
||||
// and `components` share a bucket, and saveAdditional wrote `Target` first.
|
||||
let tag = parse_snbt(
|
||||
r#"{id:"minecraft:conduit",x:4098,y:300,z:4098,components:{},Target:[I;1,2,3,4]}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(
|
||||
pretty_piston(&tag, PistonPart::BlockEntity).plain(),
|
||||
r#"{Target: [I; 1, 2, 3, 4], components: {}, x: 4098, y: 300, z: 4098, id: "minecraft:conduit"}"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,49 @@ impl Button {
|
||||
fn bit(self) -> u32 {
|
||||
1 << self as u32
|
||||
}
|
||||
/// What is printed on the button for a pad family (`Auto` reads as Xbox).
|
||||
pub fn label(self, style: GlyphStyle) -> &'static str {
|
||||
let i = self as usize;
|
||||
match style {
|
||||
GlyphStyle::PlayStation => [
|
||||
"×", "○", "□", "△", "L1", "R1", "L2", "R2", "Create", "Options", "L3", "R3", "↑",
|
||||
"↓", "←", "→",
|
||||
][i],
|
||||
GlyphStyle::Nintendo => [
|
||||
"B", "A", "Y", "X", "L", "R", "ZL", "ZR", "−", "+", "LS", "RS", "↑", "↓", "←", "→",
|
||||
][i],
|
||||
GlyphStyle::Xbox | GlyphStyle::Auto => [
|
||||
"A", "B", "X", "Y", "LB", "RB", "LT", "RT", "View", "Menu", "LS", "RS", "↑", "↓",
|
||||
"←", "→",
|
||||
][i],
|
||||
}
|
||||
}
|
||||
/// The glyph's colour (RGB): face buttons in their family's colours,
|
||||
/// the rest white.
|
||||
pub fn color(self, style: GlyphStyle) -> [u8; 3] {
|
||||
let face = match self {
|
||||
Button::South => 0,
|
||||
Button::East => 1,
|
||||
Button::West => 2,
|
||||
Button::North => 3,
|
||||
_ => return [255, 255, 255],
|
||||
};
|
||||
match style {
|
||||
GlyphStyle::PlayStation => [
|
||||
[124, 178, 232],
|
||||
[235, 110, 110],
|
||||
[222, 145, 205],
|
||||
[92, 205, 170],
|
||||
][face],
|
||||
GlyphStyle::Nintendo => [255, 255, 255],
|
||||
GlyphStyle::Xbox | GlyphStyle::Auto => [
|
||||
[110, 200, 90],
|
||||
[235, 90, 80],
|
||||
[80, 150, 240],
|
||||
[240, 200, 60],
|
||||
][face],
|
||||
}
|
||||
}
|
||||
/// The face button that plays this one's part on a Nintendo pad going by its letters
|
||||
/// (`PadSettings::nintendo_letters`): the A it prints on the right is South, which confirms
|
||||
/// and jumps as A does on a Switch; B (bottom) is East, X (top) West, Y (left) North. It is
|
||||
@@ -384,101 +427,6 @@ impl HeldDrop {
|
||||
}
|
||||
}
|
||||
|
||||
/// A job a menu's fixed buttons do. Menus aren't rebindable: this is the one map from a job to
|
||||
/// its button, and a glyph for a menu asks for the job (`GlyphOf::Menu`), never the button.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum MenuJob {
|
||||
/// A: press, take, type.
|
||||
Confirm,
|
||||
/// B: back, close.
|
||||
Back,
|
||||
/// X: the screen's second job (Apply, Options, half a stack, delete a letter).
|
||||
Alternate,
|
||||
/// Y: the screen's third job (the exit, a quick move, a space).
|
||||
Extra,
|
||||
/// LB: the page or tab before.
|
||||
PagePrev,
|
||||
/// RB: the page or tab after.
|
||||
PageNext,
|
||||
/// LT: the group before; held, the modifier (shift, direct craft).
|
||||
GroupPrev,
|
||||
/// RT: the group after.
|
||||
GroupNext,
|
||||
/// View: throw.
|
||||
View,
|
||||
/// Menu (Start): done, and cancelling a binding.
|
||||
Start,
|
||||
/// The left stick pressed in.
|
||||
LeftStick,
|
||||
}
|
||||
impl MenuJob {
|
||||
pub const ALL: [MenuJob; 11] = [
|
||||
MenuJob::Confirm,
|
||||
MenuJob::Back,
|
||||
MenuJob::Alternate,
|
||||
MenuJob::Extra,
|
||||
MenuJob::PagePrev,
|
||||
MenuJob::PageNext,
|
||||
MenuJob::GroupPrev,
|
||||
MenuJob::GroupNext,
|
||||
MenuJob::View,
|
||||
MenuJob::Start,
|
||||
MenuJob::LeftStick,
|
||||
];
|
||||
/// The part that does the job (by place; a pad going by its letters is read so the part
|
||||
/// still lands: `PadState::by_letter`).
|
||||
pub const fn button(self) -> Button {
|
||||
match self {
|
||||
MenuJob::Confirm => Button::South,
|
||||
MenuJob::Back => Button::East,
|
||||
MenuJob::Alternate => Button::West,
|
||||
MenuJob::Extra => Button::North,
|
||||
MenuJob::PagePrev => Button::LeftBumper,
|
||||
MenuJob::PageNext => Button::RightBumper,
|
||||
MenuJob::GroupPrev => Button::LeftTrigger,
|
||||
MenuJob::GroupNext => Button::RightTrigger,
|
||||
MenuJob::View => Button::Select,
|
||||
MenuJob::Start => Button::Start,
|
||||
MenuJob::LeftStick => Button::LeftStick,
|
||||
}
|
||||
}
|
||||
/// The job a part does in menus.
|
||||
pub fn of(button: Button) -> Option<MenuJob> {
|
||||
Self::ALL.into_iter().find(|job| job.button() == button)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a glyph stands for: what the button does, never the button. `pad_glyph::Glyph` is built
|
||||
/// from one, so a rebinding or a pad's letters reach every glyph.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum GlyphOf {
|
||||
/// A gameplay action (`key.jump`...), on the player's binding.
|
||||
Bound(&'static str),
|
||||
/// A menu's fixed job.
|
||||
Menu(MenuJob),
|
||||
}
|
||||
impl From<MenuJob> for GlyphOf {
|
||||
fn from(job: MenuJob) -> Self {
|
||||
GlyphOf::Menu(job)
|
||||
}
|
||||
}
|
||||
impl GlyphOf {
|
||||
/// The part that does it under these settings; none while the action is unbound.
|
||||
pub fn button(self, settings: &PadSettings) -> Option<Button> {
|
||||
match self {
|
||||
GlyphOf::Bound(action) => {
|
||||
// A misspelt action would quietly have no glyph.
|
||||
debug_assert!(
|
||||
ACTIONS.iter().any(|a| a.0 == action),
|
||||
"no pad action {action}"
|
||||
);
|
||||
settings.binding(action)
|
||||
}
|
||||
GlyphOf::Menu(job) => Some(job.button()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which button pictures prompts use.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum GlyphStyle {
|
||||
@@ -635,10 +583,6 @@ 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
|
||||
@@ -670,7 +614,6 @@ 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,
|
||||
@@ -687,10 +630,6 @@ fn bool_text(v: bool) -> &'static str {
|
||||
if v { "true" } else { "false" }
|
||||
}
|
||||
impl PadSettings {
|
||||
/// A pad of `family` goes by its letters: only a Nintendo pad has letters to go by.
|
||||
pub fn by_letter(&self, family: GlyphStyle) -> bool {
|
||||
self.nintendo_letters && family == GlyphStyle::Nintendo
|
||||
}
|
||||
pub fn load(dir: &Path) -> Self {
|
||||
fs::read_to_string(dir.join(FILE))
|
||||
.map(|t| Self::parse(&t))
|
||||
@@ -745,7 +684,6 @@ 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),
|
||||
@@ -807,8 +745,7 @@ impl PadSettings {
|
||||
self.keyboard.name(),
|
||||
);
|
||||
text.push_str(&format!(
|
||||
"craftOnServers:{}\ngameplayGuide:{}\npointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
|
||||
bool_text(self.craft_on_servers),
|
||||
"gameplayGuide:{}\npointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
|
||||
self.guide_detail.name(),
|
||||
self.pointer_speed, self.scroll_speed, self.repeat_delay, self.repeat_interval,
|
||||
));
|
||||
@@ -1977,6 +1914,11 @@ bind_key.hotbar.2:north
|
||||
assert!(parts.held(Button::LeftBumper));
|
||||
assert_eq!(parts.left, [0.5, -0.25]);
|
||||
assert_eq!(parts.by_letter(), pad);
|
||||
// A part's glyph is the letter of the button playing it.
|
||||
assert_eq!(Button::South.by_letter().label(GlyphStyle::Nintendo), "A");
|
||||
assert_eq!(Button::East.by_letter().label(GlyphStyle::Nintendo), "B");
|
||||
assert_eq!(Button::West.by_letter().label(GlyphStyle::Nintendo), "X");
|
||||
assert_eq!(Button::North.by_letter().label(GlyphStyle::Nintendo), "Y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2404,7 +2346,12 @@ bind_key.hotbar.2:north
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles_resolve_and_conflicts() {
|
||||
fn labels_and_conflicts() {
|
||||
assert_eq!(Button::South.label(GlyphStyle::Xbox), "A");
|
||||
assert_eq!(Button::South.label(GlyphStyle::PlayStation), "×");
|
||||
assert_eq!(Button::South.label(GlyphStyle::Nintendo), "B");
|
||||
assert_eq!(Button::RightTrigger.label(GlyphStyle::PlayStation), "R2");
|
||||
assert_eq!(Button::DPadLeft.label(GlyphStyle::Xbox), "←");
|
||||
assert_eq!(
|
||||
GlyphStyle::Auto.resolve(GlyphStyle::PlayStation),
|
||||
GlyphStyle::PlayStation
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! and of an inventory it drives (Horizon's; vanilla has no gamepad input). The game reads the facts
|
||||
//! from its own hit results and capabilities (`game/pad/guide.rs`); this turns them into rows, and
|
||||
//! places the rows around what was drawn.
|
||||
use crate::gamepad::{Button, GlyphOf, GuideDetail, MenuJob, PadSettings, Rect};
|
||||
use crate::gamepad::{Button, GuideDetail, PadSettings, Rect};
|
||||
|
||||
/// What a use does, by what takes it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -100,27 +100,26 @@ pub struct Facts {
|
||||
pub off_hand_item: bool,
|
||||
}
|
||||
|
||||
/// One button's line: what the button does (its glyph follows the binding and the pad's letters)
|
||||
/// and the translation key of the caption.
|
||||
/// One button's line: the button and the translation key of what it does.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Row {
|
||||
/// Shown before `of`: held with it, or its other half (LB and RB for pages).
|
||||
pub with: Option<GlyphOf>,
|
||||
pub of: GlyphOf,
|
||||
/// Shown before `button`: held with it, or its other half (LB and RB for pages).
|
||||
pub with: Option<Button>,
|
||||
pub button: Button,
|
||||
pub caption: &'static str,
|
||||
}
|
||||
impl Row {
|
||||
fn new(of: impl Into<GlyphOf>, caption: &'static str) -> Self {
|
||||
fn new(button: Button, caption: &'static str) -> Self {
|
||||
Row {
|
||||
with: None,
|
||||
of: of.into(),
|
||||
button,
|
||||
caption,
|
||||
}
|
||||
}
|
||||
fn pair(with: MenuJob, job: MenuJob, caption: &'static str) -> Self {
|
||||
fn pair(with: Button, button: Button, caption: &'static str) -> Self {
|
||||
Row {
|
||||
with: Some(GlyphOf::Menu(with)),
|
||||
of: GlyphOf::Menu(job),
|
||||
with: Some(with),
|
||||
button,
|
||||
caption,
|
||||
}
|
||||
}
|
||||
@@ -156,11 +155,11 @@ impl Guide {
|
||||
/// The rows for these facts on these bindings. An unbound action has no row, and neither has one
|
||||
/// on Start, which is always pause.
|
||||
pub fn resolve(facts: &Facts, settings: &PadSettings) -> Guide {
|
||||
let row = |action: &'static str, caption: Option<&'static str>| {
|
||||
settings
|
||||
let row = |action: &str, caption: Option<&'static str>| {
|
||||
let button = settings
|
||||
.binding(action)
|
||||
.filter(|button| *button != Button::Start)?;
|
||||
Some(Row::new(GlyphOf::Bound(action), caption?))
|
||||
Some(Row::new(button, caption?))
|
||||
};
|
||||
let player = !facts.spectator;
|
||||
let any_item = facts.main_hand_item || facts.off_hand_item;
|
||||
@@ -309,10 +308,10 @@ pub struct MenuFacts {
|
||||
/// An inventory's rows, by what they ask of the player: at the bottom right one face button, at the
|
||||
/// bottom left anything else (a trigger held with a button, the bumpers, View, a stick click). What
|
||||
/// the crosshair is on comes first in its column, the screen's own rows after, except Close, which
|
||||
/// stays lowest so the rest don't move it. Menu buttons aren't rebindable, so these name menu jobs.
|
||||
/// stays lowest so the rest don't move it. Menu buttons aren't rebindable, so these name them.
|
||||
pub fn resolve_menu(f: &MenuFacts) -> Guide {
|
||||
use MenuJob::{Alternate, Back, Confirm, Extra, GroupNext, GroupPrev};
|
||||
use MenuJob::{LeftStick, PageNext, PagePrev, View};
|
||||
use Button::{East, LeftBumper, LeftStick, LeftTrigger, North, RightBumper, RightTrigger};
|
||||
use Button::{Select, South, West};
|
||||
const TAKE: &str = "horizon.controller.guide.take";
|
||||
const MOVE: &str = "horizon.controller.guide.quick_move";
|
||||
let under = match f.under {
|
||||
@@ -322,25 +321,25 @@ pub fn resolve_menu(f: &MenuFacts) -> Guide {
|
||||
result: true,
|
||||
same,
|
||||
..
|
||||
} if !f.carrying || same => vec![Row::new(Confirm, TAKE), Row::new(Extra, MOVE)],
|
||||
} if !f.carrying || same => vec![Row::new(South, TAKE), Row::new(North, MOVE)],
|
||||
Under::Slot { result: true, .. } => Vec::new(),
|
||||
Under::Slot { accepts: false, .. } if f.carrying => Vec::new(),
|
||||
Under::Slot { item, same, .. } if f.carrying => vec![
|
||||
Row::new(
|
||||
Confirm,
|
||||
South,
|
||||
if item && !same {
|
||||
"horizon.controller.guide.swap_stack"
|
||||
} else {
|
||||
"horizon.controller.guide.put"
|
||||
},
|
||||
),
|
||||
Row::new(Alternate, "horizon.controller.guide.put_one"),
|
||||
Row::new(West, "horizon.controller.guide.put_one"),
|
||||
],
|
||||
Under::Slot { item: true, .. } => vec![
|
||||
Row::new(Confirm, TAKE),
|
||||
Row::new(Alternate, "horizon.controller.guide.take_half"),
|
||||
Row::new(Extra, MOVE),
|
||||
Row::new(View, "horizon.controller.guide.drop"),
|
||||
Row::new(South, TAKE),
|
||||
Row::new(West, "horizon.controller.guide.take_half"),
|
||||
Row::new(North, MOVE),
|
||||
Row::new(Select, "horizon.controller.guide.drop"),
|
||||
Row::new(LeftStick, "horizon.controller.guide.drop_stack"),
|
||||
],
|
||||
Under::Nothing | Under::Slot { .. } => Vec::new(),
|
||||
@@ -348,44 +347,43 @@ pub fn resolve_menu(f: &MenuFacts) -> Guide {
|
||||
direct_craft,
|
||||
alternatives,
|
||||
} => [
|
||||
Some(Row::new(Confirm, "horizon.controller.guide.place_recipe")),
|
||||
Some(Row::new(South, "horizon.controller.guide.place_recipe")),
|
||||
direct_craft.then_some(Row::pair(
|
||||
GroupPrev,
|
||||
Confirm,
|
||||
LeftTrigger,
|
||||
South,
|
||||
"horizon.controller.direct_craft",
|
||||
)),
|
||||
Some(Row::new(Extra, "horizon.controller.guide.place_recipe_all")),
|
||||
alternatives.then_some(Row::new(Alternate, "horizon.controller.guide.alternatives")),
|
||||
Some(Row::new(North, "horizon.controller.guide.place_recipe_all")),
|
||||
alternatives.then_some(Row::new(West, "horizon.controller.guide.alternatives")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
Under::Control => vec![Row::new(Confirm, "horizon.controller.guide.select")],
|
||||
Under::Control => vec![Row::new(South, "horizon.controller.guide.select")],
|
||||
};
|
||||
let screen = [
|
||||
f.book.then_some(Row::pair(
|
||||
PagePrev,
|
||||
PageNext,
|
||||
LeftBumper,
|
||||
RightBumper,
|
||||
"horizon.controller.guide.pages",
|
||||
)),
|
||||
f.book.then_some(Row::pair(
|
||||
GroupPrev,
|
||||
GroupNext,
|
||||
LeftTrigger,
|
||||
RightTrigger,
|
||||
"horizon.controller.guide.categories",
|
||||
)),
|
||||
(f.tabs && !f.book).then_some(Row::pair(
|
||||
PagePrev,
|
||||
PageNext,
|
||||
LeftBumper,
|
||||
RightBumper,
|
||||
"horizon.controller.guide.tabs",
|
||||
)),
|
||||
];
|
||||
let close = Row::new(Back, "horizon.controller.guide.close");
|
||||
let close = Row::new(East, "horizon.controller.guide.close");
|
||||
let rows = std::iter::once(close)
|
||||
.chain(under)
|
||||
.chain(screen.into_iter().flatten());
|
||||
let (right, left) = rows.partition(|r| {
|
||||
r.with.is_none() && matches!(r.of, GlyphOf::Menu(Confirm | Back | Alternate | Extra))
|
||||
});
|
||||
let (right, left) =
|
||||
rows.partition(|r| r.with.is_none() && matches!(r.button, South | East | West | North));
|
||||
Guide { left, right }
|
||||
}
|
||||
|
||||
@@ -497,11 +495,8 @@ mod tests {
|
||||
..f
|
||||
}
|
||||
}
|
||||
/// Each row's button on these settings, and its caption.
|
||||
fn rows(rows: &[Row], settings: &PadSettings) -> Vec<(Button, &'static str)> {
|
||||
rows.iter()
|
||||
.map(|r| (r.of.button(settings).unwrap(), r.caption))
|
||||
.collect()
|
||||
fn rows(rows: &[Row]) -> Vec<(Button, &'static str)> {
|
||||
rows.iter().map(|r| (r.button, r.caption)).collect()
|
||||
}
|
||||
fn captions(rows: &[Row]) -> Vec<&'static str> {
|
||||
rows.iter().map(|r| r.caption).collect()
|
||||
@@ -514,10 +509,9 @@ mod tests {
|
||||
item_use: Some(Verb::Place),
|
||||
..on_block(walking(), true, None)
|
||||
};
|
||||
let settings = PadSettings::default();
|
||||
let guide = resolve(&facts, &settings);
|
||||
let guide = resolve(&facts, &PadSettings::default());
|
||||
assert_eq!(
|
||||
rows(&guide.right, &settings),
|
||||
rows(&guide.right),
|
||||
[
|
||||
(Button::RightTrigger, "horizon.controller.guide.break"),
|
||||
(Button::LeftTrigger, "horizon.controller.guide.place"),
|
||||
@@ -525,7 +519,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
rows(&guide.left, &settings),
|
||||
rows(&guide.left),
|
||||
[
|
||||
(Button::South, "key.jump"),
|
||||
(Button::RightStick, "key.sneak"),
|
||||
@@ -549,7 +543,7 @@ mod tests {
|
||||
};
|
||||
let guide = resolve(&facts, &settings);
|
||||
assert_eq!(
|
||||
rows(&guide.right, &settings),
|
||||
rows(&guide.right),
|
||||
[(Button::North, "horizon.controller.guide.break")]
|
||||
);
|
||||
// Start only ever pauses.
|
||||
@@ -851,8 +845,8 @@ mod tests {
|
||||
["close", "place_recipe", "place_recipe_all", "alternatives"]
|
||||
);
|
||||
assert_eq!(recipe.left.len(), 1);
|
||||
assert_eq!(recipe.left[0].with, Some(GlyphOf::Menu(MenuJob::GroupPrev)));
|
||||
assert_eq!(recipe.left[0].of, GlyphOf::Menu(MenuJob::Confirm));
|
||||
assert_eq!(recipe.left[0].with, Some(Button::LeftTrigger));
|
||||
assert_eq!(recipe.left[0].button, Button::South);
|
||||
let plain = on(
|
||||
Under::Recipe {
|
||||
direct_craft: false,
|
||||
@@ -898,8 +892,8 @@ mod detail_tests {
|
||||
|
||||
fn rows() -> Guide {
|
||||
Guide {
|
||||
left: vec![Row::new(MenuJob::Confirm, "left")],
|
||||
right: vec![Row::new(MenuJob::Back, "right")],
|
||||
left: vec![Row::new(Button::South, "left")],
|
||||
right: vec![Row::new(Button::East, "right")],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,7 +927,7 @@ mod detail_tests {
|
||||
mod slot_tests {
|
||||
use super::*;
|
||||
|
||||
/// The rows for the slot: Confirm or Alternate, whichever column they're in.
|
||||
/// The rows for the slot: South or West, whichever column they're in.
|
||||
fn rows(item: bool, same: bool, result: bool, accepts: bool, carrying: bool) -> usize {
|
||||
let guide = resolve_menu(&MenuFacts {
|
||||
under: Under::Slot {
|
||||
@@ -948,7 +942,7 @@ mod slot_tests {
|
||||
[guide.left, guide.right]
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|row| matches!(row.of, GlyphOf::Menu(MenuJob::Confirm | MenuJob::Alternate)))
|
||||
.filter(|row| matches!(row.button, Button::South | Button::West))
|
||||
.count()
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@ rustix = { version = "1", default-features = false, features = ["std", "time", "
|
||||
# Use the registered URI handler without routing links through Explorer/cmd.
|
||||
open = { version = "5.4.5", features = ["shellexecute-on-windows"] }
|
||||
# DirectX 12, where Vulkan has no adapter or device, or by choice (`graphicsApi`, graphics.rs).
|
||||
# Shaders compile with FXC (d3dcompiler_47.dll, part of Windows), or on integrated GPUs with DXC
|
||||
# (dxcompiler.dll beside the game, gfx/dx12_compiler.rs).
|
||||
# Shaders compile with FXC (d3dcompiler_47.dll, part of Windows), or DXC when dxcompiler.dll is
|
||||
# on the PATH.
|
||||
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -23,7 +23,6 @@ horizon-narrator = { path = "../horizon-narrator" }
|
||||
# OptiFine-format pack files: random entity textures (hud.rs), connected textures
|
||||
# (connected.rs) and custom item texture rules (cit.rs).
|
||||
horizon-optifine = { path = "../horizon-optifine" }
|
||||
horizon-polytone = { path = "../horizon-polytone" }
|
||||
horizon-protocol = { path = "../horizon-protocol" }
|
||||
horizon-shader-api = { path = "../horizon-shader-api" }
|
||||
horizon-shaders = { path = "../horizon-shaders" }
|
||||
|
||||
@@ -32,11 +32,6 @@ 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);
|
||||
|
||||
@@ -245,7 +245,6 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
settings.fxaa,
|
||||
settings.taa,
|
||||
settings.upscale,
|
||||
0.85,
|
||||
);
|
||||
let mut notes = Vec::new();
|
||||
let mut note = |what: &str, result: horizon_client_render::Result<()>| {
|
||||
@@ -661,7 +660,6 @@ fn remesh(
|
||||
blend_radius,
|
||||
smooth_lighting,
|
||||
cutout_leaves,
|
||||
opaque_ice: false,
|
||||
translucent_layers: &[],
|
||||
leaf_culling,
|
||||
};
|
||||
|
||||
@@ -50,14 +50,10 @@ 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(
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
//! A world warp (`native_world_warp`, docs/native-mods.md "World warps") through the real
|
||||
//! `Renderer`, offscreen: a wall of terrain and an entity quad drawn without a warp, with one,
|
||||
//! with one that is off, with one that cuts the wall, with a pixel stage of its own and with
|
||||
//! one that doesn't compile. Run with throwaway HORIZON_HOME, HORIZON_LAUNCHER_HOME,
|
||||
//! XDG_DATA_HOME and XDG_CONFIG_HOME (AGENTS.md):
|
||||
//! `cargo run -p horizon-client-render --example world_warp_check -- <output-dir>`
|
||||
//! `--release ... -- <output-dir> --bench`: instead, the time of a frame of rolling land 49 by 49
|
||||
//! chunks around the camera without a warp, and with one (nothing culled), at 1920 by 1080.
|
||||
use client::{
|
||||
gfx::{Frame, Renderer, ScreenshotRequest, native_render},
|
||||
graphics::GraphicsApi,
|
||||
hud::HudBatch,
|
||||
mesher::{SectionMesh, TerrainVertex, Vertex},
|
||||
};
|
||||
use horizon_client_render as client;
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
/// What the mod below gives the renderer each frame.
|
||||
static WARP: Mutex<Option<native_render::WorldWarp>> = Mutex::new(None);
|
||||
struct Mod;
|
||||
impl native_render::RenderHooks for Mod {
|
||||
fn world_warp(&self, event: &mut native_render::WorldWarpEvent<'_>) {
|
||||
if let Some(warp) = WARP.lock().unwrap().as_ref() {
|
||||
*event.warp = warp.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifts what is left of x = 8 by `params[0].x` and tints it red.
|
||||
const LIFT: &str = "fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped {
|
||||
let here = pos.x + warp.camera.x + warp.camera_rest.x;
|
||||
if here < 8.0 { return Warped(pos + vec3(0.0, warp.params[0].x, 0.0), vec3(1.0, 0.0, 0.0)); }
|
||||
return Warped(pos, vec3(1.0));
|
||||
}
|
||||
";
|
||||
/// Cuts every face.
|
||||
const CUT: &str = "fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped {
|
||||
return Warped(pos, vec3(-1.0));
|
||||
}
|
||||
";
|
||||
/// Paints the terrain green by its pixels and moves nothing.
|
||||
const PAINT: &str = "fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped {
|
||||
return Warped(pos, vec3(1.0));
|
||||
}
|
||||
// [fragments]
|
||||
fn world_fragment(f: Fragment) -> Painted {
|
||||
if !f.front { return Painted(vec4(0.0, 0.0, 0.0, -1.0), 1.0); }
|
||||
return Painted(vec4(0.0, 1.0, 0.0, 1.0), 0.0);
|
||||
}
|
||||
";
|
||||
/// Moves entities ten blocks right: off the screen's middle.
|
||||
const SHIFT: &str = "fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped {
|
||||
return Warped(pos + vec3(-10.0, 0.0, 0.0), vec3(1.0));
|
||||
}
|
||||
";
|
||||
|
||||
fn read(path: &Path) -> Vec<[u8; 4]> {
|
||||
let decoder = png::Decoder::new(std::io::BufReader::new(std::fs::File::open(path).unwrap()));
|
||||
let mut reader = decoder.read_info().unwrap();
|
||||
let mut bytes = vec![0; reader.output_buffer_size().unwrap()];
|
||||
let info = reader.next_frame(&mut bytes).unwrap();
|
||||
assert_eq!(info.color_type, png::ColorType::Rgba);
|
||||
bytes.truncate(info.buffer_size());
|
||||
bytes.as_chunks::<4>().0.to_vec()
|
||||
}
|
||||
|
||||
/// Pixels like `want` (within 40 a channel), of an image.
|
||||
fn count(pixels: &[[u8; 4]], want: [u8; 3]) -> usize {
|
||||
pixels
|
||||
.iter()
|
||||
.filter(|p| (0..3).all(|i| p[i].abs_diff(want[i]) < 40))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// A wall across x 0..16, y 0..16 at z = 12 of the section at y 96, both windings.
|
||||
fn wall() -> SectionMesh {
|
||||
let mut mesh = SectionMesh::default();
|
||||
let corners = [
|
||||
[0., 0., 12.],
|
||||
[0., 16., 12.],
|
||||
[16., 16., 12.],
|
||||
[16., 0., 12.],
|
||||
];
|
||||
let uvs = [[0., 1.], [0., 0.], [1., 0.], [1., 1.]];
|
||||
for order in [[0, 1, 2, 3], [3, 2, 1, 0]] {
|
||||
for i in order {
|
||||
mesh.solid
|
||||
.push(TerrainVertex::new(corners[i], uvs[i], 0, u32::MAX, 0xf0f0));
|
||||
}
|
||||
}
|
||||
mesh.sort_solid();
|
||||
mesh
|
||||
}
|
||||
|
||||
fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let out = std::env::args().nth(1).ok_or("output directory required")?;
|
||||
let out = Path::new(&out);
|
||||
std::fs::create_dir_all(out)?;
|
||||
native_render::install(Box::new([&Mod])).map_err(|_| "mod already installed")?;
|
||||
let white = [255; 16 * 16 * 4];
|
||||
let zero = [0; 4];
|
||||
let mut renderer = pollster::block_on(Renderer::new(
|
||||
window,
|
||||
true,
|
||||
&white,
|
||||
16,
|
||||
16,
|
||||
1,
|
||||
&[],
|
||||
&[],
|
||||
(&white, 16, 16),
|
||||
(&zero, 1, 1),
|
||||
(&zero, 1, 1),
|
||||
(&zero, 1),
|
||||
Arc::new(client::gfx::panorama::Faces::colour([0, 0, 0, 255])),
|
||||
None,
|
||||
None,
|
||||
GraphicsApi::Vulkan,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
))?;
|
||||
renderer.resize(320, 180);
|
||||
renderer.fullbright = true;
|
||||
// (keys count sections from the bottom of the world, y -64: 10 is y 96 to 112)
|
||||
renderer.upload((0, 10, 0), &wall());
|
||||
// an entity quad in front of the wall, middle of the screen
|
||||
let entity: Vec<Vertex> = [
|
||||
[7.5, 103.5, 6.0],
|
||||
[7.5, 104.5, 6.0],
|
||||
[8.5, 104.5, 6.0],
|
||||
[8.5, 103.5, 6.0],
|
||||
]
|
||||
.into_iter()
|
||||
.zip([[0., 1.], [0., 0.], [1., 0.], [1., 1.]])
|
||||
.map(|(p, uv)| Vertex {
|
||||
pos: [p[0] - 8.0, p[1] - 104.0, p[2] - 0.5],
|
||||
uv,
|
||||
layer: 0,
|
||||
color: 0xff40_40ff,
|
||||
light: 0xf0,
|
||||
normal: [0, 0, 0, 0],
|
||||
glint_uv: [0.0; 2],
|
||||
})
|
||||
.collect();
|
||||
if std::env::args().any(|a| a == "--bench") {
|
||||
return bench(&mut renderer, out);
|
||||
}
|
||||
let hud = HudBatch::new([320, 180]);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.entities = &entity;
|
||||
let mut shot = |name: &str, warp: Option<native_render::WorldWarp>| {
|
||||
*WARP.lock().unwrap() = warp;
|
||||
let path = out.join(format!("{name}.png"));
|
||||
renderer.screenshot = Some(ScreenshotRequest {
|
||||
path: path.clone(),
|
||||
size: None,
|
||||
scaled: false,
|
||||
});
|
||||
// twice: the first frame of a new warp builds it
|
||||
renderer.render(&frame).unwrap();
|
||||
renderer.render(&frame).unwrap();
|
||||
read(&path)
|
||||
};
|
||||
let warp = |source: &str, on: bool, lift: f32| {
|
||||
let mut w = native_render::WorldWarp::default();
|
||||
w.source = Some(source.into());
|
||||
w.on = on;
|
||||
w.params[0] = lift;
|
||||
Some(w)
|
||||
};
|
||||
let plain = shot("plain", None);
|
||||
let wall_px = count(&plain, [255, 255, 255]);
|
||||
let entity_px = count(&plain, [255, 64, 64]);
|
||||
assert!(wall_px > 1000, "the wall shows without a warp: {wall_px}");
|
||||
assert!(
|
||||
entity_px > 50,
|
||||
"the entity shows without a warp: {entity_px}"
|
||||
);
|
||||
|
||||
let off = shot("off", warp(LIFT, false, 4.0));
|
||||
assert!(off == plain, "a warp that is off draws as no warp");
|
||||
|
||||
let lifted = shot("lift", warp(LIFT, true, 4.0));
|
||||
let red = count(&lifted, [255, 0, 0]);
|
||||
assert!(red > 500, "the warp tints the wall's left half red: {red}");
|
||||
assert!(lifted != plain);
|
||||
|
||||
let cut = shot("cut", warp(CUT, true, 0.0));
|
||||
let left = count(&cut, [255, 255, 255]);
|
||||
assert!(
|
||||
left < wall_px / 20,
|
||||
"a cut leaves the wall out: {left} of {wall_px}"
|
||||
);
|
||||
|
||||
let painted = shot("paint", warp(PAINT, true, 0.0));
|
||||
let green = count(&painted, [0, 255, 0]);
|
||||
assert!(
|
||||
green > wall_px / 2,
|
||||
"the warp's own pixels paint the wall: {green}"
|
||||
);
|
||||
|
||||
let shifted = shot("shift", warp(SHIFT, true, 0.0));
|
||||
let entity_left = count(&shifted[..], [255, 64, 64]);
|
||||
let mid = |p: &[[u8; 4]]| -> Vec<[u8; 4]> {
|
||||
(60..120)
|
||||
.flat_map(|y| p[y * 320 + 130..y * 320 + 190].to_vec())
|
||||
.collect()
|
||||
};
|
||||
assert!(
|
||||
count(&mid(&shifted), [255, 64, 64]) < count(&mid(&plain), [255, 64, 64]) / 4,
|
||||
"the warp moves the entity out of the middle ({entity_left} red-ish pixels left)"
|
||||
);
|
||||
|
||||
let broken = shot(
|
||||
"broken",
|
||||
warp("fn world_warp() -> f32 { return 1; }", true, 0.0),
|
||||
);
|
||||
assert!(broken == plain, "a warp that doesn't compile is left out");
|
||||
|
||||
let back = shot("after", None);
|
||||
assert!(back == plain, "with the warp gone the frame is as before");
|
||||
std::fs::write(
|
||||
out.join("result.json"),
|
||||
serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"wall_pixels": wall_px, "entity_pixels": entity_px, "lifted_red_pixels": red,
|
||||
"cut_wall_pixels_left": left, "painted_green_pixels": green,
|
||||
"off_equals_plain": true, "broken_equals_plain": true, "after_equals_plain": true,
|
||||
"graphics_adapter": client::crash::system_details("fixture").into_iter()
|
||||
.find(|(name, _)| name == "Graphics Adapter").map(|(_, detail)| detail),
|
||||
}))?,
|
||||
)?;
|
||||
println!(
|
||||
"world warp check passed: {}",
|
||||
out.join("result.json").display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rolling land: a column of a section's top faces and the sides they show, at y 64 to 80.
|
||||
fn hills(cx: i32, cz: i32) -> SectionMesh {
|
||||
let height = |x: i32, z: i32| {
|
||||
let (x, z) = (f64::from(cx * 16 + x), f64::from(cz * 16 + z));
|
||||
(8.0 + 4.0 * (x * 0.09).sin() + 3.0 * (z * 0.07 + x * 0.02).cos()).floor() as i32
|
||||
};
|
||||
let mut mesh = SectionMesh::default();
|
||||
let mut quad = |c: [[f32; 3]; 4]| {
|
||||
for (p, uv) in c.into_iter().zip([[0., 1.], [0., 0.], [1., 0.], [1., 1.]]) {
|
||||
mesh.solid
|
||||
.push(TerrainVertex::new(p, uv, 0, 0xff60_a060, 0xf0));
|
||||
}
|
||||
};
|
||||
for x in 0..16 {
|
||||
for z in 0..16 {
|
||||
let h = height(x, z);
|
||||
let (fx, fz, fh) = (x as f32, z as f32, h as f32);
|
||||
quad([
|
||||
[fx, fh, fz],
|
||||
[fx, fh, fz + 1.],
|
||||
[fx + 1., fh, fz + 1.],
|
||||
[fx + 1., fh, fz],
|
||||
]);
|
||||
// the step down to a lower neighbour, toward +x and +z
|
||||
for (dx, dz) in [(1, 0), (0, 1)] {
|
||||
let n = height(x + dx, z + dz);
|
||||
if n < h {
|
||||
let (lo, hi) = (n as f32, fh);
|
||||
if dx == 1 {
|
||||
quad([
|
||||
[fx + 1., lo, fz],
|
||||
[fx + 1., hi, fz],
|
||||
[fx + 1., hi, fz + 1.],
|
||||
[fx + 1., lo, fz + 1.],
|
||||
]);
|
||||
} else {
|
||||
quad([
|
||||
[fx, lo, fz + 1.],
|
||||
[fx + 1., lo, fz + 1.],
|
||||
[fx + 1., hi, fz + 1.],
|
||||
[fx, hi, fz + 1.],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mesh.sort_solid();
|
||||
mesh
|
||||
}
|
||||
|
||||
/// `--bench`: milliseconds a frame without a warp and with one.
|
||||
fn bench(renderer: &mut Renderer, out: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
const R: i32 = 24;
|
||||
renderer.resize(1920, 1080);
|
||||
let mut sections = 0;
|
||||
for cx in -R..=R {
|
||||
for cz in -R..=R {
|
||||
// (keys count sections from y -64: 8 is y 64 to 80)
|
||||
renderer.upload((cx, 8, cz), &hills(cx, cz));
|
||||
sections += 1;
|
||||
}
|
||||
}
|
||||
let hud = HudBatch::new([1920, 1080]);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.camera = glam::DVec3::new(8.0, 90.0, 8.0);
|
||||
frame.eye = frame.camera;
|
||||
frame.pitch = 15.0;
|
||||
frame.eye_rotation = [0.0, 15.0];
|
||||
frame.distance = 16.0 * R as f32;
|
||||
frame.detail_distance = frame.distance;
|
||||
frame.depth_far = 2000.0;
|
||||
frame.cave_culling = true;
|
||||
let mut time = |warp: Option<native_render::WorldWarp>| {
|
||||
*WARP.lock().unwrap() = warp;
|
||||
for _ in 0..30 {
|
||||
renderer.render(&frame).unwrap();
|
||||
}
|
||||
let started = std::time::Instant::now();
|
||||
const FRAMES: u32 = 240;
|
||||
for _ in 0..FRAMES {
|
||||
renderer.render(&frame).unwrap();
|
||||
}
|
||||
// the last frame's picture, so the GPU has caught up
|
||||
renderer.screenshot = Some(ScreenshotRequest {
|
||||
path: out.join("bench.png"),
|
||||
size: None,
|
||||
scaled: false,
|
||||
});
|
||||
renderer.render(&frame).unwrap();
|
||||
renderer.render(&frame).unwrap();
|
||||
started.elapsed().as_secs_f64() * 1000.0 / f64::from(FRAMES + 2)
|
||||
};
|
||||
let mut wave = native_render::WorldWarp::default();
|
||||
wave.source = Some(
|
||||
"fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped {
|
||||
let x = pos.x + warp.camera.x + warp.camera_rest.x;
|
||||
return Warped(pos + vec3(0.0, sin(x * 0.05) * 3.0, 0.0), vec3(1.0));
|
||||
}\n"
|
||||
.into(),
|
||||
);
|
||||
wave.on = true;
|
||||
let plain = time(None);
|
||||
let warped = time(Some(wave));
|
||||
let again = time(None);
|
||||
let result = serde_json::json!({
|
||||
"sections": sections, "frame_ms_without_warp": plain, "frame_ms_with_warp": warped,
|
||||
"frame_ms_without_warp_after": again,
|
||||
"graphics_adapter": client::crash::system_details("bench").into_iter()
|
||||
.find(|(name, _)| name == "Graphics Adapter").map(|(_, detail)| detail),
|
||||
});
|
||||
println!("{result}");
|
||||
std::fs::write(out.join("bench.json"), serde_json::to_vec_pretty(&result)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct App;
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(Window::default_attributes().with_visible(false))
|
||||
.unwrap(),
|
||||
);
|
||||
if let Err(error) = fixture(window) {
|
||||
eprintln!("world warp check failed: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
event_loop.exit();
|
||||
}
|
||||
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {}
|
||||
}
|
||||
fn main() {
|
||||
for key in [
|
||||
"HORIZON_HOME",
|
||||
"HORIZON_LAUNCHER_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
] {
|
||||
assert!(
|
||||
std::env::var_os(key).is_some(),
|
||||
"set throwaway {key} (AGENTS.md)"
|
||||
);
|
||||
}
|
||||
EventLoop::new().unwrap().run_app(&mut App).unwrap();
|
||||
}
|
||||
|
||||
fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
Frame {
|
||||
weather: &[],
|
||||
weather_time: client::weather::Time::new(0, 0.0),
|
||||
camera: glam::DVec3::new(8.0, 104.0, 0.5),
|
||||
yaw: 0.0,
|
||||
pitch: 0.0,
|
||||
fov_degrees: 85.0,
|
||||
eye: glam::DVec3::new(8.0, 104.0, 0.5),
|
||||
eye_rotation: [0.0, 0.0],
|
||||
first_person: true,
|
||||
bob: glam::Mat4::IDENTITY,
|
||||
spin: glam::Mat4::IDENTITY,
|
||||
sky: client::environment::Sky::default(),
|
||||
shadow_scene_revision: (0, 0),
|
||||
shadow_cache: false,
|
||||
distance: 128.0,
|
||||
detail_distance: 128.0,
|
||||
depth_far: 1000.0,
|
||||
underwater: false,
|
||||
water_plane: None,
|
||||
cave_culling: false,
|
||||
title: false,
|
||||
sky_blocked: false,
|
||||
skybox: false,
|
||||
end_sky: false,
|
||||
end_sky_sprite: None,
|
||||
boss_world_fog: false,
|
||||
environment_fog: None,
|
||||
water_fog: [0.0; 3],
|
||||
fog: None,
|
||||
dark_disc: false,
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
hud,
|
||||
glint_alpha: 0.75,
|
||||
glint_ticks: 0.0,
|
||||
nether_lighting: false,
|
||||
entities: &[],
|
||||
static_block_entities: &[],
|
||||
visible_static_block_entity_sections: &[],
|
||||
static_block_entity_stats: Default::default(),
|
||||
entity_motion: &[],
|
||||
entity_motion_posed: &[],
|
||||
entity_spans: &[],
|
||||
skin: None,
|
||||
glowing: &[],
|
||||
dynamic_lights: &[],
|
||||
colored_lights: &[],
|
||||
emissive_layers: &[],
|
||||
clouds: None,
|
||||
lightning: Default::default(),
|
||||
additive: &[],
|
||||
see_through: &[],
|
||||
world_primitives: Default::default(),
|
||||
shadow_only: &[],
|
||||
celestial: &[],
|
||||
stars: None,
|
||||
hand: &[],
|
||||
hand_items: &[],
|
||||
hand_fov_degrees: 70.0,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
border: None,
|
||||
game_time: 0.0,
|
||||
shader_game_time: 0.0,
|
||||
end_portal_sprites: [[0.0; 4]; 2],
|
||||
crumbling_sprite: [0.0; 4],
|
||||
ticks: 0,
|
||||
darkness: (0.0, 0.0),
|
||||
night_vision: 0.0,
|
||||
block_flicker: 0.0,
|
||||
boss_darken: 0.0,
|
||||
chunk_fade: 0.0,
|
||||
post_effects: client::post_chain::Request::none(),
|
||||
menu_blur: None,
|
||||
}
|
||||
}
|
||||
@@ -47,53 +47,6 @@ struct Channel {
|
||||
world: u32,
|
||||
/// The sound file it started and when, to tell another player's copy of it.
|
||||
started: Option<(String, std::time::Instant)>,
|
||||
/// Its sound event plays clearly through a mod's muffle (`Audio::muffle`).
|
||||
unmuffled: bool,
|
||||
}
|
||||
/// The cutoff of the muffle's low-pass at full strength: the world heard through ringing ears.
|
||||
const MUFFLED_CUTOFF: f32 = 300.0;
|
||||
/// The cutoff it sweeps down from as its strength rises.
|
||||
const OPEN_CUTOFF: f32 = 20_000.0;
|
||||
/// How long the muffle takes to glide to a new level: a game tick's step (50 ms) would click.
|
||||
const MUFFLE_GLIDE: f32 = 0.02;
|
||||
/// A mod's muffle (`Audio::muffle`) in the mixer: the other sounds are mixed apart, low-passed by
|
||||
/// two one-pole stages and turned down, then added to the sounds it leaves clear. While it is off
|
||||
/// (never set, or faded out) the sounds mix straight into the output, as without it.
|
||||
#[derive(Default)]
|
||||
struct Muffle {
|
||||
/// `[amount, lowpass]`, each 0 to 1, as the game's tick last set them.
|
||||
target: [f32; 2],
|
||||
/// Where the output is now, gliding to `target`.
|
||||
current: [f32; 2],
|
||||
/// The low-pass stages' state for the left and right output channels.
|
||||
stages: [[f32; 2]; 2],
|
||||
/// The muffled sounds of one buffer, reused.
|
||||
bus: Vec<f32>,
|
||||
}
|
||||
impl Muffle {
|
||||
fn active(&self) -> bool {
|
||||
self.target != [0.0; 2] || self.current.iter().any(|v| *v > 1e-4)
|
||||
}
|
||||
/// Adds `bus` (the muffled sounds) to `data`, low-passed and turned down.
|
||||
fn apply(&mut self, bus: &[f32], data: &mut [f32], channels: usize, rate: u32) {
|
||||
let rate = rate as f32;
|
||||
let glide = 1.0 - (-1.0 / (MUFFLE_GLIDE * rate)).exp();
|
||||
for (frame, muffled) in data.chunks_mut(channels).zip(bus.chunks(channels)) {
|
||||
for (current, target) in self.current.iter_mut().zip(self.target) {
|
||||
*current += (target - *current) * glide;
|
||||
}
|
||||
let [amount, lowpass] = self.current;
|
||||
let gain = 1.0 - amount;
|
||||
let cutoff = OPEN_CUTOFF * (MUFFLED_CUTOFF / OPEN_CUTOFF).powf(lowpass);
|
||||
let a = 1.0 - (-std::f32::consts::TAU * cutoff / rate).exp();
|
||||
for ((out, s), stage) in frame.iter_mut().zip(muffled).zip(&mut self.stages) {
|
||||
stage[0] += (s - stage[0]) * a;
|
||||
stage[1] += (stage[0] - stage[1]) * a;
|
||||
// Dry at no strength, so a muffle that only turns sounds down leaves them as they were.
|
||||
*out += (s + (stage[1] - s) * lowpass) * gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Mixer {
|
||||
@@ -112,8 +65,6 @@ struct Mixer {
|
||||
cameras: Vec<([f64; 3], Basis, u32)>,
|
||||
/// Per-source gain/pan/step cache for one buffer, reused across callbacks.
|
||||
voices: Vec<(ChannelId, [f32; 3], f64)>,
|
||||
/// A mod's muffle of the sounds it does not exempt.
|
||||
muffle: Muffle,
|
||||
limiter_peak: f32,
|
||||
/// The output device the stream was opened on (`Library.currentDeviceName`).
|
||||
#[cfg(feature = "audio")]
|
||||
@@ -149,21 +100,6 @@ pub struct Heard {
|
||||
pub range: f32,
|
||||
pub looping: bool,
|
||||
}
|
||||
/// A mod's muffle (`Audio::muffle`): its full strength, and the ticks it fades over and has left.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct MuffleFade {
|
||||
amount: f32,
|
||||
lowpass: f32,
|
||||
ticks: u32,
|
||||
left: u32,
|
||||
}
|
||||
impl MuffleFade {
|
||||
/// `[amount, lowpass]` now: full when set, falling in a straight line to none.
|
||||
fn level(&self) -> [f32; 2] {
|
||||
let left = self.left as f32 / self.ticks as f32;
|
||||
[self.amount * left, self.lowpass * left]
|
||||
}
|
||||
}
|
||||
/// The mixer side of the engine (`Backend`).
|
||||
pub struct MixerBackend {
|
||||
mixer: Arc<Mutex<Mixer>>,
|
||||
@@ -311,8 +247,6 @@ pub struct Audio {
|
||||
pub stopped: std::collections::BTreeMap<String, u32>,
|
||||
/// The volume sliders' `SoundPreviewHandler`.
|
||||
preview: crate::sound_preview::SoundPreview,
|
||||
/// A mod's muffle fading out (`muffle`), advanced by `tick`.
|
||||
muffle: Option<MuffleFade>,
|
||||
/// `SoundEngine.lastSeenDevices`: the system default output last seen.
|
||||
#[cfg(feature = "audio")]
|
||||
default_seen: Option<String>,
|
||||
@@ -505,7 +439,6 @@ impl Backend for MixerBackend {
|
||||
owner: self.owner,
|
||||
world: self.world,
|
||||
started: None,
|
||||
unmuffled: false,
|
||||
},
|
||||
);
|
||||
Some(id)
|
||||
@@ -533,9 +466,6 @@ impl Backend for MixerBackend {
|
||||
fn set_relative(&mut self, channel: ChannelId, relative: bool) {
|
||||
self.with(channel, |c| c.relative = relative);
|
||||
}
|
||||
fn set_unmuffled(&mut self, channel: ChannelId, unmuffled: bool) {
|
||||
self.with(channel, |c| c.unmuffled = unmuffled);
|
||||
}
|
||||
/// `getCompleteBuffer` (cached, else decoded on the decoder thread) or
|
||||
/// `getStream` (decoded on a thread of its own), then attach and
|
||||
/// `alSourcePlay`. A sound that cannot load leaves its source initial,
|
||||
@@ -772,13 +702,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
|
||||
voices.push((*id, [left, right, gain], step));
|
||||
}
|
||||
// A mod's muffle: the sounds it doesn't exempt mix into a bus of their own first.
|
||||
let muffling = mixer.muffle.active();
|
||||
let mut bus = std::mem::take(&mut mixer.muffle.bus);
|
||||
if muffling {
|
||||
bus.clear();
|
||||
bus.resize(data.len(), 0.0);
|
||||
}
|
||||
for (id, [left, right, gain], step) in voices.iter().copied() {
|
||||
let Some(c) = mixer.channels.get_mut(&id) else {
|
||||
continue;
|
||||
@@ -788,12 +711,7 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
};
|
||||
let source_channels = source_channels as usize;
|
||||
let frames = samples.len() / source_channels;
|
||||
let out: &mut [f32] = if muffling && !c.unmuffled {
|
||||
&mut bus
|
||||
} else {
|
||||
&mut *data
|
||||
};
|
||||
for frame in out.chunks_mut(channels) {
|
||||
for frame in data.chunks_mut(channels) {
|
||||
let mut i = c.position as usize;
|
||||
if i + 1 >= frames {
|
||||
if c.looping && frames > 1 {
|
||||
@@ -828,14 +746,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
}
|
||||
}
|
||||
mixer.voices = voices;
|
||||
if muffling {
|
||||
mixer.muffle.apply(&bus, data, channels, rate);
|
||||
} else {
|
||||
// Off: the next muffle starts from silence, not from the last one's filter.
|
||||
mixer.muffle.current = [0.0; 2];
|
||||
mixer.muffle.stages = [[0.0; 2]; 2];
|
||||
}
|
||||
mixer.muffle.bus = bus;
|
||||
// Limit the whole buffer together, preserving waveform and stereo balance
|
||||
// instead of flattening each overloaded sample; release over 200 ms
|
||||
// (OpenAL Soft's output limiter, `ALC_OUTPUT_LIMITER_SOFT`, releases
|
||||
@@ -1161,13 +1071,12 @@ impl Audio {
|
||||
listener: [0.0; 3],
|
||||
listener_yaw: 0.0,
|
||||
master: 1.0,
|
||||
random: crate::random_source::create(),
|
||||
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
|
||||
source_override: None,
|
||||
capture: None,
|
||||
heard: Default::default(),
|
||||
stopped: Default::default(),
|
||||
preview: Default::default(),
|
||||
muffle: None,
|
||||
#[cfg(feature = "audio")]
|
||||
default_seen: if live { default_output_name() } else { None },
|
||||
#[cfg(feature = "audio")]
|
||||
@@ -1323,13 +1232,12 @@ impl Audio {
|
||||
listener: [0.0; 3],
|
||||
listener_yaw: 0.0,
|
||||
master: self.master,
|
||||
random: crate::random_source::create(),
|
||||
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
|
||||
source_override: None,
|
||||
capture: None,
|
||||
heard: Default::default(),
|
||||
stopped: Default::default(),
|
||||
preview: Default::default(),
|
||||
muffle: None,
|
||||
// The device belongs to the first player's output, which looks after it.
|
||||
#[cfg(feature = "audio")]
|
||||
default_seen: self.default_seen.clone(),
|
||||
@@ -1462,50 +1370,6 @@ impl Audio {
|
||||
}
|
||||
self.sync_volumes();
|
||||
self.engine.tick(paused, ctx, &mut self.backend);
|
||||
// A paused game holds a mod's muffle where it is, as it holds the sounds.
|
||||
if !paused && let Some(fade) = &mut self.muffle {
|
||||
fade.left = fade.left.saturating_sub(1);
|
||||
let level = fade.level();
|
||||
if fade.left == 0 {
|
||||
self.muffle = None;
|
||||
self.engine.set_unmuffled(Vec::new(), &mut self.backend);
|
||||
}
|
||||
self.set_muffle_level(level);
|
||||
}
|
||||
}
|
||||
/// A mod's muffle (`native_muffle_sounds`): `amount` (0 to 1) of every sound's volume taken
|
||||
/// away and `lowpass` (0 to 1) dulling it, from untouched to a 300 Hz low-pass, fading in a
|
||||
/// straight line to none over `ticks` client ticks; the sound events in `exempt` (namespaced,
|
||||
/// `mymod:ear.ringing`) play clearly through it, those already playing too. It replaces any
|
||||
/// muffle in place; `ticks` 0 or no strength ends it. The output's, so in splitscreen every
|
||||
/// player hears it. False for an amount or strength outside 0 to 1.
|
||||
pub fn muffle(&mut self, amount: f32, lowpass: f32, ticks: u32, exempt: &[&str]) -> bool {
|
||||
if !(0.0..=1.0).contains(&amount) || !(0.0..=1.0).contains(&lowpass) {
|
||||
return false;
|
||||
}
|
||||
let on = ticks > 0 && (amount > 0.0 || lowpass > 0.0);
|
||||
let exempt = if on {
|
||||
exempt
|
||||
.iter()
|
||||
.map(|e| crate::sound_events::normalize(e))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
self.engine.set_unmuffled(exempt, &mut self.backend);
|
||||
self.muffle = on.then_some(MuffleFade {
|
||||
amount,
|
||||
lowpass,
|
||||
ticks,
|
||||
left: ticks,
|
||||
});
|
||||
self.set_muffle_level(self.muffle.map_or([0.0; 2], |fade| fade.level()));
|
||||
true
|
||||
}
|
||||
fn set_muffle_level(&mut self, level: [f32; 2]) {
|
||||
if let Ok(mut m) = self.backend.mixer.lock() {
|
||||
m.muffle.target = level;
|
||||
}
|
||||
}
|
||||
/// The `uiSoundReserve` extra: positional sounds leave
|
||||
/// `sound_engine::HEARD_RESERVE` static channels free (off: vanilla's pool).
|
||||
@@ -1604,14 +1468,6 @@ 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) {
|
||||
@@ -1626,7 +1482,7 @@ impl Audio {
|
||||
"minecraft:ui.button.click",
|
||||
1.0,
|
||||
0.25,
|
||||
crate::random_source::create(),
|
||||
SoundEngine::unseeded(),
|
||||
),
|
||||
false,
|
||||
);
|
||||
@@ -1768,7 +1624,7 @@ impl Audio {
|
||||
f64::from(pos[2]) + 0.5,
|
||||
];
|
||||
let event = crate::sound_events::normalize(event);
|
||||
let instance = SoundInstance::for_jukebox_song(&event, at, crate::random_source::create());
|
||||
let instance = SoundInstance::for_jukebox_song(&event, at, SoundEngine::unseeded());
|
||||
self.play_instance(instance, true).0
|
||||
}
|
||||
/// Stop and drop a held instance.
|
||||
@@ -1979,7 +1835,6 @@ mod tests {
|
||||
owner: 0,
|
||||
world: 0,
|
||||
started: None,
|
||||
unmuffled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2400,123 +2255,6 @@ mod tests {
|
||||
assert_eq!(data, [0.5; 2]);
|
||||
}
|
||||
|
||||
/// One second of a `hz` sine at 48 kHz, amplitude 0.3 (looped, it repeats seamlessly).
|
||||
fn sine(hz: f32) -> Vec<f32> {
|
||||
(0..48_000)
|
||||
.map(|i| 0.3 * (std::f32::consts::TAU * hz * i as f32 / 48_000.0).sin())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn rms(data: &[f32]) -> f32 {
|
||||
(data.iter().map(|s| s * s).sum::<f32>() / data.len() as f32).sqrt()
|
||||
}
|
||||
|
||||
/// `seconds` of the mixer's stereo output, in the 10 ms buffers an output stream asks for.
|
||||
fn listen(m: &mut Mixer, seconds: f32) -> Vec<f32> {
|
||||
let mut out = Vec::new();
|
||||
let mut buffer = [0.0; 960];
|
||||
for _ in 0..(seconds * 100.0).round() as usize {
|
||||
mix(m, &mut buffer, 2, 48_000);
|
||||
out.extend_from_slice(&buffer);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_muffle_dulls_and_quietens_every_sound_but_the_exempt_ones() {
|
||||
// A looping tone heard through a muffle of `[amount, lowpass]`, once it has settled.
|
||||
let heard = |hz: f32, muffle: [f32; 2], unmuffled: bool| {
|
||||
let mut m = Mixer::default();
|
||||
let mut c = channel(sine(hz), None, [0.0, 0.0, 2.0], true);
|
||||
c.unmuffled = unmuffled;
|
||||
m.channels.insert(1, c);
|
||||
m.muffle.target = muffle;
|
||||
listen(&mut m, 0.2);
|
||||
listen(&mut m, 0.2)
|
||||
};
|
||||
let plain = |hz: f32| rms(&heard(hz, [0.0; 2], false));
|
||||
// Fully low-passed, a 6 kHz whine all but goes and a 100 Hz hum keeps most of its level.
|
||||
assert!(rms(&heard(6000.0, [0.0, 1.0], false)) < plain(6000.0) * 0.05);
|
||||
assert!(rms(&heard(100.0, [0.0, 1.0], false)) > plain(100.0) * 0.8);
|
||||
// Half the volume taken and no low-pass: half the level, the tone itself untouched.
|
||||
let ducked = rms(&heard(6000.0, [0.5, 0.0], false)) / plain(6000.0);
|
||||
assert!((ducked - 0.5).abs() < 1e-3, "{ducked}");
|
||||
// An exempt sound plays through as if there were no muffle, sample for sample.
|
||||
assert_eq!(
|
||||
heard(6000.0, [0.5, 1.0], true),
|
||||
heard(6000.0, [0.0; 2], false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_faded_muffle_leaves_the_sound_as_it_was() {
|
||||
let mixer = || {
|
||||
let mut m = Mixer::default();
|
||||
m.channels
|
||||
.insert(1, channel(sine(3000.0), None, [2.0, 0.0, 1.0], true));
|
||||
m
|
||||
};
|
||||
let (mut muffled, mut plain) = (mixer(), mixer());
|
||||
muffled.muffle.target = [0.7, 1.0];
|
||||
assert_ne!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
|
||||
muffled.muffle.target = [0.0; 2];
|
||||
listen(&mut muffled, 0.5);
|
||||
listen(&mut plain, 0.5);
|
||||
assert_eq!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mods_muffle_fades_with_the_game_ticks_and_spares_its_exempt_sounds() {
|
||||
let mut audio = Audio::silent(Path::new("/nonexistent/horizon-test-assets"));
|
||||
audio.engine.registry.add_file(
|
||||
"minecraft",
|
||||
br#"{"ambient.cave":{"sounds":["ambient/cave/cave1"]}}"#,
|
||||
&|_, _| true,
|
||||
);
|
||||
audio.engine.registry.add_file(
|
||||
"hztest",
|
||||
br#"{"ear.ringing":{"sounds":["ear/ringing"]}}"#,
|
||||
&|_, _| true,
|
||||
);
|
||||
// The engine makes its channels; with no output, none loads a file.
|
||||
audio.engine.loaded = true;
|
||||
let marks = |audio: &Audio| {
|
||||
let m = audio.backend.mixer.lock().unwrap();
|
||||
let mut marks: Vec<_> = m
|
||||
.channels
|
||||
.iter()
|
||||
.map(|(&id, c)| (id, c.unmuffled))
|
||||
.collect();
|
||||
marks.sort();
|
||||
marks
|
||||
};
|
||||
let level = |audio: &Audio| audio.backend.mixer.lock().unwrap().muffle.target;
|
||||
let close =
|
||||
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
|
||||
// Two sounds already playing, and one exempt sound started under the muffle.
|
||||
audio.play("ambient.cave", None, 1.0, 1.0);
|
||||
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
|
||||
assert!(audio.muffle(0.8, 1.0, 4, &["hztest:ear.ringing"]));
|
||||
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
|
||||
assert_eq!(marks(&audio), [(1, false), (2, true), (3, true)]);
|
||||
assert!(close(level(&audio), [0.8, 1.0]));
|
||||
let ticks = TickContext::default();
|
||||
audio.tick(false, &ticks);
|
||||
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
|
||||
// A paused game holds it.
|
||||
audio.tick(true, &ticks);
|
||||
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
|
||||
for _ in 0..3 {
|
||||
audio.tick(false, &ticks);
|
||||
}
|
||||
assert_eq!(level(&audio), [0.0; 2]);
|
||||
// Over: its exemptions go with it.
|
||||
assert_eq!(marks(&audio), [(1, false), (2, false), (3, false)]);
|
||||
// Out of range changes nothing.
|
||||
assert!(!audio.muffle(1.5, 0.0, 10, &[]));
|
||||
assert_eq!(level(&audio), [0.0; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_devices_are_looked_at_again_only_when_the_cards_change() {
|
||||
// No fingerprint (not Linux, or the cards could not be read): the list
|
||||
|
||||
@@ -191,18 +191,13 @@ pub fn discovery_url() -> String {
|
||||
.unwrap_or_else(|| DEFAULT_DISCOVERY.to_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn agent() -> ureq::Agent {
|
||||
fn agent() -> ureq::Agent {
|
||||
ureq::AgentBuilder::new()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.user_agent(&format!("Horizon/{}", horizon_core::release::VERSION))
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Whether `HORIZON_API_DISCOVERY` names another services environment.
|
||||
pub fn discovery_overridden() -> bool {
|
||||
discovery_url() != DEFAULT_DISCOVERY
|
||||
}
|
||||
|
||||
/// The discovery document, fetched once per game (a failure is retried next time).
|
||||
pub(crate) fn discovery() -> Result<Arc<serde_json::Value>, JoinError> {
|
||||
static CACHE: Mutex<Option<Arc<serde_json::Value>>> = Mutex::new(None);
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,11 @@ pub struct BanDetails {
|
||||
pub reason_message: Option<String>,
|
||||
}
|
||||
|
||||
/// authlib's `UserProperties`, as far as the title screen and the friends service read it.
|
||||
/// authlib's `UserProperties`, as far as the title screen reads it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserProperties {
|
||||
/// `UserFlag.SERVERS_ALLOWED` (`privileges.multiplayerServer.enabled`).
|
||||
pub servers_allowed: bool,
|
||||
/// `UserFlag.FRIENDS_ENABLED` (`friendsPreferences.friends`).
|
||||
pub friends_enabled: bool,
|
||||
/// `UserFlag.ACCEPT_FRIEND_INVITES` (`friendsPreferences.acceptInvites`).
|
||||
pub accept_friend_invites: bool,
|
||||
/// `bannedScopes.get("MULTIPLAYER")`.
|
||||
pub multiplayer_ban: Option<BanDetails>,
|
||||
}
|
||||
@@ -34,8 +30,6 @@ impl Default for UserProperties {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
servers_allowed: true,
|
||||
friends_enabled: false,
|
||||
accept_friend_invites: false,
|
||||
multiplayer_ban: None,
|
||||
}
|
||||
}
|
||||
@@ -215,13 +209,6 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
|
||||
.pointer("/privileges/multiplayerServer/enabled")
|
||||
.and_then(|e| e.as_bool())
|
||||
.unwrap_or(false);
|
||||
let toggle = |name: &str| {
|
||||
v.pointer(&format!("/friendsPreferences/{name}"))
|
||||
.and_then(|f| f.as_str())
|
||||
.is_some_and(|f| f.eq_ignore_ascii_case("ENABLED"))
|
||||
};
|
||||
let friends_enabled = toggle("friends");
|
||||
let accept_friend_invites = toggle("acceptInvites");
|
||||
let multiplayer_ban = v
|
||||
.pointer("/banStatus/bannedScopes/MULTIPLAYER")
|
||||
.filter(|b| b.is_object())
|
||||
@@ -235,8 +222,6 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
|
||||
});
|
||||
Some(UserProperties {
|
||||
servers_allowed,
|
||||
friends_enabled,
|
||||
accept_friend_invites,
|
||||
multiplayer_ban,
|
||||
})
|
||||
}
|
||||
@@ -290,24 +275,6 @@ mod tests {
|
||||
assert!(clean.servers_allowed && clean.multiplayer_ban.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn friends_preferences_read_their_two_flags() {
|
||||
let body = r#"{"privileges":{"multiplayerServer":{"enabled":true}},
|
||||
"friendsPreferences":{"friends":"ENABLED","acceptInvites":"DISABLED"}}"#;
|
||||
let p = parse_properties(body).unwrap();
|
||||
assert!(p.friends_enabled);
|
||||
assert!(!p.accept_friend_invites);
|
||||
let other = parse_properties(
|
||||
r#"{"friendsPreferences":{"friends":"DISABLED","acceptInvites":"ENABLED"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!other.friends_enabled);
|
||||
assert!(other.accept_friend_invites);
|
||||
// The offline fallback has neither flag, as `OFFLINE_PROPERTIES` does.
|
||||
assert!(!UserProperties::default().friends_enabled);
|
||||
assert!(!UserProperties::default().accept_friend_invites);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_profile_actions() {
|
||||
let a = parse_profile_actions(
|
||||
@@ -330,7 +297,6 @@ mod tests {
|
||||
properties: UserProperties {
|
||||
servers_allowed: true,
|
||||
multiplayer_ban: Some(ban.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
actions: ProfileActions {
|
||||
forced_name_change: true,
|
||||
@@ -351,7 +317,7 @@ mod tests {
|
||||
let no_servers = AccountStatus {
|
||||
properties: UserProperties {
|
||||
servers_allowed: false,
|
||||
..Default::default()
|
||||
multiplayer_ban: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! 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::{
|
||||
@@ -107,31 +106,11 @@ 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>>,
|
||||
/// The biome's environment attributes (`Biome.getAttributes`): its layer in the
|
||||
/// environment attribute stack.
|
||||
pub attributes: horizon_world::environment_attributes::AttributeMap,
|
||||
}
|
||||
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 {
|
||||
@@ -266,7 +245,6 @@ 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()
|
||||
@@ -280,11 +258,6 @@ impl BiomeInfo {
|
||||
.collect()
|
||||
})
|
||||
.collect(),
|
||||
attributes: horizon_world::environment_attributes::AttributeMap::parse(attributes)
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("biome attributes: {e}");
|
||||
Default::default()
|
||||
}),
|
||||
name,
|
||||
}
|
||||
}
|
||||
@@ -401,17 +374,6 @@ impl Biomes {
|
||||
pub fn get(&self, id: u16) -> &BiomeInfo {
|
||||
&self.list[usize::from(id).min(self.list.len() - 1)]
|
||||
}
|
||||
/// The name of biome `id`, plains (as for an unknown name) when the id is
|
||||
/// past this table: a column a server sent under its earlier, longer
|
||||
/// registry (a proxy's previous server) keeps its ids until the next
|
||||
/// login replaces it. `get` clamps such an id to the last entry instead,
|
||||
/// so for that stale column the colours and this name can come from
|
||||
/// different biomes until the login.
|
||||
pub fn name(&self, id: u16) -> &str {
|
||||
self.list
|
||||
.get(usize::from(id))
|
||||
.map_or("minecraft:plains", |b| b.name.as_str())
|
||||
}
|
||||
pub fn grass(&self, id: u16, x: f64, z: f64) -> u32 {
|
||||
let b = self.get(id);
|
||||
match b.modifier {
|
||||
@@ -601,9 +563,7 @@ 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"},
|
||||
"attributes": {"minecraft:visual/ambient_particles": {
|
||||
"argument": [{"particle": {"type": "minecraft:ash"}, "probability": 0.25}]}}});
|
||||
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"}});
|
||||
let hot = json!({"effects": {"grass_color": 0x123456}});
|
||||
let files = [
|
||||
("pack:zeta", &hot),
|
||||
@@ -616,9 +576,6 @@ 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);
|
||||
@@ -643,49 +600,6 @@ mod pack_tests {
|
||||
assert_eq!(other.get(2).water, 0x3F76E4);
|
||||
assert!(!other.has_packs());
|
||||
}
|
||||
|
||||
/// A column keeps the ids of the table it was decoded with. When a
|
||||
/// proxy's next server lacks a biome the last one added, that id is past
|
||||
/// the next table, and it names plains instead of indexing past the end
|
||||
/// (#12148: "the len is 67 but the index is 67").
|
||||
#[test]
|
||||
fn an_id_past_the_table_names_plains() {
|
||||
let next = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
|
||||
let last = next.with_packs([("limbo:void", &json!({}))]);
|
||||
let stale = last.id("limbo:void");
|
||||
assert_eq!((stale, last.name(stale)), (3, "limbo:void"));
|
||||
assert_eq!(next.name(stale), "minecraft:plains");
|
||||
assert_eq!(next.name(u16::MAX), "minecraft:plains");
|
||||
assert_eq!(next.name(2), "minecraft:river");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Biomes {
|
||||
/// Biomes from their files' JSON, by name, in id order: for tests elsewhere.
|
||||
pub(crate) fn from_jsons(entries: &[(&str, Value)]) -> Self {
|
||||
let maps = Colormaps {
|
||||
grass: Vec::new(),
|
||||
foliage: Vec::new(),
|
||||
dry: Vec::new(),
|
||||
};
|
||||
let list: Vec<_> = entries
|
||||
.iter()
|
||||
.map(|(name, v)| BiomeInfo::from_json((*name).to_owned(), v, &maps))
|
||||
.collect();
|
||||
Self {
|
||||
builtin: Arc::new(list.clone()),
|
||||
index: list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, b)| (b.name.clone(), i as u16))
|
||||
.collect(),
|
||||
list,
|
||||
info_noise: SimplexNoise::with_skipped_offsets(&mut LegacyRandom::new(2345), true),
|
||||
colormaps: Arc::new(maps),
|
||||
packs: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -77,9 +77,6 @@ pub struct BlockEntities {
|
||||
/// The fluid tanks of mod blocks' block entities, by block state id;
|
||||
/// built with `states`, like the containers.
|
||||
mod_fluids: FxHashMap<u32, Arc<ModFluidTanks>>,
|
||||
/// State-aware native factory ownership. Shared across level snapshots;
|
||||
/// an empty slice means the declarations have not yet been compiled.
|
||||
stateful_entities: Arc<[bool]>,
|
||||
/// The energy buffers of mod blocks' block entities, by block state id;
|
||||
/// built with `states`, like the containers.
|
||||
mod_energy: FxHashMap<u32, Arc<ModEnergyBuffer>>,
|
||||
@@ -212,21 +209,6 @@ impl BlockEntities {
|
||||
pub fn mod_fluids(&self, state: u32) -> Option<&Arc<ModFluidTanks>> {
|
||||
self.mod_fluids.get(&state)
|
||||
}
|
||||
pub fn set_stateful_entities(&mut self, states: Vec<bool>) {
|
||||
self.stateful_entities = states.into();
|
||||
}
|
||||
pub fn has_stateful_entity_table(&self) -> bool {
|
||||
!self.stateful_entities.is_empty()
|
||||
}
|
||||
pub fn stateful_entities(&self) -> Arc<[bool]> {
|
||||
self.stateful_entities.clone()
|
||||
}
|
||||
pub fn stateful_entity(&self, state: u32) -> bool {
|
||||
self.stateful_entities
|
||||
.get(state as usize)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
pub fn set_mod_energy(&mut self, energy: FxHashMap<u32, Arc<ModEnergyBuffer>>) {
|
||||
self.mod_energy = energy;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
//! decorated pots, conduits, enchanting-table and lectern books and copper
|
||||
//! golem statues, End portals and End gateways. Models are vanilla's baked `LayerDefinitions` layers
|
||||
//! (`entity-models.json`); transforms, animation and textures follow each
|
||||
//! vanilla renderer. Faces carry their tint and world normal: the entity
|
||||
//! shader (and a shader pack) applies `minecraft_mix_light` from the normal, as
|
||||
//! vanilla's does.
|
||||
//! vanilla renderer. Faces are lit like the `entity` shader: the lightmap at
|
||||
//! the block plus `minecraft_mix_light` with the level's two diffuse lights.
|
||||
use crate::{
|
||||
Result,
|
||||
bidi::visual,
|
||||
block_entity_anim::{self, EnchantingBook},
|
||||
breaking::entity_geometry,
|
||||
fast_hash::FxHashMap,
|
||||
font::{Font, Glyph},
|
||||
font::Glyph,
|
||||
gfx::mark_oit,
|
||||
hud::Rect,
|
||||
mesher::{CULL_BACK, Vertex},
|
||||
models::load_visibility,
|
||||
signs::{self, SignData, SignText},
|
||||
text::{Style, Text},
|
||||
world::World,
|
||||
};
|
||||
use glam::{DVec3, Mat4, Quat, Vec3};
|
||||
@@ -130,14 +128,6 @@ fn pack_color(rgb: u32, alpha: u32, brightness: f32) -> u32 {
|
||||
let c = |shift: u32| (((rgb >> shift) & 255) as f32 * brightness) as u32;
|
||||
alpha << 24 | c(0) << 16 | c(8) << 8 | c(16)
|
||||
}
|
||||
/// A world normal as `Vertex::normal` holds it: RGBA8_SNORM, `w` positive so
|
||||
/// the entity shader and a shader pack both shade by it. Block-entity models
|
||||
/// carry no normals of their own (vanilla computes each face's from its
|
||||
/// polygon, then `minecraft_mix_light` uses it in the shader).
|
||||
fn pack_normal(normal: Vec3) -> [i8; 4] {
|
||||
let c = |v: f32| (v.clamp(-1.0, 1.0) * 127.0).round() as i8;
|
||||
[c(normal.x), c(normal.y), c(normal.z), 127]
|
||||
}
|
||||
|
||||
/// Everything a draw call needs besides the geometry.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -175,7 +165,6 @@ pub fn draw_part(
|
||||
light,
|
||||
lights,
|
||||
glow,
|
||||
..
|
||||
} = paint;
|
||||
for poly in &part.polygons {
|
||||
let pos = poly.map(|v| m.transform_point3(Vec3::new(v[0], v[1], v[2]) / 16.0));
|
||||
@@ -188,21 +177,11 @@ pub fn draw_part(
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
// The colour is the tint alone when the caller shades by the level's
|
||||
// lights: the entity shader applies `minecraft_mix_light` from the
|
||||
// normal, as vanilla does, so a shader pack receives the unshaded
|
||||
// colour Iris gives it (a baked shade would be applied a second time
|
||||
// by the pack's lighting). Other lights (an item preview's
|
||||
// `Lighting.Entry`) are not the shader's to know, so those stay baked
|
||||
// into the colour, as before.
|
||||
let baked = !(lights == diffuse_lights(false) || lights == diffuse_lights(true));
|
||||
let brightness = if baked { diffuse(normal, lights) } else { 1.0 };
|
||||
let color = pack_color(tint, 255, brightness);
|
||||
let normal = if baked { [0; 4] } else { pack_normal(normal) };
|
||||
let color = pack_color(tint, 255, diffuse(normal, lights));
|
||||
for (r, light) in std::iter::once((r, light)).chain(glow.map(|g| (g, FULL_BRIGHT))) {
|
||||
for (v, position) in poly.iter().zip(pos) {
|
||||
out.push(Vertex {
|
||||
normal,
|
||||
normal: [0; 4],
|
||||
pos: position.to_array(),
|
||||
uv: [r.u0 + (r.u1 - r.u0) * v[3], r.v0 + (r.v1 - r.v0) * v[4]],
|
||||
layer: 0,
|
||||
@@ -621,26 +600,30 @@ fn frac(x: f32) -> f32 {
|
||||
x - x.floor()
|
||||
}
|
||||
|
||||
/// `StringSplitter.findLineBreak`: first line, with styled advances.
|
||||
fn first_line(line: &Text, max_width: f32, advance: &dyn Fn(char, &Style) -> f32) -> Text {
|
||||
/// `StringSplitter.splitLines(...).get(0)` for plain text: the first line,
|
||||
/// wrapped at the last space that fits or mid-word when none does.
|
||||
fn first_line(line: &str, max_width: f32, advance: &dyn Fn(char) -> f32) -> String {
|
||||
let mut width = 0.0;
|
||||
let mut visible = false;
|
||||
let mut last_space = None;
|
||||
for (i, (c, style)) in line.chars().enumerate() {
|
||||
for (i, c) in line.char_indices() {
|
||||
if c == '\n' {
|
||||
return line.slice(0, i);
|
||||
return line[..i].to_owned();
|
||||
}
|
||||
if c == ' ' {
|
||||
last_space = Some(i);
|
||||
}
|
||||
let step = advance(c, style);
|
||||
width += step;
|
||||
if visible && width > max_width.max(1.0) {
|
||||
return line.slice(0, last_space.unwrap_or(i));
|
||||
width += advance(c);
|
||||
if width > max_width {
|
||||
let end = match last_space {
|
||||
Some(s) if s > 0 => s,
|
||||
_ if i > 0 => i,
|
||||
// The first char alone is too wide: take that one char.
|
||||
_ => c.len_utf8(),
|
||||
};
|
||||
return line[..end].to_owned();
|
||||
}
|
||||
visible |= step != 0.0;
|
||||
}
|
||||
line.clone()
|
||||
line.to_owned()
|
||||
}
|
||||
|
||||
/// `AbstractSignRenderer.isOutlineVisible`: the camera within 16 blocks of
|
||||
@@ -658,7 +641,7 @@ fn sign_text_near(pos: [i32; 3], camera: DVec3) -> bool {
|
||||
/// `AbstractSignRenderer.submitSignText`: four centred lines, glowing text
|
||||
/// full-bright with an 8x outline in the dark colour. The font colours each
|
||||
/// glyph by its style (`Font.StringRenderOutput`): a line's own colour wins,
|
||||
/// else the sign's. Keep style boundaries for glyphs and decoration effects.
|
||||
/// else the sign's. A line is one call per run of one colour.
|
||||
fn sign_text_calls(
|
||||
scene: &Scene,
|
||||
data: &SignData,
|
||||
@@ -670,43 +653,27 @@ fn sign_text_calls(
|
||||
let line_height = data.line_height();
|
||||
let midpoint = (4.0 * line_height / 2.0).floor();
|
||||
let dark = 0xFF00_0000 | text.dark_color();
|
||||
let (color, outline, light, tier) = if text.glowing {
|
||||
let (color, outline, light) = if text.glowing {
|
||||
let c = text.text_color();
|
||||
(
|
||||
0xFF00_0000 | c,
|
||||
if c == 0 || near { dark } else { 0 },
|
||||
240 | 240 << 8,
|
||||
crate::mesher::LOW,
|
||||
)
|
||||
} else {
|
||||
(dark, 0, vertex_light(packed), 0)
|
||||
};
|
||||
let advance = |c: char, style: &Style| {
|
||||
let g = scene
|
||||
.font
|
||||
.map_or_else(|| (scene.glyph)(c), |f| f.glyph_styled(c, style));
|
||||
g.advance + if style.is_bold() { g.offset } else { 0.0 }
|
||||
(dark, 0, vertex_light(packed))
|
||||
};
|
||||
let advance = |c: char| (scene.glyph)(c).advance;
|
||||
let mut calls: Vec<TextCall> = Vec::with_capacity(text.lines.len());
|
||||
for i in 0..text.lines.len() {
|
||||
// The line that fits, in the order it is drawn (getVisualOrder).
|
||||
let line = text.line_text(i);
|
||||
let shown = if let Some(font) = scene.font {
|
||||
font.split_formatted(&line, data.max_line_width())
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.resolved()
|
||||
.into_owned()
|
||||
} else {
|
||||
first_line(&line.resolved(), data.max_line_width(), &advance)
|
||||
};
|
||||
let line = line.resolved();
|
||||
let flat: String = line.chars().map(|(c, _)| c).collect();
|
||||
let fits = first_line(&flat, data.max_line_width(), &advance);
|
||||
let shown = line.slice(0, fits.chars().count());
|
||||
let shown = visual(&shown);
|
||||
let width = shown
|
||||
.chars()
|
||||
.map(|(c, s)| advance(c, s))
|
||||
.sum::<f32>()
|
||||
.ceil();
|
||||
let width = shown.chars().map(|(c, _)| advance(c)).sum::<f32>().ceil();
|
||||
let x = -((width as i32) / 2) as f32;
|
||||
let y = i as f32 * line_height - midpoint;
|
||||
let first = calls.len();
|
||||
@@ -716,22 +683,19 @@ fn sign_text_calls(
|
||||
.style
|
||||
.color
|
||||
.map_or(color, |rgb| 0xFF00_0000 | (rgb & 0xFF_FFFF));
|
||||
calls.push(TextCall {
|
||||
matrix: m,
|
||||
x: x + pen,
|
||||
y,
|
||||
text: span.text.clone(),
|
||||
style: span.style.clone(),
|
||||
color: run,
|
||||
outline,
|
||||
light,
|
||||
tier,
|
||||
});
|
||||
pen += span
|
||||
.text
|
||||
.chars()
|
||||
.map(|c| advance(c, &span.style))
|
||||
.sum::<f32>();
|
||||
match calls[first..].last_mut() {
|
||||
Some(last) if last.color == run => last.text.push_str(&span.text),
|
||||
_ => calls.push(TextCall {
|
||||
matrix: m,
|
||||
x: x + pen,
|
||||
y,
|
||||
text: span.text.clone(),
|
||||
color: run,
|
||||
outline,
|
||||
light,
|
||||
}),
|
||||
}
|
||||
pen += span.text.chars().map(advance).sum::<f32>();
|
||||
}
|
||||
if calls.len() == first {
|
||||
// A line with no text is still a call, of no glyphs.
|
||||
@@ -740,28 +704,15 @@ fn sign_text_calls(
|
||||
x,
|
||||
y,
|
||||
text: String::new(),
|
||||
style: Style::default(),
|
||||
color,
|
||||
outline,
|
||||
light,
|
||||
tier,
|
||||
});
|
||||
}
|
||||
}
|
||||
calls
|
||||
}
|
||||
|
||||
fn sign_text_animated(data: &SignData) -> bool {
|
||||
[&data.front, &data.back].into_iter().any(|side| {
|
||||
(0..side.lines.len()).any(|i| {
|
||||
side.line_text(i)
|
||||
.resolved()
|
||||
.chars()
|
||||
.any(|(c, s)| c != ' ' && s.obfuscated == Some(true))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// How far the text of an outlined call sits in front of its outline, in
|
||||
/// text units (`Font.DisplayMode.POLYGON_OFFSET`, see `draw_text`).
|
||||
const TEXT_OVER_OUTLINE: f32 = 0.05;
|
||||
@@ -775,131 +726,72 @@ const TEXT_OVER_OUTLINE: f32 = 0.05;
|
||||
/// reader instead (a twentieth of a font pixel, invisible) rather than
|
||||
/// z-fighting with the outline under it.
|
||||
fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
|
||||
let glyph = |c| {
|
||||
let Some(font) = scene.font else {
|
||||
return (scene.glyph)(c);
|
||||
};
|
||||
let g = font.glyph_styled(c, &call.style);
|
||||
if call.style.obfuscated == Some(true) && c != ' ' {
|
||||
font.random_glyph(&call.style, g.advance)
|
||||
} else {
|
||||
g
|
||||
}
|
||||
};
|
||||
text_vertices(&glyph, scene.font.map(Font::white), call, out);
|
||||
text_vertices(scene.glyph, call, out);
|
||||
}
|
||||
|
||||
/// `BakedSheetGlyph.renderChar` / `Font.PreparedTextBuilder.accept`: the
|
||||
/// `draw_text` with the font's glyph cells and advances from `glyph`: the
|
||||
/// text of `call` as glyph quads in its matrix's plane, 1 unit a font pixel,
|
||||
/// y down as the font's, with its outline when it has one. A native mod's
|
||||
/// block entity draws its text this way (`native_draw_text`).
|
||||
pub fn text_vertices(
|
||||
glyph: &dyn Fn(char) -> Glyph,
|
||||
white: Option<Rect>,
|
||||
call: &TextCall,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let bold = call.style.is_bold();
|
||||
let mut quad = |corners: [(f32, f32); 4], cell: Rect, z: f32, argb: u32| {
|
||||
let uvs = [
|
||||
[cell.u0, cell.v1],
|
||||
[cell.u1, cell.v1],
|
||||
[cell.u1, cell.v0],
|
||||
[cell.u0, cell.v0],
|
||||
];
|
||||
let color = 0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
|
||||
for ((x, y), uv) in corners.into_iter().zip(uvs) {
|
||||
out.push(Vertex {
|
||||
normal: [0; 4],
|
||||
pos: call.matrix.transform_point3(Vec3::new(x, y, z)).to_array(),
|
||||
uv,
|
||||
layer: u32::from(call.tier) << 16,
|
||||
color,
|
||||
light: call.light,
|
||||
glint_uv: [0.0; 2],
|
||||
});
|
||||
}
|
||||
};
|
||||
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32, outline: bool| {
|
||||
let mut pen = call.x;
|
||||
for (i, ch) in call.text.chars().enumerate() {
|
||||
pub fn text_vertices(glyph: &dyn Fn(char) -> Glyph, call: &TextCall, out: &mut Vec<Vertex>) {
|
||||
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32| {
|
||||
let mut x = call.x + dx;
|
||||
let y0 = call.y + dy;
|
||||
for ch in call.text.chars() {
|
||||
let g = glyph(ch);
|
||||
let x = pen + dx * g.offset;
|
||||
let y = call.y + dy * g.offset;
|
||||
let advance = g.advance + if bold { g.offset } else { 0.0 };
|
||||
if let Some(cell) = g.rect {
|
||||
let thick = if bold { 0.1 } else { 0.0 };
|
||||
let shear = |v| {
|
||||
if call.style.is_italic() {
|
||||
1.0 - 0.25 * v
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
for copy in 0..=usize::from(bold) {
|
||||
let x0 = x + copy as f32 * g.offset + g.left - thick;
|
||||
let x1 = x + copy as f32 * g.offset + g.right + thick;
|
||||
let top = y + g.top - thick;
|
||||
let bottom = y + g.bottom + thick;
|
||||
quad(
|
||||
[
|
||||
(x0 + shear(g.bottom), bottom),
|
||||
(x1 + shear(g.bottom), bottom),
|
||||
(x1 + shear(g.top), top),
|
||||
(x0 + shear(g.top), top),
|
||||
],
|
||||
cell,
|
||||
z + copy as f32 * 0.001,
|
||||
argb,
|
||||
);
|
||||
// BakedSheetGlyph: the glyph's own box from the pen, not a
|
||||
// fixed 8x8 cell (a Unifont glyph is 0.5 units per texel).
|
||||
let (x0, x1) = (x + g.left, x + g.right);
|
||||
let (top, bottom) = (y0 + g.top, y0 + g.bottom);
|
||||
let corners = [(x0, bottom), (x1, bottom), (x1, top), (x0, top)];
|
||||
let uvs = [
|
||||
[cell.u0, cell.v1],
|
||||
[cell.u1, cell.v1],
|
||||
[cell.u1, cell.v0],
|
||||
[cell.u0, cell.v0],
|
||||
];
|
||||
let c =
|
||||
0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
|
||||
for ((cx, cy), uv) in corners.into_iter().zip(uvs) {
|
||||
out.push(Vertex {
|
||||
normal: [0; 4],
|
||||
pos: call
|
||||
.matrix
|
||||
.transform_point3(Vec3::new(cx, cy, z))
|
||||
.to_array(),
|
||||
uv,
|
||||
layer: 0,
|
||||
color: c,
|
||||
light: call.light,
|
||||
|
||||
glint_uv: [0.0, 0.0],
|
||||
});
|
||||
}
|
||||
}
|
||||
// `Font.prepare8xTextOutline` discards effects: only the front
|
||||
// text gets underline/strikethrough, including over spaces.
|
||||
if !outline && let Some(white) = white {
|
||||
let x0 = if i == 0 { x - 1.0 } else { x };
|
||||
for (enabled, top) in [
|
||||
(call.style.strikethrough, 3.5),
|
||||
(call.style.underlined, 8.0),
|
||||
] {
|
||||
if enabled == Some(true) {
|
||||
quad(
|
||||
[
|
||||
(x0, y + top + 1.0),
|
||||
(x + advance, y + top + 1.0),
|
||||
(x + advance, y + top),
|
||||
(x0, y + top),
|
||||
],
|
||||
white,
|
||||
z + 0.01,
|
||||
argb,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
pen += advance;
|
||||
x += g.advance;
|
||||
}
|
||||
};
|
||||
if call.outline != 0 {
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
if dx != 0 || dy != 0 {
|
||||
emit(dx as f32, dy as f32, 0.0, call.outline, true);
|
||||
}
|
||||
}
|
||||
for (dx, dy) in [
|
||||
(-1.0, -1.0),
|
||||
(0.0, -1.0),
|
||||
(1.0, -1.0),
|
||||
(-1.0, 0.0),
|
||||
(1.0, 0.0),
|
||||
(-1.0, 1.0),
|
||||
(0.0, 1.0),
|
||||
(1.0, 1.0),
|
||||
] {
|
||||
emit(dx, dy, 0.0, call.outline);
|
||||
}
|
||||
}
|
||||
emit(
|
||||
0.0,
|
||||
0.0,
|
||||
if call.outline != 0 {
|
||||
TEXT_OVER_OUTLINE
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
call.color,
|
||||
false,
|
||||
);
|
||||
let z = if call.outline != 0 {
|
||||
TEXT_OVER_OUTLINE
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
emit(0.0, 0.0, z, call.color);
|
||||
}
|
||||
|
||||
/// Block-entity fields the level ticks, at one position (vanilla's own field
|
||||
@@ -932,16 +824,12 @@ pub struct TextCall {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub text: String,
|
||||
pub style: Style,
|
||||
/// ARGB.
|
||||
pub color: u32,
|
||||
/// ARGB outline colour, 0 for none.
|
||||
pub outline: u32,
|
||||
/// Horizon packed light (sky*16 | block*16 << 8).
|
||||
pub light: u32,
|
||||
/// HDR emission tier of its glyphs (`mesher::hdr_tier`), 0 for none: glowing sign text
|
||||
/// shines in HDR output and in shader packs (`horizonTier`), as full-bright text does in SDR.
|
||||
pub tier: u8,
|
||||
}
|
||||
|
||||
/// Inputs from the game for one frame of block-entity rendering.
|
||||
@@ -980,8 +868,6 @@ pub struct Scene<'a> {
|
||||
pub skull: &'a dyn Fn([i32; 3]) -> Option<String>,
|
||||
/// Font glyph: atlas cell (none for blank glyphs such as the space), box and advance.
|
||||
pub glyph: &'a dyn Fn(char) -> Glyph,
|
||||
/// Font providers, effects and random glyphs for styled sign text.
|
||||
pub font: Option<&'a Font>,
|
||||
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
|
||||
/// text laid out under another value is laid out again.
|
||||
pub glyphs: u64,
|
||||
@@ -1143,7 +1029,6 @@ struct SignTextEntry {
|
||||
/// Game time it was last drawn.
|
||||
used: i64,
|
||||
vertices: Vec<Vertex>,
|
||||
animated: bool,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct StaticKey {
|
||||
@@ -2450,11 +2335,9 @@ impl BlockEntityRenderer {
|
||||
let entry = match signs.entry(pos) {
|
||||
std::collections::hash_map::Entry::Occupied(mut occupied) => {
|
||||
let entry = occupied.get_mut();
|
||||
let changed = entry.data != *data;
|
||||
if (entry.state, entry.packed, entry.near, entry.glyphs)
|
||||
!= (state, packed, near, glyphs)
|
||||
|| changed
|
||||
|| entry.animated
|
||||
|| entry.data != *data
|
||||
{
|
||||
entry.vertices.clear();
|
||||
self.render_one(
|
||||
@@ -2469,10 +2352,7 @@ impl BlockEntityRenderer {
|
||||
&mut entry.vertices,
|
||||
);
|
||||
entry.state = state;
|
||||
if changed {
|
||||
entry.animated = sign_text_animated(data);
|
||||
entry.data.clone_from(data);
|
||||
}
|
||||
entry.data.clone_from(data);
|
||||
entry.packed = packed;
|
||||
entry.near = near;
|
||||
entry.glyphs = glyphs;
|
||||
@@ -2493,7 +2373,6 @@ impl BlockEntityRenderer {
|
||||
&mut vertices,
|
||||
);
|
||||
vacant.insert(SignTextEntry {
|
||||
animated: sign_text_animated(data),
|
||||
state,
|
||||
data: data.clone(),
|
||||
packed,
|
||||
@@ -3733,7 +3612,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// Compare the cached world path with the original model renderer,
|
||||
@@ -3896,105 +3774,6 @@ mod tests {
|
||||
// Straight down: ambient only.
|
||||
assert!((diffuse(-Vec3::Y, lights) - 0.4).abs() < 1e-6);
|
||||
}
|
||||
/// A face's colour carries its tint alone and its normal carries the
|
||||
/// shading: the entity shader applies `minecraft_mix_light` from it, as a
|
||||
/// shader pack's lighting does, so a baked shade would be applied twice.
|
||||
#[test]
|
||||
fn a_face_keeps_its_tint_and_normal_for_the_shader_to_shade() {
|
||||
let quad = |y: f32, z0: f32, z1: f32| {
|
||||
[
|
||||
[0.0, y, z0, 0.0, 0.0],
|
||||
[0.0, y, z1, 0.0, 1.0],
|
||||
[16.0, y, z1, 1.0, 1.0],
|
||||
[16.0, y, z0, 1.0, 0.0],
|
||||
]
|
||||
};
|
||||
// Opposite windings: the first faces up, the second down.
|
||||
let part = ModelPart {
|
||||
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
polygons: vec![quad(0.0, 0.0, 16.0), quad(16.0, 16.0, 0.0)],
|
||||
children: vec![],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_part(
|
||||
&part,
|
||||
"root",
|
||||
Mat4::IDENTITY,
|
||||
&|_, _| {},
|
||||
Paint {
|
||||
sprite: Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
tint: 0x123456,
|
||||
light: 0,
|
||||
lights: diffuse_lights(false),
|
||||
glow: None,
|
||||
},
|
||||
&mut out,
|
||||
);
|
||||
assert_eq!(out.len(), 8);
|
||||
// The same tint on both faces: no directional shade baked in (`pack_color`
|
||||
// stores the tint in the vertex colour's BGR byte order).
|
||||
let tint = pack_color(0x12_3456, 255, 1.0);
|
||||
assert!(out.iter().all(|v| v.color == tint));
|
||||
let (up, down) = (&out[0], &out[4]);
|
||||
assert_eq!(up.normal, [0, 127, 0, 127]);
|
||||
assert_eq!(down.normal, [0, -127, 0, 127]);
|
||||
// The shader still gets the difference the old baked colours held.
|
||||
let lights = diffuse_lights(false);
|
||||
let up_normal = Vec3::new(0.0, f32::from(up.normal[1]) / 127.0, 0.0);
|
||||
let down_normal = Vec3::new(0.0, f32::from(down.normal[1]) / 127.0, 0.0);
|
||||
assert!(diffuse(up_normal, lights) > diffuse(down_normal, lights));
|
||||
}
|
||||
/// Lights the entity shader does not know (an item preview's
|
||||
/// `Lighting.Entry`) stay baked into the colour, and `normal.w` 0 tells
|
||||
/// the shader to keep it: `game/item_draw` passes those for block-entity
|
||||
/// items in inventories and hands.
|
||||
#[test]
|
||||
fn lights_other_than_the_levels_stay_baked() {
|
||||
let part = ModelPart {
|
||||
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
polygons: vec![[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 16.0, 0.0, 1.0],
|
||||
[16.0, 0.0, 16.0, 1.0, 1.0],
|
||||
[16.0, 0.0, 0.0, 1.0, 0.0],
|
||||
]],
|
||||
children: vec![],
|
||||
};
|
||||
let lights = (Vec3::X, Vec3::Z);
|
||||
let mut out = Vec::new();
|
||||
draw_part(
|
||||
&part,
|
||||
"root",
|
||||
Mat4::IDENTITY,
|
||||
&|_, _| {},
|
||||
Paint {
|
||||
sprite: Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
tint: 0x12_3456,
|
||||
light: 0,
|
||||
lights,
|
||||
glow: None,
|
||||
},
|
||||
&mut out,
|
||||
);
|
||||
let baked = pack_color(0x12_3456, 255, diffuse(Vec3::Y, lights));
|
||||
assert!(out.iter().all(|v| v.color == baked));
|
||||
assert!(out.iter().all(|v| v.normal == [0; 4]));
|
||||
assert_ne!(baked, pack_color(0x12_3456, 255, 1.0));
|
||||
}
|
||||
#[test]
|
||||
fn part_poses_compose_like_model_part() {
|
||||
let part = ModelPart {
|
||||
@@ -4128,7 +3907,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut call = TextCall {
|
||||
@@ -4136,11 +3914,9 @@ mod tests {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
text: "Hi".into(),
|
||||
style: Style::default(),
|
||||
color: 0xFFFF_FF00,
|
||||
outline: 0xFF33_3300,
|
||||
light: 240 | 240 << 8,
|
||||
tier: 0,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_text(&scene, &call, &mut out);
|
||||
@@ -4204,7 +3980,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let call = TextCall {
|
||||
@@ -4212,11 +3987,9 @@ mod tests {
|
||||
x: 10.0,
|
||||
y: 2.0,
|
||||
text: "ab".into(),
|
||||
style: Style::default(),
|
||||
color: 0xFF00_0000,
|
||||
outline: 0,
|
||||
light: 240 | 240 << 8,
|
||||
tier: 0,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_text(&scene, &call, &mut out);
|
||||
@@ -4236,83 +4009,6 @@ mod tests {
|
||||
assert_eq!(boxes, [[10.0, 14.0, 2.0, 10.0], [15.0, 19.0, 2.0, 10.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_sign_quads_follow_vanilla_glyph_and_effect_bounds() {
|
||||
let white = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 0.1,
|
||||
v1: 0.1,
|
||||
w: 1,
|
||||
h: 1,
|
||||
};
|
||||
let glyph = |c| Glyph {
|
||||
rect: (c != ' ').then_some(white),
|
||||
left: 0.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
bottom: 8.0,
|
||||
advance: 5.0,
|
||||
offset: 0.5,
|
||||
};
|
||||
let mut call = TextCall {
|
||||
matrix: Mat4::IDENTITY,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
text: "A ".into(),
|
||||
color: 0xff123456,
|
||||
outline: 0,
|
||||
light: 240,
|
||||
tier: crate::mesher::LOW,
|
||||
style: Style {
|
||||
bold: Some(true),
|
||||
italic: Some(true),
|
||||
underlined: Some(true),
|
||||
strikethrough: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
text_vertices(&glyph, Some(white), &call, &mut out);
|
||||
// BakedSheetGlyph: bold expands by 0.1, italic shears by 1 - y/4,
|
||||
// the second bold glyph is 0.5 along and 0.001 in front (Unihex).
|
||||
assert_eq!(out.len(), 24);
|
||||
for (got, expected) in out[0].pos.iter().zip([-1.1, 8.1, 0.0]) {
|
||||
assert!((got - expected).abs() < 1e-6);
|
||||
}
|
||||
assert!((out[4].pos[0] - out[0].pos[0] - 0.5).abs() < 1e-6);
|
||||
assert_eq!(out[4].pos[2], 0.001);
|
||||
assert_eq!(out[8].pos, [-1.0, 4.5, 0.01]);
|
||||
assert_eq!(out[12].pos, [-1.0, 9.0, 0.01]);
|
||||
assert_eq!(out[21].pos, [11.0, 9.0, 0.01]); // underline crosses the space
|
||||
assert!(out.iter().all(|v| v.color == 0xff563412 && v.light == 240));
|
||||
call.outline = 0xff010203;
|
||||
out.clear();
|
||||
text_vertices(&glyph, Some(white), &call, &mut out);
|
||||
// Eight outlines contain glyphs only; effects appear once, in front.
|
||||
assert_eq!(out.len(), 8 * 8 + 24);
|
||||
assert!((out[0].pos[0] - -1.6).abs() < 1e-6);
|
||||
assert!(out[64..].iter().all(|v| v.pos[2] >= TEXT_OVER_OUTLINE));
|
||||
// The glow tier reaches bold copies, outlines and decoration quads.
|
||||
assert!(out.iter().all(|v| v.layer >> 16 == 1));
|
||||
call.tier = 0;
|
||||
out.clear();
|
||||
text_vertices(&glyph, Some(white), &call, &mut out);
|
||||
assert!(out.iter().all(|v| v.layer >> 16 == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_sign_clipping_counts_bold_width_and_preserves_resets() {
|
||||
let line = Text::legacy("§lAB§r東Z");
|
||||
let advance = |_: char, s: &Style| 5.0 + if s.is_bold() { 0.5 } else { 0.0 };
|
||||
let shown = first_line(&line, 16.0, &advance);
|
||||
assert_eq!(shown.plain(), "AB東");
|
||||
assert_eq!(
|
||||
shown.chars().map(|(_, s)| s.is_bold()).collect::<Vec<_>>(),
|
||||
[true, true, false]
|
||||
);
|
||||
}
|
||||
|
||||
/// The front text calls of a sign whose every glyph advances 6.
|
||||
fn front_calls(data: &SignData, near: bool) -> Vec<(String, f32, f32, u32, u32)> {
|
||||
let world = World::new(BlockStateId(0), 1);
|
||||
@@ -4341,7 +4037,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
sign_text_calls(&scene, data, &data.front, Mat4::IDENTITY, 0, near)
|
||||
@@ -4481,9 +4176,7 @@ mod tests {
|
||||
..changed.clone()
|
||||
};
|
||||
let wide = std::cell::Cell::new(6.0);
|
||||
let glyph_reads = std::cell::Cell::new(0);
|
||||
let glyph = |c: char| {
|
||||
glyph_reads.set(glyph_reads.get() + 1);
|
||||
let u = f32::from(c as u8 % 16) / 16.0;
|
||||
let rect = Rect {
|
||||
u0: u,
|
||||
@@ -4526,7 +4219,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let check = |scene: &Scene, pos: [i32; 3], state: u32| {
|
||||
@@ -4570,23 +4262,6 @@ mod tests {
|
||||
wide.set(5.0);
|
||||
scene.glyphs = 1;
|
||||
check(&scene, pos, 1);
|
||||
// With the same game time and data, plain text reuses its vertices,
|
||||
// but obfuscated text must ask the font for fresh glyphs each frame.
|
||||
let mut vertices = Vec::new();
|
||||
let reads = glyph_reads.get();
|
||||
renderer.render(&scene, [(pos, 1)].iter(), &mut vertices);
|
||||
assert_eq!(glyph_reads.get(), reads);
|
||||
let animated = SignData {
|
||||
front: text("§kSecret", false),
|
||||
..Default::default()
|
||||
};
|
||||
let animated_sign = |_| Some(&animated);
|
||||
scene.sign = &animated_sign;
|
||||
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
|
||||
let reads = glyph_reads.get();
|
||||
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
|
||||
assert!(glyph_reads.get() > reads);
|
||||
scene.sign = &first_sign;
|
||||
// Signs out of view for a while are dropped.
|
||||
scene.game_time = 2 * SIGN_TEXT_KEEP;
|
||||
check(&scene, pos, 0);
|
||||
@@ -4594,23 +4269,11 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn first_line_wraps_on_bytes_without_splitting_a_char() {
|
||||
let advance = |c: char, _: &Style| if c == ' ' { 4.0 } else { 6.0 };
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("ab cd ef"), 13.0, &advance).plain(),
|
||||
"ab"
|
||||
);
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("abcdef"), 13.0, &advance).plain(),
|
||||
"ab"
|
||||
);
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("\u{6771}"), 3.0, &advance).plain(),
|
||||
"\u{6771}"
|
||||
);
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("ab\ncd"), 100.0, &advance).plain(),
|
||||
"ab"
|
||||
);
|
||||
let advance = |c: char| if c == ' ' { 4.0 } else { 6.0 };
|
||||
assert_eq!(first_line("ab cd ef", 13.0, &advance), "ab");
|
||||
assert_eq!(first_line("abcdef", 13.0, &advance), "ab");
|
||||
assert_eq!(first_line("\u{6771}", 3.0, &advance), "\u{6771}");
|
||||
assert_eq!(first_line("ab\ncd", 100.0, &advance), "ab");
|
||||
}
|
||||
/// `rendertype_end_portal` culls back faces, and `AbstractEndPortalRenderer`
|
||||
/// submits the unit cube's two Y faces. Both are wound outward, so of each
|
||||
@@ -4714,7 +4377,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut verts = Vec::new();
|
||||
@@ -4801,7 +4463,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// A closed single chest of `block` drawn by `renderer`: positions and UVs.
|
||||
|
||||
@@ -358,7 +358,6 @@ fn batched_callbacks_preserve_entity_ranges_and_allow_immediate_culling() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// Reverse local-position order to check that cache indexing does not
|
||||
@@ -448,7 +447,6 @@ fn static_cache_qualifies_local_positions_by_section_and_drops_old_entries() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -504,7 +502,6 @@ fn gpu_static_cache_uses_section_local_chests_and_keeps_visibility_separate() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([100, 70, -32], 0)];
|
||||
@@ -692,7 +689,6 @@ fn sign_text_cache_can_be_disabled_and_reenabled_without_changing_output() {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -747,7 +743,6 @@ fn shulker_cache_tracks_settled_poses_and_does_not_freeze_opening_animation() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -850,7 +845,6 @@ fn shulker_cache_covers_all_colors_and_facing_directions() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -906,7 +900,6 @@ fn cached_bells_keep_shaking_until_the_animation_ends() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -969,7 +962,6 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1001,9 +993,7 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
|
||||
scene.nether = true;
|
||||
let nether_closed = render_cached(&scene);
|
||||
assert_same_vertices(&nether_closed, &render_direct(&renderer, &scene, &blocks));
|
||||
// The nether is the entity shader's flag now (`entity_light`'s `l1` and
|
||||
// `globals.time.y`), not a baked colour, so the cached geometry serves both.
|
||||
assert_same_vertices(&closed_again, &nether_closed);
|
||||
assert_vertices_differ(&closed_again, &nether_closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1068,7 +1058,6 @@ fn closed_chest_cache_covers_all_materials_and_atlas_reloads() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
@@ -1158,7 +1147,6 @@ fn double_chest_cache_bypasses_both_halves_while_either_lid_is_open() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pair = [([0, 64, 0], 0), ([1, 64, 0], 1)];
|
||||
@@ -1235,7 +1223,6 @@ fn decorated_pot_cache_tracks_sherds_and_bypasses_wobble_animation() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1315,7 +1302,6 @@ fn banner_cache_keeps_the_pole_static_while_the_flag_and_patterns_update() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1383,7 +1369,6 @@ fn cached_wall_banner_poles_use_the_wall_model() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
@@ -1432,7 +1417,6 @@ fn copper_statue_cache_rebuilds_when_its_pose_state_changes() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -1539,7 +1523,6 @@ fn copper_statue_cache_covers_weathering_waxing_and_all_poses() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
|
||||
@@ -37,37 +37,6 @@ pub fn potion_name_key(item: &str, potion: Option<&str>) -> String {
|
||||
let suffix = potion.and_then(self::potion).map_or("empty", |p| p.name);
|
||||
format!("item.minecraft.{prefix}.effect.{suffix}")
|
||||
}
|
||||
/// `PotionItem.getName` and `TippedArrowItem.getName`: a potion or a tipped
|
||||
/// arrow is named by its contents (`PotionContents.getName`: their own
|
||||
/// `custom_name`, else the potion's name, else `empty`), whatever its
|
||||
/// `item_name`. None for other items, and for a stack whose contents were
|
||||
/// taken off (`!minecraft:potion_contents`), which takes the item's name.
|
||||
pub fn contents_name_key(stack: &ItemStack) -> Option<String> {
|
||||
use horizon_world::nbt::Tag;
|
||||
let item = &*stack.item;
|
||||
let named = matches!(
|
||||
item,
|
||||
"minecraft:potion"
|
||||
| "minecraft:splash_potion"
|
||||
| "minecraft:lingering_potion"
|
||||
| "minecraft:tipped_arrow"
|
||||
);
|
||||
let data = stack.data();
|
||||
if !named || data.is_some_and(|d| d.other("!minecraft:potion_contents").is_some()) {
|
||||
return None;
|
||||
}
|
||||
let prefix = item.trim_start_matches("minecraft:");
|
||||
let own = data
|
||||
.and_then(|d| d.potion_extra.iter().find(|(k, _)| k == "custom_name"))
|
||||
.and_then(|(_, name)| match name {
|
||||
Tag::String(s) => Some(s.to_string_lossy()),
|
||||
_ => None,
|
||||
});
|
||||
Some(match own {
|
||||
Some(suffix) => format!("item.minecraft.{prefix}.effect.{suffix}"),
|
||||
None => potion_name_key(item, stack.potion()),
|
||||
})
|
||||
}
|
||||
/// `PotionContents.getAllEffects`: the potion's effects, then the stack's
|
||||
/// `custom_effects` (id, duration, amplifier). Unknown effect ids are dropped.
|
||||
pub fn contents_effects(stack: &ItemStack) -> Vec<(&'static str, i32, i32)> {
|
||||
@@ -500,40 +469,6 @@ mod tests {
|
||||
potion_name_key("minecraft:splash_potion", Some("minecraft:long_swiftness")),
|
||||
"item.minecraft.splash_potion.effect.swiftness"
|
||||
);
|
||||
// A potion or a tipped arrow is named by its contents, over its
|
||||
// item_name; one whose contents were taken off is not.
|
||||
use horizon_world::nbt::Tag;
|
||||
let mut swift =
|
||||
ItemStack::new("minecraft:potion", 1).with_potion("minecraft:long_swiftness");
|
||||
swift
|
||||
.data_mut()
|
||||
.set_other("minecraft:item_name", Some(Tag::String("Bottle".into())));
|
||||
assert_eq!(
|
||||
contents_name_key(&swift).as_deref(),
|
||||
Some("item.minecraft.potion.effect.swiftness")
|
||||
);
|
||||
assert_eq!(
|
||||
contents_name_key(&ItemStack::new("minecraft:splash_potion", 1)).as_deref(),
|
||||
Some("item.minecraft.splash_potion.effect.empty")
|
||||
);
|
||||
let mut own = ItemStack::new("minecraft:tipped_arrow", 1).with_potion("minecraft:poison");
|
||||
own.data_mut()
|
||||
.potion_extra
|
||||
.push(("custom_name".into(), Tag::String("luck".into())));
|
||||
assert_eq!(
|
||||
contents_name_key(&own).as_deref(),
|
||||
Some("item.minecraft.tipped_arrow.effect.luck")
|
||||
);
|
||||
let mut taken = ItemStack::new("minecraft:potion", 1);
|
||||
taken.data_mut().set_other(
|
||||
"!minecraft:potion_contents",
|
||||
Some(Tag::compound(Vec::<(&str, Tag)>::new())),
|
||||
);
|
||||
assert_eq!(contents_name_key(&taken), None);
|
||||
assert_eq!(
|
||||
contents_name_key(&ItemStack::new("minecraft:stone", 1)),
|
||||
None
|
||||
);
|
||||
assert_eq!(format_duration(9600), "08:00");
|
||||
assert_eq!(format_duration(450), "00:22");
|
||||
assert_eq!(format_duration(140), "00:07");
|
||||
|
||||
@@ -357,24 +357,6 @@ impl Light {
|
||||
}
|
||||
/// The smallest form of packed light per block.
|
||||
pub fn packed(light: Box<[u8; 4096]>) -> Self {
|
||||
match Self::classify(&light) {
|
||||
(false, false, first) => Self::Uniform(first),
|
||||
(true, false, first) => Self::Sky(Self::pack_layer(&light, 4), first & 15),
|
||||
(false, true, first) => Self::Block(Self::pack_layer(&light, 0), first >> 4),
|
||||
(true, true, _) => Self::Full(light),
|
||||
}
|
||||
}
|
||||
/// The smallest form of packed light per block, allocating a 4 KiB box only
|
||||
/// when both sky and block light vary within the section.
|
||||
pub fn from_slice(light: &[u8; 4096]) -> Self {
|
||||
match Self::classify(light) {
|
||||
(false, false, first) => Self::Uniform(first),
|
||||
(true, false, first) => Self::Sky(Self::pack_layer(light, 4), first & 15),
|
||||
(false, true, first) => Self::Block(Self::pack_layer(light, 0), first >> 4),
|
||||
(true, true, _) => Self::Full(Box::new(*light)),
|
||||
}
|
||||
}
|
||||
fn classify(light: &[u8; 4096]) -> (bool, bool, u8) {
|
||||
let first = light[0];
|
||||
let (mut sky_varies, mut block_varies) = (false, false);
|
||||
for &v in light.iter() {
|
||||
@@ -382,14 +364,19 @@ impl Light {
|
||||
sky_varies |= d & 0xF0 != 0;
|
||||
block_varies |= d & 15 != 0;
|
||||
}
|
||||
(sky_varies, block_varies, first)
|
||||
}
|
||||
fn pack_layer(light: &[u8; 4096], shift: u32) -> Box<[u8; 2048]> {
|
||||
let mut layer = Box::new([0u8; 2048]);
|
||||
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
|
||||
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
|
||||
let layer = |shift: u32| {
|
||||
let mut layer = Box::new([0u8; 2048]);
|
||||
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
|
||||
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
|
||||
}
|
||||
layer
|
||||
};
|
||||
match (sky_varies, block_varies) {
|
||||
(false, false) => Self::Uniform(first),
|
||||
(true, false) => Self::Sky(layer(4), first & 15),
|
||||
(false, true) => Self::Block(layer(0), first >> 4),
|
||||
(true, true) => Self::Full(light),
|
||||
}
|
||||
layer
|
||||
}
|
||||
/// The packed light of blocks `at..at + out.len()`, into `out`.
|
||||
#[inline]
|
||||
@@ -589,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), Tag)>,
|
||||
pub block_names: Vec<((i32, i32, i32), String)>,
|
||||
/// Generation postprocessing marks (ProtoChunk.postProcessing) as world
|
||||
/// positions, applied once by LevelChunk.postProcessGeneration.
|
||||
pub postprocess: Vec<[i32; 3]>,
|
||||
@@ -633,14 +620,6 @@ impl Chunk {
|
||||
entities: Vec::new(),
|
||||
}
|
||||
}
|
||||
/// The original `structures.starts`, including starts with unknown pieces.
|
||||
pub fn saved_structure_starts(&self) -> Option<&horizon_world::nbt::Compound> {
|
||||
let structures = self.saved.as_ref()?.root.get(&"structures".into())?;
|
||||
match structures.get("starts")? {
|
||||
horizon_world::nbt::Tag::Compound(starts) => Some(starts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The minimum block Y of the column.
|
||||
#[inline]
|
||||
pub fn min_y(&self) -> i32 {
|
||||
|
||||
@@ -59,9 +59,6 @@ pub enum BlockEntity {
|
||||
CreakingHeart(Box<CreakingHeart>),
|
||||
/// `BannerBlockEntity`: the pattern layers.
|
||||
Banner(Banner),
|
||||
/// `CopperGolemStatueBlockEntity`: its own components, which hold the
|
||||
/// custom name of the golem it was (`createStatue`).
|
||||
CopperGolemStatue(Compound),
|
||||
/// `CrafterBlockEntity`.
|
||||
Crafter(Crafter),
|
||||
/// `SpawnerBlockEntity` (`crate::mobs::spawner`).
|
||||
@@ -83,13 +80,6 @@ pub enum BlockEntity {
|
||||
/// `DaylightDetectorBlockEntity`: no data, only the ticker
|
||||
/// (`DaylightDetectorBlock.tickEntity`).
|
||||
DaylightDetector,
|
||||
/// `BellBlockEntity`: saved identity only; ringing state is kept separately.
|
||||
Bell,
|
||||
/// `ConduitBlockEntity`: the UUID of its `destroyTarget` (`Target`). Its
|
||||
/// shape and animation are kept separately (`block_entity_anim`).
|
||||
Conduit(Option<[i32; 4]>),
|
||||
/// `EnchantingTableBlockEntity`: its custom name is the world's block name.
|
||||
EnchantingTable,
|
||||
/// Suspicious sand and gravel: saved loot and the transient brushed face.
|
||||
Brushable(Box<Brushable>),
|
||||
/// A shelf holds three displayed item stacks.
|
||||
@@ -459,9 +449,6 @@ 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 {
|
||||
@@ -520,11 +507,6 @@ pub struct Info {
|
||||
pub pos: [f64; 3],
|
||||
pub source: Option<Who>,
|
||||
pub owner: Option<Who>,
|
||||
/// `uuid`: the source's saved identity (`source`), taken when the event
|
||||
/// was posted. `source` is the live entity and comes back `None` on load.
|
||||
pub source_uuid: Option<[i32; 4]>,
|
||||
/// `projectileOwnerUuid` (`projectile_owner`), likewise beside `owner`.
|
||||
pub owner_uuid: Option<[i32; 4]>,
|
||||
}
|
||||
/// `VibrationSelector`: the closest, then highest-frequency candidate of a tick.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
|
||||
@@ -208,7 +208,7 @@ where
|
||||
Ok(resolve_world_movement(
|
||||
self,
|
||||
MovementRequest {
|
||||
entity: context.entity().unwrap_or(EntityIdentity(0)),
|
||||
entity: EntityIdentity(0),
|
||||
context,
|
||||
bounds,
|
||||
movement,
|
||||
@@ -279,7 +279,7 @@ impl SceneCollisions<'_, '_> {
|
||||
Ok(resolve_world_movement(
|
||||
self,
|
||||
MovementRequest {
|
||||
entity: context.entity().unwrap_or(EntityIdentity(0)),
|
||||
entity: EntityIdentity(0),
|
||||
context,
|
||||
bounds,
|
||||
movement,
|
||||
@@ -300,7 +300,6 @@ impl SceneCollisions<'_, '_> {
|
||||
CollisionContext::for_entity(
|
||||
CollisionEntity {
|
||||
identity: EntityIdentity(0),
|
||||
kind: horizon_world::collision_context::EntityContextKind::Living,
|
||||
y,
|
||||
descending: false,
|
||||
main_hand: Some(CollisionItem::EMPTY),
|
||||
@@ -336,58 +335,6 @@ where
|
||||
{
|
||||
no_collision_in(world, bounds, true)
|
||||
}
|
||||
/// `CollisionGetter.noCollision(entity, bounds)`: retain the real entity snapshot.
|
||||
pub fn no_collision_with_context<W, E>(
|
||||
world: &mut W,
|
||||
bounds: Aabb,
|
||||
context: CollisionContext<'_>,
|
||||
) -> bool
|
||||
where
|
||||
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
|
||||
{
|
||||
no_world_collision(
|
||||
world,
|
||||
CollisionCheckRequest {
|
||||
entity: context.entity().unwrap_or(EntityIdentity(0)),
|
||||
context,
|
||||
bounds,
|
||||
candidate_budget: 1_000_000,
|
||||
},
|
||||
)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
/// `Level.loadedAndEntityCanStandOn(pos, projectile)` with the projectile's
|
||||
/// feet at `entity_y` (`loadedAndEntityCanStandOnFace` with `UP`):
|
||||
/// `Block.isFaceFull(state.getCollisionShape(level, pos,
|
||||
/// CollisionContext.of(projectile)), UP)`. A projectile is not living and never
|
||||
/// falls (it moves by `setPos`), so powder snow and liquids hold nothing up
|
||||
/// and only scaffolding reads the height. An unloaded or out-of-world block
|
||||
/// reads as air.
|
||||
pub fn projectile_can_stand_on(
|
||||
world: &World,
|
||||
binding: &BlockCollisionRegistry<'_>,
|
||||
pos: BlockPos,
|
||||
entity_y: f64,
|
||||
) -> bool {
|
||||
use horizon_core::geometry::{Direction, ShapeOp};
|
||||
let mut scene = SceneCollisions {
|
||||
world,
|
||||
binding,
|
||||
subject: None,
|
||||
};
|
||||
let state = world.block(pos.x, pos.y, pos.z);
|
||||
let context = CollisionContext::position(entity_y);
|
||||
binding
|
||||
.collision_shape_with_context(&mut scene, state, pos, &context)
|
||||
.is_ok_and(|shape| {
|
||||
shape.is_block()
|
||||
|| shape.face_shape(Direction::Up).is_ok_and(|face| {
|
||||
VoxelShape::block()
|
||||
.join_is_not_empty(&face, ShapeOp::XOR)
|
||||
.is_ok_and(|differs| !differs)
|
||||
})
|
||||
})
|
||||
}
|
||||
fn no_collision_in<W, E>(world: &mut W, bounds: Aabb, fluids: bool) -> bool
|
||||
where
|
||||
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
|
||||
|
||||
@@ -19,75 +19,26 @@ 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();
|
||||
|
||||
/// 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 {
|
||||
/// 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 {
|
||||
if MOD_MENU_TYPES.get().is_some() {
|
||||
return false;
|
||||
}
|
||||
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,
|
||||
},
|
||||
);
|
||||
let menus = menus
|
||||
.into_iter()
|
||||
.map(|(id, slots, data_slots, results)| ModMenuType {
|
||||
id: Box::leak(id.into_boxed_str()),
|
||||
slots,
|
||||
data_slots,
|
||||
results,
|
||||
});
|
||||
MOD_MENU_TYPES.set(menus.collect()).is_ok()
|
||||
}
|
||||
|
||||
@@ -441,21 +392,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:conduit" {
|
||||
return Some(Self::Conduit(None));
|
||||
}
|
||||
if name == "minecraft:enchanting_table" {
|
||||
return Some(Self::EnchantingTable);
|
||||
}
|
||||
if name == "minecraft:daylight_detector" {
|
||||
return Some(Self::DaylightDetector);
|
||||
}
|
||||
if name.ends_with("copper_golem_statue") {
|
||||
return Some(Self::CopperGolemStatue(Default::default()));
|
||||
}
|
||||
if name == "minecraft:beacon" {
|
||||
return Some(Self::Beacon(Box::new(Beacon {
|
||||
last_check_y: i32::MIN / 2,
|
||||
@@ -521,7 +460,6 @@ impl BlockEntity {
|
||||
Self::CommandBlock(_) => crate::command_block::Mode::of_block(name).is_some(),
|
||||
Self::Skull(_) => is_skull(name),
|
||||
Self::Banner(_) => name.ends_with("_banner"),
|
||||
Self::CopperGolemStatue(_) => name.ends_with("copper_golem_statue"),
|
||||
Self::Shelf(_) => name == "minecraft:shelf" || name.ends_with("_shelf"),
|
||||
Self::Mod(_) => !name.starts_with("minecraft:"),
|
||||
Self::Beehive(_) => matches!(name, "minecraft:beehive" | "minecraft:bee_nest"),
|
||||
@@ -545,9 +483,6 @@ 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::Conduit(_) => name == "minecraft:conduit",
|
||||
Self::EnchantingTable => name == "minecraft:enchanting_table",
|
||||
Self::StructureBlock(_) => name == "minecraft:structure_block",
|
||||
Self::TestBlock(_) => name == "minecraft:test_block",
|
||||
Self::TestInstanceBlock(_) => name == "minecraft:test_instance_block",
|
||||
@@ -575,7 +510,6 @@ impl BlockEntity {
|
||||
Self::Lectern(_) => "minecraft:lectern",
|
||||
Self::Sculk(s) => s.id(),
|
||||
Self::Banner(_) => "minecraft:banner",
|
||||
Self::CopperGolemStatue(_) => "minecraft:copper_golem_statue",
|
||||
Self::Crafter(_) => "minecraft:crafter",
|
||||
Self::CreakingHeart(_) => "minecraft:creaking_heart",
|
||||
Self::Spawner(_) => "minecraft:mob_spawner",
|
||||
@@ -586,9 +520,6 @@ impl BlockEntity {
|
||||
Self::Skull(_) => "minecraft:skull",
|
||||
Self::EndGateway(_) => "minecraft:end_gateway",
|
||||
Self::DaylightDetector => "minecraft:daylight_detector",
|
||||
Self::Bell => "minecraft:bell",
|
||||
Self::Conduit(_) => "minecraft:conduit",
|
||||
Self::EnchantingTable => "minecraft:enchanting_table",
|
||||
Self::Brushable(_) => "minecraft:brushable_block",
|
||||
Self::Campfire(_) => "minecraft:campfire",
|
||||
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
|
||||
@@ -623,14 +554,11 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Conduit(_)
|
||||
| Self::EnchantingTable
|
||||
| Self::Brushable(_) => Vec::new(),
|
||||
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
|
||||
Self::Lectern(l) => l.book.iter().cloned().collect(),
|
||||
Self::Sculk(_) => Vec::new(),
|
||||
Self::Banner(_) | Self::CopperGolemStatue(_) => Vec::new(),
|
||||
Self::Banner(_) => Vec::new(),
|
||||
Self::Crafter(c) => c.items.iter().flatten().cloned().collect(),
|
||||
Self::Campfire(c) => c.items.iter().flatten().cloned().collect(),
|
||||
Self::ChiseledBookshelf(b) => b.items.iter().flatten().cloned().collect(),
|
||||
@@ -647,7 +575,6 @@ impl BlockEntity {
|
||||
| Self::Lectern(_)
|
||||
| Self::Sculk(_)
|
||||
| Self::Banner(_)
|
||||
| Self::CopperGolemStatue(_)
|
||||
| Self::EndGateway(_)
|
||||
| Self::Skull(_)
|
||||
| Self::PotentSulfur(_)
|
||||
@@ -656,9 +583,6 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Conduit(_)
|
||||
| Self::EnchantingTable
|
||||
| Self::Brushable(_) => &[],
|
||||
Self::Jukebox(j) => &j.items,
|
||||
Self::Crafter(c) => &c.items,
|
||||
@@ -685,7 +609,6 @@ impl BlockEntity {
|
||||
| Self::Lectern(_)
|
||||
| Self::Sculk(_)
|
||||
| Self::Banner(_)
|
||||
| Self::CopperGolemStatue(_)
|
||||
| Self::EndGateway(_)
|
||||
| Self::Skull(_)
|
||||
| Self::PotentSulfur(_)
|
||||
@@ -694,9 +617,6 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Conduit(_)
|
||||
| Self::EnchantingTable
|
||||
| Self::Brushable(_) => &mut [],
|
||||
Self::Jukebox(j) => &mut j.items,
|
||||
Self::Crafter(c) => &mut c.items,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -141,8 +141,7 @@ impl DataPack {
|
||||
pub fn discover(world: &Path) -> Vec<DataPack> {
|
||||
discover_folder(&world.join("datapacks"))
|
||||
}
|
||||
/// `FolderRepositorySource.loadPacks`: packs in Create World's temporary
|
||||
/// folder or a world's data-pack directory, with required folder metadata.
|
||||
/// Packs in Create World's temporary pack folder, before a world exists.
|
||||
pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return Vec::new();
|
||||
@@ -151,7 +150,7 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
|
||||
for e in entries.flatten() {
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let path = e.path();
|
||||
if path.is_dir() && folder_metadata_present(&path) {
|
||||
if path.is_dir() && path.join("pack.mcmeta").exists() {
|
||||
packs.push(DataPack {
|
||||
id: format!("file/{name}"),
|
||||
source: Source::Dir(path),
|
||||
@@ -169,22 +168,6 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
|
||||
packs.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
packs
|
||||
}
|
||||
/// `Pack.readPackMetadata`: even its fallback codec requires a pack section
|
||||
/// and description. Keep this folder gate separate from format compatibility
|
||||
/// and the existing interpretation of the description component.
|
||||
fn folder_metadata_present(path: &Path) -> bool {
|
||||
let Some(meta) = std::fs::read(path.join("pack.mcmeta"))
|
||||
.ok()
|
||||
.and_then(|bytes| crate::packs::parse_meta(&bytes))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
meta.as_object()
|
||||
.and_then(|root| root.get("pack"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|pack| pack.get("description"))
|
||||
.is_some_and(|description| !description.is_null())
|
||||
}
|
||||
/// The resources root's built-in feature packs (`ServerPacksSource`
|
||||
/// scanning the game's `data/minecraft/datapacks`): every folder with a
|
||||
/// `pack.mcmeta`, a feature pack (`PackSource.FEATURE`). Off until a world
|
||||
@@ -278,43 +261,6 @@ pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::Pac
|
||||
}
|
||||
infos
|
||||
}
|
||||
/// The folder of a game folder whose packs every new world there starts with: a modpack's profile
|
||||
/// keeps the packs of its data-only jars here (`horizon-port-pack`). Each world gets its own copy,
|
||||
/// as Create World's packs do, so a world keeps working when the folder changes. Vanilla has no
|
||||
/// such folder, and without one nothing here happens.
|
||||
pub const NEW_WORLD_PACKS: &str = "new-world-datapacks";
|
||||
|
||||
/// Create World's temporary pack folder holding `game_dir`'s [`NEW_WORLD_PACKS`], and their ids,
|
||||
/// to show them selected in the Data Packs screen. `None` when there are none.
|
||||
pub fn seed_create_world(game_dir: &Path) -> Option<(PathBuf, Vec<String>)> {
|
||||
let from = game_dir.join(NEW_WORLD_PACKS);
|
||||
let ids: Vec<String> = discover_folder(&from).into_iter().map(|p| p.id).collect();
|
||||
if ids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let seeded = temp_folder()
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|dir| copy(&from, &dir).map(|()| dir));
|
||||
match seeded {
|
||||
Ok(dir) => Some((dir, ids)),
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: data packs for new worlds: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A world about to be created without Create World (`--world`, the control socket) gets
|
||||
/// `game_dir`'s [`NEW_WORLD_PACKS`]. The game turns on every pack it finds in a world.
|
||||
pub fn install_new_world_packs(game_dir: &Path, world: &Path) -> Result<(), String> {
|
||||
let from = game_dir.join(NEW_WORLD_PACKS);
|
||||
let exists = |name: &str| world.join(name).exists();
|
||||
if !from.is_dir() || exists("level.dat") || exists("level.dat_old") {
|
||||
return Ok(());
|
||||
}
|
||||
copy(&from, &world.join("datapacks"))
|
||||
}
|
||||
|
||||
/// A new empty folder for Create World's packs
|
||||
/// (`Files.createTempDirectory("mcworld-")`).
|
||||
pub fn temp_folder() -> std::io::Result<PathBuf> {
|
||||
@@ -662,7 +608,7 @@ pub struct DataStore {
|
||||
/// `item_modifier` files, for `/item modify`.
|
||||
pub item_modifiers: BTreeMap<String, serde_json::Value>,
|
||||
/// The content tables' raw files from the packs after the built-in one:
|
||||
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` and `tags/…/` files, by
|
||||
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/` and `tags/…/` files, by
|
||||
/// `<kind-dir>/<ns>:<path>`, each id's files in pack order. The tables
|
||||
/// merge them over what they read from the resource root.
|
||||
pub files: BTreeMap<String, Vec<Vec<u8>>>,
|
||||
@@ -671,7 +617,7 @@ pub struct DataStore {
|
||||
}
|
||||
/// `<kind-dir>/<ns>:<path>` for a pack file the content tables read:
|
||||
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `trade_set/`,
|
||||
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` or `tags/<sub>/`, where the id keeps the sub
|
||||
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/` or `tags/<sub>/`, where the id keeps the sub
|
||||
/// path so a nested tag kind stays addressable.
|
||||
fn content_id(file: &str) -> Option<String> {
|
||||
let rest = file.strip_prefix("data/")?;
|
||||
@@ -679,7 +625,7 @@ fn content_id(file: &str) -> Option<String> {
|
||||
let (kind, rest) = rest.split_once('/')?;
|
||||
let kind = match kind {
|
||||
"recipe" | "recipe_filter" | "loot_table" | "advancement" | "trade_set"
|
||||
| "villager_trade" | "dialog" | "damage_type" | "data_maps" | "enchantment" => kind,
|
||||
| "villager_trade" | "dialog" | "damage_type" | "data_maps" => kind,
|
||||
// A tag id is `{ns}:{path-under-tags}` (`tags/worldgen/biome/
|
||||
// test:sub/mytag`), the whole path after the namespace, so nested
|
||||
// registries keep their directory (`TagLoader`'s id shape).
|
||||
@@ -713,7 +659,6 @@ impl DataStore {
|
||||
"damage_type",
|
||||
"data_maps",
|
||||
"dialog",
|
||||
"enchantment",
|
||||
"function",
|
||||
"item_modifier",
|
||||
"loot_table",
|
||||
@@ -769,55 +714,15 @@ impl DataStore {
|
||||
}
|
||||
s
|
||||
}
|
||||
/// `ServerFunctionLibrary.getTag`: only valid function tags expand
|
||||
/// (nested tags included, each function once).
|
||||
/// `FunctionManager.getTag`: the functions a tag expands to (nested tags
|
||||
/// included, each function once).
|
||||
pub fn tag_functions(&self, id: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen_fns = std::collections::HashSet::new();
|
||||
let mut seen_tags = std::collections::HashSet::new();
|
||||
let invalid = self.invalid_function_tags(id);
|
||||
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags, &invalid);
|
||||
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
|
||||
out
|
||||
}
|
||||
/// `TagLoader.tryBuildTag`: missing required members invalidate their tag and
|
||||
/// required parents. Optional invalid children contribute nothing.
|
||||
/// Resolve against the current accepted function map, without caching a
|
||||
/// candidate store or changing the existing expansion's cycle handling.
|
||||
fn invalid_function_tags<'a>(&'a self, id: &'a str) -> std::collections::HashSet<&'a str> {
|
||||
let mut invalid = std::collections::HashSet::new();
|
||||
let mut parents: std::collections::HashMap<&str, Vec<&str>> =
|
||||
std::collections::HashMap::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut pending = vec![id];
|
||||
while let Some(tag) = pending.pop() {
|
||||
if !seen.insert(tag) {
|
||||
continue;
|
||||
}
|
||||
let Some(entries) = self.function_tags.get(tag) else {
|
||||
invalid.insert(tag);
|
||||
continue;
|
||||
};
|
||||
for (entry, required) in entries {
|
||||
if let Some(child) = entry.strip_prefix('#') {
|
||||
pending.push(child);
|
||||
if *required {
|
||||
parents.entry(child).or_default().push(tag);
|
||||
}
|
||||
} else if *required && !self.functions.contains_key(entry) {
|
||||
invalid.insert(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
pending.extend(invalid.iter().copied());
|
||||
while let Some(child) = pending.pop() {
|
||||
for &parent in parents.get(child).into_iter().flatten() {
|
||||
if invalid.insert(parent) {
|
||||
pending.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
invalid
|
||||
}
|
||||
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
|
||||
/// once, so reference cycles end and nesting depth is not capped.
|
||||
fn expand<'a>(
|
||||
@@ -826,19 +731,18 @@ impl DataStore {
|
||||
out: &mut Vec<String>,
|
||||
seen_fns: &mut std::collections::HashSet<&'a str>,
|
||||
seen_tags: &mut std::collections::HashSet<&'a str>,
|
||||
invalid: &std::collections::HashSet<&'a str>,
|
||||
) {
|
||||
if invalid.contains(id) || !seen_tags.insert(id) {
|
||||
if !seen_tags.insert(id) {
|
||||
return;
|
||||
}
|
||||
let Some(entries) = self.function_tags.get(id) else {
|
||||
return;
|
||||
};
|
||||
for (e, _) in entries {
|
||||
for (e, required) in entries {
|
||||
match e.strip_prefix('#') {
|
||||
Some(inner) => self.expand(inner, out, seen_fns, seen_tags, invalid),
|
||||
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
|
||||
None => {
|
||||
if self.functions.contains_key(e) && seen_fns.insert(e) {
|
||||
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
|
||||
out.push(e.clone());
|
||||
}
|
||||
}
|
||||
@@ -1020,97 +924,6 @@ pub fn worldgen_overrides(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn folder_metadata_is_required_before_contents_are_discovered() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("horizon-folder-metadata-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
for (name, metadata) in [
|
||||
("broken", Some("{")),
|
||||
("root_empty", Some("{}")),
|
||||
("root_array", Some("[]")),
|
||||
("pack_empty", Some(r#"{"pack":{}}"#)),
|
||||
("pack_string", Some(r#"{"pack":"bad"}"#)),
|
||||
("missing", None),
|
||||
(
|
||||
"full",
|
||||
Some(
|
||||
r#"{"pack":{"description":"full","pack_format":121,"min_format":[121,0],"max_format":[121,0]}}"#,
|
||||
),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
Some(r#"{"pack":{"description":"description only"}}"#),
|
||||
),
|
||||
("numeric", Some(r#"{"pack":{"description":42}}"#)),
|
||||
("null", Some(r#"{"pack":{"description":null}}"#)),
|
||||
("bom", Some("\u{feff}{\"pack\":{\"description\":\"BOM\"}}")),
|
||||
(
|
||||
"comment",
|
||||
Some("{\"pack\":{\"description\":\"comment\"}}\n// trailing"),
|
||||
),
|
||||
] {
|
||||
let dir = root.join(name);
|
||||
std::fs::create_dir_all(dir.join("data/h146/function")).unwrap();
|
||||
std::fs::write(
|
||||
dir.join(format!("data/h146/function/{name}.mcfunction")),
|
||||
"scoreboard players set admitted h146 1\n",
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(metadata) = metadata {
|
||||
std::fs::write(dir.join("pack.mcmeta"), metadata).unwrap();
|
||||
}
|
||||
}
|
||||
let packs = discover_folder(&root);
|
||||
assert_eq!(
|
||||
packs.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
|
||||
[
|
||||
"file/bom",
|
||||
"file/comment",
|
||||
"file/description",
|
||||
"file/full",
|
||||
"file/numeric"
|
||||
]
|
||||
);
|
||||
let store = DataStore::load(&packs.iter().collect::<Vec<_>>());
|
||||
assert_eq!(
|
||||
store
|
||||
.functions
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
"h146:bom",
|
||||
"h146:comment",
|
||||
"h146:description",
|
||||
"h146:full",
|
||||
"h146:numeric"
|
||||
]
|
||||
);
|
||||
let numeric = packs.iter().find(|p| p.id == "file/numeric").unwrap();
|
||||
let raw: serde_json::Value =
|
||||
serde_json::from_slice(&numeric.read("pack.mcmeta").unwrap()).unwrap();
|
||||
assert_eq!(raw["pack"]["description"], 42);
|
||||
// Builtin and feature construction still have their independent paths.
|
||||
let resources = root.join("resources");
|
||||
let feature = resources.join("data/minecraft/datapacks/feature");
|
||||
std::fs::create_dir_all(&feature).unwrap();
|
||||
std::fs::write(
|
||||
feature.join("pack.mcmeta"),
|
||||
r#"{"pack":{"description":"feature"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
feature_packs(&resources)
|
||||
.iter()
|
||||
.map(|p| p.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["feature"]
|
||||
);
|
||||
assert_eq!(DataPack::builtin(&resources).id, "vanilla");
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_map_files_are_content_files_keyed_by_map_namespace_and_registry() {
|
||||
assert_eq!(
|
||||
@@ -1138,35 +951,6 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_game_folder_s_new_world_packs_go_into_new_worlds_only() {
|
||||
let root = std::env::temp_dir().join(format!("hz-new-world-packs-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let (game, world, old) = (root.join("game"), root.join("new"), root.join("old"));
|
||||
assert!(seed_create_world(&game).is_none());
|
||||
assert_eq!(install_new_world_packs(&game, &world), Ok(()));
|
||||
assert!(!world.join("datapacks").exists());
|
||||
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"), 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();
|
||||
assert_eq!(ids, ["file/islands", "file/void"]);
|
||||
assert!(temp.join("void/data/v/function/a.mcfunction").is_file());
|
||||
let _ = std::fs::remove_dir_all(&temp);
|
||||
install_new_world_packs(&game, &world).unwrap();
|
||||
let ids: Vec<String> = discover(&world).into_iter().map(|p| p.id).collect();
|
||||
assert_eq!(ids, ["file/islands", "file/void"]);
|
||||
// A world that exists already keeps the packs it has.
|
||||
std::fs::create_dir_all(&old).unwrap();
|
||||
std::fs::write(old.join("level.dat"), "").unwrap();
|
||||
install_new_world_packs(&game, &old).unwrap();
|
||||
assert!(discover(&old).is_empty());
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_packs_override_worldgen_in_order() {
|
||||
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
|
||||
@@ -1177,7 +961,7 @@ mod tests {
|
||||
std::fs::write(path, t).unwrap();
|
||||
};
|
||||
for pack in ["a", "b", "off"] {
|
||||
write(pack, "pack.mcmeta", r#"{"pack":{"description":"fixture"}}"#);
|
||||
write(pack, "pack.mcmeta", "{}");
|
||||
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
|
||||
write(pack, "data/p/structure/room/a.nbt", pack);
|
||||
write(
|
||||
@@ -1348,20 +1132,6 @@ mod tests {
|
||||
#[test]
|
||||
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
|
||||
let mut store = DataStore::default();
|
||||
// Every referenced function exists; missing required ones invalidate a tag.
|
||||
for id in (0..25_000)
|
||||
.map(|i| format!("t:f{i}"))
|
||||
.chain(["t:x", "t:y", "t:deep"].map(str::to_owned))
|
||||
{
|
||||
store.functions.insert(
|
||||
id.clone(),
|
||||
Function {
|
||||
id,
|
||||
lines: Vec::new(),
|
||||
parameters: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let big: Vec<(String, bool)> = (0..50_000)
|
||||
.map(|i| (format!("t:f{}", i % 25_000), true))
|
||||
.collect();
|
||||
@@ -1446,13 +1216,13 @@ mod tests {
|
||||
write(
|
||||
"fns",
|
||||
"pack.mcmeta",
|
||||
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
|
||||
r#"{"pack":{"min_format":121,"max_format":121}}"#,
|
||||
);
|
||||
write("fns", "data/x/function/f.mcfunction", "say hi");
|
||||
write(
|
||||
"off",
|
||||
"pack.mcmeta",
|
||||
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
|
||||
r#"{"pack":{"min_format":121,"max_format":121}}"#,
|
||||
);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
|
||||
|
||||
@@ -33,10 +33,7 @@ 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: apply the no-pack HDR gain; HDR_WRITE_LIGHT independently keeps float alpha as coverage.
|
||||
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>;
|
||||
@@ -175,11 +172,7 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
|
||||
@vertex fn vs_entity(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
|
||||
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>,
|
||||
@location(5) normal: vec4<f32>, @location(6) glint_uv: vec2<f32>) -> Out {
|
||||
// [world warp entity]
|
||||
// (a world warp's pipelines put the vertex through the mod's `world_warp`: gfx/world_warp.rs)
|
||||
let at = position;
|
||||
// [end world warp entity]
|
||||
var out = entity_vertex(at, uv, overlay & 0xffffu, color, light, normal);
|
||||
var out = entity_vertex(position, uv, overlay & 0xffffu, color, light, normal);
|
||||
out.glint_uv = glint_uv;
|
||||
out.tier = (overlay >> 16u) & 0xffu;
|
||||
return out;
|
||||
@@ -283,19 +276,6 @@ 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, entity_color(in, front));
|
||||
}
|
||||
// Takes entity_color's result rather than calling it: FXC (DX12) fails with "argument pulled into
|
||||
// unrelated predicate" when entity_color's discards sit a function call below the entry point.
|
||||
fn entity_written_color(in: Out, color: vec4<f32>) -> vec4<f32> {
|
||||
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> {
|
||||
@@ -378,23 +358,15 @@ 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 hdr_scene_alpha(mix(in.scene, globals.hdr_light.y, fogged));
|
||||
return vec4(0.0, 0.0, 0.0, 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 hdr_mark(in.tier, y);
|
||||
return vec4(0.0, 0.0, 0.0, hdr_mark(in.tier, y));
|
||||
}
|
||||
fn brightness(c: vec3<f32>) -> f32 {
|
||||
let l = pow(max(c, vec3(0.0)), vec3(2.2));
|
||||
@@ -435,11 +407,7 @@ 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> {
|
||||
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;
|
||||
return celestial_color(in);
|
||||
}
|
||||
|
||||
// The sky's celestial pass (sun, moon, the End sky): fs_celestial's light,
|
||||
@@ -456,12 +424,7 @@ 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.
|
||||
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 HDR_WRITE_LIGHT { return vec4(added, color.a); }
|
||||
if !full { return vec4(added, 0.0); }
|
||||
if y < globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u] { return vec4(added, 0.0); }
|
||||
return vec4(added, 1.0 - hdr_mark(tier, y));
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,7 @@ use crate::{
|
||||
lang,
|
||||
options_ui::{Event, Page},
|
||||
settings::Settings,
|
||||
settings_shell::{self as shell, Binding, Group, Output, Row, Screen, Sheet, Value},
|
||||
settings_shell::{Group, Output, Row, Screen, Sheet, Value},
|
||||
};
|
||||
|
||||
const TITLE: &str = "horizon.pages.extras";
|
||||
@@ -36,7 +36,7 @@ pub(crate) enum ExtraRow {
|
||||
fn item_row(item: Item, s: &Settings) -> Row<ExtraRow> {
|
||||
let e = &s.extras;
|
||||
let value = match (item.key_name(), item.switch(e)) {
|
||||
(Some(key), _) => Value::Binding(Binding::key(key, item.value(e))),
|
||||
(Some(key), _) => Value::Key(key, item.value(e)),
|
||||
(None, Some(on)) => Value::Switch(on),
|
||||
(None, None) => Value::Choice(item.value(e)),
|
||||
};
|
||||
@@ -85,10 +85,6 @@ impl Screen for ExtrasScreen {
|
||||
fn note_lines(&self) -> usize {
|
||||
NOTE_LINES
|
||||
}
|
||||
/// The rows read the extras, the mods and whether cosmetics are offered.
|
||||
fn stamp(&self, s: &Settings) -> u64 {
|
||||
shell::stamp_of(&(&s.extras, &s.mods, horizon_cosmetics::enabled()))
|
||||
}
|
||||
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
|
||||
let mut groups = vec![Group {
|
||||
name: MODS,
|
||||
@@ -116,10 +112,13 @@ impl Screen for ExtrasScreen {
|
||||
}
|
||||
fn press(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Vec<Output> {
|
||||
match row {
|
||||
ExtraRow::Item(item) => {
|
||||
item.cycle(&mut s.extras, back);
|
||||
vec![Output::Event(Event::Changed)]
|
||||
}
|
||||
ExtraRow::Item(item) => match item.key_name() {
|
||||
Some(key) => vec![Output::BindKey(key)],
|
||||
None => {
|
||||
item.cycle(&mut s.extras, back);
|
||||
vec![Output::Event(Event::Changed)]
|
||||
}
|
||||
},
|
||||
ExtraRow::Zoom(i) => {
|
||||
s.mods.toggle(i);
|
||||
vec![Output::Event(Event::Changed)]
|
||||
@@ -205,21 +204,15 @@ mod tests {
|
||||
/// The sort key binds instead of cycling; the wheel leaves it be.
|
||||
#[test]
|
||||
fn the_key_row_waits_for_a_key() {
|
||||
use crate::settings_shell::Capture;
|
||||
let mut s = Settings::default();
|
||||
s.extras.sort = true;
|
||||
let mut shell = Shell::<ExtrasScreen>::default();
|
||||
let mut screen = ExtrasScreen;
|
||||
let row = ExtraRow::Item(Item::SortKey);
|
||||
let key = s.extras.sort_key.clone();
|
||||
let at = shell.point_at(&s, SIZE, row);
|
||||
assert_eq!(
|
||||
shell.click(&mut s, at, SIZE, false, false, false),
|
||||
vec![
|
||||
Output::Event(Event::ButtonClick),
|
||||
Output::Bind(Capture::Key(extras::SORT_KEY))
|
||||
]
|
||||
screen.press(&mut s, row, false),
|
||||
vec![Output::BindKey(extras::SORT_KEY)]
|
||||
);
|
||||
ExtrasScreen.wheel_step(&mut s, row, true);
|
||||
screen.wheel_step(&mut s, row, true);
|
||||
assert_eq!(s.extras.sort_key, key);
|
||||
}
|
||||
|
||||
@@ -235,66 +228,4 @@ mod tests {
|
||||
assert!(s.extras.sort);
|
||||
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]
|
||||
fn down_from_the_search_field_lands_on_the_first_row() {
|
||||
use crate::{hud::HudBatch, settings_shell::Columns, ui};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
let atlas = crate::hud::HudAtlas::build(std::path::Path::new("/nonexistent")).unwrap();
|
||||
let mut s = Settings::default();
|
||||
let mut shell = Shell::<ExtrasScreen>::default();
|
||||
let mut batch = HudBatch::new([1708, 960]);
|
||||
shell.draw(&s, &mut batch, &atlas, (0.0, 0.0), false, None);
|
||||
crate::widgets::end_frame(1);
|
||||
let size = (batch.width, batch.height);
|
||||
let cols = Columns::of(size.0, size.1).with_note_lines(NOTE_LINES);
|
||||
// The Mods group's note can't take the focus: the first item can.
|
||||
let first = ExtraRow::Item(Item::OwnNameTag);
|
||||
let at = shell.point_at(&s, size, first);
|
||||
let [x, y, w, h] = crate::widgets::linked_step(cols.search(), true).unwrap();
|
||||
assert!(ui::hovered(at, x, y, w, h));
|
||||
assert_eq!(crate::widgets::linked_step(cols.search(), false), None);
|
||||
let [x, y, w, h] = cols.search();
|
||||
shell.click(
|
||||
&mut s,
|
||||
(x + w / 2.0, y + h / 2.0),
|
||||
size,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
shell.key(&mut s, K::ArrowDown, None, false, size);
|
||||
assert_eq!(shell.focused_row(), Some(first));
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use crate::{
|
||||
environment::Sky,
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::random::{LegacyRandom, RandomSource};
|
||||
use std::path::Path;
|
||||
|
||||
/// `FogType` of the camera position.
|
||||
@@ -65,7 +64,7 @@ impl FogTables {
|
||||
}
|
||||
|
||||
/// Fog state that evolves over time.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FogState {
|
||||
/// `LocalPlayer.waterVisionTime` (0..600).
|
||||
pub water_vision_time: i32,
|
||||
@@ -73,28 +72,24 @@ pub struct FogState {
|
||||
pub rain_fog_multiplier: f32,
|
||||
/// `LightmapRenderStateExtractor.blockLightFlicker`.
|
||||
pub block_light_flicker: f32,
|
||||
/// `LightmapRenderStateExtractor.random`: `RandomSource.create()`.
|
||||
flicker_random: LegacyRandom,
|
||||
}
|
||||
impl Default for FogState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
water_vision_time: 0,
|
||||
rain_fog_multiplier: 0.0,
|
||||
block_light_flicker: 0.0,
|
||||
flicker_random: crate::random_source::create(),
|
||||
}
|
||||
}
|
||||
flicker_seed: u64,
|
||||
}
|
||||
impl FogState {
|
||||
fn next_float(&mut self) -> f32 {
|
||||
// splitmix64: the vanilla source is unseeded `RandomSource.create()`.
|
||||
self.flicker_seed = self.flicker_seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = self.flicker_seed;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
((z ^ (z >> 31)) >> 40) as f32 / (1u32 << 24) as f32
|
||||
}
|
||||
/// `LightmapRenderStateExtractor.tick`: the torch light flickers.
|
||||
pub fn tick_flicker(&mut self) {
|
||||
let r = &mut self.flicker_random;
|
||||
let (a, b, c, d) = (
|
||||
r.next_float(),
|
||||
r.next_float(),
|
||||
r.next_float(),
|
||||
r.next_float(),
|
||||
self.next_float(),
|
||||
self.next_float(),
|
||||
self.next_float(),
|
||||
self.next_float(),
|
||||
);
|
||||
self.block_light_flicker += (a - b) * c * d * 0.1;
|
||||
self.block_light_flicker *= 0.9;
|
||||
@@ -755,7 +750,7 @@ mod tests {
|
||||
rain_fog_multiplier: 0.1,
|
||||
..FogState::default()
|
||||
};
|
||||
let mut settled = start.clone();
|
||||
let mut settled = start;
|
||||
settle_rain_fog(&mut settled, 0.5, 15, true);
|
||||
assert_eq!(settled.rain_fog_multiplier, 0.5);
|
||||
let mut eased = start;
|
||||
|
||||
@@ -126,16 +126,11 @@ struct Cache {
|
||||
random: horizon_core::random::LegacyRandom,
|
||||
}
|
||||
pub struct Font {
|
||||
/// The atlas rects of every texture, by resource name: a bitmap provider's
|
||||
/// `file` may name any texture, not only one under `textures/font`.
|
||||
/// The atlas rects of the sheets under `textures/font`.
|
||||
sheets: HashMap<String, Rect>,
|
||||
missing: Glyph,
|
||||
white: Rect,
|
||||
root: PathBuf,
|
||||
/// The game's own resources, below a pack overlay: a font file the overlay
|
||||
/// lacks (a pack adds providers over vanilla's, or the overlay left a
|
||||
/// directory out) is read from here, as vanilla's resource stack does.
|
||||
fallback: Option<PathBuf>,
|
||||
/// Atlas size and the reserved dynamic region (y, height).
|
||||
atlas: (u32, u32),
|
||||
region: (u32, u32),
|
||||
@@ -204,13 +199,6 @@ fn parse_hex(text: &[u8], out: &mut HashMap<u32, HexGlyph>) {
|
||||
out.insert(codepoint, HexGlyph { rows, left, right });
|
||||
}
|
||||
}
|
||||
/// A resource file's bytes, from `root` or, when the overlay lacks it, from
|
||||
/// the game's own resources below (`defaults`).
|
||||
fn read_resource(root: &Path, defaults: Option<&Path>, path: &str) -> Option<Vec<u8>> {
|
||||
std::fs::read(root.join(path))
|
||||
.ok()
|
||||
.or_else(|| defaults.and_then(|d| std::fs::read(d.join(path)).ok()))
|
||||
}
|
||||
fn read_json(root: &Path, path: &str) -> Option<serde_json::Value> {
|
||||
serde_json::from_slice(&std::fs::read(root.join(path)).ok()?).ok()
|
||||
}
|
||||
@@ -293,7 +281,6 @@ fn hex_advance(h: &HexGlyph) -> f32 {
|
||||
/// pixels, copied into the atlas when first drawn.
|
||||
fn load_bitmap(
|
||||
root: &Path,
|
||||
defaults: Option<&Path>,
|
||||
p: &serde_json::Value,
|
||||
sheets: &HashMap<String, Rect>,
|
||||
pixels: &[u8],
|
||||
@@ -303,36 +290,22 @@ fn load_bitmap(
|
||||
let mut out = HashMap::new();
|
||||
let file = p["file"].as_str().unwrap_or("");
|
||||
let (ns, name) = file.split_once(':').unwrap_or(("minecraft", file));
|
||||
let name = name.trim_end_matches(".png");
|
||||
// The atlas names the default namespace's textures by their path and every
|
||||
// other namespace's as `namespace:path` (HudAtlas::build).
|
||||
let atlas_name = if ns == "minecraft" {
|
||||
name.to_owned()
|
||||
} else {
|
||||
format!("{ns}:{name}")
|
||||
};
|
||||
let in_atlas = sheets.get(&atlas_name).copied();
|
||||
let in_atlas = (ns == "minecraft")
|
||||
.then(|| sheets.get(name.trim_end_matches(".png")).copied())
|
||||
.flatten();
|
||||
// The sheet: its size, and its pixels when they are not in the atlas.
|
||||
let (sw, sh, sheet) = match in_atlas {
|
||||
Some(rect) => {
|
||||
let (w, h) = rect.texels(atlas);
|
||||
(w, h, None)
|
||||
}
|
||||
None => {
|
||||
let path = resource_path(file, "textures", "");
|
||||
let sheet = crate::hud::decode(&root.join(&path)).or_else(|first| {
|
||||
defaults
|
||||
.and_then(|d| crate::hud::decode(&d.join(&path)).ok())
|
||||
.ok_or(first)
|
||||
});
|
||||
match sheet {
|
||||
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
|
||||
Err(_) => {
|
||||
log::warn!("font sheet {file} could not be read");
|
||||
return out;
|
||||
}
|
||||
None => match crate::hud::decode(&root.join(resource_path(file, "textures", ""))) {
|
||||
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
|
||||
Err(_) => {
|
||||
log::warn!("font sheet {file} could not be read");
|
||||
return out;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
let rows: Vec<Vec<char>> = p["chars"]
|
||||
.as_array()
|
||||
@@ -409,13 +382,11 @@ fn load_bitmap(
|
||||
}
|
||||
/// `TrueTypeGlyphProviderDefinition.load`: a TrueType file (not CFF) under
|
||||
/// `font/`, its Unicode characters but the `skip` ones.
|
||||
fn load_true_type(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> Option<TrueType> {
|
||||
fn load_true_type(root: &Path, p: &serde_json::Value) -> Option<TrueType> {
|
||||
let file = p["file"].as_str()?;
|
||||
let path = resource_path(file, "font", "");
|
||||
let Some(data) = read_resource(root, defaults, &path) else {
|
||||
log::warn!("font file {file} could not be read");
|
||||
return None;
|
||||
};
|
||||
let data = std::fs::read(root.join(resource_path(file, "font", "")))
|
||||
.map_err(|e| log::warn!("font file {file}: {e}"))
|
||||
.ok()?;
|
||||
// FT_Get_Font_Format must say TrueType: CFF outlines ("OTTO") are refused.
|
||||
if data.starts_with(b"OTTO") {
|
||||
log::warn!("font file {file} is not in TTF format");
|
||||
@@ -515,16 +486,13 @@ impl TrueType {
|
||||
)
|
||||
}
|
||||
}
|
||||
fn load_hex(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
|
||||
fn load_hex(root: &Path, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
|
||||
let mut glyphs = HashMap::new();
|
||||
let Some(file) = p["hex_file"].as_str() else {
|
||||
return glyphs;
|
||||
};
|
||||
let path = resource_path(file, "", "").replace("//", "/");
|
||||
let Some(zip) = Zip::open(&root.join(&path))
|
||||
.ok()
|
||||
.or_else(|| defaults.and_then(|d| Zip::open(&d.join(&path)).ok()))
|
||||
else {
|
||||
let path = root.join(resource_path(file, "", "").replace("//", "/"));
|
||||
let Ok(zip) = Zip::open(&path) else {
|
||||
return glyphs;
|
||||
};
|
||||
let names: Vec<String> = zip
|
||||
@@ -635,24 +603,7 @@ impl Font {
|
||||
region: (u32, u32),
|
||||
white: Rect,
|
||||
) -> Self {
|
||||
Self::load_with(root, None, sprites, pixels, atlas, region, white, false)
|
||||
}
|
||||
/// [`Font::load`] over a pack overlay: a sheet, TrueType file or Unihex
|
||||
/// file the overlay lacks is read from `defaults`, the game's own
|
||||
/// resources, as vanilla's resource stack does. (Definitions stay the
|
||||
/// overlay's: a pack's filter may hide one from a lower pack.)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn load_over(
|
||||
root: &Path,
|
||||
defaults: &Path,
|
||||
sprites: &HashMap<String, Rect>,
|
||||
pixels: &[u8],
|
||||
atlas: (u32, u32),
|
||||
region: (u32, u32),
|
||||
white: Rect,
|
||||
) -> Self {
|
||||
let fallback = (root != defaults).then_some(defaults);
|
||||
Self::load_with(root, fallback, sprites, pixels, atlas, region, white, false)
|
||||
Self::load_with(root, sprites, pixels, atlas, region, white, false)
|
||||
}
|
||||
/// A server measures text but never draws it. Decode bitmap sheets only
|
||||
/// to obtain advances, then release their pixels; never rasterize glyphs.
|
||||
@@ -665,21 +616,10 @@ impl Font {
|
||||
w: 1,
|
||||
h: 1,
|
||||
};
|
||||
Self::load_with(
|
||||
root,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
&[],
|
||||
(1, 1),
|
||||
(0, 0),
|
||||
white,
|
||||
true,
|
||||
)
|
||||
Self::load_with(root, &HashMap::new(), &[], (1, 1), (0, 0), white, true)
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn load_with(
|
||||
root: &Path,
|
||||
fallback: Option<&Path>,
|
||||
sprites: &HashMap<String, Rect>,
|
||||
pixels: &[u8],
|
||||
atlas: (u32, u32),
|
||||
@@ -687,7 +627,11 @@ impl Font {
|
||||
white: Rect,
|
||||
metrics_only: bool,
|
||||
) -> Self {
|
||||
let sheets = sprites.clone();
|
||||
let sheets = sprites
|
||||
.iter()
|
||||
.filter(|(k, _)| k.starts_with("font/"))
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
let font = Self {
|
||||
sheets,
|
||||
missing: Glyph {
|
||||
@@ -701,7 +645,6 @@ impl Font {
|
||||
},
|
||||
white,
|
||||
root: root.to_owned(),
|
||||
fallback: fallback.map(|p| p.to_owned()),
|
||||
atlas,
|
||||
region,
|
||||
metrics_only,
|
||||
@@ -713,8 +656,11 @@ impl Font {
|
||||
pending: Vec::new(),
|
||||
full: false,
|
||||
},
|
||||
// `Font.random`: `RandomSource.create()`.
|
||||
random: crate::random_source::create(),
|
||||
random: horizon_core::random::LegacyRandom::new(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_nanos() as i64),
|
||||
),
|
||||
}),
|
||||
generation: std::cell::Cell::new(next_generation()),
|
||||
uniform: false,
|
||||
@@ -752,7 +698,6 @@ impl Font {
|
||||
Filter::of(p),
|
||||
load_bitmap(
|
||||
&self.root,
|
||||
self.fallback.as_deref(),
|
||||
p,
|
||||
&self.sheets,
|
||||
pixels,
|
||||
@@ -763,7 +708,7 @@ impl Font {
|
||||
"unihex" => Some(Provider::Unihex(Filter::of(p), p.clone(), None)),
|
||||
"ttf" => Some(Provider::TrueType(
|
||||
Filter::of(p),
|
||||
Box::new(load_true_type(&self.root, self.fallback.as_deref(), p)?),
|
||||
Box::new(load_true_type(&self.root, p)?),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
@@ -923,8 +868,7 @@ impl Font {
|
||||
}
|
||||
}
|
||||
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
|
||||
let glyphs = glyphs
|
||||
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
|
||||
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
|
||||
if let Some(h) = glyphs.get(&u32::from(c)).copied()
|
||||
&& !(non_fishy && fishy_advance(hex_advance(&h)))
|
||||
{
|
||||
@@ -1082,8 +1026,7 @@ impl Font {
|
||||
}
|
||||
}
|
||||
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
|
||||
let glyphs = glyphs
|
||||
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
|
||||
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
|
||||
if let Some(h) = glyphs.get(&u32::from(c)) {
|
||||
return Some(hex_advance(h));
|
||||
}
|
||||
@@ -1569,52 +1512,6 @@ mod tests {
|
||||
assert!(texels.iter().any(|t| t[0] == 255));
|
||||
}
|
||||
|
||||
/// A bitmap provider's `file` can name any texture. When the atlas has that
|
||||
/// texture (HUD packs keep their glyph sheets outside `textures/font`), the
|
||||
/// glyphs are read from it instead of being copied into the small run-time
|
||||
/// region one by one; the region then cannot fill up and turn later glyphs
|
||||
/// into the missing one, whose advance shifts the rest of a HUD line.
|
||||
#[test]
|
||||
fn a_bitmap_sheet_outside_font_is_read_from_the_atlas() {
|
||||
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
|
||||
let (_, _, pixels) = crate::hud::decode_bytes(&sheet).unwrap();
|
||||
let rect = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 8,
|
||||
};
|
||||
for (ns, file, key) in [
|
||||
("minecraft", "minecraft:hud/sheet.png", "hud/sheet"),
|
||||
(
|
||||
"hudengine",
|
||||
"hudengine:font/sheet.png",
|
||||
"hudengine:font/sheet",
|
||||
),
|
||||
] {
|
||||
let def = format!(
|
||||
r#"{{"providers":[{{"type":"bitmap","file":"{file}","ascent":7,"chars":["ab"]}}]}}"#
|
||||
);
|
||||
// The image is only in the atlas: the pack's copy is not on disk.
|
||||
let root = root(
|
||||
ns,
|
||||
&[("assets/minecraft/font/default.json", def.as_bytes())],
|
||||
);
|
||||
let sprites = HashMap::from([(key.to_string(), rect)]);
|
||||
let font = Font::load(&root, &sprites, &pixels, (16, 8), (0, 8), white());
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(font.glyph('a').advance, 3.0, "{ns}");
|
||||
assert_eq!(font.glyph('b').advance, 7.0, "{ns}");
|
||||
assert!(font.glyph('a').rect.is_some(), "{ns}");
|
||||
assert!(
|
||||
font.take_patches().is_empty(),
|
||||
"{ns}: the atlas sheet is not copied into the run-time region"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles_pick_their_font_and_unknown_fonts_draw_missing_glyphs() {
|
||||
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
|
||||
|
||||
+250
-1282
File diff suppressed because it is too large.
Load diff
@@ -5,9 +5,6 @@
|
||||
//! (`Arena::translucent_indices`): their back-to-front list then stays in one
|
||||
//! page of each and draws in a few multi-draws.
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
#[cfg(test)]
|
||||
#[path = "arena/source_tests.rs"]
|
||||
mod source_tests;
|
||||
|
||||
/// Ranges are rounded up to this many vertices to limit fragmentation, and
|
||||
/// start at multiples of it (`flat` names sections by these blocks).
|
||||
@@ -85,8 +82,6 @@ impl FreeList {
|
||||
/// One vertex buffer of the arena; only the newest page grows.
|
||||
struct Page {
|
||||
buffer: wgpu::Buffer,
|
||||
/// Optional shader-pack source records, indexed by the vertex index / 4.
|
||||
sources: Option<wgpu::Buffer>,
|
||||
/// Capacity in vertices.
|
||||
capacity: u64,
|
||||
free: FreeList,
|
||||
@@ -121,7 +116,6 @@ pub struct Arena {
|
||||
closing: Option<u32>,
|
||||
/// A page had to go to system memory (logged once).
|
||||
spilled: bool,
|
||||
sources_enabled: bool,
|
||||
/// Tests: GPU-local pages fail as if video memory were full.
|
||||
#[cfg(test)]
|
||||
video_memory_full: bool,
|
||||
@@ -206,7 +200,6 @@ impl Arena {
|
||||
usage,
|
||||
closing: None,
|
||||
spilled: false,
|
||||
sources_enabled: false,
|
||||
#[cfg(test)]
|
||||
video_memory_full: false,
|
||||
}
|
||||
@@ -256,11 +249,7 @@ impl Arena {
|
||||
}
|
||||
/// Bytes of every page's buffer.
|
||||
pub fn bytes(&self) -> u64 {
|
||||
self.pages
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|p| p.capacity * self.stride + p.sources.as_ref().map_or(0, wgpu::Buffer::size))
|
||||
.sum()
|
||||
self.pages.iter().flatten().map(|p| p.capacity).sum::<u64>() * self.stride
|
||||
}
|
||||
/// The most vertices a page holds.
|
||||
pub fn page_vertices(&self) -> u64 {
|
||||
@@ -292,65 +281,6 @@ impl Arena {
|
||||
.expect("live arena page")
|
||||
.buffer
|
||||
}
|
||||
|
||||
pub fn sources(&self, page: u32) -> Option<&wgpu::Buffer> {
|
||||
self.pages[page as usize].as_ref()?.sources.as_ref()
|
||||
}
|
||||
|
||||
fn source_buffer(device: &wgpu::Device, capacity: u64) -> Option<wgpu::Buffer> {
|
||||
if capacity / 4 * 8 > device.limits().max_storage_buffer_binding_size {
|
||||
return None;
|
||||
}
|
||||
try_create(
|
||||
device,
|
||||
"Terrain quad sources",
|
||||
capacity / 4 * 8,
|
||||
wgpu::BufferUsages::STORAGE
|
||||
| wgpu::BufferUsages::COPY_DST
|
||||
| wgpu::BufferUsages::COPY_SRC,
|
||||
)
|
||||
}
|
||||
|
||||
/// Native meshes allocate no sidecar. Once needed, existing pages also get a zeroed
|
||||
/// sidecar so moves can retain sources. Failure leaves geometry intact and attributes
|
||||
/// unknown; a later source upload may retry its page.
|
||||
pub fn write_sources(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
page: u32,
|
||||
start: u64,
|
||||
vertices: u64,
|
||||
sources: &[crate::mesher::TerrainSource],
|
||||
) {
|
||||
if !sources.is_empty() && !self.sources_enabled {
|
||||
self.sources_enabled = true;
|
||||
for p in self.pages.iter_mut().flatten() {
|
||||
p.sources = Self::source_buffer(device, p.capacity);
|
||||
}
|
||||
}
|
||||
let p = self.pages[page as usize].as_mut().expect("live arena page");
|
||||
if p.sources.is_none() && !sources.is_empty() {
|
||||
p.sources = Self::source_buffer(device, p.capacity);
|
||||
}
|
||||
if let Some(buffer) = &p.sources {
|
||||
let bytes = vertices / 4 * 8;
|
||||
if bytes > 0 {
|
||||
// Recycled ranges must never keep another mesh's identity, even for a
|
||||
// missing or incomplete sidecar. Zero records have no valid bit.
|
||||
encoder.clear_buffer(buffer, start / 4 * 8, Some(bytes));
|
||||
if sources.len() as u64 == vertices / 4 {
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
buffer,
|
||||
start / 4 * 8,
|
||||
bytemuck::cast_slice(sources),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Bytes of one vertex (or index).
|
||||
pub fn stride(&self) -> u64 {
|
||||
self.stride
|
||||
@@ -406,10 +336,6 @@ impl Arena {
|
||||
let start = free.take(len).expect("a new page fits one section");
|
||||
let page = Page {
|
||||
buffer,
|
||||
sources: self
|
||||
.sources_enabled
|
||||
.then(|| Self::source_buffer(device, capacity))
|
||||
.flatten(),
|
||||
capacity,
|
||||
free,
|
||||
host,
|
||||
@@ -525,10 +451,6 @@ impl Arena {
|
||||
let free = FreeList::new(capacity);
|
||||
self.pages.push(Some(Page {
|
||||
buffer,
|
||||
sources: self
|
||||
.sources_enabled
|
||||
.then(|| Self::source_buffer(device, capacity))
|
||||
.flatten(),
|
||||
capacity,
|
||||
free,
|
||||
host,
|
||||
@@ -561,21 +483,6 @@ impl Arena {
|
||||
to.1 * self.stride,
|
||||
vertices * self.stride,
|
||||
);
|
||||
if let Some(to_sources) = self.sources(to.0) {
|
||||
let bytes = vertices / 4 * 8;
|
||||
if bytes > 0 {
|
||||
match self.sources(from.0) {
|
||||
Some(from_sources) => encoder.copy_buffer_to_buffer(
|
||||
from_sources,
|
||||
from.1 / 4 * 8,
|
||||
to_sources,
|
||||
to.1 / 4 * 8,
|
||||
bytes,
|
||||
),
|
||||
None => encoder.clear_buffer(to_sources, to.1 / 4 * 8, Some(bytes)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn release(&mut self, page: u32, start: u64, vertices: u64) {
|
||||
let len = Self::span(vertices).min(self.page_limit);
|
||||
@@ -633,17 +540,9 @@ impl Arena {
|
||||
let Some((buffer, host)) = self.create(device, capacity, spill) else {
|
||||
return false;
|
||||
};
|
||||
let sources = self
|
||||
.sources_enabled
|
||||
.then(|| Self::source_buffer(device, capacity))
|
||||
.flatten();
|
||||
let old = self.pages[page].as_mut().expect("live arena page");
|
||||
// Copies recorded for the old buffer run before this one.
|
||||
encoder.copy_buffer_to_buffer(&old.buffer, 0, &buffer, 0, old.capacity * self.stride);
|
||||
if let (Some(from), Some(to)) = (&old.sources, &sources) {
|
||||
encoder.copy_buffer_to_buffer(from, 0, to, 0, from.size());
|
||||
}
|
||||
old.sources = sources;
|
||||
let previous = old.capacity;
|
||||
old.buffer = buffer;
|
||||
old.host = host;
|
||||
@@ -980,7 +879,6 @@ 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()),
|
||||
@@ -1149,15 +1047,7 @@ mod tests {
|
||||
hdr: None,
|
||||
}
|
||||
};
|
||||
let mut flat = flat::Flat::new(
|
||||
&device,
|
||||
&terrain_layout,
|
||||
4 * GRAIN,
|
||||
0,
|
||||
false,
|
||||
pipeline,
|
||||
solid,
|
||||
);
|
||||
let mut flat = flat::Flat::new(&device, &terrain_layout, 4 * GRAIN, 0, 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;
|
||||
@@ -1193,7 +1083,6 @@ 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()),
|
||||
@@ -1421,7 +1310,6 @@ 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()),
|
||||
@@ -1548,7 +1436,6 @@ mod tests {
|
||||
let free = FreeList(BTreeMap::from([(used, capacity - used)]));
|
||||
let buffer = buffer.clone();
|
||||
arena.pages = vec![Some(Page {
|
||||
sources: None,
|
||||
buffer,
|
||||
capacity,
|
||||
free,
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
//! Original sidecar lifetime controls, executed explicitly on an offscreen adapter.
|
||||
use super::*;
|
||||
use crate::mesher::{TerrainSource, TerrainVertex};
|
||||
|
||||
fn read(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
buffer: &wgpu::Buffer,
|
||||
offset: u64,
|
||||
size: u64,
|
||||
) -> Vec<u8> {
|
||||
let out = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
encoder.copy_buffer_to_buffer(buffer, offset, &out, 0, size);
|
||||
queue.submit([encoder.finish()]);
|
||||
out.slice(..).map_async(wgpu::MapMode::Read, |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
out.slice(..).get_mapped_range().unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "offscreen GPU control; run explicitly with an available adapter"]
|
||||
fn terrain_source_arena_gpu_grow_move_recycle_and_refusal() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default()))
|
||||
.expect("GPU control requires an adapter");
|
||||
eprintln!("TERRAIN_SOURCE_ARENA_ADAPTER {:?}", adapter.get_info());
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let mut arena = Arena::new(&device, 24, GRAIN, true);
|
||||
arena.page_limit = GRAIN * 2;
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let mut uploads = super::super::upload::Uploads::new(&device);
|
||||
let first = arena.alloc(&device, &mut encoder, 4).unwrap();
|
||||
assert!(
|
||||
arena.sources(first.0).is_none(),
|
||||
"default geometry allocates no source buffer"
|
||||
);
|
||||
let geometry = [TerrainVertex::new([2.25, 3.75, 4.5], [0.25, 0.75], 7, 0xff123456, 0x8070); 4];
|
||||
uploads.buffer(
|
||||
&mut encoder,
|
||||
arena.buffer(first.0),
|
||||
first.1 * 24,
|
||||
bytemuck::cast_slice(&geometry),
|
||||
);
|
||||
let source = TerrainSource::new(71, [2, 3, 4], 13, false);
|
||||
arena.write_sources(
|
||||
&device,
|
||||
&mut encoder,
|
||||
&mut uploads,
|
||||
first.0,
|
||||
first.1,
|
||||
4,
|
||||
&[source],
|
||||
);
|
||||
let second = arena.alloc(&device, &mut encoder, GRAIN).unwrap();
|
||||
assert_eq!(second, (first.0, GRAIN), "grew the first page");
|
||||
let third = arena.alloc(&device, &mut encoder, GRAIN).unwrap();
|
||||
assert_ne!(third.0, first.0, "move crosses pages");
|
||||
arena.copy(&mut encoder, first, third, 4);
|
||||
arena.release(first.0, first.1, 4);
|
||||
let reused = arena.alloc_existing(4).unwrap();
|
||||
assert_eq!(reused, first);
|
||||
arena.write_sources(
|
||||
&device,
|
||||
&mut encoder,
|
||||
&mut uploads,
|
||||
reused.0,
|
||||
reused.1,
|
||||
4,
|
||||
&[],
|
||||
);
|
||||
uploads.finish(&encoder);
|
||||
queue.submit([encoder.finish()]);
|
||||
assert_eq!(
|
||||
read(
|
||||
&device,
|
||||
&queue,
|
||||
arena.sources(third.0).unwrap(),
|
||||
third.1 / 4 * 8,
|
||||
8
|
||||
),
|
||||
bytemuck::bytes_of(&source)
|
||||
);
|
||||
assert_eq!(
|
||||
read(
|
||||
&device,
|
||||
&queue,
|
||||
arena.sources(first.0).unwrap(),
|
||||
first.1 / 4 * 8,
|
||||
8
|
||||
),
|
||||
[0; 8]
|
||||
);
|
||||
assert_eq!(
|
||||
read(&device, &queue, arena.buffer(third.0), third.1 * 24, 96),
|
||||
bytemuck::cast_slice::<_, u8>(&geometry)
|
||||
);
|
||||
|
||||
// A real requested binding limit refuses metadata/table allocation. The native
|
||||
// vertex allocation still fits; this is a controlled refusal, not a real OOM.
|
||||
let limits = wgpu::Limits {
|
||||
max_storage_buffer_binding_size: 256,
|
||||
..Default::default()
|
||||
};
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: limits,
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let mut arena = Arena::translucent(&device, 24, GRAIN);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let mut uploads = super::super::upload::Uploads::new(&device);
|
||||
let at = arena.alloc(&device, &mut encoder, 4).unwrap();
|
||||
uploads.buffer(
|
||||
&mut encoder,
|
||||
arena.buffer(at.0),
|
||||
at.1 * 24,
|
||||
bytemuck::cast_slice(&geometry),
|
||||
);
|
||||
arena.write_sources(
|
||||
&device,
|
||||
&mut encoder,
|
||||
&mut uploads,
|
||||
at.0,
|
||||
at.1,
|
||||
4,
|
||||
&[source],
|
||||
);
|
||||
assert!(
|
||||
arena.sources(at.0).is_none(),
|
||||
"sidecar refused without losing vertices"
|
||||
);
|
||||
assert!(super::super::pack_post::block_ids(&device, &queue, &[0; 128], &[0, 1]).is_none());
|
||||
uploads.finish(&encoder);
|
||||
queue.submit([encoder.finish()]);
|
||||
assert_eq!(
|
||||
read(&device, &queue, arena.buffer(at.0), at.1 * 24, 96),
|
||||
bytemuck::cast_slice::<_, u8>(&geometry)
|
||||
);
|
||||
}
|
||||
@@ -64,12 +64,9 @@ impl Copies {
|
||||
fn copy(mesh: &SectionMesh) -> SectionMesh {
|
||||
SectionMesh {
|
||||
solid: mesh.solid.clone(),
|
||||
solid_sources: mesh.solid_sources.clone(),
|
||||
sides: mesh.sides,
|
||||
translucent: mesh.translucent.clone(),
|
||||
translucent_sources: mesh.translucent_sources.clone(),
|
||||
packed: mesh.packed.clone(),
|
||||
packed_far: mesh.packed_far,
|
||||
solid_tints: Vec::new(),
|
||||
visibility: mesh.visibility,
|
||||
block_entities: mesh.block_entities.clone(),
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
//! 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 around the mesh's
|
||||
//! geometry (padded by a block) are drawn front faces
|
||||
//! 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
|
||||
//! 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
|
||||
@@ -27,13 +27,11 @@
|
||||
//! as many draws as without culling: drivers that unroll an indirect
|
||||
//! multi-draw on the CPU (Mesa's on Intel Gen9) pay per entry. Phase 2's
|
||||
//! counts come from the GPU, which reads them from the draws themselves:
|
||||
//! Vulkan's native multi-draw, DirectX 12's `ExecuteIndirect`, and Metal's,
|
||||
//! which wgpu replays as one indirect draw per entry. Translucent draws are
|
||||
//! not culled.
|
||||
//! Vulkan's native multi-draw, and Metal's, which wgpu replays as one
|
||||
//! indirect draw per entry. Translucent draws are not culled.
|
||||
//!
|
||||
//! Metal cannot force the depth test before a fragment shader that writes
|
||||
//! storage (naga writes no `[[early_fragment_tests]]`), nor can DirectX 12
|
||||
//! (`[earlydepthstencil]`, which wgpu doesn't offer there), so there the boxes
|
||||
//! storage (naga writes no `[[early_fragment_tests]]`), so there the boxes
|
||||
//! are drawn in a pass of their own between the phases, which reads phase
|
||||
//! 1's depth as a texture and makes the same comparison in the shader: the
|
||||
//! fragment's depth against the stored one, greater or equal passing. The
|
||||
@@ -47,15 +45,6 @@ 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;
|
||||
@@ -226,9 +215,9 @@ impl Cull {
|
||||
adapter.features() & wgpu::Features::SHADER_EARLY_DEPTH_TEST
|
||||
}
|
||||
/// Where the device can run the test, if it can: Vulkan with early
|
||||
/// depth tests, or Metal and DirectX 12 with the test in the shader, all
|
||||
/// drawing an indirect multi-draw's entries with the instance counts the
|
||||
/// GPU wrote, with fragment shaders that write storage.
|
||||
/// depth tests, or Metal, both drawing an indirect multi-draw's entries
|
||||
/// with the instance counts the GPU wrote, with fragment shaders that
|
||||
/// write storage.
|
||||
pub fn supported(adapter: &wgpu::Adapter, device: &wgpu::Device) -> Option<DepthTest> {
|
||||
let depth_test = match adapter.get_info().backend {
|
||||
wgpu::Backend::Vulkan | wgpu::Backend::Gl
|
||||
@@ -238,9 +227,7 @@ impl Cull {
|
||||
{
|
||||
DepthTest::Early
|
||||
}
|
||||
// DirectX 12 has no forced early depth test in wgpu either: the test runs in the
|
||||
// shader there too.
|
||||
wgpu::Backend::Metal | wgpu::Backend::Dx12 => DepthTest::InShader,
|
||||
wgpu::Backend::Metal => DepthTest::InShader,
|
||||
_ => return None,
|
||||
};
|
||||
(device
|
||||
|
||||
@@ -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
|
||||
// padded mesh bounds, or -1 and 17 (`SECTION_BOX`) without mesh bounds.
|
||||
// Then its box's lowest and highest corner around the origin (a section's:
|
||||
// -1 and 17, `SECTION_BOX`).
|
||||
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 geometry box, padded by a block,
|
||||
// drawn after phase 1 against its depth
|
||||
// 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
|
||||
// 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,9 +33,6 @@ struct Uniform {
|
||||
brightness: f32,
|
||||
mode: u32,
|
||||
pad: [u32; 2],
|
||||
hdr_settings: [f32; 4],
|
||||
hdr_shares: [f32; 4],
|
||||
hdr_light: [f32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@@ -48,8 +45,6 @@ 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,
|
||||
@@ -141,10 +136,7 @@ impl CustomSky {
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Custom sky shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl"))
|
||||
.into(),
|
||||
),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("custom_sky.wgsl").into()),
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Custom sky layout"),
|
||||
@@ -154,11 +146,7 @@ impl CustomSky {
|
||||
let attributes = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32x4];
|
||||
let pipelines = MODES
|
||||
.iter()
|
||||
.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)| {
|
||||
.map(|&mode| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Custom sky layer"),
|
||||
layout: Some(&pipeline_layout),
|
||||
@@ -186,31 +174,14 @@ impl CustomSky {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
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()
|
||||
},
|
||||
entry_point: Some("fs_custom_sky"),
|
||||
compilation_options: 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: if written_screen {
|
||||
wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Constant,
|
||||
..blend(mode)
|
||||
}
|
||||
} else {
|
||||
blend(mode)
|
||||
},
|
||||
color: blend(mode),
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Zero,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
@@ -244,8 +215,6 @@ impl CustomSky {
|
||||
});
|
||||
Self {
|
||||
pipelines,
|
||||
written_gain: None,
|
||||
written_debug: false,
|
||||
layout,
|
||||
sampler,
|
||||
uniform,
|
||||
@@ -326,20 +295,7 @@ 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() {
|
||||
@@ -351,9 +307,6 @@ 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));
|
||||
@@ -370,35 +323,7 @@ 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() {
|
||||
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_pipeline(&self.pipelines[usize::from(mode)]);
|
||||
pass.set_bind_group(
|
||||
0,
|
||||
&self.groups[usize::from(image)],
|
||||
@@ -469,10 +394,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_shader_validates() {
|
||||
let module = naga::front::wgsl::parse_str(
|
||||
&(include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl")),
|
||||
)
|
||||
.unwrap();
|
||||
let module = naga::front::wgsl::parse_str(include_str!("custom_sky.wgsl")).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -491,259 +413,4 @@ 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 hdr_gain = (1.0f32 + 3.0 * (1.0 - 206.0 / 255.0) / 0.24).powf(1.0 / 2.2);
|
||||
for (mode_index, mode, debug, written) in MODES
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, m)| (i, m, false, true))
|
||||
.chain([(5, Blend::Screen, true, true)])
|
||||
.chain(
|
||||
MODES
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, m)| (i, m, false, false)),
|
||||
)
|
||||
{
|
||||
let gain = if written { hdr_gain } else { 1.0 };
|
||||
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, f32::from(u8::from(written))],
|
||||
};
|
||||
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 alpha = half::f16::from_le_bytes([data[6], data[7]]).to_f32();
|
||||
let coverage = if mode == Blend::Alpha { k } else { 1.0 };
|
||||
assert!(
|
||||
(alpha - coverage).abs() < 0.001,
|
||||
"{mode:?}: alpha {alpha}, expected {coverage}"
|
||||
);
|
||||
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 with and without gain, plus debug Screen, checking RGB and coverage"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,6 @@ 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>;
|
||||
@@ -46,34 +42,20 @@ const REPLACE: u32 = 6u;
|
||||
const OVERLAY: u32 = 7u;
|
||||
const ALPHA: u32 = 8u;
|
||||
|
||||
fn custom_sky_color(in: Out) -> vec4<f32> {
|
||||
@fragment fn fs_custom_sky(in: Out) -> @location(0) 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(color, 1.0); }
|
||||
case REPLACE: { return vec4(t.rgb, 1.0); }
|
||||
// mix(dst, src, alpha).
|
||||
case ALPHA: { return vec4(color, k); }
|
||||
case ALPHA: { return vec4(t.rgb, k); }
|
||||
// add, subtract, dodge, burn, screen: the layer premultiplied, black leaves it alone.
|
||||
default: { return vec4(color * k, 1.0); }
|
||||
default: { return vec4(t.rgb * 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);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use crate::mesher::TerrainVertex;
|
||||
|
||||
/// The solid quads the grid can draw a frame, and the sections they come from.
|
||||
const MAX_QUADS: u64 = 1 << 16;
|
||||
pub(crate) const MAX_SECTIONS: usize = 64;
|
||||
const MAX_SECTIONS: usize = 64;
|
||||
/// Bytes of one quad's corners.
|
||||
const QUAD_BYTES: u64 = 4 * std::mem::size_of::<TerrainVertex>() as u64;
|
||||
/// How far past the flat face the relief may reach, in blocks, for the view test.
|
||||
@@ -413,8 +413,6 @@ pub struct Displacement {
|
||||
pub shadow: Option<Picks>,
|
||||
/// Group 1's layout for the grid's pipelines.
|
||||
pub draw_layout: wgpu::BindGroupLayout,
|
||||
/// CPU-side section table reused between displacement preparation passes.
|
||||
table: Vec<Section>,
|
||||
/// Sections the pick ran over this frame (0: nothing to draw).
|
||||
pub drawn: usize,
|
||||
}
|
||||
@@ -600,7 +598,6 @@ impl Displacement {
|
||||
view,
|
||||
shadow,
|
||||
draw_layout,
|
||||
table: Vec::new(),
|
||||
drawn: 0,
|
||||
}
|
||||
}
|
||||
@@ -613,8 +610,7 @@ impl Displacement {
|
||||
near: &[Near<'_>],
|
||||
view_proj: glam::Mat4,
|
||||
) {
|
||||
self.table.clear();
|
||||
self.table.reserve(near.len().min(MAX_SECTIONS));
|
||||
let mut table = Vec::with_capacity(near.len().min(MAX_SECTIONS));
|
||||
let mut first = 0u64;
|
||||
let mut most = 0u32;
|
||||
for n in near.iter().take(MAX_SECTIONS) {
|
||||
@@ -630,7 +626,7 @@ impl Displacement {
|
||||
first * QUAD_BYTES,
|
||||
quads * QUAD_BYTES,
|
||||
);
|
||||
self.table.push(Section {
|
||||
table.push(Section {
|
||||
origin: [
|
||||
n.origin[0],
|
||||
n.origin[1],
|
||||
@@ -644,11 +640,11 @@ impl Displacement {
|
||||
first += quads;
|
||||
most = most.max(quads as u32);
|
||||
}
|
||||
self.drawn = self.table.len();
|
||||
if self.table.is_empty() {
|
||||
self.drawn = table.len();
|
||||
if table.is_empty() {
|
||||
return;
|
||||
}
|
||||
queue.write_buffer(&self.sections, 0, bytemuck::cast_slice(&self.table));
|
||||
queue.write_buffer(&self.sections, 0, bytemuck::cast_slice(&table));
|
||||
let lists = std::iter::once((&self.view, 0)).chain(self.shadow.iter().map(|s| (s, 1)));
|
||||
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some("Displacement pick"),
|
||||
@@ -659,7 +655,7 @@ impl Displacement {
|
||||
let params = Params {
|
||||
view_proj: view_proj.to_cols_array_2d(),
|
||||
distance: self.distance,
|
||||
sections: self.table.len() as u32,
|
||||
sections: table.len() as u32,
|
||||
margin: RELIEF_MARGIN,
|
||||
capacity: MAX_QUADS as u32,
|
||||
shadow,
|
||||
@@ -669,7 +665,7 @@ impl Displacement {
|
||||
let vertices = grid_vertices(picks.cells, picks.steps);
|
||||
queue.write_buffer(&picks.draw, 0, bytemuck::cast_slice(&[vertices, 0, 0, 0]));
|
||||
pass.set_bind_group(0, &picks.pick_group, &[]);
|
||||
pass.dispatch_workgroups(most.div_ceil(64), self.table.len() as u32, 1);
|
||||
pass.dispatch_workgroups(most.div_ceil(64), table.len() as u32, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
//! Which compiler turns DirectX 12's shaders into bytecode: DXC on integrated
|
||||
//! GPUs, FXC on every other adapter (#8513).
|
||||
//!
|
||||
//! Measured on one laptop at render distance 32 (2560x1440, the same world and
|
||||
//! view): DXC took Intel Iris Xe from 44 to 65 FPS (22.1 to 14.7 ms of GPU
|
||||
//! time) and an NVIDIA RTX 3050 Ti from 159 down to 150 FPS. An RX 9070 gained
|
||||
//! about 9 % of GPU time at render distance 32 and lost 0.03 ms at 8. So
|
||||
//! discrete GPUs keep FXC (`d3dcompiler_47.dll`, part of Windows), and only
|
||||
//! integrated ones load DXC (`dxcompiler.dll`, which the Windows x64 zip puts
|
||||
//! beside the game: `tools/ci/fetch_dxc.py`).
|
||||
//!
|
||||
//! wgpu chooses the compiler for a whole instance, before any adapter is
|
||||
//! known. `open_backend` therefore asks for the adapter on an FXC instance and,
|
||||
//! when that adapter is an integrated GPU on DirectX 12, asks again on an
|
||||
//! instance with DXC. That happens once, when the device is opened; nothing
|
||||
//! here runs per frame. Without the DLL, or when it does not load, the game
|
||||
//! keeps the FXC instance and logs one line. `WGPU_DX12_COMPILER` (`fxc`,
|
||||
//! `dxc`, `auto`) overrides the choice: wgpu applies it to every instance.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The DLL's name, as the Windows zip ships it beside `horizon-client.exe`.
|
||||
pub(super) const DLL: &str = "dxcompiler.dll";
|
||||
|
||||
/// Whether `info`, opened on an FXC instance, should be opened again with DXC:
|
||||
/// an integrated GPU on DirectX 12, unless `forced` (`WGPU_DX12_COMPILER` is
|
||||
/// set, so its compiler is already the one every instance uses).
|
||||
pub(super) fn wants_dxc(info: &wgpu::AdapterInfo, forced: bool) -> bool {
|
||||
!forced
|
||||
&& info.backend == wgpu::Backend::Dx12
|
||||
&& info.device_type == wgpu::DeviceType::IntegratedGpu
|
||||
}
|
||||
|
||||
/// Whether `WGPU_DX12_COMPILER` names the compiler.
|
||||
pub(super) fn forced() -> bool {
|
||||
std::env::var_os("WGPU_DX12_COMPILER").is_some()
|
||||
}
|
||||
|
||||
/// `dxcompiler.dll` beside the running executable, if it is there. An absolute
|
||||
/// path, so a copy elsewhere on `PATH` (an older Windows SDK's) is never the
|
||||
/// one loaded.
|
||||
pub(super) fn bundled_dll() -> Option<PathBuf> {
|
||||
let dll = std::env::current_exe().ok()?.parent()?.join(DLL);
|
||||
dll.is_file().then_some(dll)
|
||||
}
|
||||
|
||||
/// The compiler setting that loads `dll`, and fails the instance (rather
|
||||
/// than falling back to FXC on its own) when it doesn't load: `open_backend`
|
||||
/// then keeps the FXC instance it already has.
|
||||
pub(super) fn dxc(dll: &std::path::Path) -> wgpu::Dx12Compiler {
|
||||
wgpu::Dx12Compiler::DynamicDxc {
|
||||
dxc_path: dll.to_string_lossy().into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the DXC instance offered the adapter the FXC instance had: on a
|
||||
/// laptop with two GPUs, the same request should pick the same one, and the
|
||||
/// game must not switch GPUs because the compiler changed.
|
||||
pub(super) fn same_adapter(fxc: &wgpu::AdapterInfo, dxc: &wgpu::AdapterInfo) -> bool {
|
||||
fxc.backend == dxc.backend
|
||||
&& fxc.vendor == dxc.vendor
|
||||
&& fxc.device == dxc.device
|
||||
&& fxc.device_type == dxc.device_type
|
||||
&& fxc.device_pci_bus_id == dxc.device_pci_bus_id
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn adapter(backend: wgpu::Backend, device_type: wgpu::DeviceType) -> wgpu::AdapterInfo {
|
||||
wgpu::AdapterInfo {
|
||||
name: "Intel(R) Iris(R) Xe Graphics".into(),
|
||||
vendor: 0x8086,
|
||||
device: 0x9a49,
|
||||
device_type,
|
||||
device_pci_bus_id: "0000:00:02.0".into(),
|
||||
driver: String::new(),
|
||||
driver_info: String::new(),
|
||||
backend,
|
||||
subgroup_min_size: 0,
|
||||
subgroup_max_size: 0,
|
||||
transient_saves_memory: None,
|
||||
limit_bucket: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_integrated_gpus_on_directx_12_get_dxc() {
|
||||
use wgpu::{Backend, DeviceType};
|
||||
assert!(wants_dxc(
|
||||
&adapter(Backend::Dx12, DeviceType::IntegratedGpu),
|
||||
false
|
||||
));
|
||||
for kind in [
|
||||
DeviceType::DiscreteGpu,
|
||||
DeviceType::VirtualGpu,
|
||||
DeviceType::Cpu,
|
||||
DeviceType::Other,
|
||||
] {
|
||||
assert!(!wants_dxc(&adapter(Backend::Dx12, kind), false), "{kind:?}");
|
||||
}
|
||||
// Other backends have no shader compiler to choose.
|
||||
for backend in [Backend::Vulkan, Backend::Gl, Backend::Metal] {
|
||||
assert!(
|
||||
!wants_dxc(&adapter(backend, DeviceType::IntegratedGpu), false),
|
||||
"{backend:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_dx12_compiler_overrides_the_choice() {
|
||||
let igpu = adapter(wgpu::Backend::Dx12, wgpu::DeviceType::IntegratedGpu);
|
||||
assert!(!wants_dxc(&igpu, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dxc_instance_must_offer_the_same_gpu() {
|
||||
let igpu = adapter(wgpu::Backend::Dx12, wgpu::DeviceType::IntegratedGpu);
|
||||
assert!(same_adapter(&igpu, &igpu.clone()));
|
||||
let dgpu = wgpu::AdapterInfo {
|
||||
name: "NVIDIA GeForce RTX 3050 Ti Laptop GPU".into(),
|
||||
vendor: 0x10de,
|
||||
device: 0x25a0,
|
||||
device_type: wgpu::DeviceType::DiscreteGpu,
|
||||
device_pci_bus_id: "0000:01:00.0".into(),
|
||||
..igpu.clone()
|
||||
};
|
||||
assert!(!same_adapter(&igpu, &dgpu));
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,6 @@ 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).
|
||||
@@ -174,22 +172,11 @@ pub fn wanted(adapter: &wgpu::Adapter, device: &wgpu::Device) -> bool {
|
||||
|
||||
/// Whether frames without a shader pack's terrain draw flat: where the backend
|
||||
/// emulates multi-draw (Metal, past `MIN_SECTIONS`), on Intel Gen11/Gen12's Vulkan
|
||||
/// driver, on Qualcomm's Adreno under DirectX 12, or with `HORIZON_FLAT_DRAWS=1`.
|
||||
/// Elsewhere per-section draws stay.
|
||||
/// driver, or with `HORIZON_FLAT_DRAWS=1`. Elsewhere per-section draws stay.
|
||||
pub fn for_vanilla(info: &wgpu::AdapterInfo) -> bool {
|
||||
std::env::var("HORIZON_FLAT_DRAWS").as_deref() == Ok("1")
|
||||
|| info.backend == wgpu::Backend::Metal
|
||||
|| is_broken_intel_vulkan(info)
|
||||
|| is_adreno_dx12(info)
|
||||
}
|
||||
|
||||
/// Qualcomm's Adreno under DirectX 12 (Windows on Snapdragon, where Auto picks DirectX 12),
|
||||
/// where wgpu's indirect multi-draw is `ExecuteIndirect`: drawn flat, an X1-45 took 3-11%
|
||||
/// less GPU time a frame at render distance 12 and 3-13% less at 32 (perf_bench's four
|
||||
/// scenes, held still), from 389 sections in view up, the same picture. DXGI names
|
||||
/// Qualcomm `QCOM` (0x4D4F4351); Vulkan, 0x5143.
|
||||
pub fn is_adreno_dx12(info: &wgpu::AdapterInfo) -> bool {
|
||||
info.backend == wgpu::Backend::Dx12 && matches!(info.vendor, 0x4d4f_4351 | 0x5143)
|
||||
}
|
||||
|
||||
/// Intel Gen11 (Ice Lake) and Gen12 (Tiger Lake, Rocket Lake, Alder Lake,
|
||||
@@ -378,7 +365,6 @@ impl Flat {
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
page_vertices: u64,
|
||||
min_sections: usize,
|
||||
indirect_execution: bool,
|
||||
pipeline: impl Fn(
|
||||
&wgpu::PipelineLayout,
|
||||
&str,
|
||||
@@ -503,16 +489,10 @@ impl Flat {
|
||||
device,
|
||||
"Flat section draw calls, phase 2",
|
||||
20,
|
||||
storage_usage
|
||||
| if indirect_execution {
|
||||
wgpu::BufferUsages::INDIRECT
|
||||
} else {
|
||||
wgpu::BufferUsages::empty()
|
||||
},
|
||||
wgpu::BufferUsages::INDIRECT | storage_usage,
|
||||
),
|
||||
phase2_call_capacity: 1,
|
||||
phase2_call_data: Vec::new(),
|
||||
indirect_execution,
|
||||
phase2_group: None,
|
||||
draw_layout,
|
||||
blocks_per_page: page_vertices.div_ceil(GRAIN),
|
||||
@@ -665,7 +645,6 @@ 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 {
|
||||
@@ -774,12 +753,7 @@ impl Flat {
|
||||
device,
|
||||
"Flat section draw calls, phase 2",
|
||||
self.phase2_call_capacity * 20,
|
||||
wgpu::BufferUsages::STORAGE
|
||||
| if self.indirect_execution {
|
||||
wgpu::BufferUsages::INDIRECT
|
||||
} else {
|
||||
wgpu::BufferUsages::empty()
|
||||
},
|
||||
wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::STORAGE,
|
||||
);
|
||||
}
|
||||
if phase2_calls > 0 {
|
||||
@@ -1055,30 +1029,6 @@ impl Flat {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adreno_draws_flat_under_dx12_only() {
|
||||
let mut info = wgpu::AdapterInfo {
|
||||
name: "Qualcomm(R) Adreno(TM) X1-45 GPU".into(),
|
||||
vendor: 0x4d4f_4351,
|
||||
device: 0,
|
||||
device_type: wgpu::DeviceType::IntegratedGpu,
|
||||
device_pci_bus_id: String::new(),
|
||||
driver: "31.0.128.0".into(),
|
||||
driver_info: String::new(),
|
||||
backend: wgpu::Backend::Dx12,
|
||||
subgroup_min_size: 0,
|
||||
subgroup_max_size: 0,
|
||||
transient_saves_memory: None,
|
||||
limit_bucket: None,
|
||||
};
|
||||
assert!(is_adreno_dx12(&info));
|
||||
info.backend = wgpu::Backend::Vulkan;
|
||||
assert!(!is_adreno_dx12(&info));
|
||||
info.backend = wgpu::Backend::Dx12;
|
||||
info.vendor = 0x10de;
|
||||
assert!(!is_adreno_dx12(&info));
|
||||
}
|
||||
|
||||
/// Draws as `render` writes them: solid (page, quads at a base vertex)
|
||||
/// first, then translucent (page, sorted page, count, first sorted index,
|
||||
/// base vertex).
|
||||
|
||||
@@ -2735,9 +2735,8 @@ mod tests {
|
||||
|
||||
/// `test` on each device here that draws counts with first instances:
|
||||
/// Vulkan, and DirectX 12 with each of its compilers. Players' DirectX 12
|
||||
/// compiles with FXC, part of Windows, and with DXC (`dxcompiler.dll`,
|
||||
/// beside the game in the Windows x64 zip) on integrated GPUs
|
||||
/// (`gfx/dx12_compiler.rs`).
|
||||
/// compiles with FXC, part of Windows: wgpu takes DXC only where
|
||||
/// `dxcompiler.dll` loads, and the game ships none.
|
||||
fn on_each_device(test: impl Fn(&wgpu::Device, &Queue)) {
|
||||
let mut tested = false;
|
||||
for (backends, compiler, name) in [
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,65 +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.
|
||||
// `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);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,335 +0,0 @@
|
||||
//! A held world (`WorldHold`, the native hook `native_world_hold`): the terrain goes on being
|
||||
//! drawn as it stands while what changes under it is meshed, and all of it takes its place on
|
||||
//! one frame. A section already on screen that is meshed again is uploaded to the arena as ever,
|
||||
//! over as many frames as its mesh takes to come, but waits beside the mesh drawn (`Hold`);
|
||||
//! letting go only swaps which of the two is drawn, with nothing left to upload. Sections not on
|
||||
//! screen yet (terrain streaming in) are not held.
|
||||
//!
|
||||
//! Both meshes of a waiting section are in memory, so a hold costs up to what the changed land's
|
||||
//! new meshes come to. It is bounded, and each bound lets go at once rather than losing anything:
|
||||
//! `MAX_BYTES` of waiting meshes, `MAX_HOLD` of time, and the arena running out
|
||||
//! (`Renderer::upload`). It never outlives what asked for it: the mods are asked every frame,
|
||||
//! and the sections' being cleared (the world left, another dimension) drops what waits. A hold
|
||||
//! that ended by itself (spent) stays ended until no mod has asked for a frame.
|
||||
use super::SectionKey;
|
||||
use crate::fast_hash::{FxHashMap, FxHashSet};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// The longest a hold lasts.
|
||||
pub(super) const MAX_HOLD: Duration = Duration::from_secs(30);
|
||||
/// The most bytes of new meshes that wait.
|
||||
pub(super) const MAX_BYTES: u64 = 1 << 30;
|
||||
/// The most circles a hold is kept to (`WorldHold::hold_within`).
|
||||
pub const MAX_AREAS: usize = 64;
|
||||
|
||||
/// What the mods ask of the terrain's picture this frame (`native_world_hold`): nothing, all of
|
||||
/// it held, or only the sections inside some circles. Several mods add up: one holding all of it
|
||||
/// holds all of it, and circles join.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct WorldHold {
|
||||
all: bool,
|
||||
within: Vec<[f64; 3]>,
|
||||
}
|
||||
impl WorldHold {
|
||||
/// Hold the whole world's terrain as it is drawn, this frame.
|
||||
pub fn hold(&mut self) {
|
||||
self.all = true;
|
||||
}
|
||||
/// Hold the sections whose middle lies within `radius` blocks of (`x`, `z`), this frame. A
|
||||
/// circle that isn't finite or has a negative radius is left out, as are those past
|
||||
/// `MAX_AREAS`.
|
||||
pub fn hold_within(&mut self, x: f64, z: f64, radius: f64) {
|
||||
let good = x.is_finite() && z.is_finite() && radius.is_finite() && radius >= 0.0;
|
||||
if good && self.within.len() < MAX_AREAS {
|
||||
self.within.push([x, z, radius]);
|
||||
}
|
||||
}
|
||||
/// Whether anything is held.
|
||||
pub fn is_held(&self) -> bool {
|
||||
self.all || !self.within.is_empty()
|
||||
}
|
||||
/// Whether section `key` is in what is held: all of the world, or a circle its middle lies in.
|
||||
pub fn covers(&self, key: SectionKey) -> bool {
|
||||
if self.all {
|
||||
return true;
|
||||
}
|
||||
let (x, z) = (f64::from(key.0) * 16.0 + 8.0, f64::from(key.2) * 16.0 + 8.0);
|
||||
self.within
|
||||
.iter()
|
||||
.any(|&[cx, cz, r]| (x - cx).powi(2) + (z - cz).powi(2) <= r * r)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a frame's ask means for the sections waiting.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum Release {
|
||||
/// They go on waiting.
|
||||
None,
|
||||
/// Those the hold no longer covers take their place.
|
||||
Outside,
|
||||
/// All of them take their place.
|
||||
All,
|
||||
}
|
||||
|
||||
/// A section waiting behind the hold and the bytes its new mesh takes.
|
||||
struct Waiting<S> {
|
||||
section: S,
|
||||
bytes: u64,
|
||||
}
|
||||
|
||||
/// The renderer's side of a hold: whether it is on, what it covers, and the sections waiting.
|
||||
pub(super) struct Hold<S> {
|
||||
ask: WorldHold,
|
||||
since: Option<Instant>,
|
||||
spent: bool,
|
||||
waiting: FxHashMap<SectionKey, Waiting<S>>,
|
||||
bytes: u64,
|
||||
}
|
||||
impl<S> Default for Hold<S> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ask: WorldHold::default(),
|
||||
since: None,
|
||||
spent: false,
|
||||
waiting: FxHashMap::default(),
|
||||
bytes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<S> Hold<S> {
|
||||
/// Whether a new mesh of section `key` waits.
|
||||
pub fn holds(&self, key: SectionKey) -> bool {
|
||||
self.since.is_some() && !self.spent && self.ask.covers(key)
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.waiting.is_empty()
|
||||
}
|
||||
pub fn bytes(&self) -> u64 {
|
||||
self.bytes
|
||||
}
|
||||
/// A frame's ask, at `now`.
|
||||
pub fn ask(&mut self, ask: &WorldHold, now: Instant) -> Release {
|
||||
if !ask.is_held() {
|
||||
let was = self.since.is_some();
|
||||
(self.since, self.spent) = (None, false);
|
||||
self.ask = WorldHold::default();
|
||||
return if was && !self.waiting.is_empty() {
|
||||
Release::All
|
||||
} else {
|
||||
Release::None
|
||||
};
|
||||
}
|
||||
if self.spent {
|
||||
return Release::None;
|
||||
}
|
||||
let since = *self.since.get_or_insert(now);
|
||||
if now.saturating_duration_since(since) >= MAX_HOLD {
|
||||
log::warn!(
|
||||
"Horizon: a mod held the world for {} s: let go",
|
||||
MAX_HOLD.as_secs()
|
||||
);
|
||||
self.spend();
|
||||
return Release::All;
|
||||
}
|
||||
if *ask == self.ask {
|
||||
return Release::None;
|
||||
}
|
||||
// (fewer circles or smaller: some of what waits is let go; more: nothing waits that didn't)
|
||||
self.ask.clone_from(ask);
|
||||
if self.waiting.is_empty() {
|
||||
Release::None
|
||||
} else {
|
||||
Release::Outside
|
||||
}
|
||||
}
|
||||
/// Ended by itself: nothing waits from now on, until no mod has asked for a frame.
|
||||
/// What waited is the caller's to place (`take_all`) or drop.
|
||||
pub fn spend(&mut self) {
|
||||
self.spent = self.since.is_some();
|
||||
}
|
||||
/// Section `key`'s new mesh waits: the one that waited before for it comes back, to free.
|
||||
/// The bool is whether what waits is now too much (`MAX_BYTES`).
|
||||
pub fn wait(&mut self, key: SectionKey, section: S, bytes: u64) -> (Option<S>, bool) {
|
||||
let old = self.take(key);
|
||||
self.bytes += bytes;
|
||||
self.waiting.insert(key, Waiting { section, bytes });
|
||||
(old, self.bytes > MAX_BYTES)
|
||||
}
|
||||
/// Section `key`'s waiting mesh, no longer waiting.
|
||||
pub fn take(&mut self, key: SectionKey) -> Option<S> {
|
||||
if self.waiting.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let w = self.waiting.remove(&key)?;
|
||||
self.bytes -= w.bytes;
|
||||
Some(w.section)
|
||||
}
|
||||
/// Everything waiting, no longer waiting.
|
||||
pub fn take_all(&mut self) -> Vec<(SectionKey, S)> {
|
||||
self.bytes = 0;
|
||||
self.waiting.drain().map(|(k, w)| (k, w.section)).collect()
|
||||
}
|
||||
/// What waits that the hold no longer covers.
|
||||
pub fn take_outside(&mut self) -> Vec<(SectionKey, S)> {
|
||||
let ask = &self.ask;
|
||||
let all = self.since.is_none() || self.spent;
|
||||
let out: Vec<_> = self
|
||||
.waiting
|
||||
.extract_if(|&k, _| all || !ask.covers(k))
|
||||
.collect();
|
||||
self.bytes -= out.iter().map(|(_, w)| w.bytes).sum::<u64>();
|
||||
out.into_iter().map(|(k, w)| (k, w.section)).collect()
|
||||
}
|
||||
/// What waits in these columns, which are going.
|
||||
pub fn take_columns(&mut self, columns: &FxHashSet<(i32, i32)>) -> Vec<S> {
|
||||
if self.waiting.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let out: Vec<_> = self
|
||||
.waiting
|
||||
.extract_if(|k, _| columns.contains(&(k.0, k.2)))
|
||||
.collect();
|
||||
self.bytes -= out.iter().map(|(_, w)| w.bytes).sum::<u64>();
|
||||
out.into_iter().map(|(_, w)| w.section).collect()
|
||||
}
|
||||
/// The sections are all gone: what waits goes with them and the hold is spent.
|
||||
pub fn clear(&mut self) {
|
||||
self.waiting.clear();
|
||||
self.bytes = 0;
|
||||
self.spend();
|
||||
}
|
||||
pub fn sections(&self) -> impl Iterator<Item = &S> {
|
||||
self.waiting.values().map(|w| &w.section)
|
||||
}
|
||||
pub fn sections_mut(&mut self) -> impl Iterator<Item = &mut S> {
|
||||
self.waiting.values_mut().map(|w| &mut w.section)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn all() -> WorldHold {
|
||||
let mut h = WorldHold::default();
|
||||
h.hold();
|
||||
h
|
||||
}
|
||||
fn within(circles: &[[f64; 3]]) -> WorldHold {
|
||||
let mut h = WorldHold::default();
|
||||
for &[x, z, r] in circles {
|
||||
h.hold_within(x, z, r);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hold_covers_sections_by_their_middle() {
|
||||
assert!(all().covers((900, 0, -900)));
|
||||
assert!(!WorldHold::default().covers((0, 0, 0)));
|
||||
assert!(within(&[[8.0, 8.0, 1.0]]).covers((0, 3, 0)));
|
||||
assert!(!within(&[[0.0, 0.0, 11.0]]).covers((0, 3, 0)));
|
||||
assert!(within(&[[0.0, 0.0, 12.0]]).covers((0, 3, 0)));
|
||||
assert!(within(&[[1000.0, 0.0, 1.0], [24.0, 8.0, 0.5]]).covers((1, 0, 0)));
|
||||
assert!(within(&[[-8.0, -8.0, 0.0]]).covers((-1, 0, -1)));
|
||||
}
|
||||
#[test]
|
||||
fn bad_circles_are_left_out_and_there_are_at_most_max_areas() {
|
||||
let h = within(&[
|
||||
[f64::NAN, 0.0, 5.0],
|
||||
[0.0, f64::INFINITY, 5.0],
|
||||
[0.0, 0.0, -1.0],
|
||||
[0.0, 0.0, f64::INFINITY],
|
||||
]);
|
||||
assert!(!h.is_held());
|
||||
let mut many = WorldHold::default();
|
||||
for i in 0..MAX_AREAS + 10 {
|
||||
many.hold_within(i as f64 * 100.0, 0.0, 1.0);
|
||||
}
|
||||
assert_eq!(many.within.len(), MAX_AREAS);
|
||||
// (several mods: all of it from one holds all of it)
|
||||
many.hold();
|
||||
assert!(many.covers((-5000, 0, 5000)));
|
||||
}
|
||||
#[test]
|
||||
fn a_hold_starts_and_lets_go_when_no_one_asks() {
|
||||
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
|
||||
assert_eq!(g.ask(&WorldHold::default(), t), Release::None);
|
||||
assert!(!g.holds((0, 0, 0)));
|
||||
assert_eq!(g.ask(&all(), t), Release::None);
|
||||
assert!(g.holds((0, 0, 0)));
|
||||
assert_eq!(g.ask(&all(), t + Duration::from_secs(5)), Release::None);
|
||||
// (nothing waited: nothing to place)
|
||||
assert_eq!(g.ask(&WorldHold::default(), t), Release::None);
|
||||
g.ask(&all(), t);
|
||||
g.wait((0, 0, 0), 7, 100);
|
||||
assert_eq!(g.ask(&WorldHold::default(), t), Release::All);
|
||||
assert_eq!(g.take_all(), vec![((0, 0, 0), 7)]);
|
||||
assert!(!g.holds((0, 0, 0)));
|
||||
}
|
||||
#[test]
|
||||
fn a_hold_ends_by_itself_and_stays_ended_until_let_go() {
|
||||
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
|
||||
g.ask(&all(), t);
|
||||
assert_eq!(g.ask(&all(), t + MAX_HOLD), Release::All);
|
||||
assert!(!g.holds((0, 0, 0)));
|
||||
assert_eq!(g.ask(&all(), t + MAX_HOLD * 2), Release::None);
|
||||
assert!(!g.holds((0, 0, 0)));
|
||||
g.ask(&WorldHold::default(), t + MAX_HOLD * 2);
|
||||
g.ask(&all(), t + MAX_HOLD * 3);
|
||||
assert!(g.holds((0, 0, 0)));
|
||||
}
|
||||
#[test]
|
||||
fn smaller_circles_let_go_what_they_leave() {
|
||||
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
|
||||
g.ask(&within(&[[0.0, 0.0, 100.0]]), t);
|
||||
assert!(g.holds((0, 0, 0)) && g.holds((5, 0, 0)) && !g.holds((7, 0, 0)));
|
||||
g.wait((0, 0, 0), 1, 10);
|
||||
g.wait((5, 0, 0), 2, 20);
|
||||
assert_eq!(g.ask(&within(&[[0.0, 0.0, 100.0]]), t), Release::None);
|
||||
assert_eq!(g.ask(&within(&[[0.0, 0.0, 40.0]]), t), Release::Outside);
|
||||
assert_eq!(g.take_outside(), vec![((5, 0, 0), 2)]);
|
||||
assert_eq!((g.sections().count(), g.bytes()), (1, 10));
|
||||
assert!(!g.holds((5, 0, 0)));
|
||||
// (to all of it again: nothing more is let go, and all of it waits again)
|
||||
assert_eq!(g.ask(&all(), t), Release::Outside);
|
||||
assert!(g.take_outside().is_empty());
|
||||
assert!(g.holds((5, 0, 0)));
|
||||
}
|
||||
#[test]
|
||||
fn what_waits_is_counted_and_capped() {
|
||||
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
|
||||
g.ask(&all(), t);
|
||||
assert_eq!(g.wait((0, 0, 0), 1, MAX_BYTES - 10), (None, false));
|
||||
// (a newer mesh of the same section replaces it, and the old one comes back to free)
|
||||
assert_eq!(g.wait((0, 0, 0), 2, MAX_BYTES - 20), (Some(1), false));
|
||||
assert_eq!(g.bytes(), MAX_BYTES - 20);
|
||||
assert_eq!(g.wait((1, 0, 0), 3, 20), (None, false));
|
||||
assert_eq!(g.wait((2, 0, 0), 4, 1), (None, true));
|
||||
assert_eq!(g.take((1, 0, 0)), Some(3));
|
||||
assert_eq!(g.take((1, 0, 0)), None);
|
||||
assert_eq!(g.bytes(), MAX_BYTES - 19);
|
||||
}
|
||||
#[test]
|
||||
fn columns_going_and_the_sections_cleared_drop_what_waits() {
|
||||
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
|
||||
g.ask(&all(), t);
|
||||
g.wait((0, 0, 0), 1, 10);
|
||||
g.wait((0, 5, 0), 2, 10);
|
||||
g.wait((3, 0, 0), 3, 10);
|
||||
let gone: FxHashSet<(i32, i32)> = [(0, 0)].into_iter().collect();
|
||||
let mut dropped = g.take_columns(&gone);
|
||||
dropped.sort_unstable();
|
||||
assert_eq!(dropped, vec![1, 2]);
|
||||
assert_eq!((g.sections().count(), g.bytes()), (1, 10));
|
||||
g.clear();
|
||||
assert!(g.is_empty() && g.bytes() == 0 && !g.holds((3, 0, 0)));
|
||||
// (cleared while held: spent until let go; with no hold, nothing to end)
|
||||
assert_eq!(g.ask(&all(), t), Release::None);
|
||||
assert!(!g.holds((3, 0, 0)));
|
||||
let mut none = Hold::<u32>::default();
|
||||
none.clear();
|
||||
none.ask(&all(), t);
|
||||
assert!(none.holds((3, 0, 0)));
|
||||
}
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
//! HUD coverage beside its unchanged colour and alpha, only on the float HDR lane.
|
||||
|
||||
pub(crate) const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
|
||||
|
||||
pub(crate) fn supported(
|
||||
attachments: u32,
|
||||
flags: wgpu::DownlevelFlags,
|
||||
format: wgpu::TextureFormatFeatures,
|
||||
) -> bool {
|
||||
attachments >= 2
|
||||
&& flags.contains(wgpu::DownlevelFlags::INDEPENDENT_BLEND)
|
||||
&& format
|
||||
.allowed_usages
|
||||
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
|
||||
&& format
|
||||
.flags
|
||||
.contains(wgpu::TextureFormatFeatureFlags::BLENDABLE)
|
||||
}
|
||||
|
||||
pub(crate) fn attachment(view: &wgpu::TextureView) -> wgpu::RenderPassColorAttachment<'_> {
|
||||
wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear(encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
|
||||
let mut color = attachment(view);
|
||||
color.ops.load = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("HUD coverage reset"),
|
||||
color_attachments: &[Some(color)],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) struct Pipeline {
|
||||
plain: wgpu::RenderPipeline,
|
||||
coverage: Option<wgpu::RenderPipeline>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
/// `None` when `enabled` and the shader can't be given a coverage output (a resource
|
||||
/// pack's, which the caller then doesn't use).
|
||||
pub(crate) fn new(
|
||||
device: &wgpu::Device,
|
||||
desc: &wgpu::RenderPipelineDescriptor<'_>,
|
||||
source: wgpu::ShaderSource<'_>,
|
||||
enabled: bool,
|
||||
) -> Option<Self> {
|
||||
let coverage = if enabled {
|
||||
Some(pipeline(device, desc, source)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Self {
|
||||
plain: device.create_render_pipeline(desc),
|
||||
coverage,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, coverage: bool) -> &wgpu::RenderPipeline {
|
||||
if coverage {
|
||||
self.coverage.as_ref().expect("HUD coverage pipeline")
|
||||
} else {
|
||||
&self.plain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the primary output and all its blend factors, including destination alpha. Wrap the
|
||||
/// final shader result, so pack text's alpha, early returns and discard all apply to coverage.
|
||||
/// `None` for a fragment shader without a colour output at location 0.
|
||||
pub(super) fn pipeline(
|
||||
device: &wgpu::Device,
|
||||
desc: &wgpu::RenderPipelineDescriptor<'_>,
|
||||
source: wgpu::ShaderSource<'_>,
|
||||
) -> Option<wgpu::RenderPipeline> {
|
||||
let mut desc = desc.clone();
|
||||
let fragment = desc.fragment.as_mut()?;
|
||||
let mut module = match source {
|
||||
wgpu::ShaderSource::Wgsl(s) => naga::front::wgsl::parse_str(&s).ok()?,
|
||||
wgpu::ShaderSource::Naga(m) => m.into_owned(),
|
||||
_ => return None,
|
||||
};
|
||||
let primary = fragment.targets.first()?.clone()?;
|
||||
let blend = primary.blend.unwrap_or(wgpu::BlendState::REPLACE).color;
|
||||
let additive = matches!(
|
||||
(blend.src_factor, blend.dst_factor),
|
||||
(
|
||||
wgpu::BlendFactor::One
|
||||
| wgpu::BlendFactor::Src
|
||||
| wgpu::BlendFactor::SrcAlpha
|
||||
| wgpu::BlendFactor::Zero,
|
||||
wgpu::BlendFactor::One
|
||||
)
|
||||
) && blend.operation == wgpu::BlendOperation::Add;
|
||||
wrap(&mut module, fragment.entry_point?, blend)?;
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: desc.label,
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
|
||||
});
|
||||
let over = wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrc,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
};
|
||||
let targets = [
|
||||
Some(primary),
|
||||
Some(wgpu::ColorTargetState {
|
||||
format: FORMAT,
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: over,
|
||||
alpha: over,
|
||||
}),
|
||||
write_mask: if additive {
|
||||
wgpu::ColorWrites::empty()
|
||||
} else {
|
||||
wgpu::ColorWrites::RED
|
||||
},
|
||||
}),
|
||||
];
|
||||
fragment.module = &shader;
|
||||
fragment.targets = &targets;
|
||||
Some(device.create_render_pipeline(&desc))
|
||||
}
|
||||
|
||||
fn wrap(module: &mut naga::Module, name: &str, blend: wgpu::BlendComponent) -> Option<()> {
|
||||
use naga::{Expression as E, Statement as S, TypeInner as T};
|
||||
use wgpu::BlendFactor as B;
|
||||
let span = naga::Span::UNDEFINED;
|
||||
let entry = module.entry_points.iter_mut().find(|e| e.name == name)?;
|
||||
// Checked before anything is taken apart: a resource pack's shader may have no colour.
|
||||
let result = entry.function.result.clone()?;
|
||||
if let T::Struct { members, .. } = &module.types[result.ty].inner {
|
||||
members
|
||||
.iter()
|
||||
.find(|m| matches!(m.binding, Some(naga::Binding::Location { location: 0, .. })))?;
|
||||
} else if !matches!(
|
||||
result.binding,
|
||||
Some(naga::Binding::Location { location: 0, .. })
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let mut original = std::mem::take(&mut entry.function);
|
||||
entry.function.arguments = original.arguments.clone();
|
||||
for arg in &mut original.arguments {
|
||||
arg.binding = None;
|
||||
}
|
||||
original.result.as_mut().unwrap().binding = None;
|
||||
original.name = Some("hud_color".into());
|
||||
let function = module.functions.append(original, span);
|
||||
let f = &mut entry.function;
|
||||
let args = (0..f.arguments.len())
|
||||
.map(|i| f.expressions.append(E::FunctionArgument(i as u32), span))
|
||||
.collect();
|
||||
let call = f.expressions.append(E::CallResult(function), span);
|
||||
f.body.push(
|
||||
S::Call {
|
||||
function,
|
||||
arguments: args,
|
||||
result: Some(call),
|
||||
},
|
||||
span,
|
||||
);
|
||||
let one = f
|
||||
.expressions
|
||||
.append(E::Literal(naga::Literal::F32(1.0)), span);
|
||||
let start = f.expressions.len();
|
||||
let (mut members, mut values, color, offset) = match &module.types[result.ty].inner {
|
||||
T::Struct {
|
||||
members,
|
||||
span: size,
|
||||
} => {
|
||||
// HUD pipelines have one target, so a pack's other colour outputs went nowhere;
|
||||
// left in, one at location 1 would collide with the coverage.
|
||||
let (members, values): (Vec<_>, Vec<_>) = members
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, m)| {
|
||||
!matches!(m.binding, Some(naga::Binding::Location { location, .. }) if location != 0)
|
||||
})
|
||||
.map(|(i, m)| {
|
||||
let value = f.expressions.append(
|
||||
E::AccessIndex {
|
||||
base: call,
|
||||
index: i as u32,
|
||||
},
|
||||
span,
|
||||
);
|
||||
(m.clone(), value)
|
||||
})
|
||||
.unzip();
|
||||
let color = members
|
||||
.iter()
|
||||
.position(|m| {
|
||||
matches!(m.binding, Some(naga::Binding::Location { location: 0, .. }))
|
||||
})
|
||||
.unwrap();
|
||||
let color = values[color];
|
||||
(members, values, color, *size)
|
||||
}
|
||||
_ => (
|
||||
vec![naga::StructMember {
|
||||
name: Some("color".into()),
|
||||
ty: result.ty,
|
||||
binding: result.binding,
|
||||
offset: 0,
|
||||
}],
|
||||
vec![call],
|
||||
call,
|
||||
16,
|
||||
),
|
||||
};
|
||||
let mut component = |index| {
|
||||
f.expressions
|
||||
.append(E::AccessIndex { base: color, index }, span)
|
||||
};
|
||||
let [r, g, b, a] = [component(0), component(1), component(2), component(3)];
|
||||
let mut math = |fun, arg, arg1| {
|
||||
f.expressions.append(
|
||||
E::Math {
|
||||
fun,
|
||||
arg,
|
||||
arg1: Some(arg1),
|
||||
arg2: None,
|
||||
arg3: None,
|
||||
},
|
||||
span,
|
||||
)
|
||||
};
|
||||
let low = math(naga::MathFunction::Min, r, g);
|
||||
let low = math(naga::MathFunction::Min, low, b);
|
||||
let high = math(naga::MathFunction::Max, r, g);
|
||||
let high = math(naga::MathFunction::Max, high, b);
|
||||
// A scalar cannot reproduce RGB-dependent replacement or inversion: surviving pixels
|
||||
// cover fully, so the later composite cannot leak world light through them.
|
||||
let c = if blend.operation != wgpu::BlendOperation::Add {
|
||||
one
|
||||
} else {
|
||||
match (blend.src_factor, blend.dst_factor) {
|
||||
(B::SrcAlpha | B::One, B::OneMinusSrcAlpha) => a,
|
||||
(B::Zero, B::OneMinusSrc) => high,
|
||||
// Multiplying retains src.rgb (or 1 - alpha + src.rgb) of the backdrop.
|
||||
// Use the greatest channel darkening, as for the credits vignette above.
|
||||
(B::Zero, B::Src)
|
||||
| (B::Dst, B::Zero)
|
||||
| (B::Dst, B::OneMinusSrcAlpha)
|
||||
| (B::Dst, B::Src) => {
|
||||
let top = if blend.dst_factor == B::OneMinusSrcAlpha {
|
||||
a
|
||||
} else {
|
||||
one
|
||||
};
|
||||
let factor = if blend.dst_factor == B::Src && blend.src_factor == B::Dst {
|
||||
f.expressions.append(
|
||||
E::Binary {
|
||||
op: naga::BinaryOperator::Add,
|
||||
left: low,
|
||||
right: low,
|
||||
},
|
||||
span,
|
||||
)
|
||||
} else {
|
||||
low
|
||||
};
|
||||
f.expressions.append(
|
||||
E::Binary {
|
||||
op: naga::BinaryOperator::Subtract,
|
||||
left: top,
|
||||
right: factor,
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
_ => one,
|
||||
}
|
||||
};
|
||||
let c = f.expressions.append(
|
||||
E::Math {
|
||||
fun: naga::MathFunction::Saturate,
|
||||
arg: c,
|
||||
arg1: None,
|
||||
arg2: None,
|
||||
arg3: None,
|
||||
},
|
||||
span,
|
||||
);
|
||||
let scalar = module.types.insert(
|
||||
naga::Type {
|
||||
name: None,
|
||||
inner: T::Scalar(naga::Scalar::F32),
|
||||
},
|
||||
span,
|
||||
);
|
||||
members.push(naga::StructMember {
|
||||
name: Some("coverage".into()),
|
||||
ty: scalar,
|
||||
binding: Some(naga::Binding::Location {
|
||||
location: 1,
|
||||
interpolation: None,
|
||||
sampling: None,
|
||||
blend_src: None,
|
||||
per_primitive: false,
|
||||
}),
|
||||
offset,
|
||||
});
|
||||
let ty = module.types.insert(
|
||||
naga::Type {
|
||||
name: Some("HudCoverage".into()),
|
||||
inner: T::Struct {
|
||||
members,
|
||||
span: (offset + 4).next_multiple_of(16),
|
||||
},
|
||||
},
|
||||
span,
|
||||
);
|
||||
values.push(c);
|
||||
let value = f.expressions.append(
|
||||
E::Compose {
|
||||
ty,
|
||||
components: values,
|
||||
},
|
||||
span,
|
||||
);
|
||||
f.result = Some(naga::FunctionResult { ty, binding: None });
|
||||
f.body.push(S::Emit(f.expressions.range_from(start)), span);
|
||||
f.body.push(S::Return { value: Some(value) }, span);
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn coverage_requires_two_attachments_and_independent_r8_blending() {
|
||||
let flags = wgpu::DownlevelFlags::INDEPENDENT_BLEND;
|
||||
let format = wgpu::TextureFormatFeatures {
|
||||
allowed_usages: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
flags: wgpu::TextureFormatFeatureFlags::BLENDABLE,
|
||||
};
|
||||
assert!(supported(2, flags, format));
|
||||
assert!(!supported(1, flags, format));
|
||||
assert!(!supported(2, wgpu::DownlevelFlags::empty(), format));
|
||||
assert!(!supported(
|
||||
2,
|
||||
flags,
|
||||
wgpu::TextureFormatFeatures {
|
||||
flags: wgpu::TextureFormatFeatureFlags::empty(),
|
||||
..format
|
||||
}
|
||||
));
|
||||
assert!(!supported(
|
||||
2,
|
||||
flags,
|
||||
wgpu::TextureFormatFeatures {
|
||||
allowed_usages: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
..format
|
||||
}
|
||||
));
|
||||
assert!(!supported(
|
||||
2,
|
||||
flags,
|
||||
wgpu::TextureFormatFeatures {
|
||||
allowed_usages: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
..format
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_coverage_builds_only_the_original_pipeline() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let Ok((device, _)) = pollster::block_on(adapter.request_device(&Default::default()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
"@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4(0.0); }".into(),
|
||||
),
|
||||
});
|
||||
let desc = wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: None,
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
};
|
||||
// The coverage source must not even be parsed outside the lane.
|
||||
let pipeline = Pipeline::new(
|
||||
&device,
|
||||
&desc,
|
||||
wgpu::ShaderSource::Wgsl("invalid".into()),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(pipeline.coverage.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_wraps_the_debug_crosshair_without_changing_world_entry_points() {
|
||||
let mut module = naga::front::wgsl::parse_str(&crate::gfx::terrain_shader()).unwrap();
|
||||
wrap(
|
||||
&mut module,
|
||||
"fs_debug_line",
|
||||
wgpu::BlendState::ALPHA_BLENDING.color,
|
||||
)
|
||||
.unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_replaces_a_colour_output_the_hud_has_no_target_for() {
|
||||
let mut module = naga::front::wgsl::parse_str(
|
||||
"struct Out { @location(0) color: vec4<f32>, @location(1) unused: vec4<f32> }
|
||||
@fragment fn fs() -> Out { return Out(vec4(0.5), vec4(1.0)); }",
|
||||
)
|
||||
.unwrap();
|
||||
wrap(&mut module, "fs", wgpu::BlendState::ALPHA_BLENDING.color).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap();
|
||||
let result = module.entry_points[0].function.result.as_ref().unwrap();
|
||||
let naga::TypeInner::Struct { members, .. } = &module.types[result.ty].inner else {
|
||||
panic!("the wrapped entry returns a struct");
|
||||
};
|
||||
let names: Vec<_> = members.iter().map(|m| m.name.as_deref().unwrap()).collect();
|
||||
assert_eq!(names, ["color", "coverage"]);
|
||||
}
|
||||
}
|
||||
@@ -98,24 +98,8 @@ 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 = super::entity_shader();
|
||||
let entity = crate::overlay::wgsl_constants() + include_str!("../entity.wgsl");
|
||||
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",
|
||||
@@ -140,29 +124,14 @@ fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
|
||||
(
|
||||
"improved transparency world",
|
||||
vec!["terrain.wgsl", "oit.wgsl", "oit_world.wgsl"],
|
||||
super::terrain_shader()
|
||||
include_str!("../terrain.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl"),
|
||||
),
|
||||
(
|
||||
"visibility buffer",
|
||||
vec!["terrain.wgsl", "visbuffer.wgsl"],
|
||||
super::terrain_shader() + include_str!("../visbuffer.wgsl"),
|
||||
),
|
||||
// A native mod's world warp (gfx/world_warp.rs): its copies with the plainest warp.
|
||||
(
|
||||
"world warp terrain",
|
||||
vec![
|
||||
"terrain.wgsl",
|
||||
"world_warp.wgsl",
|
||||
"world_warp_fragment.wgsl",
|
||||
],
|
||||
super::world_warp::terrain_shader(super::world_warp::IDENTITY).unwrap(),
|
||||
),
|
||||
(
|
||||
"world warp entities",
|
||||
vec!["entity.wgsl", "static_entity.wgsl", "world_warp.wgsl"],
|
||||
super::world_warp::entity_shader(super::world_warp::IDENTITY).unwrap(),
|
||||
include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency world border",
|
||||
|
||||
@@ -50,10 +50,8 @@ impl Pending {
|
||||
}
|
||||
}
|
||||
|
||||
/// Small, its meshes boxed: `draw_frame` walks every tile each frame (thousands at long far
|
||||
/// distances), and that walk is bound by the memory it reads.
|
||||
pub(super) struct Tile {
|
||||
pub mesh: Box<SectionGpu>,
|
||||
pub mesh: SectionGpu,
|
||||
bounds: Bounds,
|
||||
/// When it began to fade in; none while the rest of its job uploads.
|
||||
shown: Option<Instant>,
|
||||
@@ -64,7 +62,7 @@ pub(super) struct Tile {
|
||||
/// Its mesh before near chunks took some of its columns, drawn in its
|
||||
/// place until they have faded in (from the job's end, the instant), so
|
||||
/// no hole shows through them.
|
||||
previous: Option<Box<(SectionGpu, Option<Instant>)>>,
|
||||
previous: Option<(SectionGpu, Option<Instant>)>,
|
||||
/// Its slot in occlusion culling's results (`cull::TILE`), kept while
|
||||
/// meshes of the same tile replace one another.
|
||||
pub cull_slot: u32,
|
||||
@@ -157,12 +155,10 @@ pub(super) struct Visible {
|
||||
impl Tile {
|
||||
/// The mesh drawn this frame: the previous one while it stands in.
|
||||
pub fn drawn(&self) -> &SectionGpu {
|
||||
self.previous
|
||||
.as_deref()
|
||||
.map_or(&self.mesh, |(mesh, _)| mesh)
|
||||
self.previous.as_ref().map_or(&self.mesh, |(mesh, _)| mesh)
|
||||
}
|
||||
pub fn drawn_mut(&mut self) -> &mut SectionGpu {
|
||||
match self.previous.as_deref_mut() {
|
||||
match &mut self.previous {
|
||||
Some((mesh, _)) => mesh,
|
||||
None => &mut self.mesh,
|
||||
}
|
||||
@@ -181,7 +177,7 @@ impl Tile {
|
||||
}
|
||||
/// Both meshes it holds in the arena: its own and the stand-in, if any.
|
||||
pub fn meshes_mut(&mut self) -> impl Iterator<Item = &mut SectionGpu> {
|
||||
std::iter::once(&mut *self.mesh).chain(self.previous.as_deref_mut().map(|(mesh, _)| mesh))
|
||||
std::iter::once(&mut self.mesh).chain(self.previous.as_mut().map(|(mesh, _)| mesh))
|
||||
}
|
||||
pub fn visible(
|
||||
&self,
|
||||
@@ -218,12 +214,12 @@ impl Renderer {
|
||||
let (shown, previous) = match self.lods.remove(&key) {
|
||||
Some(old) if old.dying.is_none() => match old.previous {
|
||||
Some(previous) => {
|
||||
self.release_mesh(*old.mesh);
|
||||
self.release_mesh(old.mesh);
|
||||
(old.shown, Some(previous))
|
||||
}
|
||||
None if keep => (old.shown, Some(Box::new((*old.mesh, None)))),
|
||||
None if keep => (old.shown, Some((old.mesh, None))),
|
||||
None => {
|
||||
self.release_mesh(*old.mesh);
|
||||
self.release_mesh(old.mesh);
|
||||
(old.shown, None)
|
||||
}
|
||||
},
|
||||
@@ -244,7 +240,7 @@ impl Renderer {
|
||||
self.lods.insert(
|
||||
key,
|
||||
Tile {
|
||||
mesh: Box::new(mesh),
|
||||
mesh,
|
||||
bounds,
|
||||
shown,
|
||||
dying: None,
|
||||
@@ -255,7 +251,7 @@ impl Renderer {
|
||||
);
|
||||
}
|
||||
None => {
|
||||
if let Some((previous, _)) = previous.map(|p| *p) {
|
||||
if let Some((previous, _)) = previous {
|
||||
self.release_mesh(previous);
|
||||
}
|
||||
self.lod_cull_free.extend(old_slot);
|
||||
@@ -264,8 +260,8 @@ impl Renderer {
|
||||
}
|
||||
|
||||
fn release_tile(&mut self, tile: Tile) {
|
||||
self.release_mesh(*tile.mesh);
|
||||
if let Some((previous, _)) = tile.previous.map(|p| *p) {
|
||||
self.release_mesh(tile.mesh);
|
||||
if let Some((previous, _)) = tile.previous {
|
||||
self.release_mesh(previous);
|
||||
}
|
||||
}
|
||||
@@ -341,10 +337,8 @@ impl Renderer {
|
||||
let stood_in: Vec<_> = self
|
||||
.lods
|
||||
.values_mut()
|
||||
.filter_map(|tile| match tile.previous.as_deref() {
|
||||
Some((_, Some(since))) if start - *since >= FADE => {
|
||||
tile.previous.take().map(|p| *p)
|
||||
}
|
||||
.filter_map(|tile| match tile.previous {
|
||||
Some((_, Some(since))) if start - since >= FADE => tile.previous.take(),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -446,7 +440,7 @@ impl Renderer {
|
||||
if tile.shown.is_none() {
|
||||
tile.shown = Some(if over_replaced(key) { start } else { whole });
|
||||
}
|
||||
if let Some((_, since)) = tile.previous.as_deref_mut() {
|
||||
if let Some((_, since)) = &mut tile.previous {
|
||||
since.get_or_insert(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,23 +49,9 @@ struct HorizonMaterialOut {
|
||||
@location(14) @interpolate(flat) normal: vec3<f32>,
|
||||
}
|
||||
fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
|
||||
light: vec2<u32>, origin: vec4<f32>) -> HorizonMaterialOut {
|
||||
let pos = horizon_material_displace(position + origin.xyz, uv, layer, color, light);
|
||||
var out = horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
|
||||
// The section's fade-in (`RenderSection.getVisibility`), as how far it is from whole, in the
|
||||
// high 16 bits of `hdr_code`: no varying of its own, and 0 (whole) where nothing fades. It is
|
||||
// the same for the whole section, so the flat varying loses nothing.
|
||||
let instance = horizon_terrain_instance_visibility(origin.w);
|
||||
out.hdr_code |= u32(round((1.0 - instance) * 65535.0)) << 16u;
|
||||
return out;
|
||||
}
|
||||
// terrain.wgsl's fade-in (terrain.vsh's chunkVisibility): whole within 16 blocks, the section's
|
||||
// own visibility from 32 out, toward the fog colour, alpha kept. The distance blend runs here on
|
||||
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle.
|
||||
fn horizon_material_faded(in: HorizonMaterialOut, color: vec4<f32>) -> vec4<f32> {
|
||||
let instance = 1.0 - f32(in.hdr_code >> 16u) / 65535.0;
|
||||
let visibility = mix(1.0, instance, clamp((in.spherical - 16.0) / 16.0, 0.0, 1.0));
|
||||
return vec4(mix(horizon_terrain_globals.fog_color.rgb, color.rgb, visibility), color.a);
|
||||
light: vec2<u32>, origin: vec3<f32>) -> HorizonMaterialOut {
|
||||
let pos = horizon_material_displace(position + origin, uv, layer, color, light);
|
||||
return horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
|
||||
}
|
||||
// `light`: sky, block, 16 per level.
|
||||
fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
|
||||
@@ -108,7 +94,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
|
||||
@location(5) origin: vec4<f32>) -> HorizonMaterialOut {
|
||||
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
|
||||
color, light, origin);
|
||||
color, light, origin.xyz);
|
||||
}
|
||||
@vertex fn horizon_material_vs_flat(
|
||||
@builtin(vertex_index) vertex: u32,
|
||||
@@ -117,7 +103,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
|
||||
) -> HorizonMaterialOut {
|
||||
let origin = horizon_terrain_slot_origins[horizon_terrain_block_slots[vertex >> 8u]];
|
||||
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
|
||||
color, light, origin);
|
||||
color, light, origin.xyz);
|
||||
}
|
||||
// The prelude's `block_normal_map` and `block_specular_map`: the resource packs' LabPBR maps
|
||||
// in the pack's frame group (3), one array each, a blank 1x1 without them.
|
||||
@@ -195,11 +181,9 @@ struct HorizonMaterialSolid {
|
||||
// What `fs_solid` leaves in the frame's alpha for HDR output, from the sprite before the pack's
|
||||
// material: the pack decides the colour, Horizon still knows which pixels are light sources.
|
||||
fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
|
||||
if horizon_terrain_HDR_WRITE_LIGHT { return 1.0; }
|
||||
let code = in.hdr_code & 15u;
|
||||
if code > 0u {
|
||||
let whole = code > 4u;
|
||||
let tier = select(code, code - 4u, whole);
|
||||
if in.hdr_code > 0u {
|
||||
let whole = in.hdr_code > 4u;
|
||||
let tier = select(in.hdr_code, in.hdr_code - 4u, whole);
|
||||
let l = in.layer & 0x7fffu;
|
||||
let t = textureLoad(horizon_terrain_emission_thresholds,
|
||||
vec2(i32(l % horizon_terrain_SPRITE_SPLIT), i32(l / horizon_terrain_SPRITE_SPLIT)), 0);
|
||||
@@ -218,7 +202,7 @@ fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
|
||||
return horizon_material_solid(in);
|
||||
}
|
||||
fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
let color = horizon_material_faded(in, horizon_material_color(in, false));
|
||||
let color = horizon_material_color(in, false);
|
||||
let texel = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
|
||||
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
|
||||
if color.a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
|
||||
@@ -232,7 +216,7 @@ fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_material_fs_translucent(in: HorizonMaterialOut) -> @location(0) vec4<f32> {
|
||||
let color = horizon_material_faded(in, horizon_material_color(in, true));
|
||||
let color = horizon_material_color(in, true);
|
||||
if color.a < 0.01 { discard; }
|
||||
return horizon_material_fogged(color, in.spherical, in.cylindrical);
|
||||
}
|
||||
@@ -310,8 +294,7 @@ pub struct NativeMaterial {
|
||||
/// every name it declares prefixed.
|
||||
fn vanilla_terrain() -> String {
|
||||
prefixed(
|
||||
&(include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
|
||||
PREFIX,
|
||||
)
|
||||
}
|
||||
@@ -433,10 +416,6 @@ impl NativeMaterial {
|
||||
];
|
||||
constants.extend(sprite_names.into_iter().zip(shape.sprites));
|
||||
constants.push(("horizon_material_fog", f64::from(u8::from(fog))));
|
||||
constants.push((
|
||||
"horizon_terrain_HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(shape.format == super::LINEAR_FRAME)),
|
||||
));
|
||||
constants.push((
|
||||
"horizon_displace_distance",
|
||||
displaced.map_or(0.0, |d| f64::from(d.distance)),
|
||||
@@ -585,39 +564,6 @@ impl NativeMaterial {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
|
||||
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
|
||||
let manifest = format!(
|
||||
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
|
||||
);
|
||||
let material = "fn material(input: MaterialInput) -> Surface { return Surface(vec4<f32>(1.4, 0.3, 0.1, 0.7)); }\nfn displacement(input: DisplacementInput) -> f32 { return 0.0; }\n";
|
||||
let files = [(MANIFEST, manifest.as_str()), ("material.wgsl", material)]
|
||||
.map(|(n, text)| (n.to_owned(), text.as_bytes().to_vec()))
|
||||
.into();
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let stage = report
|
||||
.stages
|
||||
.iter()
|
||||
.find(|s| s.kind == StageKind::Material)
|
||||
.unwrap();
|
||||
shader_source(&stage.source, &[], &[], (false, true), api)
|
||||
+ r#"
|
||||
override TEST_TIER: u32 = 0u;
|
||||
override TEST_OVERLAY: u32 = 0u;
|
||||
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> HorizonMaterialOut {
|
||||
var o: HorizonMaterialOut;
|
||||
o.clip = vec4(vec2(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
|
||||
o.uv = vec2(0.5);
|
||||
o.tint = vec4(1.0);
|
||||
o.lightmap = vec3(1.0);
|
||||
o.hdr_code = TEST_TIER;
|
||||
o.base = vec3(1.0);
|
||||
return o;
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -636,25 +582,6 @@ mod tests {
|
||||
report.stages.remove(0)
|
||||
}
|
||||
|
||||
/// The fade-in packed above the HDR code: a section a quarter visible (`section_instance`
|
||||
/// with step 1: mantissa `1 - visibility` = 0.75) keeps its visibility through the code, and
|
||||
/// a fragment 24 blocks away, half-way into the 16-block blend, is 1 - (1 - 0.25) / 2 = 0.625
|
||||
/// visible; the code's low nibble survives.
|
||||
#[test]
|
||||
fn the_fade_in_rides_the_hdr_code() {
|
||||
let w = f32::from_bits(1.0f32.to_bits() | (0.75 * 8_388_608.0) as u32);
|
||||
// `horizon_material_vertex`.
|
||||
let instance = 1.0 - (w.to_bits() & 0x7fffff) as f32 / 8_388_608.0;
|
||||
let code = 7u32 | (((1.0 - instance) * 65535.0).round() as u32) << 16;
|
||||
// `horizon_material_faded` and `horizon_material_mark`.
|
||||
let unpacked = 1.0 - (code >> 16) as f32 / 65535.0;
|
||||
assert!((unpacked - 0.25).abs() < 1.0 / 65535.0);
|
||||
let blend = ((24.0f32 - 16.0) / 16.0).clamp(0.0, 1.0);
|
||||
let visibility = 1.0 + (unpacked - 1.0) * blend;
|
||||
assert!((visibility - 0.625).abs() < 1.0 / 65535.0);
|
||||
assert_eq!(code & 15, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_terrain_takes_a_prefix_and_validates_with_a_pack() {
|
||||
let terrain = vanilla_terrain();
|
||||
|
||||
@@ -90,27 +90,6 @@ impl NativePack {
|
||||
report: Report,
|
||||
constants: &[(String, f64)],
|
||||
lights: super::block_light::LightTable,
|
||||
) -> Result<(Self, Vec<String>), String> {
|
||||
// A backend may refuse a layout or pipeline the pack asks for (a stage's bindings
|
||||
// past its sampled-texture limit, say, as OpenGL's 16 was before #10095): the pack
|
||||
// is refused with that error instead of a validation panic in the renderer.
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let built = Self::build(device, queue, terrain, report, constants, lights);
|
||||
let refused = pollster::block_on(scope.pop());
|
||||
match (built, refused) {
|
||||
(Ok(built), None) => Ok(built),
|
||||
(Ok(_), Some(e)) => Err(e.to_string()),
|
||||
(Err(e), _) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn build(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
terrain: &TerrainShape<'_>,
|
||||
report: Report,
|
||||
constants: &[(String, f64)],
|
||||
lights: super::block_light::LightTable,
|
||||
) -> Result<(Self, Vec<String>), String> {
|
||||
let (globals, format) = (terrain.globals, terrain.format);
|
||||
let textures = super::native_post::pack_textures(device, queue, &report);
|
||||
@@ -299,90 +278,9 @@ impl NativePack {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{NativePack, TerrainShape, chosen_shadow};
|
||||
use super::chosen_shadow;
|
||||
use horizon_shader_api::manifest::{OptionKind, PackOption, Shadow};
|
||||
|
||||
/// A device with OpenGL's minimum 16 sampled textures per stage refuses a pack whose
|
||||
/// sky adds the terrain's 10 to the frame's 9: that is the pack's error to report, not
|
||||
/// a validation panic (#10095).
|
||||
#[test]
|
||||
fn a_pack_the_backend_refuses_is_an_error_not_a_panic() {
|
||||
use horizon_shader_api::{MANIFEST, Pack, check};
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let mut limits = adapter.limits();
|
||||
limits.max_sampled_textures_per_shader_stage = 16;
|
||||
let Ok((device, queue)) =
|
||||
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: limits,
|
||||
..Default::default()
|
||||
}))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The terrain's group 0: its uniform and the 10 sampled textures (_terrain.wgsl_'s
|
||||
// 8 sprite arrays, the lightmap and the emission thresholds).
|
||||
let entry = |binding, ty| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty,
|
||||
count: None,
|
||||
};
|
||||
let mut entries = vec![entry(
|
||||
0,
|
||||
wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
)];
|
||||
entries.extend((1..=10).map(|binding| {
|
||||
entry(
|
||||
binding,
|
||||
wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
)
|
||||
}));
|
||||
let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &entries,
|
||||
});
|
||||
let empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &[],
|
||||
});
|
||||
let terrain = TerrainShape {
|
||||
globals: &globals,
|
||||
flat: None,
|
||||
sprites: [1.0, 2048.0],
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
sprite_tags: &[],
|
||||
sprite_materials: &[],
|
||||
entities: &empty,
|
||||
weather_groups: [&empty, &empty],
|
||||
pbr: None,
|
||||
};
|
||||
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
|
||||
[sky]\nfile = \"sky.wgsl\"\n";
|
||||
let files = [
|
||||
(MANIFEST, manifest),
|
||||
(
|
||||
"sky.wgsl",
|
||||
"fn sky(input: SkyInput) -> vec4<f32> { return input.color; }\n",
|
||||
),
|
||||
]
|
||||
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
|
||||
.into();
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let built = NativePack::new(&device, &queue, &terrain, report, &[], Default::default());
|
||||
assert!(built.is_err(), "the pack should be refused");
|
||||
}
|
||||
|
||||
fn option(id: &str, kind: OptionKind) -> PackOption {
|
||||
PackOption {
|
||||
id: id.into(),
|
||||
|
||||
@@ -7,16 +7,14 @@ use super::Vertex;
|
||||
use super::pack_draw::EntitySpan;
|
||||
use glam::{DVec3, Mat4};
|
||||
use std::sync::{
|
||||
Arc, OnceLock,
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
mod generated {
|
||||
include!(concat!(env!("OUT_DIR"), "/native_render_generated.rs"));
|
||||
}
|
||||
pub use generated::{
|
||||
HandDrawnEvent, RenderHooks, RenderNeedsEvent, WorldDrawnEvent, WorldWarpEvent,
|
||||
};
|
||||
pub use generated::{HandDrawnEvent, RenderHooks, RenderNeedsEvent, WorldDrawnEvent};
|
||||
pub use wgpu;
|
||||
|
||||
static RENDERERS: OnceLock<Box<[&'static dyn RenderHooks]>> = OnceLock::new();
|
||||
@@ -75,15 +73,10 @@ 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,
|
||||
/// `(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).
|
||||
/// 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).
|
||||
pub motion: &'a [(u32, u64)],
|
||||
/// The shared atlas entity UVs point into.
|
||||
pub atlas: Option<&'a wgpu::Texture>,
|
||||
@@ -180,38 +173,6 @@ pub fn render_needs() -> RenderNeeds {
|
||||
needs
|
||||
}
|
||||
|
||||
/// A world warp ([`native_world_warp`]): WGSL that every terrain vertex (far terrain's tiles too)
|
||||
/// and entity vertex goes through, so a mod can bend the drawn world itself: its real geometry,
|
||||
/// light and depth, seen from any side. docs/native-mods.md, "World warps", has the WGSL contract.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct WorldWarp {
|
||||
/// WGSL with `fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped`, and optionally,
|
||||
/// after a line `// [fragments]`, `fn world_fragment(f: Fragment) -> Painted` for every
|
||||
/// pixel of the terrain. `None`: no warp.
|
||||
pub source: Option<Arc<str>>,
|
||||
/// Whether this frame draws with the warp. A source given while this is false is still
|
||||
/// built, so a mod can pay for its pipelines before the frame that needs them.
|
||||
pub on: bool,
|
||||
/// What the warp reads as `warp.params`: four `vec4<f32>`.
|
||||
pub params: [f32; 16],
|
||||
}
|
||||
|
||||
/// Asked once a frame, before the world is drawn (not on the title screen's panorama): set
|
||||
/// [`WorldWarp`] to have the frame drawn through your warp. Mods are asked in order and one warp
|
||||
/// draws: the last one set. Keep it cheap: compare and reuse your `Arc<str>`.
|
||||
#[inline(never)]
|
||||
pub fn native_world_warp(warp: &mut WorldWarp) {
|
||||
generated::dispatch_world_warp(RENDERERS.get().map_or(&[], |renderers| renderers), warp);
|
||||
}
|
||||
|
||||
/// This frame's world warp ([`native_world_warp`]).
|
||||
pub fn world_warp() -> WorldWarp {
|
||||
let mut warp = WorldWarp::default();
|
||||
native_world_warp(&mut warp);
|
||||
warp
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -412,33 +373,6 @@ mod tests {
|
||||
assert_eq!(texels, expected);
|
||||
}
|
||||
|
||||
struct Bend(&'static str);
|
||||
impl RenderHooks for Bend {
|
||||
fn world_warp(&self, event: &mut WorldWarpEvent<'_>) {
|
||||
event.warp.source = Some(self.0.into());
|
||||
event.warp.on = true;
|
||||
event.warp.params[0] += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_last_mod_to_set_a_warp_draws() {
|
||||
let mut warp = WorldWarp::default();
|
||||
generated::dispatch_world_warp(&[&Depth, &Bend("a"), &Bend("b")], &mut warp);
|
||||
assert_eq!(warp.source.as_deref(), Some("b"));
|
||||
assert!(warp.on);
|
||||
// each saw what the one before left
|
||||
assert_eq!(warp.params[0], 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_mods_there_is_no_warp() {
|
||||
let mut warp = WorldWarp::default();
|
||||
generated::dispatch_world_warp(&[], &mut warp);
|
||||
assert_eq!(warp, WorldWarp::default());
|
||||
assert!(warp.source.is_none() && !warp.on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_accumulate_in_mod_order() {
|
||||
let mut needs = RenderNeeds::default();
|
||||
|
||||
@@ -512,8 +512,7 @@ fn shader_source(
|
||||
format!(
|
||||
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{}{displace}{ENTRIES}",
|
||||
prefixed(
|
||||
&(include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
|
||||
PREFIX
|
||||
),
|
||||
super::native_post::line_helpers(api)
|
||||
|
||||
@@ -30,7 +30,6 @@ 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(
|
||||
(super::terrain_shader()
|
||||
(include_str!("../terrain.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl"))
|
||||
.into(),
|
||||
@@ -253,14 +253,7 @@ impl Oit {
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let constants = [
|
||||
("SPRITE_ARRAYS", sprites[0]),
|
||||
("SPRITE_SPLIT", sprites[1]),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
),
|
||||
];
|
||||
let constants = [("SPRITE_ARRAYS", sprites[0]), ("SPRITE_SPLIT", sprites[1])];
|
||||
let add = wgpu::BlendOperation::Add;
|
||||
let depth = |write| {
|
||||
Some(wgpu::DepthStencilState {
|
||||
@@ -471,10 +464,7 @@ impl Oit {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &cloud_shader,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants[2..],
|
||||
..Default::default()
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
targets: &stage_targets(stage),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
@@ -498,14 +488,7 @@ impl Oit {
|
||||
Stage::Accumulate,
|
||||
),
|
||||
];
|
||||
let features = Features::new(
|
||||
device,
|
||||
&layouts,
|
||||
features[0],
|
||||
features[1],
|
||||
features[2],
|
||||
format,
|
||||
);
|
||||
let features = Features::new(device, &layouts, features[0], features[1], features[2]);
|
||||
Self {
|
||||
uniform,
|
||||
layouts,
|
||||
@@ -929,7 +912,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_terrain_shader_validates() {
|
||||
validate(
|
||||
&(crate::gfx::terrain_shader()
|
||||
&(include_str!("../terrain.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl")),
|
||||
&[
|
||||
|
||||
@@ -55,7 +55,6 @@ 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"),
|
||||
@@ -114,15 +113,6 @@ 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),
|
||||
@@ -146,15 +136,8 @@ impl Features {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module,
|
||||
entry_point: Some(&fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: if kind.name == "border" {
|
||||
&[]
|
||||
} else {
|
||||
&constants
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
entry_point: Some(&format!("fs_oit_{}_{suffix}", kind.name)),
|
||||
compilation_options: Default::default(),
|
||||
targets: &stage_targets(stage),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
|
||||
@@ -630,12 +630,13 @@ impl PackAtlas {
|
||||
"layout(set = 1, binding = 0) readonly buffer HzBlockSlots { uint hz_block_slots[]; };\n\
|
||||
layout(set = 1, binding = 1) readonly buffer HzSlotOrigins { vec4 hz_slot_origins[]; };\n",
|
||||
);
|
||||
// Source metadata follows each quad in its arena page. The immutable table
|
||||
// holds sprite rectangles and the current pack's ids of source states.
|
||||
// Of each sprite layer, `mc_Entity` (id, then 1 for fluids or 0) and
|
||||
// `at_midBlock.w` (its block's light), the game's table; and the arena page's
|
||||
// vertices (`TerrainVertex`, six words each).
|
||||
declarations.push_str(
|
||||
"layout(set = 3, binding = 0) readonly buffer HzQuadSources { uvec2 hz_sources[]; };\n\
|
||||
"layout(set = 3, binding = 0) readonly buffer HzBlockIds { int hz_block_ids[]; };\n\
|
||||
layout(set = 3, binding = 1) readonly buffer HzVertices { uint hz_vertices[]; };\n\
|
||||
layout(set = 3, binding = 2) readonly buffer HzTerrainTables { int hz_tables[]; };\n",
|
||||
layout(set = 3, binding = 2) readonly buffer HzSpriteRects { uvec2 hz_sprite_rects[]; };\n",
|
||||
);
|
||||
"hz_slot_origins[hz_block_slots[uint(gl_VertexIndex) >> 8u]]"
|
||||
} else {
|
||||
@@ -648,8 +649,7 @@ impl PackAtlas {
|
||||
let columns = self.columns;
|
||||
let (cell, side, scale) = if flat {
|
||||
(
|
||||
"uvec2 hz_rect = uvec2(0u, 1u);\n \
|
||||
if (2u * hz_layer + 1u < uint(hz_tables[0])) hz_rect = uvec2(hz_tables[2u + 2u * hz_layer], hz_tables[3u + 2u * hz_layer]);\n \
|
||||
"uvec2 hz_rect = hz_sprite_rects[hz_layer];\n \
|
||||
hz_cell = vec2(float(hz_rect.x & 65535u), float(hz_rect.x >> 16u)) / float(hz_rect.y);\n "
|
||||
.to_owned(),
|
||||
"float(hz_rect.y)",
|
||||
@@ -701,27 +701,20 @@ impl PackAtlas {
|
||||
let mut body = body;
|
||||
if flat {
|
||||
body.push_str(FACE);
|
||||
body.push_str(
|
||||
" uint hz_quad = uint(gl_VertexIndex) / 4u;\n \
|
||||
uvec2 hz_source = uvec2(0u);\n \
|
||||
if (hz_quad < uint(hz_sources.length())) hz_source = hz_sources[hz_quad];\n \
|
||||
bool hz_known_source = (hz_source.y & 8192u) != 0u;\n \
|
||||
int hz_material = -1;\n \
|
||||
if (hz_known_source && hz_source.x < uint(hz_tables[1])) hz_material = hz_tables[2u + uint(hz_tables[0]) + 2u * hz_source.x + ((hz_source.y >> 12u) & 1u)];\n",
|
||||
);
|
||||
}
|
||||
declarations.push_str(
|
||||
"vec2 hz_cell; vec2 hz_scale; vec2 hz_mid; float hz_uv_scale; vec4 hz_tangent;\n",
|
||||
);
|
||||
let entity = if flat {
|
||||
"vec4(float(hz_material), hz_known_source ? float((hz_source.y >> 12u) & 1u) : 0.0, 0.0, 0.0)"
|
||||
"vec4(float(hz_block_ids[3u * hz_layer]), float(hz_block_ids[3u * hz_layer + 1u]), 0.0, 0.0)"
|
||||
} else {
|
||||
"vec4(-1.0, 0.0, 0.0, 0.0)"
|
||||
};
|
||||
// The mesher's owning cell, even for inset, offset, multipart or extended faces;
|
||||
// w is the source's emitted light, independent of sprite and smooth light.
|
||||
// The block a face belongs to: half a block behind its centre (the quad's diagonal);
|
||||
// w, the light it gives (Iris's `BLOCK_EMISSION_ATTRIBUTE`, 0 to 15).
|
||||
let mid_block = if flat {
|
||||
"(hz_known_source ? vec4((vec3(float(hz_source.y & 15u), float((hz_source.y >> 4u) & 15u), float((hz_source.y >> 8u) & 15u)) + 0.5 - hz_v_pos) * 64.0, float((hz_source.y >> 14u) & 15u)) : vec4(0.0))"
|
||||
"vec4((floor((hz_p0 + hz_p2) * 0.5 - 0.5 * hz_Normal) + 0.5 - hz_v_pos) * 64.0, \
|
||||
float(hz_block_ids[3u * hz_layer + 2u]))"
|
||||
} else {
|
||||
"vec4(0.0)"
|
||||
};
|
||||
@@ -735,13 +728,6 @@ impl PackAtlas {
|
||||
),
|
||||
("mc_Entity", entity),
|
||||
("at_midBlock", mid_block),
|
||||
// Horizon's own: the face's HDR tier (`mesher::emitting`'s code in the colour's low
|
||||
// alpha nibble): 0 for none, 1 to 4 for low to very bright, plus 4 when the whole
|
||||
// face shines (`mesher::WHOLE`).
|
||||
(
|
||||
"horizonTier",
|
||||
"float(15u - (uint(round(hz_v_color.a * 255.0)) & 15u))",
|
||||
),
|
||||
(
|
||||
"mc_midTexCoord",
|
||||
"vec4((hz_cell + hz_mid) * hz_scale, 0.0, 1.0)",
|
||||
@@ -764,7 +750,6 @@ impl PackAtlas {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1146,3 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pack_atlas/source_ownership_test.rs"]
|
||||
mod source_ownership_test;
|
||||
@@ -49,12 +49,12 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
|
||||
std::env::temp_dir().join(format!("horizon-terrain-fade-{}", std::process::id())),
|
||||
);
|
||||
std::fs::create_dir_all(folder.0.join("shaders")).unwrap();
|
||||
// The initializer, varying and integer output are independent probe code. The program reads
|
||||
// `mc_chunkFade` unguarded: Horizon defines no `IRIS_FEATURE_*` (spec OQ-2).
|
||||
// The guard, initializer, varying and integer output are independent probe code.
|
||||
let vertex = "#version 330\nout vec4 observation;\n\
|
||||
float first = mc_chunkFade;\n\
|
||||
void main() {\n\
|
||||
#ifdef IRIS_FEATURE_FADE_VARIABLE\nfloat first = mc_chunkFade;\n#endif\n\
|
||||
void main() {\n#ifdef IRIS_FEATURE_FADE_VARIABLE\n\
|
||||
observation = vec4(mc_chunkFade, first, gl_Color.a, gl_MultiTexCoord0.x);\n\
|
||||
#else\nobservation = vec4(1.0, 1.0, gl_Color.a, gl_MultiTexCoord0.x);\n#endif\n\
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n}";
|
||||
let fragment = "#version 330\nin vec4 observation;\nlayout(location = 0) out uvec4 pixel;\n\
|
||||
void main() { pixel = floatBitsToUint(observation); }";
|
||||
@@ -157,10 +157,8 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
|
||||
flat,
|
||||
geometry,
|
||||
origin,
|
||||
0x80ffffff,
|
||||
);
|
||||
// The fade reaches the program whether the pack requested it or not.
|
||||
let fade = expected;
|
||||
let fade = if requested { expected } else { 1.0 };
|
||||
let expected = [
|
||||
fade,
|
||||
if geometry { 0.8125 } else { fade },
|
||||
@@ -184,96 +182,6 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
|
||||
assert_eq!(count, 98);
|
||||
}
|
||||
|
||||
/// `horizonTier`: the HDR tier the mesher marks a light source's faces with (`mesher::emitting`),
|
||||
/// read by a pack's terrain program, flat or instanced.
|
||||
#[test]
|
||||
fn terrain_tier_reaches_pack_pixels() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = match pollster::block_on(instance.request_adapter(&Default::default())) {
|
||||
Ok(adapter) => adapter,
|
||||
Err(error) => {
|
||||
eprintln!("SKIP terrain tier GPU control: no adapter: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let array = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 2,
|
||||
height: 2,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let atlas = PackAtlas::new(&device, &mut encoder, &[array], 1, 1, 2).unwrap();
|
||||
queue.submit([encoder.finish()]);
|
||||
let folder = ProbePack(
|
||||
std::env::temp_dir().join(format!("horizon-terrain-tier-{}", std::process::id())),
|
||||
);
|
||||
std::fs::create_dir_all(folder.0.join("shaders")).unwrap();
|
||||
std::fs::write(
|
||||
folder.0.join("shaders/gbuffers_terrain.vsh"),
|
||||
"#version 330\nin float horizonTier;\nout vec4 observation;\n\
|
||||
void main() {\nobservation = vec4(horizonTier, 0.0, 0.0, 0.0);\n\
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n}",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
folder.0.join("shaders/gbuffers_terrain.fsh"),
|
||||
"#version 330\nin vec4 observation;\nlayout(location = 0) out uvec4 pixel;\n\
|
||||
void main() { pixel = floatBitsToUint(observation); }",
|
||||
)
|
||||
.unwrap();
|
||||
let pack = horizon_shaders::Pack::open(&folder.0).unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
use crate::mesher::{BRIGHT, LOW, VERY_BRIGHT, WHOLE, emitting};
|
||||
// No light, low, very bright, and bright all over (4 more).
|
||||
let cases = [
|
||||
(0, 0.0),
|
||||
(LOW, 1.0),
|
||||
(VERY_BRIGHT, 4.0),
|
||||
(BRIGHT | WHOLE, 7.0),
|
||||
];
|
||||
for flat in [false, true] {
|
||||
let post = horizon_shaders::load_post(
|
||||
&pack,
|
||||
"",
|
||||
&properties,
|
||||
¯os,
|
||||
&horizon_shaders::GameInputs {
|
||||
terrain: Some(atlas.terrain_inputs(flat)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let program = post
|
||||
.terrain
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| panic!("{:?}", post.notes));
|
||||
for (tier, expected) in cases {
|
||||
let origin = section_instance([0.0; 3], 0, 1.0);
|
||||
let pixel = sample(
|
||||
&device,
|
||||
&queue,
|
||||
&atlas,
|
||||
program.program.as_ref().unwrap(),
|
||||
flat,
|
||||
false,
|
||||
origin,
|
||||
emitting(0xffff_ffff, tier),
|
||||
);
|
||||
assert_eq!(pixel[0], expected, "tier {tier} flat={flat}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sample(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
@@ -282,7 +190,6 @@ fn sample(
|
||||
flat: bool,
|
||||
geometry: bool,
|
||||
origin: [f32; 4],
|
||||
color: u32,
|
||||
) -> [f32; 4] {
|
||||
let buffer = |bytes: &[u8], usage| {
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
@@ -302,18 +209,15 @@ fn sample(
|
||||
let slots = buffer(bytemuck::bytes_of(&0u32), wgpu::BufferUsages::STORAGE);
|
||||
let vertices: Vec<_> = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]]
|
||||
.into_iter()
|
||||
.map(|pos| TerrainVertex::new(pos, [0.25, 0.75], 0, color, 0))
|
||||
.map(|pos| TerrainVertex::new(pos, [0.25, 0.75], 0, 0x80ffffff, 0))
|
||||
.collect();
|
||||
let data = buffer(
|
||||
bytemuck::cast_slice(&vertices),
|
||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
let ids = buffer(bytemuck::bytes_of(&[0u32; 2]), wgpu::BufferUsages::STORAGE);
|
||||
let ids = buffer(bytemuck::bytes_of(&[0i32; 3]), wgpu::BufferUsages::STORAGE);
|
||||
let rects = buffer(
|
||||
bytemuck::cast_slice(&super::super::pack_post::terrain_tables(
|
||||
&[],
|
||||
&atlas.rects_table(),
|
||||
)),
|
||||
bytemuck::cast_slice(&atlas.rects_table()),
|
||||
wgpu::BufferUsages::STORAGE,
|
||||
);
|
||||
let binding = |n, storage| wgpu::BindGroupLayoutEntry {
|
||||
|
||||
@@ -406,16 +406,13 @@ fn sample_probe(
|
||||
bytemuck::bytes_of(&[0., 0., 0., scale]),
|
||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
let ids = buffer(bytemuck::bytes_of(&[0u32; 2]), wgpu::BufferUsages::STORAGE);
|
||||
let ids = buffer(bytemuck::bytes_of(&[0i32; 15]), wgpu::BufferUsages::STORAGE);
|
||||
let data = buffer(
|
||||
bytemuck::cast_slice(vertices),
|
||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
let rects = buffer(
|
||||
bytemuck::cast_slice(&super::super::pack_post::terrain_tables(
|
||||
&[],
|
||||
&atlas.rects_table(),
|
||||
)),
|
||||
bytemuck::cast_slice(&atlas.rects_table()),
|
||||
wgpu::BufferUsages::STORAGE,
|
||||
);
|
||||
let bindings = [
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
//! Original numeric controls for the production terrain attribute expressions.
|
||||
//! A compute invocation stands for a terrain vertex; it runs the actual input body.
|
||||
use super::*;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
fn dispatch(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
pipeline: &wgpu::ComputePipeline,
|
||||
groups: &[wgpu::BindGroup],
|
||||
result: &wgpu::Buffer,
|
||||
) -> Vec<[f32; 4]> {
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: result.size(),
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let mut pass = encoder.begin_compute_pass(&Default::default());
|
||||
pass.set_pipeline(pipeline);
|
||||
for (i, g) in groups.iter().enumerate() {
|
||||
pass.set_bind_group(i as u32, g, &[]);
|
||||
}
|
||||
pass.dispatch_workgroups((result.size() / 32) as u32, 1, 1);
|
||||
}
|
||||
encoder.copy_buffer_to_buffer(result, 0, &read, 0, result.size());
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
bytemuck::cast_slice(&read.slice(..).get_mapped_range().unwrap()).to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "offscreen GPU control; run explicitly with an available adapter"]
|
||||
fn terrain_source_ownership_gpu() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default()))
|
||||
.expect("executed GPU control requires an adapter");
|
||||
eprintln!("TERRAIN_SOURCE_ADAPTER {:?}", adapter.get_info());
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let texture = 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::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let atlas = PackAtlas::new(&device, &mut encoder, &[texture], 1, 1, 1).unwrap();
|
||||
let inputs = atlas.terrain_inputs(true);
|
||||
// Inset faces of two source states sharing sprite zero, an extended multipart face,
|
||||
// and the second state's fluid face. Ownership is authored
|
||||
// fixture data, not inferred from normal, sprite, position or diagonal.
|
||||
let mut vertices = Vec::new();
|
||||
for (x, y) in [(2.0, 3.25), (6.0, 3.25), (7.25, 3.75), (6.0, 3.875)] {
|
||||
for [dx, dz] in [[0.0, 0.0], [0.0, 0.5], [0.5, 0.5], [0.5, 0.0]] {
|
||||
vertices.push(crate::mesher::TerrainVertex::new(
|
||||
[x + dx, y, 4.0 + dz],
|
||||
[0.0; 2],
|
||||
0,
|
||||
u32::MAX,
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
let declarations = inputs
|
||||
.declarations
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with("layout(location"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let attribute = |name: &str| {
|
||||
inputs
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|(n, _)| n == name)
|
||||
.unwrap()
|
||||
.1
|
||||
.as_str()
|
||||
};
|
||||
let source = format!(
|
||||
"#version 450\n{declarations}\n\
|
||||
layout(local_size_x=1) in;\n\
|
||||
layout(set=0,binding=0) buffer Results {{ vec4 result[]; }};\n\
|
||||
void main() {{\n\
|
||||
uint hz_index=gl_GlobalInvocationID.x;\n\
|
||||
vec3 hz_v_pos=uintBitsToFloat(uvec3(hz_vertices[6u*hz_index],hz_vertices[6u*hz_index+1u],hz_vertices[6u*hz_index+2u]));\n\
|
||||
uvec2 hz_v_uv=uvec2(0); uint hz_v_layer=0u; vec4 hz_v_color=vec4(1); uvec2 hz_v_light=uvec2(0);\n\
|
||||
vec4 hz_Vertex, hz_MultiTexCoord0, hz_MultiTexCoord1, hz_MultiTexCoord2, hz_Color; vec3 hz_Normal;\n\
|
||||
{}\nresult[2u*hz_index]={}; result[2u*hz_index+1u]={};\n}}",
|
||||
inputs.body.replace("gl_VertexIndex", "hz_index"),
|
||||
attribute("mc_Entity"),
|
||||
attribute("at_midBlock")
|
||||
);
|
||||
let mut frontend = naga::front::glsl::Frontend::default();
|
||||
let module = frontend
|
||||
.parse(
|
||||
&naga::front::glsl::Options::from(naga::ShaderStage::Compute),
|
||||
&source,
|
||||
)
|
||||
.unwrap();
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Original terrain numeric control"),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
|
||||
});
|
||||
let layouts: Vec<_> = [1, 2, 0, 3]
|
||||
.into_iter()
|
||||
.map(|count| {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &(0..count)
|
||||
.map(|binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: if count == 1 {
|
||||
wgpu::ShaderStages::COMPUTE
|
||||
} else {
|
||||
wgpu::ShaderStages::COMPUTE | wgpu::ShaderStages::VERTEX
|
||||
},
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage {
|
||||
read_only: binding != 0 || count != 1,
|
||||
},
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &layouts.iter().map(Some).collect::<Vec<_>>(),
|
||||
..Default::default()
|
||||
});
|
||||
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&layout),
|
||||
module: &shader,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
let storage = |bytes: &[u8]| {
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytes,
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
})
|
||||
};
|
||||
let result = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: vertices.len() as u64 * 32,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let slots = storage(bytemuck::cast_slice(&[0u32]));
|
||||
let origins = storage(bytemuck::cast_slice(&[[128.0f32, -32.0, -256.0, -1.0]]));
|
||||
let ids = storage(bytemuck::cast_slice(&[
|
||||
crate::mesher::TerrainSource::new(0, [2, 3, 4], 0, false),
|
||||
crate::mesher::TerrainSource::new(1, [6, 3, 4], 13, false),
|
||||
crate::mesher::TerrainSource::new(1, [6, 3, 4], 13, false),
|
||||
crate::mesher::TerrainSource::new(1, [6, 3, 4], 13, true),
|
||||
]));
|
||||
let vertex_storage = storage(bytemuck::cast_slice(&vertices));
|
||||
let rects = storage(bytemuck::cast_slice(
|
||||
&super::super::pack_post::terrain_tables(&[31, -1, 47, 59], &[0, 1]),
|
||||
));
|
||||
let group = |set: u32, buffers: &[&wgpu::Buffer]| {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pipeline.get_bind_group_layout(set),
|
||||
entries: &buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, b)| wgpu::BindGroupEntry {
|
||||
binding: i as u32,
|
||||
resource: b.as_entire_binding(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
};
|
||||
let groups = [
|
||||
group(0, &[&result]),
|
||||
group(1, &[&slots, &origins]),
|
||||
group(2, &[]),
|
||||
group(3, &[&ids, &vertex_storage, &rects]),
|
||||
];
|
||||
queue.submit([encoder.finish()]);
|
||||
let actual = dispatch(&device, &queue, &pipeline, &groups, &result);
|
||||
let mut mismatches = Vec::new();
|
||||
for (i, vertex) in vertices.iter().enumerate() {
|
||||
let q = i / 4;
|
||||
let expected_id = [31.0, 47.0, 47.0, 59.0][q];
|
||||
let expected_light = [0.0, 13.0, 13.0, 13.0][q];
|
||||
let owner = [
|
||||
[2.5, 3.5, 4.5],
|
||||
[6.5, 3.5, 4.5],
|
||||
[6.5, 3.5, 4.5],
|
||||
[6.5, 3.5, 4.5],
|
||||
][q];
|
||||
let expected_mid = [
|
||||
(owner[0] - vertex.pos[0]) * 64.0,
|
||||
(owner[1] - vertex.pos[1]) * 64.0,
|
||||
(owner[2] - vertex.pos[2]) * 64.0,
|
||||
expected_light,
|
||||
];
|
||||
if actual[i * 2] != [expected_id, if q == 3 { 1.0 } else { 0.0 }, 0.0, 0.0]
|
||||
|| actual[i * 2 + 1] != expected_mid
|
||||
{
|
||||
mismatches.push((
|
||||
i,
|
||||
actual[i * 2],
|
||||
actual[i * 2 + 1],
|
||||
expected_id,
|
||||
expected_mid,
|
||||
));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
mismatches.is_empty(),
|
||||
"terrain source mismatches: {mismatches:?}"
|
||||
);
|
||||
|
||||
// Rasterize the same native vertices and production input body, with a diagnostic
|
||||
// projection placing one face in each strip. The fragment reports id, emission and
|
||||
// reconstructed owner Y/X after a nonzero section translation; no texture color
|
||||
// can hide a wrong ownership value.
|
||||
let vertex_source = format!(
|
||||
"#version 450\n{}\nlayout(location=0) flat out vec4 control;\n\
|
||||
void main(){{vec4 hz_Vertex,hz_MultiTexCoord0,hz_MultiTexCoord1,hz_MultiTexCoord2,hz_Color;vec3 hz_Normal;\n\
|
||||
{}\nvec4 entity={};vec4 mid={};\n\
|
||||
control=vec4(entity.x,mid.w,hz_Vertex.y+mid.y/64.0,hz_Vertex.x+mid.x/64.0);\n\
|
||||
uint corner=uint(gl_VertexIndex)%4u;float q=float(uint(gl_VertexIndex)/4u);\n\
|
||||
gl_Position=vec4(-1.0+(q+float(corner>=2u))*0.5,(corner==1u||corner==2u)?1.0:-1.0,0.5,1.0);}}",
|
||||
inputs.declarations,
|
||||
inputs.body,
|
||||
attribute("mc_Entity"),
|
||||
attribute("at_midBlock")
|
||||
);
|
||||
let vertex_module = frontend
|
||||
.parse(
|
||||
&naga::front::glsl::Options::from(naga::ShaderStage::Vertex),
|
||||
&vertex_source,
|
||||
)
|
||||
.unwrap();
|
||||
let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(vertex_module)),
|
||||
});
|
||||
let fragment_shader=device.create_shader_module(wgpu::ShaderModuleDescriptor { label:None,source:wgpu::ShaderSource::Wgsl(
|
||||
"@fragment fn fs(@location(0) @interpolate(flat) control:vec4<f32>)->@location(0) vec4<f32>{return control;}".into()) });
|
||||
let raster = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vertex_shader,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 24,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &super::super::TERRAIN_ATTRIBUTES,
|
||||
})],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fragment_shader,
|
||||
entry_point: Some("fs"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Rgba32Float,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let indices: Vec<u32> = (0..4)
|
||||
.flat_map(|q| [4 * q, 4 * q + 1, 4 * q + 2, 4 * q, 4 * q + 2, 4 * q + 3])
|
||||
.collect();
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
let target = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 16,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba32Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = target.create_view(&Default::default());
|
||||
let pixels = 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 mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&raster);
|
||||
for (i, g) in groups.iter().enumerate() {
|
||||
pass.set_bind_group(i as u32, g, &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, vertex_buffer.slice(..));
|
||||
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..24, 0, 0..1);
|
||||
}
|
||||
encoder.copy_texture_to_buffer(
|
||||
target.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &pixels,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(256),
|
||||
rows_per_image: Some(1),
|
||||
},
|
||||
},
|
||||
target.size(),
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
pixels
|
||||
.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let pixels = pixels.slice(..).get_mapped_range().unwrap();
|
||||
let pixels: &[[f32; 4]] = bytemuck::cast_slice(&pixels);
|
||||
for (i, pixel) in pixels.iter().enumerate() {
|
||||
assert_eq!(
|
||||
*pixel,
|
||||
[
|
||||
[31.0, 0.0, -28.5, 130.5],
|
||||
[47.0, 13.0, -28.5, 134.5],
|
||||
[47.0, 13.0, -28.5, 134.5],
|
||||
[59.0, 13.0, -28.5, 134.5]
|
||||
][i / 4],
|
||||
"diagnostic raster pixel {i}"
|
||||
);
|
||||
}
|
||||
|
||||
// Reload changes only the immutable mapping; the same raw state/owner records
|
||||
// resolve to new material ids without baking a previous pack's ids into a mesh.
|
||||
let changed = storage(bytemuck::cast_slice(
|
||||
&super::super::pack_post::terrain_tables(&[211, -1, 223, 59], &[0, 1]),
|
||||
));
|
||||
let changed_groups = [
|
||||
groups[0].clone(),
|
||||
groups[1].clone(),
|
||||
groups[2].clone(),
|
||||
group(3, &[&ids, &vertex_storage, &changed]),
|
||||
];
|
||||
let reloaded = dispatch(&device, &queue, &pipeline, &changed_groups, &result);
|
||||
for i in 0..vertices.len() {
|
||||
assert_eq!(
|
||||
reloaded[2 * i],
|
||||
[
|
||||
if i < 4 {
|
||||
211.0
|
||||
} else if i < 12 {
|
||||
223.0
|
||||
} else {
|
||||
59.0
|
||||
},
|
||||
if i >= 12 { 1.0 } else { 0.0 },
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
);
|
||||
assert_eq!(reloaded[2 * i + 1], actual[2 * i + 1]);
|
||||
}
|
||||
let missing = storage(&[0; 8]);
|
||||
let missing_groups = [
|
||||
groups[0].clone(),
|
||||
groups[1].clone(),
|
||||
groups[2].clone(),
|
||||
group(3, &[&missing, &vertex_storage, &rects]),
|
||||
];
|
||||
let neutral = dispatch(&device, &queue, &pipeline, &missing_groups, &result);
|
||||
for i in 0..vertices.len() {
|
||||
assert_eq!(neutral[2 * i], [-1.0, 0.0, 0.0, 0.0]);
|
||||
assert_eq!(neutral[2 * i + 1], [0.0; 4]);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ pub fn buffers() -> [Option<wgpu::VertexBufferLayout<'static>>; 1] {
|
||||
/// What `gbuffers_entities` and `gbuffers_block` read from an entity vertex: camera-relative
|
||||
/// position, the shared atlas's UV, colour, light (block, sky in 16ths) and the world-oriented
|
||||
/// normal. The quad's corners, read from the same vertices at set 1, give the face normal where
|
||||
/// the vertex has none (custom geometry) and `at_tangent` along the texture's u, as for terrain.
|
||||
/// the vertex has none (block entities) and `at_tangent` along the texture's u, as for terrain.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
let words = std::mem::size_of::<Vertex>() / 4;
|
||||
let mut declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
|
||||
@@ -107,10 +107,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
"at_velocity",
|
||||
"hz_velocity(gbufferModelView, gbufferPreviousModelView)",
|
||||
),
|
||||
// Horizon's own, as for terrain: the HDR tier of a light-giving item, particle or glowing
|
||||
// text (`mesher::hdr_tier`, the overlay's high bits), 0 for none, 1 to 4 for low to very
|
||||
// bright.
|
||||
("horizonTier", "float((hz_v_overlay >> 16u) & 255u)"),
|
||||
];
|
||||
horizon_shaders::GbuffersInputs {
|
||||
declarations,
|
||||
@@ -127,8 +123,6 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +186,6 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +240,7 @@ impl Casters {
|
||||
pub fn cast(&self, span: &EntitySpan, program: usize) -> bool {
|
||||
match program {
|
||||
ENTITY => self.entities && (!self.only_player || span.player),
|
||||
BLOCK | BLOCK_TRANSLUCENT => self.blocks && (!self.only_lit || span.emissive),
|
||||
BLOCK => self.blocks && (!self.only_lit || span.emissive),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -440,7 +433,7 @@ impl PackEntities {
|
||||
let posed = skin.map_or(0, |s| s.vertices);
|
||||
if let Some(skin) = skin {
|
||||
for (span, range, overlay) in skin_runs(skin) {
|
||||
let program = material_program(&span, overlay, drawn).expect("tagged skin stage");
|
||||
let program = program_of(&span).expect("tagged skin stage");
|
||||
let pack = drawn[program] && !span.hidden;
|
||||
let casts = shadow.cast(&span, program);
|
||||
if !pack && !casts {
|
||||
@@ -462,7 +455,7 @@ impl PackEntities {
|
||||
extend_without_portals(rest, &entities[..first.min(entities.len())]);
|
||||
let (mut other, mut portal_faces, mut beam_faces) = (Vec::new(), Vec::new(), Vec::new());
|
||||
let mut without_fluids = Vec::new();
|
||||
for (span, range, material) in material_runs(entities, spans, drawn) {
|
||||
for (span, range) in runs(entities.len(), spans) {
|
||||
let run_start = range.start as u32;
|
||||
let whole = &entities[range];
|
||||
// A native fluid carries sprite-array UVs, not shared-atlas UVs.
|
||||
@@ -490,8 +483,7 @@ impl PackEntities {
|
||||
// gateway: its cube and its beam): each goes to its own program.
|
||||
// Beacon beams come in a run of their own, at stage NONE (game.rs: no stage of 5.4
|
||||
// is theirs), for gbuffers_beaconbeam.
|
||||
let split = (matches!(material, Some(BLOCK | BLOCK_TRANSLUCENT))
|
||||
|| span.stage == render_stage("NONE"))
|
||||
let split = (program_of(&span) == Some(BLOCK) || span.stage == render_stage("NONE"))
|
||||
&& whole
|
||||
.chunks(4)
|
||||
.any(|q| matches!(q[0].layer & 0xffff, 6 | 9 | 10));
|
||||
@@ -521,7 +513,7 @@ impl PackEntities {
|
||||
let program = match part {
|
||||
Part::Portal => Some(BLOCK),
|
||||
Part::Beam => Some(BEAM),
|
||||
Part::Plain => material.filter(|&p| {
|
||||
Part::Plain => program_of(&span).filter(|&p| {
|
||||
plain(run)
|
||||
|| (p == DESTROY && run.iter().all(|v| matches!(v.layer, 11 | 12)))
|
||||
}),
|
||||
@@ -636,7 +628,7 @@ impl PackEntities {
|
||||
portal: bool,
|
||||
) {
|
||||
let (entity_id, block_id) = match program {
|
||||
BLOCK | BLOCK_TRANSLUCENT => (0, span.id),
|
||||
BLOCK => (0, span.id),
|
||||
_ => (span.id, 0),
|
||||
};
|
||||
let mut bytes = [0u8; SLOT as usize];
|
||||
@@ -783,76 +775,7 @@ pub const PARTICLE: usize = 3;
|
||||
pub const DESTROY: usize = 4;
|
||||
/// Beacon and End gateway beams (`gbuffers_beaconbeam`).
|
||||
pub const BEAM: usize = 5;
|
||||
pub const BLOCK_TRANSLUCENT: usize = 6;
|
||||
pub const PARTICLE_TRANSLUCENT: usize = 7;
|
||||
pub const PROGRAMS: usize = 8;
|
||||
|
||||
/// Translucent blocks retain the ordinary block shadow program and blockEntityId.
|
||||
pub fn shadow_program(program: usize) -> usize {
|
||||
if program == BLOCK_TRANSLUCENT {
|
||||
BLOCK
|
||||
} else {
|
||||
program
|
||||
}
|
||||
}
|
||||
|
||||
fn material_program(span: &EntitySpan, layer: u32, drawn: [bool; PROGRAMS]) -> Option<usize> {
|
||||
Some(match program_of(span)? {
|
||||
BLOCK
|
||||
if drawn[BLOCK_TRANSLUCENT]
|
||||
&& (span.translucent
|
||||
|| layer & super::OIT_ENTITY != 0
|
||||
|| matches!(
|
||||
layer & 0xffff,
|
||||
super::TRANSLUCENT_ITEM_OVERLAY | super::TRANSLUCENT_GLINT_ITEM_OVERLAY
|
||||
)) =>
|
||||
{
|
||||
BLOCK_TRANSLUCENT
|
||||
}
|
||||
PARTICLE
|
||||
if drawn[PARTICLE_TRANSLUCENT]
|
||||
&& layer & 0xffff == super::TRANSLUCENT_PARTICLE_OVERLAY =>
|
||||
{
|
||||
PARTICLE_TRANSLUCENT
|
||||
}
|
||||
p => p,
|
||||
})
|
||||
}
|
||||
|
||||
/// Split only where an available variant changes a quad's program. Keep contiguous runs,
|
||||
/// their vertex order and the original single draw when no material variant is usable.
|
||||
fn material_runs<'a>(
|
||||
vertices: &'a [Vertex],
|
||||
spans: &'a [EntitySpan],
|
||||
drawn: [bool; PROGRAMS],
|
||||
) -> impl Iterator<Item = (EntitySpan, Range<usize>, Option<usize>)> + 'a {
|
||||
runs(vertices.len(), spans).flat_map(move |(span, range)| {
|
||||
let split = match program_of(&span) {
|
||||
Some(BLOCK) => drawn[BLOCK_TRANSLUCENT],
|
||||
Some(PARTICLE) => drawn[PARTICLE_TRANSLUCENT],
|
||||
_ => false,
|
||||
};
|
||||
let mut start = range.start;
|
||||
std::iter::from_fn(move || {
|
||||
if start >= range.end {
|
||||
return None;
|
||||
}
|
||||
let program = material_program(&span, vertices[start].layer, drawn);
|
||||
let end = if split {
|
||||
(start..range.end)
|
||||
.step_by(4)
|
||||
.skip(1)
|
||||
.find(|&i| material_program(&span, vertices[i].layer, drawn) != program)
|
||||
.unwrap_or(range.end)
|
||||
} else {
|
||||
range.end
|
||||
};
|
||||
let run = start..end;
|
||||
start = end;
|
||||
Some((span, run, program))
|
||||
})
|
||||
})
|
||||
}
|
||||
pub const PROGRAMS: usize = 6;
|
||||
|
||||
/// Which program draws `span`'s run, given which of them (or their fallbacks) the pack has;
|
||||
/// `None`: Horizon draws it.
|
||||
@@ -1010,17 +933,11 @@ mod tests {
|
||||
assert_eq!(drawn_by_pack(&spans[0], all), Some(ENTITY));
|
||||
assert_eq!(drawn_by_pack(&spans[1], all), Some(PARTICLE));
|
||||
assert_eq!(
|
||||
drawn_by_pack(
|
||||
&spans[1],
|
||||
[true, true, true, false, true, true, false, false]
|
||||
),
|
||||
drawn_by_pack(&spans[1], [true, true, true, false, true, true]),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
drawn_by_pack(
|
||||
&spans[2],
|
||||
[true, false, true, true, true, true, false, false]
|
||||
),
|
||||
drawn_by_pack(&spans[2], [true, false, true, true, true, true]),
|
||||
None
|
||||
);
|
||||
assert_eq!(drawn_by_pack(&spans[2], all), Some(BLOCK));
|
||||
@@ -1711,47 +1628,3 @@ mod velocity_translate_tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tier_translate_tests {
|
||||
/// A pack's entity and hand programs may read `horizonTier`, the overlay's HDR tier.
|
||||
#[test]
|
||||
fn a_pack_reading_horizon_tier_translates_for_entities_and_the_hand() {
|
||||
let dir = std::env::temp_dir().join(format!("hz-entity-tier-{}", std::process::id()));
|
||||
let shaders = dir.join("shaders");
|
||||
std::fs::create_dir_all(&shaders).unwrap();
|
||||
std::fs::write(
|
||||
shaders.join("gbuffers_entities.vsh"),
|
||||
"#version 120\nattribute float horizonTier;\nvarying float t;\n\
|
||||
void main() {\n gl_Position = ftransform();\n t = horizonTier;\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
shaders.join("gbuffers_entities.fsh"),
|
||||
"#version 120\nvarying float t;\nvoid main() {\n gl_FragData[0] = vec4(t);\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
let pack = horizon_shaders::Pack::open(&dir).unwrap();
|
||||
let m = horizon_shaders::Macros::standard();
|
||||
let vsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.vsh", &m);
|
||||
let fsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.fsh", &m);
|
||||
for (who, inputs) in [
|
||||
("entities", super::inputs()),
|
||||
("hand", super::super::pack_hand::inputs()),
|
||||
] {
|
||||
let attribute = inputs.attributes.iter().find(|(n, _)| n == "horizonTier");
|
||||
assert!(attribute.is_some(), "{who}");
|
||||
horizon_shaders::translate_gbuffers(
|
||||
&vsh,
|
||||
&fsh,
|
||||
horizon_shaders::Target::Buffers,
|
||||
&inputs,
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("{who}: {e:?}"));
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod material_route_tests;
|
||||
@@ -1,62 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn optional_variants_split_contiguous_materials_without_guessing_from_alpha() {
|
||||
let mut vertices = Vec::new();
|
||||
for layer in [
|
||||
0,
|
||||
super::super::OIT_ENTITY,
|
||||
0,
|
||||
super::super::TRANSLUCENT_PARTICLE_OVERLAY,
|
||||
0,
|
||||
] {
|
||||
vertices.extend(
|
||||
[Vertex {
|
||||
layer: layer | 3 << 16,
|
||||
color: 0x40808080,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
}; 4],
|
||||
);
|
||||
}
|
||||
let spans = [
|
||||
EntitySpan {
|
||||
stage: render_stage("BLOCK_ENTITIES"),
|
||||
id: 71,
|
||||
..Default::default()
|
||||
},
|
||||
EntitySpan {
|
||||
start: 8,
|
||||
stage: render_stage("PARTICLES"),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let collect = |drawn| {
|
||||
material_runs(&vertices, &spans, drawn)
|
||||
.map(|(_, range, p)| (range, p))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut legacy = [true; PROGRAMS];
|
||||
legacy[BLOCK_TRANSLUCENT] = false;
|
||||
legacy[PARTICLE_TRANSLUCENT] = false;
|
||||
assert_eq!(
|
||||
collect(legacy),
|
||||
[(0..8, Some(BLOCK)), (8..20, Some(PARTICLE))]
|
||||
);
|
||||
assert_eq!(
|
||||
collect([true; PROGRAMS]),
|
||||
[
|
||||
(0..4, Some(BLOCK)),
|
||||
(4..8, Some(BLOCK_TRANSLUCENT)),
|
||||
(8..12, Some(PARTICLE)),
|
||||
(12..16, Some(PARTICLE_TRANSLUCENT)),
|
||||
(16..20, Some(PARTICLE)),
|
||||
]
|
||||
);
|
||||
let casters = Casters {
|
||||
blocks: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(casters.cast(&spans[0], BLOCK_TRANSLUCENT));
|
||||
assert_eq!(shadow_program(BLOCK_TRANSLUCENT), BLOCK);
|
||||
assert!(!casters.cast(&spans[1], PARTICLE_TRANSLUCENT));
|
||||
}
|
||||
@@ -82,9 +82,6 @@ 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,7 +52,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
//! 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,9 +61,6 @@ mod shadow_faces_tests;
|
||||
#[cfg(test)]
|
||||
mod r11f_image_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod work_group_fold_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod forced_flip_tests;
|
||||
|
||||
@@ -433,9 +430,6 @@ 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
|
||||
@@ -533,7 +527,8 @@ pub(super) struct World<'a> {
|
||||
pub lightmap: wgpu::TextureView,
|
||||
/// Terrain's geometry, when the pack draws it.
|
||||
pub terrain: Option<Geometry<'a>>,
|
||||
/// Per source block state: its model material id, then its fluid material id.
|
||||
/// Of each block sprite layer, `mc_Entity` (id, then 1 for fluids or 0) and `at_midBlock.w`
|
||||
/// (its block's light): three values a layer.
|
||||
pub block_ids: &'a [i32],
|
||||
/// The atlas of the sun and moon's sprites.
|
||||
pub celestial: Option<wgpu::TextureView>,
|
||||
@@ -666,10 +661,10 @@ pub struct PackPost {
|
||||
down: HashMap<wgpu::TextureFormat, (wgpu::RenderPipeline, wgpu::BindGroupLayout)>,
|
||||
/// The per-draw uniforms of gbuffers programs.
|
||||
draws: DrawUniforms,
|
||||
/// Set 3 of flat terrain: an unknown-source fallback and immutable state/sprite tables.
|
||||
/// Each page's bind group tracks both its vertex buffer and optional source sidecar.
|
||||
/// Set 3 of flat terrain (`mc_Entity`'s table, then the sprites' places) and its bind group
|
||||
/// for each arena page, with the page buffer it was made for.
|
||||
block_ids: Option<(wgpu::BindGroupLayout, wgpu::Buffer, wgpu::Buffer)>,
|
||||
page_groups: HashMap<u32, (wgpu::Buffer, Option<wgpu::Buffer>, wgpu::BindGroup)>,
|
||||
page_groups: HashMap<u32, (wgpu::Buffer, wgpu::BindGroup)>,
|
||||
gui_size: [u32; 2],
|
||||
/// GPU time of each pass, while the renderer asks for it (`pack_timing.rs`).
|
||||
pub timing: Option<super::pack_timing::PassTiming>,
|
||||
@@ -679,8 +674,6 @@ 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,
|
||||
@@ -1209,44 +1202,30 @@ pub(super) struct ShadowView {
|
||||
pub depth_planes: bool,
|
||||
}
|
||||
|
||||
/// An arena page's key, native vertices and optional per-quad source records.
|
||||
pub(super) type TerrainPage = (u32, wgpu::Buffer, Option<wgpu::Buffer>);
|
||||
|
||||
/// Header (sprite-rect words, state count), sprite rectangles, then model/fluid ids.
|
||||
/// Keeping immutable tables together leaves the existing three storage bindings intact.
|
||||
pub(super) fn terrain_tables(table: &[i32], rects: &[u32]) -> Vec<i32> {
|
||||
let rects = if rects.is_empty() { &[0, 0][..] } else { rects };
|
||||
[rects.len() as i32, (table.len() / 2) as i32]
|
||||
.into_iter()
|
||||
.chain(rects.iter().map(|&v| v as i32))
|
||||
.chain(table.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn block_ids(
|
||||
/// Set 3 of flat terrain, read by the vertex stage: the sprite layers' table (`mc_Entity`'s id
|
||||
/// and fluid, `at_midBlock.w`; -1, -1, 0 when empty), per arena page, the page's vertices
|
||||
/// (a vertex reads its quad's corners for its normal), and each sprite's place in a stitched
|
||||
/// atlas (`rects`; zeros for a grid, which doesn't read them).
|
||||
fn block_ids(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
table: &[i32],
|
||||
rects: &[u32],
|
||||
) -> Option<(wgpu::BindGroupLayout, wgpu::Buffer, wgpu::Buffer)> {
|
||||
) -> (wgpu::BindGroupLayout, wgpu::Buffer, wgpu::Buffer) {
|
||||
let empty = [-1, -1, 0];
|
||||
let table = if table.is_empty() { &empty[..] } else { table };
|
||||
let buffer = |label, contents: &[u8]| {
|
||||
if contents.len() as u64 > device.limits().max_storage_buffer_binding_size {
|
||||
return None;
|
||||
}
|
||||
let buffer = super::arena::try_create(
|
||||
wgpu::util::DeviceExt::create_buffer_init(
|
||||
device,
|
||||
label,
|
||||
contents.len() as u64,
|
||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
)?;
|
||||
queue.write_buffer(&buffer, 0, contents);
|
||||
Some(buffer)
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(label),
|
||||
contents,
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
},
|
||||
)
|
||||
};
|
||||
let ids = buffer("Unknown terrain sources", &[0; 8])?;
|
||||
let rects = buffer(
|
||||
"Shader pack terrain tables",
|
||||
bytemuck::cast_slice(&terrain_tables(table, rects)),
|
||||
)?;
|
||||
let rects = if rects.is_empty() { &[0, 0][..] } else { rects };
|
||||
let ids = buffer("Shader pack block ids", bytemuck::cast_slice(table));
|
||||
let rects = buffer("Shader pack sprite rects", bytemuck::cast_slice(rects));
|
||||
let storage = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
@@ -1261,7 +1240,7 @@ pub(super) fn block_ids(
|
||||
label: Some("Shader pack terrain"),
|
||||
entries: &[storage(0), storage(1), storage(2)],
|
||||
});
|
||||
Some((layout, ids, rects))
|
||||
(layout, ids, rects)
|
||||
}
|
||||
|
||||
/// The game's geometry a gbuffers program draws: its vertex buffers and, for flat draws
|
||||
@@ -2200,7 +2179,6 @@ 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(),
|
||||
@@ -2341,7 +2319,6 @@ fn build_compute(
|
||||
draw: false,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
paged: false,
|
||||
ids: false,
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
@@ -2380,13 +2357,6 @@ 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.
|
||||
///
|
||||
@@ -2477,12 +2447,6 @@ impl PackPost {
|
||||
let textures = pack_textures::upload(device, queue, post.textures, &mut notes);
|
||||
let shadow_config = post.config.shadow.clone();
|
||||
let noise_size = post.config.noise_resolution.unwrap_or(256);
|
||||
let half_lives = post.config.half_lives;
|
||||
for (name, ticks) in horizon_shaders::HALF_LIVES.iter().zip(half_lives) {
|
||||
if let Some(t) = ticks {
|
||||
notes.push(format!("`{name}` is {t} ticks"));
|
||||
}
|
||||
}
|
||||
// shadowcolor2 to 7 only for a pack that names `HIGHER_SHADOWCOLOR`.
|
||||
let shadow_colors = (1u16 << post.shadow_colors) - 1;
|
||||
let mut custom = post.custom;
|
||||
@@ -2596,20 +2560,6 @@ impl PackPost {
|
||||
.fold(0, |m, n| m | 1 << n),
|
||||
jobs: defer.then(Default::default),
|
||||
};
|
||||
// Flat terrain resolves each quad's source state against the current pack.
|
||||
let block_ids = world
|
||||
.terrain
|
||||
.as_ref()
|
||||
.filter(|g| g.table.is_some())
|
||||
.map(|_| {
|
||||
block_ids(device, queue, world.block_ids, world.sprite_rects).ok_or_else(|| {
|
||||
Refused {
|
||||
reason: "terrain source tables could not be allocated".into(),
|
||||
notes: notes.clone(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let mut build = |mut p: horizon_shaders::PostProgram, geometry: Option<&Geometry<'_>>| {
|
||||
// Not wanted any more: the rest is skipped and the build refused below.
|
||||
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
@@ -2721,6 +2671,12 @@ impl PackPost {
|
||||
}
|
||||
Some(pass)
|
||||
};
|
||||
// Flat terrain reads `mc_Entity` from a table of the sprite layers.
|
||||
let block_ids = world
|
||||
.terrain
|
||||
.as_ref()
|
||||
.filter(|g| g.table.is_some())
|
||||
.map(|_| block_ids(device, world.block_ids, world.sprite_rects));
|
||||
let terrain_geometry = world.terrain.as_ref().map(|g| Geometry {
|
||||
buffers: g.buffers,
|
||||
table: g.table,
|
||||
@@ -2800,18 +2756,14 @@ impl PackPost {
|
||||
post.particles,
|
||||
post.damaged_block,
|
||||
post.beacon_beam,
|
||||
post.block_translucent,
|
||||
post.particles_translucent,
|
||||
];
|
||||
let mut late = [
|
||||
let late = [
|
||||
false,
|
||||
false,
|
||||
post.translucent_late,
|
||||
post.particles_late,
|
||||
false,
|
||||
true,
|
||||
post.translucent_late,
|
||||
true,
|
||||
];
|
||||
let shadow_only = (post.shadow_only_player, post.shadow_only_lit_blocks);
|
||||
let shadow_programs = [post.shadow_entities, post.shadow_block];
|
||||
@@ -2828,13 +2780,6 @@ 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" {
|
||||
@@ -2847,20 +2792,15 @@ impl PackPost {
|
||||
ids: g.ids,
|
||||
cull: false,
|
||||
};
|
||||
paged(build(p, Some(&decal)))
|
||||
build(p, Some(&decal))
|
||||
} else {
|
||||
paged(build(p, Some(g)))
|
||||
build(p, Some(g))
|
||||
}
|
||||
})
|
||||
});
|
||||
// Translation alone is insufficient: a refused GPU pipeline must retain the
|
||||
// legacy single particle pass and its timing as well as its geometry grouping.
|
||||
if entity_passes[super::pack_entities::PARTICLE_TRANSLUCENT].is_some() {
|
||||
late[super::pack_entities::PARTICLE] = false;
|
||||
}
|
||||
let shadow_entities = shadow_programs.map(|p| {
|
||||
p.zip(entity_geometry.as_ref())
|
||||
.and_then(|(p, g)| paged(build(p, Some(g))))
|
||||
.and_then(|(p, g)| build(p, Some(g)))
|
||||
});
|
||||
let entities = entity_vertices
|
||||
.filter(|_| (entity_passes.iter().chain(&shadow_entities)).any(Option::is_some))
|
||||
@@ -2881,12 +2821,10 @@ impl PackPost {
|
||||
cull: false,
|
||||
});
|
||||
let hides_clouds = post.clouds_off || post.clouds.is_some() && cloud_geometry.is_some();
|
||||
// Rain and snow test depth but never write it: Iris 1.11.7 left none of either in
|
||||
// `depthtex0`, whatever `rain.depth` said (probe packs of ours, #10492).
|
||||
let weather_geometry = world.weather.then(|| Geometry {
|
||||
buffers: &entity_buffers,
|
||||
table: None,
|
||||
depth_write: false,
|
||||
depth_write: post.rain_depth,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
ids: None,
|
||||
@@ -2905,7 +2843,7 @@ impl PackPost {
|
||||
ids: None,
|
||||
cull: false,
|
||||
};
|
||||
paged(build(p, Some(&g))).map(|pass| (pass, geometry))
|
||||
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| {
|
||||
@@ -2919,7 +2857,7 @@ impl PackPost {
|
||||
ids: None,
|
||||
cull: false,
|
||||
};
|
||||
paged(build(p, Some(&g)))
|
||||
build(p, Some(&g))
|
||||
});
|
||||
let weather = post
|
||||
.weather
|
||||
@@ -3358,7 +3296,6 @@ 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),
|
||||
@@ -3481,7 +3418,6 @@ impl PackPost {
|
||||
let mut u = FrameUniforms::default();
|
||||
u.atlas = world.atlas.map_or([0; 2], |a| a.1);
|
||||
u.shadow = shadow_config;
|
||||
u.half_lives = half_lives;
|
||||
u
|
||||
},
|
||||
shadow_cache: Default::default(),
|
||||
@@ -3720,12 +3656,6 @@ 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()
|
||||
@@ -3826,16 +3756,6 @@ 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]);
|
||||
}
|
||||
@@ -3970,7 +3890,7 @@ impl PackPost {
|
||||
}
|
||||
let (groups, ids) = (&self.page_groups, pass.ids);
|
||||
let page = |rp: &mut wgpu::RenderPass<'_>, page: u32| {
|
||||
if let Some((_, _, group)) = groups.get(&page).filter(|_| ids) {
|
||||
if let Some((_, group)) = groups.get(&page).filter(|_| ids) {
|
||||
rp.set_bind_group(3, group, &[]);
|
||||
}
|
||||
};
|
||||
@@ -4188,7 +4108,7 @@ impl PackPost {
|
||||
continue;
|
||||
};
|
||||
let mut draws = (geometry.draws.iter())
|
||||
.filter(|d| d.shadow && super::pack_entities::shadow_program(d.program) == program)
|
||||
.filter(|d| d.shadow && d.program == program)
|
||||
.peekable();
|
||||
if draws.peek().is_none() {
|
||||
continue;
|
||||
@@ -4200,11 +4120,7 @@ impl PackPost {
|
||||
encoder,
|
||||
pass,
|
||||
size,
|
||||
(geometry.draws.iter()).any(|d| {
|
||||
d.shadow
|
||||
&& super::pack_entities::shadow_program(d.program) == program
|
||||
&& d.portal
|
||||
}),
|
||||
(geometry.draws.iter()).any(|d| d.shadow && d.program == program && d.portal),
|
||||
);
|
||||
let mut on_portal = false;
|
||||
let mut rp = self.begin(encoder, pass);
|
||||
@@ -5189,7 +5105,7 @@ impl PackPost {
|
||||
inputs: &PackInputs,
|
||||
size: [u32; 2],
|
||||
window: [u32; 2],
|
||||
pages: &[TerrainPage],
|
||||
pages: &[(u32, wgpu::Buffer)],
|
||||
terrain: &mut TerrainDraw<'_>,
|
||||
) {
|
||||
self.resize(device, size);
|
||||
@@ -5225,8 +5141,8 @@ impl PackPost {
|
||||
if let Some((layout, ids, rects)) = &self.block_ids {
|
||||
// A page that grew has a new buffer.
|
||||
self.page_groups
|
||||
.retain(|p, (b, s, _)| pages.iter().any(|(q, c, t)| q == p && c == b && s == t));
|
||||
for (page, buffer, sources) in pages {
|
||||
.retain(|p, (b, _)| pages.iter().any(|(q, c)| q == p && c == b));
|
||||
for (page, buffer) in pages {
|
||||
self.page_groups.entry(*page).or_insert_with(|| {
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Shader pack terrain"),
|
||||
@@ -5234,7 +5150,7 @@ impl PackPost {
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: sources.as_ref().unwrap_or(ids).as_entire_binding(),
|
||||
resource: ids.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
@@ -5246,7 +5162,7 @@ impl PackPost {
|
||||
},
|
||||
],
|
||||
});
|
||||
(buffer.clone(), sources.clone(), group)
|
||||
(buffer.clone(), group)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6340,34 +6256,6 @@ 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;
|
||||
@@ -6413,10 +6301,6 @@ 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;
|
||||
|
||||
@@ -6449,6 +6333,3 @@ mod mipmap_box_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod colortex_mip_blend_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod material_route_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,paged_maps: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 }),
|
||||
hand:hand.then(crate::gfx::pack_hand::inputs),..Default::default()
|
||||
};
|
||||
let mut post = gpu.load(
|
||||
@@ -719,7 +719,6 @@ fn shadow_depth_map_reads(
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
//! Original end-to-end material routing, timing and blending control; no pack assets.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::gfx::{OIT_ENTITY, TRANSLUCENT_PARTICLE_OVERLAY, Vertex, pack_draw::EntitySpan};
|
||||
use crate::hud::HudBatch;
|
||||
use horizon_shaders::render_stage;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an offscreen graphics adapter; run explicitly"]
|
||||
fn authored_material_programs_render_and_legacy_draws_stay_unsplit() {
|
||||
let gpu = Gpu::new().expect("an executed adapter is required for this control");
|
||||
let mut vertices = Vec::new();
|
||||
for (i, layer) in [0, OIT_ENTITY, 0, TRANSLUCENT_PARTICLE_OVERLAY]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let x = -1.0 + i as f32 * 0.5;
|
||||
for pos in [
|
||||
[x, -1.0, 0.0],
|
||||
[x + 0.5, -1.0, 0.0],
|
||||
[x + 0.5, 1.0, 0.0],
|
||||
[x, 1.0, 0.0],
|
||||
] {
|
||||
vertices.push(Vertex {
|
||||
pos,
|
||||
layer: layer | 2 << 16,
|
||||
color: 0xffff_ffff,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
});
|
||||
}
|
||||
}
|
||||
let spans = [
|
||||
EntitySpan {
|
||||
stage: render_stage("BLOCK_ENTITIES"),
|
||||
id: 17,
|
||||
..Default::default()
|
||||
},
|
||||
EntitySpan {
|
||||
start: 8,
|
||||
stage: render_stage("PARTICLES"),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
for variants in [false, true] {
|
||||
let vertex = "#version 120\nvoid main(){gl_Position=gl_Vertex;}";
|
||||
let mut files = vec![
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
("final.fsh".into(), "#version 120\nuniform sampler2D colortex0; varying vec2 uv; void main(){gl_FragColor=vec4(texture2D(colortex0,uv).rgb,1.0);}".into()),
|
||||
("deferred.vsh".into(), VERTEX.into()),
|
||||
("deferred.fsh".into(), "#version 120\nuniform sampler2D colortex0; varying vec2 uv; void main(){gl_FragData[0]=vec4(texture2D(colortex0,uv).rgb*0.5,1.0);}".into()),
|
||||
("shaders.properties".into(), "particles.ordering=mixed\nseparateEntityDraws=true\nblend.gbuffers_block=off\nblend.gbuffers_particles=off\nblend.gbuffers_block_translucent=SRC_ALPHA ONE_MINUS_SRC_ALPHA ZERO ZERO\nblend.gbuffers_particles_translucent=SRC_ALPHA ONE_MINUS_SRC_ALPHA ZERO ZERO\n".into()),
|
||||
];
|
||||
let programs = [
|
||||
("gbuffers_block", "vec4(1.0,0.0,0.0,1.0)"),
|
||||
("gbuffers_particles", "vec4(0.0,0.0,1.0,1.0)"),
|
||||
("gbuffers_block_translucent", "vec4(0.0,1.0,0.0,0.5)"),
|
||||
("gbuffers_particles_translucent", "vec4(1.0,1.0,0.0,0.5)"),
|
||||
];
|
||||
for (name, color) in programs.into_iter().take(if variants { 4 } else { 2 }) {
|
||||
let expected_id = if name.starts_with("gbuffers_block") {
|
||||
17
|
||||
} else {
|
||||
0
|
||||
};
|
||||
files.push((format!("{name}.vsh"), vertex.into()));
|
||||
files.push((format!("{name}.fsh"), format!("#version 120\nuniform int blockEntityId; void main(){{gl_FragData[0]={color}; if (blockEntityId != {expected_id}) gl_FragData[0]=vec4(1.0);}}")));
|
||||
}
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
let mut input = super::super::pack_entities::inputs();
|
||||
input.body = "hz_Vertex=vec4(hz_v_pos,1.0); hz_Color=vec4(1.0);".into();
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs {
|
||||
entities: Some(input),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
for frame in 0..2 {
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut rest = Vec::new();
|
||||
post.prepare_entities(
|
||||
&gpu.device,
|
||||
&gpu.queue,
|
||||
&vertices,
|
||||
&spans,
|
||||
[0.; 4],
|
||||
[0.; 4],
|
||||
&mut rest,
|
||||
None,
|
||||
(&[], &[]),
|
||||
);
|
||||
assert!(rest.is_empty());
|
||||
let entities = post.entities.as_ref().unwrap();
|
||||
assert_eq!(entities.geometry.draws.len(), if variants { 4 } else { 2 });
|
||||
let pixels = gpu.render(
|
||||
&mut post,
|
||||
&color,
|
||||
&depth,
|
||||
&fixture_frame(&hud),
|
||||
&mut |_, _, _, _| {},
|
||||
);
|
||||
// Early opaque draws are halved by deferred. Late variants blend alpha 1/2
|
||||
// over world clear 1/16 halved by deferred, giving 1/64 + color/2.
|
||||
let expected = if variants {
|
||||
[[128, 0, 0], [4, 132, 4], [0, 0, 128], [132, 132, 4]]
|
||||
} else {
|
||||
[[128, 0, 0], [128, 0, 0], [0, 0, 255], [0, 0, 255]]
|
||||
};
|
||||
for (i, rgb) in expected.into_iter().enumerate() {
|
||||
let pixel = pixels[(SIZE[1] / 2 * SIZE[0] + i as u32 * 4 + 2) as usize];
|
||||
eprintln!(
|
||||
"MATERIAL_ROUTE variants={variants} frame={frame} strip={i} actual={pixel:?} expected={rgb:?}"
|
||||
);
|
||||
assert!(pixel[..3].iter().zip(rgb).all(|(&a, b)| a.abs_diff(b) <= 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ 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);\
|
||||
|
||||
@@ -5,7 +5,7 @@ use wgpu::util::DeviceExt;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a host GPU; missing adapter is a failure"]
|
||||
fn rain_and_snow_leave_depth_alone_whatever_rain_depth_says() {
|
||||
fn rain_depth_controls_writes_for_rain_snow_and_fallbacks() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
|
||||
eprintln!("Weather depth GPU: {:?}", adapter.get_info());
|
||||
@@ -16,10 +16,14 @@ fn rain_and_snow_leave_depth_alone_whatever_rain_depth_says() {
|
||||
}))
|
||||
.unwrap();
|
||||
let mut depth_failures = Vec::new();
|
||||
// Iris 1.11.7 left no rain or snow in `depthtex0` with `rain.depth` absent, true or false,
|
||||
// drawn by `gbuffers_weather` or its fallback (probe packs of ours, #10492).
|
||||
for program in ["gbuffers_weather", "gbuffers_textured_lit"] {
|
||||
for property in ["rain.depth=true", "rain.depth=false", ""] {
|
||||
for (property, writes) in [
|
||||
("rain.depth=true", true),
|
||||
("rain.depth=false", false),
|
||||
// An evidence-limited compatibility choice: preserve accepted main here.
|
||||
// The permitted packs do not establish the external omitted-value default.
|
||||
("", true),
|
||||
] {
|
||||
let mut post = pack(&device, &queue, &adapter, program, property);
|
||||
// Retain the pipelines for another frame, then resize their targets.
|
||||
for size in [[6, 1], [6, 1], [12, 2]] {
|
||||
@@ -86,7 +90,7 @@ fn rain_and_snow_leave_depth_alone_whatever_rain_depth_says() {
|
||||
}
|
||||
let depth =
|
||||
f32::from_le_bytes(depths[offset..offset + 4].try_into().unwrap());
|
||||
let expected_depth = 0.5;
|
||||
let expected_depth = if visible && writes { 0.25 } else { 0.5 };
|
||||
if depth != expected_depth {
|
||||
depth_failures.push(format!(
|
||||
"{program} {property:?} size={size:?} ({x},{y}): depth={depth}, expected={expected_depth}"
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
//! 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,7 +104,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: true,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +131,6 @@ pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: true,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,25 +355,6 @@ pub const NAMES: &[&str] = &[
|
||||
"hasSkylight",
|
||||
"hasCeiling",
|
||||
"cloudTime",
|
||||
// Horizon's own: Horizon Extras' dynamic lights (`dynamic_light.rs`), nearest first: how
|
||||
// many, and each one's camera-relative position and light level (zeros past the count).
|
||||
"horizonDynamicLightCount",
|
||||
"horizonDynamicLight0",
|
||||
"horizonDynamicLight1",
|
||||
"horizonDynamicLight2",
|
||||
"horizonDynamicLight3",
|
||||
"horizonDynamicLight4",
|
||||
"horizonDynamicLight5",
|
||||
"horizonDynamicLight6",
|
||||
"horizonDynamicLight7",
|
||||
"horizonDynamicLight8",
|
||||
"horizonDynamicLight9",
|
||||
"horizonDynamicLight10",
|
||||
"horizonDynamicLight11",
|
||||
"horizonDynamicLight12",
|
||||
"horizonDynamicLight13",
|
||||
"horizonDynamicLight14",
|
||||
"horizonDynamicLight15",
|
||||
"isElytraFlying",
|
||||
"cloudHeight",
|
||||
];
|
||||
@@ -405,8 +386,6 @@ pub struct FrameUniforms {
|
||||
center_depth: Option<f32>,
|
||||
/// The pack's shadow settings (7.1).
|
||||
pub shadow: Shadow,
|
||||
/// The pack's smoothing half-lives in ticks (`horizon_shaders::HALF_LIVES`).
|
||||
pub half_lives: [Option<f32>; 4],
|
||||
}
|
||||
|
||||
impl Default for FrameUniforms {
|
||||
@@ -424,7 +403,6 @@ impl Default for FrameUniforms {
|
||||
center_depth_raw: None,
|
||||
center_depth: None,
|
||||
shadow: Shadow::default(),
|
||||
half_lives: [None; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -737,7 +715,6 @@ impl FrameUniforms {
|
||||
*self = Self {
|
||||
atlas: self.atlas,
|
||||
shadow: std::mem::take(&mut self.shadow),
|
||||
half_lives: self.half_lives,
|
||||
..Self::default()
|
||||
};
|
||||
}
|
||||
@@ -802,13 +779,10 @@ impl FrameUniforms {
|
||||
// a tenth of a second a tick: Iris 1.11.7 smooths that slowly (measured frame by frame
|
||||
// with a probe pack: wetness 60 s rising, 20 s falling; eyeBrightnessSmooth 1 s;
|
||||
// centerDepthSmooth 0.1 s).
|
||||
// The pack's `wetnessHalflife`/`drynessHalflife` when it sets them (OptiFine's defaults
|
||||
// otherwise), as for `eyeBrightnessHalflife` and `centerDepthHalflife` below.
|
||||
let [wet, dry, eye_half, depth_half] = self.half_lives;
|
||||
let half_life = if inputs.rain > self.wetness {
|
||||
wet.unwrap_or(600.0)
|
||||
600.0
|
||||
} else {
|
||||
dry.unwrap_or(200.0)
|
||||
200.0
|
||||
};
|
||||
self.wetness = smooth(self.wetness, inputs.rain, half_life * SECONDS_A_TICK, dt);
|
||||
let eye = inputs.eye_light.map(|l| (l * 16) as f32);
|
||||
@@ -817,18 +791,13 @@ impl FrameUniforms {
|
||||
self.eye_smooth = eye;
|
||||
}
|
||||
for (smoothed, target) in self.eye_smooth.iter_mut().zip(eye) {
|
||||
*smoothed = smooth(
|
||||
*smoothed,
|
||||
target,
|
||||
eye_half.unwrap_or(10.0) * SECONDS_A_TICK,
|
||||
dt,
|
||||
);
|
||||
*smoothed = smooth(*smoothed, target, 10.0 * SECONDS_A_TICK, dt);
|
||||
}
|
||||
// `centerDepthHalflife`'s default, 1 tick (OptiFine's shaders.txt); the first value
|
||||
// read back is taken as it is.
|
||||
if let Some(raw) = self.center_depth_raw {
|
||||
self.center_depth = Some(match self.center_depth {
|
||||
Some(v) => smooth(v, raw, depth_half.unwrap_or(1.0) * SECONDS_A_TICK, dt),
|
||||
Some(v) => smooth(v, raw, SECONDS_A_TICK, dt),
|
||||
None => raw,
|
||||
});
|
||||
}
|
||||
@@ -960,12 +929,6 @@ impl FrameUniforms {
|
||||
"gbufferProjectionInverse" => m4(projection_inverse),
|
||||
"gbufferPreviousModelView" => m4(previous_view),
|
||||
"gbufferPreviousProjection" => m4(previous_projection),
|
||||
"horizonDynamicLightCount" => Value::i(&[frame.dynamic_lights.len() as i32]),
|
||||
n if dynamic_light_index(n).is_some() => Value::f(
|
||||
&(dynamic_light_index(n).and_then(|k| frame.dynamic_lights.get(k)))
|
||||
.copied()
|
||||
.unwrap_or([0.0; 4]),
|
||||
),
|
||||
"heldItemId" => Value::i(&[inputs.held_items[0]]),
|
||||
"heldItemId2" => Value::i(&[inputs.held_items[1]]),
|
||||
"heldBlockLightValue" => Value::i(&[inputs.held_light[0]]),
|
||||
@@ -1272,12 +1235,10 @@ mod tests {
|
||||
format!(r#"{{"attributes": {attributes}}}"#),
|
||||
)
|
||||
.unwrap();
|
||||
let environment = crate::environment::Environment::load_dimension(&root, name).unwrap();
|
||||
let environment =
|
||||
crate::environment::Environment::load_dimension(&root, name, false).unwrap();
|
||||
std::fs::remove_dir_all(&root).unwrap();
|
||||
environment.sample(&crate::environment::Sample {
|
||||
day_time: 6000,
|
||||
..Default::default()
|
||||
})
|
||||
environment.sample(6000, None, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1824,29 +1785,6 @@ pub(super) mod initial_history_tests {
|
||||
assert_eq!(crate::clouds::drift(256 * 400 + 3, 0.25, 256), 3.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dynamic_lights_reach_the_pack_nearest_first() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let mut f = frame(&hud);
|
||||
let lights = [[1.0, 2.0, 3.0, 14.0], [-4.0, 0.5, 6.0, 7.0]];
|
||||
f.dynamic_lights = &lights;
|
||||
let mut u = FrameUniforms::default();
|
||||
u.update(&f, &PackInputs::default(), [800, 450]);
|
||||
let count = index("horizonDynamicLightCount").unwrap();
|
||||
assert_eq!(int(&u, count), 2);
|
||||
assert_eq!(float(&u, "horizonDynamicLight0")[..4], lights[0]);
|
||||
assert_eq!(float(&u, "horizonDynamicLight1")[..4], lights[1]);
|
||||
// Past the count, and the last of the sixteen: zeros.
|
||||
assert_eq!(float(&u, "horizonDynamicLight2")[..4], [0.0; 4]);
|
||||
assert_eq!(float(&u, "horizonDynamicLight15")[..4], [0.0; 4]);
|
||||
assert_eq!(index("horizonDynamicLight16"), None);
|
||||
// Off (no lights): none.
|
||||
f.dynamic_lights = &[];
|
||||
u.update(&f, &PackInputs::default(), [800, 450]);
|
||||
assert_eq!(int(&u, count), 0);
|
||||
assert_eq!(float(&u, "horizonDynamicLight0")[..4], [0.0; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environmental_smoothing_starts_at_the_first_inputs_without_resetting_each_frame() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
@@ -2232,33 +2170,22 @@ pub(super) mod initial_history_tests {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
let mut u = FrameUniforms::default();
|
||||
// A scripted clock: the next frame comes exactly `seconds` after the last, however long
|
||||
// the test itself takes (a wall-clock gap of a millisecond moved centerDepthSmooth by
|
||||
// more than the tolerance on a busy CI runner).
|
||||
let mut counter = 1;
|
||||
let mut elapsed = 0.0;
|
||||
let mut after = |input: &mut PackInputs, seconds: f32| {
|
||||
counter += 1;
|
||||
elapsed += seconds;
|
||||
input.frame_timing = Some(FrameTiming {
|
||||
counter,
|
||||
elapsed,
|
||||
delta: seconds,
|
||||
});
|
||||
// The next frame comes `seconds` after the last (and a few microseconds of the test).
|
||||
let after = |u: &mut FrameUniforms, seconds: f32| {
|
||||
u.last = Some(std::time::Instant::now() - std::time::Duration::from_secs_f32(seconds));
|
||||
};
|
||||
let mut input = PackInputs {
|
||||
eye_light: [0, 15],
|
||||
frame_timing: Some(FrameTiming::nth(1, 0.0)),
|
||||
..Default::default()
|
||||
};
|
||||
u.center_depth_raw = Some(1.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
input.eye_light = [0, 0];
|
||||
u.center_depth_raw = Some(0.0);
|
||||
after(&mut input, 0.1);
|
||||
after(&mut u, 0.1);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "centerDepthSmooth")[0] - 0.5).abs() < 1e-3);
|
||||
after(&mut input, 0.6);
|
||||
after(&mut u, 0.6);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
// 240 falling toward 0 for 0.7 s: 147.8, read as 147.
|
||||
match u.values[index("eyeBrightnessSmooth").unwrap()] {
|
||||
@@ -2266,46 +2193,11 @@ pub(super) mod initial_history_tests {
|
||||
_ => panic!("integer uniform expected"),
|
||||
}
|
||||
input.rain = 1.0;
|
||||
after(&mut input, 60.0);
|
||||
after(&mut u, 60.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.5).abs() < 1e-3);
|
||||
input.rain = 0.0;
|
||||
after(&mut input, 20.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.25).abs() < 1e-3);
|
||||
}
|
||||
|
||||
/// A pack's `const float wetnessHalflife = 30.0;` (and its dryness) replace the defaults:
|
||||
/// wetness reaches half the rain in 3 s instead of 60 s.
|
||||
#[test]
|
||||
fn a_pack_s_half_lives_replace_the_defaults() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
let mut u = FrameUniforms {
|
||||
half_lives: [Some(30.0), Some(10.0), None, None],
|
||||
..Default::default()
|
||||
};
|
||||
// A scripted clock, as in `smoothed_uniforms_take_iris_half_lives`: the wall clock's gap
|
||||
// between two updates is not exact on a busy runner.
|
||||
let mut input = PackInputs {
|
||||
frame_timing: Some(FrameTiming::nth(1, 0.0)),
|
||||
..Default::default()
|
||||
};
|
||||
u.update(&f, &input, [800, 450]);
|
||||
input.rain = 1.0;
|
||||
input.frame_timing = Some(FrameTiming {
|
||||
counter: 2,
|
||||
elapsed: 3.0,
|
||||
delta: 3.0,
|
||||
});
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.5).abs() < 1e-3);
|
||||
input.rain = 0.0;
|
||||
input.frame_timing = Some(FrameTiming {
|
||||
counter: 3,
|
||||
elapsed: 4.0,
|
||||
delta: 1.0,
|
||||
});
|
||||
after(&mut u, 20.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.25).abs() < 1e-3);
|
||||
}
|
||||
@@ -2642,12 +2534,6 @@ pub(super) mod initial_history_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `horizonDynamicLight<k>`'s `k`, for the lights `Frame::dynamic_lights` holds.
|
||||
fn dynamic_light_index(name: &str) -> Option<usize> {
|
||||
let k: usize = name.strip_prefix("horizonDynamicLight")?.parse().ok()?;
|
||||
(k < crate::dynamic_light::MAX).then_some(k)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pack_uniforms/ftransform_test.rs"]
|
||||
mod ftransform_test;
|
||||
@@ -46,7 +46,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
}),
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,13 +274,7 @@ impl Panorama {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_panorama"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// CubeMap's position-only cube and panorama.vsh/fsh: sample its untransformed direction.
|
||||
override HDR_WRITE_LIGHT: bool = false;
|
||||
struct Projection { view_proj: mat4x4<f32> }
|
||||
@group(0) @binding(0) var<uniform> projection: Projection;
|
||||
@group(0) @binding(1) var panorama: texture_cube<f32>;
|
||||
@@ -27,6 +26,5 @@ struct Out {
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_panorama(in: Out) -> @location(0) vec4<f32> {
|
||||
let color = textureSample(panorama, panorama_sampler, in.direction);
|
||||
return vec4(color.rgb, select(color.a, 1.0, HDR_WRITE_LIGHT));
|
||||
return textureSample(panorama, panorama_sampler, in.direction);
|
||||
}
|
||||
@@ -124,10 +124,6 @@ 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;
|
||||
@@ -439,18 +435,17 @@ impl<T> Resident<T> {
|
||||
if list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ox = f64::from(column.x * 16) - camera.x;
|
||||
let oz = f64::from(column.z * 16) - camera.z;
|
||||
let min_x = ox as f32;
|
||||
let min_z = oz as f32;
|
||||
let low_oy = (f64::from(self.min_y + column.low * 16) - camera.y) as f32;
|
||||
let high_oy = (f64::from(self.min_y + column.high * 16) - camera.y) as f32;
|
||||
let min = Vec3::new(min_x, low_oy, min_z);
|
||||
let max = Vec3::new(min_x + 16.0, high_oy + 16.0, min_z + 16.0);
|
||||
let min = origin((column.x, column.low, column.z), self.min_y, camera).as_vec3();
|
||||
let top = origin((column.x, column.high, column.z), self.min_y, camera).as_vec3();
|
||||
let max = top + Vec3::splat(16.0);
|
||||
let inside = frustum.contains(min, max);
|
||||
if !inside && !frustum.intersects(min, max) {
|
||||
continue;
|
||||
}
|
||||
let ox = f64::from(column.x * 16) - camera.x;
|
||||
let oz = f64::from(column.z * 16) - camera.z;
|
||||
let min_x = ox as f32;
|
||||
let min_z = oz as f32;
|
||||
let dx8 = ox + 8.0;
|
||||
let dz8 = oz + 8.0;
|
||||
let xz_sq = dx8 * dx8 + dz8 * dz8;
|
||||
|
||||
@@ -46,9 +46,6 @@ 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 {
|
||||
@@ -96,9 +93,7 @@ impl Stars {
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Stars shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl")).into(),
|
||||
),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("stars.wgsl").into()),
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Stars layout"),
|
||||
@@ -134,13 +129,7 @@ impl Stars {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_stars"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
// RenderPipelines.STARS: the celestial blend (src alpha, one), with the
|
||||
@@ -176,15 +165,11 @@ 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,9 +4,6 @@ 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;
|
||||
|
||||
@@ -19,9 +16,5 @@ 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);
|
||||
}
|
||||
if HDR_WRITE_LIGHT { return vec4(stars.color.rgb * b, b); }
|
||||
return vec4(stars.color.rgb * b, 0.175);
|
||||
}
|
||||
@@ -8,10 +8,6 @@ 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,18 +65,13 @@ impl Visbuffer {
|
||||
let resolve_constants: Vec<(&str, f64)> = constants
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([
|
||||
("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(color == super::LINEAR_FRAME)),
|
||||
),
|
||||
])
|
||||
.chain([("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64)])
|
||||
.collect();
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon visibility buffer"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(super::terrain_shader() + include_str!("../visbuffer.wgsl")).into(),
|
||||
(include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"))
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let fragment = wgpu::ShaderStages::FRAGMENT;
|
||||
|
||||
@@ -55,13 +55,6 @@ pub struct Post {
|
||||
sampler: wgpu::Sampler,
|
||||
copy: wgpu::RenderPipeline,
|
||||
fxaa: wgpu::RenderPipeline,
|
||||
/// Contrast-adaptive sharpening of TAA's upscaled picture: the jittered frames are
|
||||
/// spread over more output pixels, and the blend softens edges.
|
||||
sharpen: wgpu::RenderPipeline,
|
||||
/// The strength `sharpen` was built with (`set_sharpness`), and what rebuilding it takes.
|
||||
sharpness: f32,
|
||||
shader: wgpu::ShaderModule,
|
||||
pipeline_layout: wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
/// World resolution relative to the window (0.5..=2.0).
|
||||
pub scale: f32,
|
||||
@@ -79,7 +72,6 @@ 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<(bool, [f32; 4], [f32; 4], [[f32; 4]; 2])>,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
@@ -240,12 +232,8 @@ impl Post {
|
||||
..Default::default()
|
||||
});
|
||||
Self {
|
||||
shader: shader.clone(),
|
||||
pipeline_layout: pipeline_layout.clone(),
|
||||
copy: pipeline("fs_copy", &pipeline_layout, format),
|
||||
fxaa: pipeline("fs_fxaa", &pipeline_layout, format),
|
||||
sharpen: sharpen_pipeline(device, &shader, &pipeline_layout, format, 0.85),
|
||||
sharpness: 0.85,
|
||||
layout,
|
||||
sampler,
|
||||
format,
|
||||
@@ -259,28 +247,6 @@ impl Post {
|
||||
taa_frame: 0,
|
||||
taa_previous: None,
|
||||
taa_this_frame: bytemuck::Zeroable::zeroed(),
|
||||
light_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
// History stores baked light, so a change must reject it before jitter records the next frame.
|
||||
pub(super) fn baked_light(
|
||||
&mut self,
|
||||
gain: bool,
|
||||
settings: [f32; 4],
|
||||
shares: [f32; 4],
|
||||
windows: [[f32; 4]; 2],
|
||||
) {
|
||||
// These uniforms do not change baked colour without gain. Keep compatibility
|
||||
// history stable when only its final display settings change.
|
||||
let key = Some(if gain {
|
||||
(true, settings, shares, windows)
|
||||
} else {
|
||||
(false, [0.0; 4], [0.0; 4], [[0.0; 4]; 2])
|
||||
});
|
||||
if self.light_key != key {
|
||||
self.taa_previous = None;
|
||||
self.light_key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,21 +474,6 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Upscaling Sharpness setting, 0..=1: rebuilds the sharpen pipeline when it changed.
|
||||
pub fn set_sharpness(&mut self, device: &wgpu::Device, sharpness: f32) {
|
||||
let sharpness = sharpness.clamp(0.0, 1.0);
|
||||
if sharpness != self.sharpness {
|
||||
self.sharpen = sharpen_pipeline(
|
||||
device,
|
||||
&self.shader,
|
||||
&self.pipeline_layout,
|
||||
self.format,
|
||||
sharpness,
|
||||
);
|
||||
self.sharpness = sharpness;
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the world's own target, when it has one.
|
||||
pub fn size(&self) -> Option<[u32; 2]> {
|
||||
self.target.as_ref().map(|t| t.size)
|
||||
@@ -584,12 +535,7 @@ impl Post {
|
||||
pass.set_bind_group(0, &taa.resolve[i], &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
drop(pass);
|
||||
let pipeline = if self.upscale > 1.0 {
|
||||
&self.sharpen
|
||||
} else {
|
||||
&self.copy
|
||||
};
|
||||
self.draw_onto(encoder, screen, &taa.show[i], pipeline, timing);
|
||||
self.finish_with(encoder, screen, &taa.show[i], timing);
|
||||
}
|
||||
|
||||
/// Draw what `group` binds (`bind`) onto `screen`.
|
||||
@@ -599,23 +545,6 @@ impl Post {
|
||||
screen: &wgpu::TextureView,
|
||||
group: &wgpu::BindGroup,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let pipeline = if self.antialias {
|
||||
&self.fxaa
|
||||
} else {
|
||||
&self.copy
|
||||
};
|
||||
self.draw_onto(encoder, screen, group, pipeline, timing);
|
||||
}
|
||||
|
||||
/// Draw what `group` binds onto `screen` through `pipeline`.
|
||||
fn draw_onto(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
screen: &wgpu::TextureView,
|
||||
group: &wgpu::BindGroup,
|
||||
pipeline: &wgpu::RenderPipeline,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view: screen,
|
||||
@@ -632,51 +561,16 @@ impl Post {
|
||||
color_attachments: &attachments,
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_pipeline(if self.antialias {
|
||||
&self.fxaa
|
||||
} else {
|
||||
&self.copy
|
||||
});
|
||||
pass.set_bind_group(0, group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
/// `fs_sharpen` at `sharpness` (its override), drawing onto `format`.
|
||||
fn sharpen_pipeline(
|
||||
device: &wgpu::Device,
|
||||
shader: &wgpu::ShaderModule,
|
||||
layout: &wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
sharpness: f32,
|
||||
) -> wgpu::RenderPipeline {
|
||||
let constants = [("sharpness", f64::from(sharpness))];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Horizon sharpen"),
|
||||
layout: Some(layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_post"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_sharpen"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// This frame's clip space to the previous frame's, for camera-relative projections:
|
||||
/// a point `camera - previous_camera` further from the previous camera than from this one.
|
||||
pub(super) fn reprojection(
|
||||
@@ -753,60 +647,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baked_light_changes_and_returning_from_a_pack_reject_taa_history() {
|
||||
let Some((device, _)) = test_device() else {
|
||||
eprintln!("TAA baked-light GPU skipped: no test device");
|
||||
return;
|
||||
};
|
||||
let mut post = Post::new(&device, wgpu::TextureFormat::Rgba16Float);
|
||||
post.taa = true;
|
||||
if !post.supports_taa() {
|
||||
eprintln!("TAA baked-light GPU skipped: TAA unsupported");
|
||||
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 gain in [true, false, true] {
|
||||
post.baked_light(gain, key.0, key.1, key.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.baked_light(gain, key.0, key.1, key.2);
|
||||
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.baked_light(true, changed.0, changed.1, changed.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
}
|
||||
post.baked_light(false, key.0, key.1, key.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.baked_light(false, [0.0; 4], [0.0; 4], [[0.0; 4]; 2]);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
|
||||
// A pack disables TAA even when the light settings stay the same. Its first
|
||||
// world frame after the pack is dropped must not reuse the pre-pack history.
|
||||
post.jitter([32, 32], false, Mat4::IDENTITY, DVec3::ZERO);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
|
||||
eprintln!(
|
||||
"TAA baked-light GPU checked gain, all settings/shares/windows and the first frame after a pack"
|
||||
);
|
||||
}
|
||||
|
||||
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_unmarked"),
|
||||
entry_point: Some("fs_entity"),
|
||||
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_unmarked(v:Out)->@location(0) vec4<f32> { return v.color; }
|
||||
@fragment fn fs_entity(v:Out)->@location(0) vec4<f32> { return v.color; }
|
||||
"#
|
||||
.into(),
|
||||
),
|
||||
|
||||
@@ -1,697 +0,0 @@
|
||||
//! A native mod's world warp ([`native_world_warp`](super::native_render::native_world_warp),
|
||||
//! docs/native-mods.md "World warps"): WGSL that every terrain vertex, far terrain's tiles
|
||||
//! included, and entity vertex goes through, so a mod can bend the drawn world itself (its real
|
||||
//! blocks, light and depth, seen from any side) rather than a picture of it.
|
||||
//!
|
||||
//! The stock shaders hold only comment markers where a warp goes in (terrain.wgsl's
|
||||
//! `[world warp ...]`, entity.wgsl's and static_entity.wgsl's): a frame without a warp draws
|
||||
//! with exactly the shaders it always did. For a warp, copies of them get the markers filled,
|
||||
//! the declarations of `world_warp.wgsl`, the mod's WGSL and, for the terrain, the pixel stage of
|
||||
//! `world_warp_fragment.wgsl` with the mod's `world_fragment` or [`DEFAULT_FRAGMENT`]; the
|
||||
//! pipelines built from them read the warp's uniform at group 2.
|
||||
//!
|
||||
//! Those pipelines don't cull back faces (a warp may turn a face over), and a frame drawn with
|
||||
//! them leaves nothing out by where it was (`Renderer::render`): what is out of view, hidden or
|
||||
//! facing away may be brought into view. It also keeps to the per-section draws, which these
|
||||
//! pipelines are built for.
|
||||
use super::native_render::WorldWarp;
|
||||
use super::{SolidPipeline, TERRAIN_ATTRIBUTES, TerrainVertex, Vertex};
|
||||
use glam::DVec3;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The line in a warp's source after which the terrain's pixel stage begins.
|
||||
const FRAGMENTS: &str = "// [fragments]";
|
||||
/// The pixel stage of a warp without one: the terrain as the game draws it, less faces it cut.
|
||||
pub const DEFAULT_FRAGMENT: &str = "fn world_fragment(f: Fragment) -> Painted {
|
||||
if f.cut > 0.0 { return Painted(vec4(0.0, 0.0, 0.0, -1.0), 1.0); }
|
||||
return Painted(sprite(f.uv, f.layer) * f.color, 1.0);
|
||||
}
|
||||
";
|
||||
/// What terrain.wgsl's markers are filled with: the warp's two values in `TerrainOut`, and how
|
||||
/// they cross to the pixel stage in `TerrainWire`, after its five locations.
|
||||
const VARYINGS: &str = "warp_home: vec4<f32>,
|
||||
warp_at: vec3<f32>,";
|
||||
const WIRE: &str = "@location(5) warp_home: vec4<f32>,
|
||||
@location(6) warp_at: vec3<f32>,";
|
||||
const TO_WIRE: &str = "w.warp_home = o.warp_home;
|
||||
w.warp_at = o.warp_at;";
|
||||
const FROM_WIRE: &str = "o.warp_home = w.warp_home;
|
||||
o.warp_at = w.warp_at;";
|
||||
const VERTEX: &str = "let warp_home = position + origin;
|
||||
let warped = world_warp(warp_home, origin);
|
||||
let pos = warped.pos;";
|
||||
const VERTEX_OUT: &str = "out.color = out.color * vec4(max(warped.tint, vec3(0.0)), 1.0);
|
||||
out.warp_home = vec4(warp_home, select(0.0, 1.0, warped.tint.r < 0.0));
|
||||
out.warp_at = pos;";
|
||||
/// And entity.wgsl's and static_entity.wgsl's: an entity's vertex is its own `section`; a block
|
||||
/// entity's is its section's corner.
|
||||
const ENTITY: &str = "let at = world_warp(position, position).pos;";
|
||||
const STATIC_ENTITY: &str =
|
||||
"let section_corner = vec3<f32>(block_delta) - static_entity.camera_fraction.xyz;
|
||||
let camera_relative = world_warp(section_corner + position, section_corner).pos;";
|
||||
|
||||
/// The plainest warp, for the tests of the shaders it makes.
|
||||
#[cfg(test)]
|
||||
pub(super) const IDENTITY: &str = "fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped { return Warped(pos, vec3(1.0)); }\n";
|
||||
|
||||
/// `shader` with what stands between the markers `// [name]` and `// [end name]` replaced.
|
||||
fn fill(shader: &str, name: &str, with: &str) -> Result<String, String> {
|
||||
let (from, to) = (format!("// [{name}]"), format!("// [end {name}]"));
|
||||
let a = shader
|
||||
.find(&from)
|
||||
.ok_or_else(|| format!("no `{from}` in the shader"))?;
|
||||
let b = shader[a..]
|
||||
.find(&to)
|
||||
.ok_or_else(|| format!("no `{to}` in the shader"))?
|
||||
+ a;
|
||||
Ok(format!("{}{with}{}", &shader[..a], &shader[b + to.len()..]))
|
||||
}
|
||||
|
||||
/// A warp's source split into what every warped shader takes and the terrain's pixel stage.
|
||||
fn split(source: &str) -> (&str, Option<&str>) {
|
||||
match source.find(FRAGMENTS) {
|
||||
Some(at) => (&source[..at], Some(&source[at + FRAGMENTS.len()..])),
|
||||
None => (source, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The warp's copy of terrain.wgsl.
|
||||
pub(super) fn terrain_shader(source: &str) -> Result<String, String> {
|
||||
let (vertex, fragment) = split(source);
|
||||
let fragment = fragment
|
||||
.filter(|f| f.contains("fn world_fragment"))
|
||||
.unwrap_or(DEFAULT_FRAGMENT);
|
||||
let shader = fill(&super::terrain_shader(), "world warp varyings", VARYINGS)?;
|
||||
let shader = fill(&shader, "world warp wire", WIRE)?;
|
||||
let shader = fill(&shader, "world warp to wire", TO_WIRE)?;
|
||||
let shader = fill(&shader, "world warp from wire", FROM_WIRE)?;
|
||||
let shader = fill(&shader, "world warp vertex", VERTEX)?;
|
||||
let shader = fill(&shader, "world warp out", VERTEX_OUT)?;
|
||||
Ok(format!(
|
||||
"{shader}\n{}\n{vertex}\n{}\n{fragment}",
|
||||
include_str!("world_warp.wgsl"),
|
||||
include_str!("world_warp_fragment.wgsl"),
|
||||
))
|
||||
}
|
||||
|
||||
/// The warp's copy of entity.wgsl with static_entity.wgsl, for every entity pipeline it draws.
|
||||
pub(super) fn entity_shader(source: &str) -> Result<String, String> {
|
||||
let (vertex, _) = split(source);
|
||||
let shader = fill(&super::static_entity_shader(), "world warp entity", ENTITY)?;
|
||||
let shader = fill(&shader, "world warp static entity", STATIC_ENTITY)?;
|
||||
Ok(format!(
|
||||
"{shader}\n{}\n{vertex}",
|
||||
include_str!("world_warp.wgsl")
|
||||
))
|
||||
}
|
||||
|
||||
fn validate(source: &str) -> Result<(), String> {
|
||||
let module = naga::front::wgsl::parse_str(source).map_err(|e| e.emit_to_string(source))?;
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.map_err(|e| e.emit_to_string(source))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The warp's uniform (`world_warp.wgsl`'s `WorldWarpUniform`): the mod's sixteen floats, then
|
||||
/// the camera split into f32 and what f32 left out of it, with the frame's partial tick.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Uniform {
|
||||
params: [[f32; 4]; 4],
|
||||
camera: [f32; 4],
|
||||
camera_rest: [f32; 4],
|
||||
}
|
||||
|
||||
fn uniform(warp: &WorldWarp, camera: DVec3, partial_tick: f32) -> Uniform {
|
||||
let (params, _) = warp.params.as_chunks::<4>();
|
||||
let params: [[f32; 4]; 4] = params.try_into().expect("sixteen floats");
|
||||
let at = camera.as_vec3();
|
||||
let rest = camera - at.as_dvec3();
|
||||
Uniform {
|
||||
params,
|
||||
camera: [at.x, at.y, at.z, partial_tick],
|
||||
camera_rest: [rest.x as f32, rest.y as f32, rest.z as f32, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// What the warp's pipelines are built with: the renderer's own layouts and formats.
|
||||
pub(super) struct Kit {
|
||||
/// terrain.wgsl's group 0, and with flat draws the per-section draws' page group 1
|
||||
/// (`vs_section` reads its vertices from it).
|
||||
pub terrain: wgpu::BindGroupLayout,
|
||||
pub pages: Option<wgpu::BindGroupLayout>,
|
||||
/// entity.wgsl's group 0 and static_entity.wgsl's group 1.
|
||||
pub entity: wgpu::BindGroupLayout,
|
||||
pub static_entity: wgpu::BindGroupLayout,
|
||||
pub format: wgpu::TextureFormat,
|
||||
pub sprite_constants: [(&'static str, f64); 2],
|
||||
/// Whether the display offers HDR output: the solid terrain is built lit by it too.
|
||||
pub hdr_output: bool,
|
||||
}
|
||||
|
||||
/// The warp's pipelines, in place of the renderer's while it is on.
|
||||
pub(super) struct Pipelines {
|
||||
pub solid: SolidPipeline,
|
||||
pub translucent: wgpu::RenderPipeline,
|
||||
pub entities: wgpu::RenderPipeline,
|
||||
pub static_entities: wgpu::RenderPipeline,
|
||||
pub translucent_items: wgpu::RenderPipeline,
|
||||
pub text: wgpu::RenderPipeline,
|
||||
pub entity_shadows: wgpu::RenderPipeline,
|
||||
pub hdr_mark: wgpu::RenderPipeline,
|
||||
pub hdr_entity_mark: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
/// The warp's uniform, and what binds it.
|
||||
struct Bound {
|
||||
layout: wgpu::BindGroupLayout,
|
||||
buffer: wgpu::Buffer,
|
||||
group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
/// The renderer's world warp: nothing made until a mod gives one.
|
||||
pub(super) struct WorldWarps {
|
||||
kit: Kit,
|
||||
bound: Option<Bound>,
|
||||
built: Option<(Arc<str>, Pipelines)>,
|
||||
/// The last source that didn't compile, so it is tried once.
|
||||
refused: Option<Arc<str>>,
|
||||
/// Whether the log said a warp went undrawn under a shader pack drawing the terrain.
|
||||
told_undrawable: bool,
|
||||
}
|
||||
|
||||
impl WorldWarps {
|
||||
pub fn new(kit: Kit) -> Self {
|
||||
Self {
|
||||
kit,
|
||||
bound: None,
|
||||
built: None,
|
||||
refused: None,
|
||||
told_undrawable: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Readies this frame's warp: builds its pipelines when its source is new and writes its
|
||||
/// uniform. Whether the frame draws with it: not when it is off, didn't compile, or the
|
||||
/// frame can't take it (`drawable`: a shader pack draws the terrain).
|
||||
pub fn frame(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
warp: &WorldWarp,
|
||||
camera: DVec3,
|
||||
partial_tick: f32,
|
||||
drawable: bool,
|
||||
) -> bool {
|
||||
let Some(source) = &warp.source else {
|
||||
return false;
|
||||
};
|
||||
let same = |s: &Arc<str>| Arc::ptr_eq(s, source) || **s == **source;
|
||||
if !self.built.as_ref().is_some_and(|(s, _)| same(s)) {
|
||||
if self.refused.as_ref().is_some_and(same) {
|
||||
return false;
|
||||
}
|
||||
match self.build(device, source) {
|
||||
Ok(pipelines) => self.built = Some((source.clone(), pipelines)),
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: a mod's world warp did not compile: {e}");
|
||||
self.refused = Some(source.clone());
|
||||
self.built = None;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !warp.on {
|
||||
return false;
|
||||
}
|
||||
if !drawable {
|
||||
if !self.told_undrawable {
|
||||
log::info!(
|
||||
"Horizon: a mod's world warp isn't drawn while a shader pack draws the terrain"
|
||||
);
|
||||
self.told_undrawable = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let bound = self.bound(device);
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
&bound.buffer,
|
||||
0,
|
||||
bytemuck::bytes_of(&uniform(warp, camera, partial_tick)),
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
/// The pipelines of the warp `frame` readied.
|
||||
pub fn pipelines(&self) -> Option<&Pipelines> {
|
||||
self.built.as_ref().map(|(_, p)| p)
|
||||
}
|
||||
|
||||
/// The warp's group 2, once a frame has drawn with it.
|
||||
pub fn group(&self) -> Option<&wgpu::BindGroup> {
|
||||
self.bound.as_ref().map(|b| &b.group)
|
||||
}
|
||||
|
||||
fn bound(&mut self, device: &wgpu::Device) -> &Bound {
|
||||
self.bound.get_or_insert_with(|| bind(device))
|
||||
}
|
||||
|
||||
fn build(&mut self, device: &wgpu::Device, source: &str) -> Result<Pipelines, String> {
|
||||
let started = std::time::Instant::now();
|
||||
let terrain = terrain_shader(source)?;
|
||||
let entity = entity_shader(source)?;
|
||||
validate(&terrain)?;
|
||||
validate(&entity)?;
|
||||
// Validation failures above are the mod's; past here wgpu's are the game's.
|
||||
let warp = &self.bound.get_or_insert_with(|| bind(device)).layout;
|
||||
let kit = &self.kit;
|
||||
let layout = |label, groups: &[Option<&wgpu::BindGroupLayout>]| {
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some(label),
|
||||
bind_group_layouts: groups,
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let terrain_layout = layout(
|
||||
"Warped terrain layout",
|
||||
&[Some(&kit.terrain), None, Some(warp)],
|
||||
);
|
||||
let pages_layout = kit.pages.as_ref().map(|pages| {
|
||||
layout(
|
||||
"Warped terrain layout (pulled)",
|
||||
&[Some(&kit.terrain), Some(pages), Some(warp)],
|
||||
)
|
||||
});
|
||||
let entity_layout = layout(
|
||||
"Warped entity layout",
|
||||
&[Some(&kit.entity), None, Some(warp)],
|
||||
);
|
||||
let static_layout = layout(
|
||||
"Warped static block-entity layout",
|
||||
&[Some(&kit.entity), Some(&kit.static_entity), Some(warp)],
|
||||
);
|
||||
let module = |label, source: String| {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
})
|
||||
};
|
||||
let terrain = module("Warped terrain", terrain);
|
||||
let entity = module("Warped entities", entity);
|
||||
|
||||
let instance_attributes = wgpu::vertex_attr_array![5=>Float32x4];
|
||||
let instances = wgpu::VertexBufferLayout {
|
||||
array_stride: 16,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &instance_attributes,
|
||||
};
|
||||
let vertices = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<TerrainVertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &TERRAIN_ATTRIBUTES,
|
||||
};
|
||||
let buffered = [Some(vertices), Some(instances.clone())];
|
||||
let pulled = [None, Some(instances)];
|
||||
// As gfx.rs's: a float frame takes light where others take marks.
|
||||
let write_light = (
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(kit.format == super::LINEAR_FRAME)),
|
||||
);
|
||||
let lit = [
|
||||
kit.sprite_constants[0],
|
||||
kit.sprite_constants[1],
|
||||
write_light,
|
||||
];
|
||||
let plain = [
|
||||
kit.sprite_constants[0],
|
||||
kit.sprite_constants[1],
|
||||
("HDR_LIGHTING", 0.0),
|
||||
write_light,
|
||||
];
|
||||
let depth = |write| wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(write),
|
||||
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
};
|
||||
// As gfx.rs's terrain pipelines, without back-face culling.
|
||||
let terrain_pipeline = |label,
|
||||
layout: &wgpu::PipelineLayout,
|
||||
vertex,
|
||||
buffers: &[Option<wgpu::VertexBufferLayout<'_>>],
|
||||
fragment,
|
||||
constants: &[(&str, f64)],
|
||||
blend| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &terrain,
|
||||
entry_point: Some(vertex),
|
||||
compilation_options: Default::default(),
|
||||
buffers,
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: Some(depth(true)),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &terrain,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: kit.format,
|
||||
blend,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
// The per-section solid draws read the arena page with flat draws (`vs_section`), and
|
||||
// vertex buffers without.
|
||||
let (solid_layout, solid_vertex, solid_buffers): (_, _, &[_]) = match &pages_layout {
|
||||
Some(pages) => (pages, "vs_section", &pulled),
|
||||
None => (&terrain_layout, "vs_terrain", &buffered),
|
||||
};
|
||||
let solid = |constants: &[(&str, f64)]| {
|
||||
terrain_pipeline(
|
||||
"Warped terrain solid",
|
||||
solid_layout,
|
||||
solid_vertex,
|
||||
solid_buffers,
|
||||
"fs_warp_solid",
|
||||
constants,
|
||||
None,
|
||||
)
|
||||
};
|
||||
let solid = SolidPipeline {
|
||||
plain: solid(&plain),
|
||||
hdr: kit.hdr_output.then(|| solid(&lit)),
|
||||
};
|
||||
let translucent = terrain_pipeline(
|
||||
"Warped terrain translucent",
|
||||
&terrain_layout,
|
||||
"vs_terrain",
|
||||
&buffered,
|
||||
"fs_warp_translucent",
|
||||
&lit,
|
||||
Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
);
|
||||
|
||||
let entity_attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2];
|
||||
let entity_vertices = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &entity_attributes,
|
||||
};
|
||||
let origin_attributes = wgpu::vertex_attr_array![7=>Sint32x3];
|
||||
let static_buffers = [
|
||||
Some(entity_vertices.clone()),
|
||||
Some(wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<[i32; 3]>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &origin_attributes,
|
||||
}),
|
||||
];
|
||||
let entity_buffers = [Some(entity_vertices)];
|
||||
// As gfx.rs's `entity_pipeline_with`, for the pipelines a warped frame swaps.
|
||||
let entity_pipeline = |look: &EntityLook| {
|
||||
let (layout, vertex, buffers): (_, _, &[_]) = if look.static_mesh {
|
||||
(&static_layout, "vs_static_entity", &static_buffers)
|
||||
} else {
|
||||
(&entity_layout, "vs_entity", &entity_buffers)
|
||||
};
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(look.label),
|
||||
layout: Some(layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &entity,
|
||||
entry_point: Some(vertex),
|
||||
compilation_options: Default::default(),
|
||||
buffers,
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
cull_mode: look.cull,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
bias: look.bias,
|
||||
..depth(look.depth_write)
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &entity,
|
||||
entry_point: Some(look.fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[write_light],
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: kit.format,
|
||||
blend: look.blend,
|
||||
write_mask: look.write_mask,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let pipelines = Pipelines {
|
||||
solid,
|
||||
translucent,
|
||||
entities: entity_pipeline(&ENTITIES),
|
||||
static_entities: entity_pipeline(&STATIC_ENTITIES),
|
||||
translucent_items: entity_pipeline(&TRANSLUCENT_ITEMS),
|
||||
text: entity_pipeline(&TEXT),
|
||||
entity_shadows: entity_pipeline(&ENTITY_SHADOWS),
|
||||
hdr_mark: entity_pipeline(&HDR_MARK),
|
||||
hdr_entity_mark: entity_pipeline(&HDR_ENTITY_MARK),
|
||||
};
|
||||
log::info!(
|
||||
"Horizon: a mod's world warp built in {} ms",
|
||||
started.elapsed().as_millis()
|
||||
);
|
||||
Ok(pipelines)
|
||||
}
|
||||
}
|
||||
|
||||
fn bind(device: &wgpu::Device) -> Bound {
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("World warp"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("World warp"),
|
||||
size: std::mem::size_of::<Uniform>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("World warp"),
|
||||
layout: &layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
Bound {
|
||||
layout,
|
||||
buffer,
|
||||
group,
|
||||
}
|
||||
}
|
||||
|
||||
/// How one of the entity pipelines a warped frame swaps draws: as gfx.rs builds the stock one
|
||||
/// of the same name.
|
||||
struct EntityLook {
|
||||
label: &'static str,
|
||||
static_mesh: bool,
|
||||
cull: Option<wgpu::Face>,
|
||||
depth_write: bool,
|
||||
bias: wgpu::DepthBiasState,
|
||||
blend: Option<wgpu::BlendState>,
|
||||
fragment: &'static str,
|
||||
write_mask: wgpu::ColorWrites,
|
||||
}
|
||||
const NO_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState {
|
||||
constant: 0,
|
||||
slope_scale: 0.0,
|
||||
clamp: 0.0,
|
||||
};
|
||||
const ENTITIES: EntityLook = EntityLook {
|
||||
label: "Warped entities",
|
||||
static_mesh: false,
|
||||
cull: None,
|
||||
depth_write: true,
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 128,
|
||||
slope_scale: 0.0,
|
||||
clamp: 0.0,
|
||||
},
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
fragment: "fs_entity",
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
};
|
||||
const STATIC_ENTITIES: EntityLook = EntityLook {
|
||||
label: "Warped static block entities",
|
||||
static_mesh: true,
|
||||
bias: NO_BIAS,
|
||||
blend: None,
|
||||
fragment: "fs_entity_unmarked",
|
||||
..ENTITIES
|
||||
};
|
||||
const TRANSLUCENT_ITEMS: EntityLook = EntityLook {
|
||||
label: "Warped translucent items",
|
||||
bias: NO_BIAS,
|
||||
..ENTITIES
|
||||
};
|
||||
const TEXT: EntityLook = EntityLook {
|
||||
label: "Warped polygon offset text",
|
||||
cull: Some(wgpu::Face::Back),
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 10,
|
||||
slope_scale: 1.0,
|
||||
clamp: 0.0,
|
||||
},
|
||||
fragment: "fs_entity_unmarked",
|
||||
..ENTITIES
|
||||
};
|
||||
const ENTITY_SHADOWS: EntityLook = EntityLook {
|
||||
label: "Warped entity shadows",
|
||||
cull: Some(wgpu::Face::Back),
|
||||
depth_write: false,
|
||||
bias: wgpu::DepthBiasState {
|
||||
constant: 128,
|
||||
slope_scale: 0.015,
|
||||
clamp: 0.0,
|
||||
},
|
||||
fragment: "fs_entity_shadow",
|
||||
..ENTITIES
|
||||
};
|
||||
const HDR_MARK: EntityLook = EntityLook {
|
||||
label: "Warped HDR particle mark",
|
||||
depth_write: false,
|
||||
bias: NO_BIAS,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
fragment: "fs_hdr_mark",
|
||||
write_mask: wgpu::ColorWrites::ALPHA,
|
||||
..ENTITIES
|
||||
};
|
||||
/// `HDR_MARK` at the entities' depth bias, for what they drew.
|
||||
const HDR_ENTITY_MARK: EntityLook = EntityLook {
|
||||
label: "Warped HDR entity mark",
|
||||
bias: ENTITIES.bias,
|
||||
..HDR_MARK
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const BEND: &str = "fn world_warp(pos: vec3<f32>, section: vec3<f32>) -> Warped {
|
||||
let lift = sin(pos.x * 0.1 + warp.params[0].x) * warp.params[0].y + section.y * 0.0;
|
||||
return Warped(pos + vec3(0.0, lift, 0.0), vec3(1.0));
|
||||
}
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn a_warp_without_fragments_draws_the_terrain_as_the_game_does() {
|
||||
let terrain = terrain_shader(BEND).unwrap();
|
||||
validate(&terrain).unwrap();
|
||||
assert!(terrain.contains(DEFAULT_FRAGMENT));
|
||||
let entity = entity_shader(BEND).unwrap();
|
||||
validate(&entity).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_warps_own_fragments_replace_the_default_and_stay_out_of_the_entities() {
|
||||
let source = format!(
|
||||
"{BEND}{FRAGMENTS}
|
||||
fn world_fragment(f: Fragment) -> Painted {{
|
||||
if !f.front || f.light.x < 1.0 {{ return Painted(vec4(0.0, 0.0, 0.0, -1.0), 1.0); }}
|
||||
let far = length(f.home - f.at) + f.cut;
|
||||
return Painted(sprite(f.uv * 2.0, f.layer) * f.color, 1.0 - min(far, 1.0));
|
||||
}}
|
||||
"
|
||||
);
|
||||
let terrain = terrain_shader(&source).unwrap();
|
||||
validate(&terrain).unwrap();
|
||||
assert!(!terrain.contains(DEFAULT_FRAGMENT));
|
||||
let entity = entity_shader(&source).unwrap();
|
||||
validate(&entity).unwrap();
|
||||
assert!(!entity.contains("world_fragment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_stock_shaders_hold_nothing_of_a_warp() {
|
||||
for (name, shader) in [
|
||||
("terrain", super::super::terrain_shader()),
|
||||
("entity", super::super::static_entity_shader()),
|
||||
] {
|
||||
validate(&shader).unwrap();
|
||||
for word in ["world_warp(", "warp_home", "Warped", "group(2)"] {
|
||||
assert!(!shader.contains(word), "{name}.wgsl holds {word}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_marker_is_found() {
|
||||
let terrain = terrain_shader(BEND).unwrap();
|
||||
let entity = entity_shader(BEND).unwrap();
|
||||
for shader in [&terrain, &entity] {
|
||||
assert!(!shader.contains("// [world warp"), "a marker left unfilled");
|
||||
}
|
||||
assert!(terrain.contains("let warped = world_warp(warp_home, origin);"));
|
||||
assert!(entity.contains("let at = world_warp(position, position).pos;"));
|
||||
assert!(entity.contains("world_warp(section_corner + position, section_corner)"));
|
||||
assert_eq!(
|
||||
fill("a // [x] b // [end x] c", "x", "B"),
|
||||
Ok("a B c".to_owned())
|
||||
);
|
||||
assert!(fill("a // [x] b", "x", "B").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_warp_that_does_not_compile_is_refused_with_naga_s_reason() {
|
||||
let terrain =
|
||||
terrain_shader("fn world_warp(pos: vec3<f32>) -> Warped { return 1; }").unwrap();
|
||||
let e = validate(&terrain).unwrap_err();
|
||||
assert!(e.contains("world_warp"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_uniform_keeps_the_camera_exact_far_out() {
|
||||
let mut warp = WorldWarp::default();
|
||||
warp.params[5] = 2.0;
|
||||
let camera = DVec3::new(29_999_984.3, 70.25, -12_345_678.9);
|
||||
let u = uniform(&warp, camera, 0.25);
|
||||
assert_eq!(u.params[1][1], 2.0);
|
||||
assert_eq!(u.camera[3], 0.25);
|
||||
for i in 0..3 {
|
||||
let whole = f64::from(u.camera[i]) + f64::from(u.camera_rest[i]);
|
||||
assert!((whole - camera[i]).abs() < 1e-6, "{whole} {}", camera[i]);
|
||||
}
|
||||
assert_eq!(std::mem::size_of::<Uniform>(), 96);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1413 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user