Compare commits

..
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 2325ba52d4 Launcher: native-build --prebuilt-out, the registry CI's side of prebuilt builds (#11859)
`horizon-launcher native-build --prebuilt-out DIR --version <Nightly> FOLDER` builds every
.hnmod in FOLDER through the player's own pipeline (pins, hook points, left-out mods), but
needs no prepared game files and installs nothing: it writes the program, its
native-mods.json and request.txt into DIR. request.txt is the build's request
(horizon_release_sign::native_build): target, the base's commit, each mod's id, version and
file SHA-256, so the registry's workflow and the launcher name a build alike. A set with a
post-build step is refused: the step runs on the player's game files.

native_mods: pipeline takes an export folder; the .hnmod unpacking is shared with
installed_mods; build_target names the Rust target of a build.

Measured on the desktop: zoom + minimap on Nightly 6560641, release profile, -j 4: 8 min,
request key equal to the one horizon-mods' plan computes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:53:51 +01:00
1413 changed files with 30280 additions and 173881 deletions

No files matched your search

+1 -2
View File
@@ -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
-1
View File
@@ -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)
-5
View File
@@ -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
+9 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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).
+14 -55
View File
@@ -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);
}
+2 -2
View File
@@ -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));
}
}
+3 -41
View File
@@ -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]
-18
View File
@@ -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()
+20 -1
View File
@@ -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)]
-53
View File
@@ -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"}"#,
);
}
}
+55 -108
View File
@@ -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()
}
+2 -2
View File
@@ -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]
-1
View File
@@ -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,
}
}
+5 -267
View File
@@ -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
+1 -6
View File
@@ -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);
}
}
+2 -36
View File
@@ -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 -87
View File
@@ -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");
+13 -34
View File
@@ -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)]
+2 -55
View File
@@ -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>,
+14 -94
View File
@@ -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
+15 -245
View File
@@ -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());
+5 -42
View File
@@ -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
+13 -82
View File
@@ -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));
}
}
+15 -20
View File
@@ -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;
+32 -135
View File
@@ -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));
File diff suppressed because it is too large. Load diff
+2 -115
View File
@@ -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]);
}
}
+9 -22
View File
@@ -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));
}
}
+3 -53
View File
@@ -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",
+15 -21
View File
@@ -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"),
+5 -22
View File
@@ -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,
&macros,
&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", &macros);
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, &macros, &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();
}
+45 -164
View File
@@ -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, &macros, &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;
+2 -17
View File
@@ -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